summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-19 22:11:55 +0000
commit5a8431feceba47fd8e1804d9aa1b1730606b71d5 (patch)
treee5df8e0ceee60f4af8093f5c4c2f934b8abced05
parent4d477238500c347c6553d335d920bedfc5a46869 (diff)
downloadgitlab-ce-5a8431feceba47fd8e1804d9aa1b1730606b71d5.tar.gz
Add latest changes from gitlab-org/gitlab@12-5-stable-ee
-rw-r--r--.gitignore2
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--.gitlab/CODEOWNERS8
-rw-r--r--.gitlab/ci/cng.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml15
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml163
-rw-r--r--.gitlab/ci/memory.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/notifications.gitlab-ci.yml29
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml29
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/releases.gitlab-ci.yml22
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml8
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml100
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/test-metadata.gitlab-ci.yml4
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md2
-rw-r--r--.gitlab/merge_request_templates/Documentation.md2
-rw-r--r--.haml-lint_todo.yml4
-rw-r--r--.overcommit.yml.example17
-rw-r--r--.rubocop.yml5
-rw-r--r--.rubocop_todo.yml7
-rw-r--r--CHANGELOG-EE.md2
-rw-r--r--CHANGELOG.md4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile47
-rw-r--r--Gemfile.lock196
-rw-r--r--Guardfile43
-rw-r--r--PROCESS.md2
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/cluster_app_logos/crossplane.pngbin0 -> 1850 bytes
-rw-r--r--app/assets/images/cluster_app_logos/elastic_stack.pngbin0 -> 2919 bytes
-rw-r--r--app/assets/javascripts/api.js25
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js40
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js6
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue3
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue4
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue3
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/models/list.js4
-rw-r--r--app/assets/javascripts/boards/stores/getters.js3
-rw-r--r--app/assets/javascripts/boards/stores/index.js6
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/boards/toggle_labels.js1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js52
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue191
-rw-r--r--app/assets/javascripts/clusters/components/crossplane_provider_stack.vue93
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue12
-rw-r--r--app/assets/javascripts/clusters/constants.js13
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js2
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js43
-rw-r--r--app/assets/javascripts/commit/image_file.js254
-rw-r--r--app/assets/javascripts/compare_autocomplete.js18
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue2
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue227
-rw-r--r--app/assets/javascripts/contributors/index.js23
-rw-r--r--app/assets/javascripts/contributors/services/contributors_service.js7
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js20
-rw-r--r--app/assets/javascripts/contributors/stores/getters.js33
-rw-r--r--app/assets/javascripts/contributors/stores/index.js18
-rw-r--r--app/assets/javascripts/contributors/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/contributors/stores/mutations.js17
-rw-r--r--app/assets/javascripts/contributors/stores/state.js5
-rw-r--r--app/assets/javascripts/contributors/utils.js30
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue99
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue24
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue182
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue63
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue140
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js7
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/index.js45
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js134
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js84
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/index.js15
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js32
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/state.js19
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js (renamed from app/assets/javascripts/projects/gke_cluster_namespace/index.js)0
-rw-r--r--app/assets/javascripts/create_cluster/init_create_cluster.js37
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue44
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue36
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue13
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue141
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue56
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace.vue33
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue110
-rw-r--r--app/assets/javascripts/error_tracking/details.js25
-rw-r--r--app/assets/javascripts/error_tracking/list.js (renamed from app/assets/javascripts/error_tracking/index.js)0
-rw-r--r--app/assets/javascripts/error_tracking/services/index.js2
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js63
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/details/mutation_types.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/mutations.js16
-rw-r--r--app/assets/javascripts/error_tracking/store/details/state.js6
-rw-r--r--app/assets/javascripts/error_tracking/store/index.js33
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js (renamed from app/assets/javascripts/error_tracking/store/actions.js)4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/getters.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutation_types.js (renamed from app/assets/javascripts/error_tracking/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js (renamed from app/assets/javascripts/error_tracking/store/mutations.js)0
-rw-r--r--app/assets/javascripts/error_tracking/store/list/state.js5
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue31
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue45
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js3
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js3
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/state.js1
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js19
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js11
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js1
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js44
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js3
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue103
-rw-r--r--app/assets/javascripts/grafana_integration/index.js14
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js42
-rw-r--r--app/assets/javascripts/grafana_integration/store/index.js16
-rw-r--r--app/assets/javascripts/grafana_integration/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/grafana_integration/store/mutations.js13
-rw-r--r--app/assets/javascripts/grafana_integration/store/state.js8
-rw-r--r--app/assets/javascripts/group.js24
-rw-r--r--app/assets/javascripts/helpers/monitor_helper.js44
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue3
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue3
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/ide/services/index.js31
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js27
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js16
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js7
-rw-r--r--app/assets/javascripts/ide/stores/getters.js12
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/clientside/actions.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/clientside/index.js6
-rw-r--r--app/assets/javascripts/ide/stores/utils.js9
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js19
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable.vue335
-rw-r--r--app/assets/javascripts/issuables_list/components/issuables_list_app.vue277
-rw-r--r--app/assets/javascripts/issuables_list/constants.js33
-rw-r--r--app/assets/javascripts/issuables_list/eventhub.js5
-rw-r--r--app/assets/javascripts/issuables_list/index.js24
-rw-r--r--app/assets/javascripts/issue.js15
-rw-r--r--app/assets/javascripts/jobs/components/log/log.vue26
-rw-r--r--app/assets/javascripts/jobs/store/utils.js2
-rw-r--r--app/assets/javascripts/labels_select.js161
-rw-r--r--app/assets/javascripts/lib/graphql.js6
-rw-r--r--app/assets/javascripts/lib/utils/chart_utils.js17
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js58
-rw-r--r--app/assets/javascripts/lib/utils/notify.js8
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js33
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js23
-rw-r--r--app/assets/javascripts/lib/utils/tick_formats.js39
-rw-r--r--app/assets/javascripts/line_highlighter.js17
-rw-r--r--app/assets/javascripts/main.js20
-rw-r--r--app/assets/javascripts/manual_ordering.js6
-rw-r--r--app/assets/javascripts/merge_request.js26
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue227
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue73
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue96
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue232
-rw-r--r--app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue28
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/shared/prometheus_header.vue15
-rw-r--r--app/assets/javascripts/monitoring/constants.js15
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js7
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js54
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js38
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js6
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js6
-rw-r--r--app/assets/javascripts/monitoring/utils.js19
-rw-r--r--app/assets/javascripts/network/branch_graph.js193
-rw-r--r--app/assets/javascripts/new_branch_form.js42
-rw-r--r--app/assets/javascripts/new_commit_form.js5
-rw-r--r--app/assets/javascripts/notes.js100
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue133
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue111
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue13
-rw-r--r--app/assets/javascripts/notes/mixins/description_version_history.js12
-rw-r--r--app/assets/javascripts/notes/stores/actions.js16
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js44
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index.js20
-rw-r--r--app/assets/javascripts/pages/groups/index.js20
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js7
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js91
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/clusters/new/index.js13
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/details/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/error_tracking/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/index.js26
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js140
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js379
-rw-r--r--app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js143
-rw-r--r--app/assets/javascripts/pages/projects/index.js19
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js2
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js31
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/form.js20
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/test_report/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue83
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js14
-rw-r--r--app/assets/javascripts/performance_bar/components/add_request.vue48
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue3
-rw-r--r--app/assets/javascripts/performance_bar/index.js14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue81
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue108
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue116
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue129
-rw-r--r--app/assets/javascripts/pipelines/constants.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js25
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js30
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js23
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js15
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js19
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/utils.js36
-rw-r--r--app/assets/javascripts/privacy_policy_update_callout.js8
-rw-r--r--app/assets/javascripts/profile/gl_crop.js29
-rw-r--r--app/assets/javascripts/project_find_file.js49
-rw-r--r--app/assets/javascripts/project_select.js109
-rw-r--r--app/assets/javascripts/projects/project_new.js4
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue33
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue95
-rw-r--r--app/assets/javascripts/registry/constants.js19
-rw-r--r--app/assets/javascripts/registry/stores/actions.js6
-rw-r--r--app/assets/javascripts/registry/stores/mutations.js32
-rw-r--r--app/assets/javascripts/releases/detail/components/app.vue46
-rw-r--r--app/assets/javascripts/releases/detail/index.js2
-rw-r--r--app/assets/javascripts/releases/detail/store/state.js1
-rw-r--r--app/assets/javascripts/releases/list/components/release_block.vue22
-rw-r--r--app/assets/javascripts/releases/list/components/release_block_footer.vue112
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue2
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue1
-rw-r--r--app/assets/javascripts/repository/components/directory_download_links.vue47
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue58
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue49
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue109
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue41
-rw-r--r--app/assets/javascripts/repository/components/tree_action_link.vue28
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue115
-rw-r--r--app/assets/javascripts/repository/graphql.js6
-rw-r--r--app/assets/javascripts/repository/index.js84
-rw-r--r--app/assets/javascripts/repository/log_tree.js22
-rw-r--r--app/assets/javascripts/repository/pages/index.vue21
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue24
-rw-r--r--app/assets/javascripts/repository/queries/commit.fragment.graphql8
-rw-r--r--app/assets/javascripts/repository/queries/getCommit.query.graphql9
-rw-r--r--app/assets/javascripts/repository/queries/getCommits.query.graphql9
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.query.graphql1
-rw-r--r--app/assets/javascripts/repository/queries/getReadme.query.graphql5
-rw-r--r--app/assets/javascripts/repository/queries/pathLastCommit.query.graphql21
-rw-r--r--app/assets/javascripts/repository/router.js2
-rw-r--r--app/assets/javascripts/repository/utils/commit.js13
-rw-r--r--app/assets/javascripts/repository/utils/dom.js4
-rw-r--r--app/assets/javascripts/repository/utils/readme.js21
-rw-r--r--app/assets/javascripts/repository/utils/title.js8
-rw-r--r--app/assets/javascripts/right_sidebar.js52
-rw-r--r--app/assets/javascripts/search_autocomplete.js82
-rw-r--r--app/assets/javascripts/sentry/index.js (renamed from app/assets/javascripts/raven/index.js)8
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js (renamed from app/assets/javascripts/raven/raven_config.js)60
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue26
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js4
-rw-r--r--app/assets/javascripts/sourcegraph/index.js28
-rw-r--r--app/assets/javascripts/sourcegraph/load.js6
-rw-r--r--app/assets/javascripts/tree.js14
-rw-r--r--app/assets/javascripts/user_popovers.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/slot_switch.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue76
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue4
-rw-r--r--app/assets/javascripts/zen_mode.js32
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss7
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss16
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss3
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss18
-rw-r--r--app/assets/stylesheets/framework/flash.scss88
-rw-r--r--app/assets/stylesheets/framework/layout.scss3
-rw-r--r--app/assets/stylesheets/framework/memory_graph.scss6
-rw-r--r--app/assets/stylesheets/framework/modal.scss13
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss11
-rw-r--r--app/assets/stylesheets/framework/snippets.scss7
-rw-r--r--app/assets/stylesheets/framework/variables.scss27
-rw-r--r--app/assets/stylesheets/framework/vue_transitions.scss24
-rw-r--r--app/assets/stylesheets/mailer.scss117
-rw-r--r--app/assets/stylesheets/mailer_client_specific.scss65
-rw-r--r--app/assets/stylesheets/pages/boards.scss10
-rw-r--r--app/assets/stylesheets/pages/error_details.scss18
-rw-r--r--app/assets/stylesheets/pages/experimental_separate_sign_up.scss1
-rw-r--r--app/assets/stylesheets/pages/graph.scss15
-rw-r--r--app/assets/stylesheets/pages/milestone.scss7
-rw-r--r--app/assets/stylesheets/pages/notes.scss11
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss12
-rw-r--r--app/assets/stylesheets/pages/projects.scss7
-rw-r--r--app/assets/stylesheets/pages/reports.scss1
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss62
-rw-r--r--app/assets/stylesheets/performance_bar.scss14
-rw-r--r--app/assets/stylesheets/utilities.scss10
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb5
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/identities_controller.rb4
-rw-r--r--app/controllers/admin/keys_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb46
-rw-r--r--app/controllers/boards/issues_controller.rb5
-rw-r--r--app/controllers/clusters/applications_controller.rb2
-rw-r--r--app/controllers/clusters/clusters_controller.rb100
-rw-r--r--app/controllers/concerns/confirm_email_warning.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/lfs_request.rb4
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb39
-rw-r--r--app/controllers/concerns/milestone_actions.rb8
-rw-r--r--app/controllers/concerns/preview_markdown.rb30
-rw-r--r--app/controllers/concerns/redirects_for_missing_path_on_tree.rb17
-rw-r--r--app/controllers/concerns/renders_commits.rb13
-rw-r--r--app/controllers/concerns/routable_actions.rb2
-rw-r--r--app/controllers/concerns/sourcegraph_gon.rb30
-rw-r--r--app/controllers/dashboard/todos_controller.rb4
-rw-r--r--app/controllers/groups/boards_controller.rb2
-rw-r--r--app/controllers/groups/group_links_controller.rb34
-rw-r--r--app/controllers/groups/labels_controller.rb2
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb12
-rw-r--r--app/controllers/health_controller.rb12
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/notification_settings_controller.rb18
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb2
-rw-r--r--app/controllers/projects/blame_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/boards_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb7
-rw-r--r--app/controllers/projects/error_tracking_controller.rb67
-rw-r--r--app/controllers/projects/grafana_api_controller.rb5
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb28
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/controllers/projects/pages_domains_controller.rb19
-rw-r--r--app/controllers/projects/pipelines_controller.rb22
-rw-r--r--app/controllers/projects/releases_controller.rb40
-rw-r--r--app/controllers/projects/settings/operations_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb8
-rw-r--r--app/controllers/projects/usage_ping_controller.rb13
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/controllers/registrations_controller.rb22
-rw-r--r--app/controllers/sessions_controller.rb14
-rw-r--r--app/controllers/users_controller.rb10
-rw-r--r--app/finders/abuse_reports_finder.rb18
-rw-r--r--app/finders/admin/projects_finder.rb4
-rw-r--r--app/finders/branches_finder.rb51
-rw-r--r--app/finders/container_repositories_finder.rb38
-rw-r--r--app/finders/git_refs_finder.rb56
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/projects_finder.rb5
-rw-r--r--app/finders/prometheus_metrics_finder.rb129
-rw-r--r--app/finders/releases_finder.rb6
-rw-r--r--app/finders/tags_finder.rb28
-rw-r--r--app/finders/todos_finder.rb28
-rw-r--r--app/graphql/gitlab_schema.rb4
-rw-r--r--app/graphql/mutations/merge_requests/set_assignees.rb48
-rw-r--r--app/graphql/mutations/merge_requests/set_labels.rb53
-rw-r--r--app/graphql/mutations/merge_requests/set_locked.rb29
-rw-r--r--app/graphql/mutations/merge_requests/set_milestone.rb30
-rw-r--r--app/graphql/mutations/merge_requests/set_subscription.rb26
-rw-r--r--app/graphql/mutations/todos/base.rb17
-rw-r--r--app/graphql/mutations/todos/mark_done.rb38
-rw-r--r--app/graphql/resolvers/base_resolver.rb8
-rw-r--r--app/graphql/resolvers/commit_pipelines_resolver.rb13
-rw-r--r--app/graphql/types/base_enum.rb13
-rw-r--r--app/graphql/types/commit_type.rb42
-rw-r--r--app/graphql/types/extended_issue_type.rb14
-rw-r--r--app/graphql/types/group_type.rb15
-rw-r--r--app/graphql/types/issue_sort_enum.rb4
-rw-r--r--app/graphql/types/issue_type.rb94
-rw-r--r--app/graphql/types/label_type.rb14
-rw-r--r--app/graphql/types/merge_request_type.rb156
-rw-r--r--app/graphql/types/metadata_type.rb6
-rw-r--r--app/graphql/types/milestone_type.rb23
-rw-r--r--app/graphql/types/mutation_operation_mode_enum.rb14
-rw-r--r--app/graphql/types/mutation_type.rb6
-rw-r--r--app/graphql/types/namespace_type.rb34
-rw-r--r--app/graphql/types/project_statistics_type.rb21
-rw-r--r--app/graphql/types/project_type.rb167
-rw-r--r--app/graphql/types/repository_type.rb12
-rw-r--r--app/graphql/types/task_completion_status.rb6
-rw-r--r--app/graphql/types/todo_target_enum.rb8
-rw-r--r--app/graphql/types/todo_type.rb3
-rw-r--r--app/graphql/types/tree/entry_type.rb1
-rw-r--r--app/graphql/types/user_type.rb14
-rw-r--r--app/helpers/application_helper.rb9
-rw-r--r--app/helpers/application_settings_helper.rb25
-rw-r--r--app/helpers/auth_helper.rb14
-rw-r--r--app/helpers/blob_helper.rb12
-rw-r--r--app/helpers/builds_helper.rb4
-rw-r--r--app/helpers/clusters_helper.rb24
-rw-r--r--app/helpers/dashboard_helper.rb13
-rw-r--r--app/helpers/environments_helper.rb1
-rw-r--r--app/helpers/gitlab_routing_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb5
-rw-r--r--app/helpers/markup_helper.rb13
-rw-r--r--app/helpers/milestones_helper.rb67
-rw-r--r--app/helpers/projects/error_tracking_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/releases_helper.rb3
-rw-r--r--app/helpers/repository_languages_helper.rb2
-rw-r--r--app/helpers/search_helper.rb12
-rw-r--r--app/helpers/services_helper.rb2
-rw-r--r--app/helpers/sessions_helper.rb16
-rw-r--r--app/helpers/snippets_helper.rb81
-rw-r--r--app/helpers/sourcegraph_helper.rb27
-rw-r--r--app/helpers/tab_helper.rb18
-rw-r--r--app/helpers/tree_helper.rb18
-rw-r--r--app/helpers/users_helper.rb9
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/mailers/emails/members.rb37
-rw-r--r--app/mailers/emails/pipelines.rb5
-rw-r--r--app/mailers/emails/releases.rb8
-rw-r--r--app/mailers/previews/notify_preview.rb6
-rw-r--r--app/models/abuse_report.rb4
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb19
-rw-r--r--app/models/application_setting.rb59
-rw-r--r--app/models/application_setting_implementation.rb14
-rw-r--r--app/models/award_emoji.rb5
-rw-r--r--app/models/aws/role.rb6
-rw-r--r--app/models/ci/build.rb57
-rw-r--r--app/models/ci/build_metadata.rb1
-rw-r--r--app/models/ci/pipeline.rb91
-rw-r--r--app/models/clusters/applications/cert_manager.rb2
-rw-r--r--app/models/clusters/applications/crossplane.rb60
-rw-r--r--app/models/clusters/applications/elastic_stack.rb108
-rw-r--r--app/models/clusters/applications/ingress.rb73
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb67
-rw-r--r--app/models/clusters/clusters_hierarchy.rb41
-rw-r--r--app/models/clusters/concerns/application_core.rb18
-rw-r--r--app/models/clusters/instance.rb4
-rw-r--r--app/models/clusters/providers/aws.rb26
-rw-r--r--app/models/clusters/providers/gcp.rb4
-rw-r--r--app/models/commit_status_enums.rb4
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb66
-rw-r--r--app/models/concerns/ci/metadatable.rb4
-rw-r--r--app/models/concerns/ci/processable.rb8
-rw-r--r--app/models/concerns/deployment_platform.rb2
-rw-r--r--app/models/concerns/issuable.rb24
-rw-r--r--app/models/concerns/milestoneish.rb4
-rw-r--r--app/models/concerns/noteable.rb4
-rw-r--r--app/models/concerns/protected_ref.rb4
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb40
-rw-r--r--app/models/concerns/subscribable.rb8
-rw-r--r--app/models/concerns/worker_attributes.rb46
-rw-r--r--app/models/container_repository.rb5
-rw-r--r--app/models/dashboard_group_milestone.rb4
-rw-r--r--app/models/deployment.rb33
-rw-r--r--app/models/deployment_merge_request.rb6
-rw-r--r--app/models/description_version.rb4
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb23
-rw-r--r--app/models/global_milestone.rb6
-rw-r--r--app/models/grafana_integration.rb6
-rw-r--r--app/models/group.rb51
-rw-r--r--app/models/group_group_link.rb23
-rw-r--r--app/models/import_export_upload.rb1
-rw-r--r--app/models/issue.rb24
-rw-r--r--app/models/lfs_object.rb5
-rw-r--r--app/models/merge_request.rb114
-rw-r--r--app/models/merge_request_diff.rb17
-rw-r--r--app/models/milestone.rb3
-rw-r--r--app/models/notification_reason.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb43
-rw-r--r--app/models/project_ci_cd_setting.rb3
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb30
-rw-r--r--app/models/project_services/chat_message/push_message.rb16
-rw-r--r--app/models/project_services/data_fields.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb25
-rw-r--r--app/models/project_snippet.rb7
-rw-r--r--app/models/project_wiki.rb6
-rw-r--r--app/models/prometheus_metric.rb6
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/release.rb13
-rw-r--r--app/models/releases/source.rb4
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/todo.rb7
-rw-r--r--app/models/user.rb12
-rw-r--r--app/models/wiki_page.rb5
-rw-r--r--app/models/zoom_meeting.rb26
-rw-r--r--app/policies/base_policy.rb8
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/policies/personal_snippet_policy.rb4
-rw-r--r--app/policies/project_policy.rb15
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/policies/todo_policy.rb1
-rw-r--r--app/presenters/clusterable_presenter.rb16
-rw-r--r--app/presenters/commit_status_presenter.rb6
-rw-r--r--app/presenters/instance_clusterable_presenter.rb20
-rw-r--r--app/presenters/project_presenter.rb22
-rw-r--r--app/presenters/release_presenter.rb57
-rw-r--r--app/presenters/todo_presenter.rb2
-rw-r--r--app/serializers/cluster_application_entity.rb2
-rw-r--r--app/serializers/container_repositories_serializer.rb4
-rw-r--r--app/serializers/diff_file_base_entity.rb8
-rw-r--r--app/serializers/diff_file_entity.rb20
-rw-r--r--app/serializers/error_tracking/detailed_error_entity.rb27
-rw-r--r--app/serializers/error_tracking/detailed_error_serializer.rb7
-rw-r--r--app/serializers/error_tracking/error_event_entity.rb7
-rw-r--r--app/serializers/error_tracking/error_event_serializer.rb7
-rw-r--r--app/serializers/issuable_sidebar_extras_entity.rb9
-rw-r--r--app/serializers/issue_board_entity.rb1
-rw-r--r--app/serializers/job_artifact_report_entity.rb2
-rw-r--r--app/serializers/merge_request_diff_entity.rb2
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb6
-rw-r--r--app/serializers/note_entity.rb2
-rw-r--r--app/serializers/projects/serverless/service_entity.rb38
-rw-r--r--app/services/base_service.rb12
-rw-r--r--app/services/ci/compare_reports_base_service.rb5
-rw-r--r--app/services/ci/create_pipeline_service.rb7
-rw-r--r--app/services/ci/find_exposed_artifacts_service.rb70
-rw-r--r--app/services/ci/generate_exposed_artifacts_report_service.rb30
-rw-r--r--app/services/ci/register_job_service.rb83
-rw-r--r--app/services/clusters/applications/base_service.rb16
-rw-r--r--app/services/clusters/aws/fetch_credentials_service.rb56
-rw-r--r--app/services/clusters/aws/finalize_creation_service.rb139
-rw-r--r--app/services/clusters/aws/provision_service.rb85
-rw-r--r--app/services/clusters/aws/proxy_service.rb134
-rw-r--r--app/services/clusters/aws/verify_provision_status_service.rb50
-rw-r--r--app/services/clusters/destroy_service.rb34
-rw-r--r--app/services/clusters/kubernetes/create_or_update_service_account_service.rb32
-rw-r--r--app/services/clusters/kubernetes/kubernetes.rb2
-rw-r--r--app/services/clusters/update_service.rb50
-rw-r--r--app/services/cohorts_service.rb2
-rw-r--r--app/services/commits/change_service.rb5
-rw-r--r--app/services/commits/create_service.rb13
-rw-r--r--app/services/concerns/git/logger.rb10
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/deployments/after_create_service.rb9
-rw-r--r--app/services/deployments/link_merge_requests_service.rb66
-rw-r--r--app/services/deployments/update_service.rb17
-rw-r--r--app/services/error_tracking/base_service.rb66
-rw-r--r--app/services/error_tracking/issue_details_service.rb15
-rw-r--r--app/services/error_tracking/issue_latest_event_service.rb15
-rw-r--r--app/services/error_tracking/list_issues_service.rb42
-rw-r--r--app/services/error_tracking/list_projects_service.rb52
-rw-r--r--app/services/groups/group_links/create_service.rb29
-rw-r--r--app/services/groups/group_links/destroy_service.rb26
-rw-r--r--app/services/groups/import_export/export_service.rb71
-rw-r--r--app/services/groups/transfer_service.rb6
-rw-r--r--app/services/groups/update_service.rb5
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/issues/zoom_link_service.rb71
-rw-r--r--app/services/merge_requests/base_service.rb13
-rw-r--r--app/services/merge_requests/build_service.rb8
-rw-r--r--app/services/merge_requests/ff_merge_service.rb14
-rw-r--r--app/services/merge_requests/merge_base_service.rb28
-rw-r--r--app/services/merge_requests/merge_service.rb16
-rw-r--r--app/services/merge_requests/rebase_service.rb6
-rw-r--r--app/services/merge_requests/refresh_service.rb7
-rw-r--r--app/services/merge_requests/squash_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/merge_requests/working_copy_base_service.rb26
-rw-r--r--app/services/metrics/dashboard/custom_metric_embed_service.rb9
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb160
-rw-r--r--app/services/metrics/dashboard/project_dashboard_service.rb3
-rw-r--r--app/services/metrics/dashboard/system_dashboard_service.rb3
-rw-r--r--app/services/notes/create_service.rb15
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb4
-rw-r--r--app/services/preview_markdown_service.rb10
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb36
-rw-r--r--app/services/projects/hashed_storage/base_attachment_service.rb33
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb2
-rw-r--r--app/services/projects/hashed_storage/migrate_attachments_service.rb48
-rw-r--r--app/services/projects/hashed_storage/migration_service.rb12
-rw-r--r--app/services/projects/hashed_storage/rollback_attachments_service.rb7
-rw-r--r--app/services/projects/hashed_storage/rollback_service.rb18
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb26
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/services/quick_actions/target_service.rb2
-rw-r--r--app/services/system_note_service.rb87
-rw-r--r--app/services/system_notes/merge_requests_service.rb145
-rw-r--r--app/services/users/signup_service.rb34
-rw-r--r--app/services/zoom_notes_service.rb42
-rw-r--r--app/validators/same_project_association_validator.rb21
-rw-r--r--app/validators/zoom_url_validator.rb13
-rw-r--r--app/views/admin/abuse_reports/index.html.haml18
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml6
-rw-r--r--app/views/admin/application_settings/_eks.html.haml31
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml4
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml41
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml4
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml11
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml38
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml28
-rw-r--r--app/views/admin/application_settings/integrations.html.haml33
-rw-r--r--app/views/admin/application_settings/network.html.haml2
-rw-r--r--app/views/admin/application_settings/repository.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml37
-rw-r--r--app/views/admin/sessions/_new_base.html.haml2
-rw-r--r--app/views/admin/sessions/_signin_box.html.haml2
-rw-r--r--app/views/admin/sessions/_tabs_normal.html.haml2
-rw-r--r--app/views/admin/sessions/new.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml2
-rw-r--r--app/views/ci/group_variables/_content.html.haml1
-rw-r--r--app/views/ci/group_variables/_header.html.haml5
-rw-r--r--app/views/ci/group_variables/_index.html.haml13
-rw-r--r--app/views/ci/group_variables/_variable_header.html.haml5
-rw-r--r--app/views/ci/variables/_header.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml5
-rw-r--r--app/views/ci/variables/_url_query_variable_row.html.haml28
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml21
-rw-r--r--app/views/clusters/clusters/_banner.html.haml6
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/clusters/clusters/aws/_new.html.haml20
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml6
-rw-r--r--app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml6
-rw-r--r--app/views/clusters/clusters/eks/_index.html.haml2
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml13
-rw-r--r--app/views/clusters/clusters/gcp/_new.html.haml7
-rw-r--r--app/views/clusters/clusters/index.html.haml2
-rw-r--r--app/views/clusters/clusters/new.html.haml33
-rw-r--r--app/views/clusters/clusters/show.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml8
-rw-r--r--app/views/clusters/clusters/user/_header.html.haml2
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml2
-rw-r--r--app/views/devise/sessions/new.html.haml6
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/errors/not_found.html.haml2
-rw-r--r--app/views/groups/issues.html.haml8
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/help/_shortcuts.html.haml2
-rw-r--r--app/views/import/manifest/_form.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/_mailer.html.haml72
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml9
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml27
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml10
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml12
-rw-r--r--app/views/notify/member_access_denied_email.html.haml11
-rw-r--r--app/views/notify/member_access_granted_email.html.haml16
-rw-r--r--app/views/notify/member_access_requested_email.html.haml9
-rw-r--r--app/views/notify/member_invite_accepted_email.html.haml13
-rw-r--r--app/views/notify/member_invite_declined_email.html.haml11
-rw-r--r--app/views/notify/member_invited_email.html.haml27
-rw-r--r--app/views/profiles/preferences/_sourcegraph.html.haml26
-rw-r--r--app/views/profiles/preferences/show.html.haml3
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/_files.html.haml6
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_options_settings.html.haml6
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml13
-rw-r--r--app/views/projects/blob/_markdown_buttons.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml13
-rw-r--r--app/views/projects/buttons/_download_links.html.haml6
-rw-r--r--app/views/projects/compare/_form.html.haml10
-rw-r--r--app/views/projects/deployments/_confirm_rollback_modal.html.haml2
-rw-r--r--app/views/projects/environments/empty_logs.html.haml14
-rw-r--r--app/views/projects/environments/empty_metrics.html.haml (renamed from app/views/projects/environments/empty.html.haml)4
-rw-r--r--app/views/projects/error_tracking/details.html.haml4
-rw-r--r--app/views/projects/graphs/show.html.haml28
-rw-r--r--app/views/projects/issues/_issue.html.haml3
-rw-r--r--app/views/projects/labels/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml10
-rw-r--r--app/views/projects/merge_requests/edit.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml53
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml10
-rw-r--r--app/views/projects/mirrors/_mirror_repos_form.html.haml4
-rw-r--r--app/views/projects/pages/_access.html.haml6
-rw-r--r--app/views/projects/pages/_list.html.haml4
-rw-r--r--app/views/projects/pages/show.html.haml36
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml79
-rw-r--r--app/views/projects/pages_domains/_dns.html.haml33
-rw-r--r--app/views/projects/pages_domains/_form.html.haml71
-rw-r--r--app/views/projects/pages_domains/_lets_encrypt_callout.html.haml13
-rw-r--r--app/views/projects/pages_domains/edit.html.haml15
-rw-r--r--app/views/projects/pages_domains/new.html.haml1
-rw-r--r--app/views/projects/pages_domains/show.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml14
-rw-r--r--app/views/projects/pipelines/new.html.haml7
-rw-r--r--app/views/projects/pipelines/show.html.haml3
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml3
-rw-r--r--app/views/projects/services/_form.html.haml1
-rw-r--r--app/views/projects/services/_index.html.haml2
-rw-r--r--app/views/projects/services/edit.html.haml3
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/projects/settings/operations/_grafana_integration.html.haml2
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml12
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/registrations/welcome.html.haml15
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_results.html.haml1
-rw-r--r--app/views/search/results/_blob.html.haml4
-rw-r--r--app/views/search/results/_blob_data.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml5
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml3
-rw-r--r--app/views/shared/_field.html.haml4
-rw-r--r--app/views/shared/_group_form.html.haml7
-rw-r--r--app/views/shared/_mobile_clone_panel.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml1
-rw-r--r--app/views/shared/_service_settings.html.haml2
-rw-r--r--app/views/shared/form_elements/_description.html.haml3
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml307
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml8
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml31
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml14
-rw-r--r--app/views/shared/members/_access_request_links.html.haml2
-rw-r--r--app/views/shared/milestones/_description.html.haml8
-rw-r--r--app/views/shared/milestones/_header.html.haml38
-rw-r--r--app/views/shared/milestones/_milestone.html.haml14
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml21
-rw-r--r--app/views/shared/milestones/_tabs.html.haml47
-rw-r--r--app/views/shared/milestones/_top.html.haml56
-rw-r--r--app/views/shared/notifications/_button.html.haml6
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml3
-rw-r--r--app/views/shared/notifications/_new_button.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml2
-rw-r--r--app/views/shared/runners/_runner_description.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml3
-rw-r--r--app/views/shared/snippets/_embed.html.haml2
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml5
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/authorized_projects_worker.rb1
-rw-r--r--app/workers/build_finished_worker.rb2
-rw-r--r--app/workers/build_hooks_worker.rb1
-rw-r--r--app/workers/build_queue_worker.rb2
-rw-r--r--app/workers/build_success_worker.rb1
-rw-r--r--app/workers/chat_notification_worker.rb5
-rw-r--r--app/workers/ci/build_schedule_worker.rb1
-rw-r--r--app/workers/cluster_install_app_worker.rb2
-rw-r--r--app/workers/cluster_patch_app_worker.rb2
-rw-r--r--app/workers/cluster_project_configure_worker.rb2
-rw-r--r--app/workers/cluster_provision_worker.rb8
-rw-r--r--app/workers/cluster_upgrade_app_worker.rb2
-rw-r--r--app/workers/cluster_wait_for_app_installation_worker.rb3
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb2
-rw-r--r--app/workers/clusters/applications/uninstall_worker.rb2
-rw-r--r--app/workers/clusters/applications/wait_for_uninstall_app_worker.rb3
-rw-r--r--app/workers/clusters/cleanup/app_worker.rb16
-rw-r--r--app/workers/clusters/cleanup/project_namespace_worker.rb16
-rw-r--r--app/workers/clusters/cleanup/service_account_worker.rb16
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb1
-rw-r--r--app/workers/create_pipeline_worker.rb2
-rw-r--r--app/workers/deployments/finished_worker.rb1
-rw-r--r--app/workers/deployments/success_worker.rb1
-rw-r--r--app/workers/email_receiver_worker.rb1
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/expire_build_artifacts_worker.rb19
-rw-r--r--app/workers/expire_job_cache_worker.rb1
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/gitlab_shell_worker.rb5
-rw-r--r--app/workers/group_export_worker.rb15
-rw-r--r--app/workers/hashed_storage/project_migrate_worker.rb2
-rw-r--r--app/workers/import_issues_csv_worker.rb1
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb1
-rw-r--r--app/workers/merge_worker.rb1
-rw-r--r--app/workers/namespaces/prune_aggregation_schedules_worker.rb1
-rw-r--r--app/workers/new_issue_worker.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/new_note_worker.rb2
-rw-r--r--app/workers/new_release_worker.rb2
-rw-r--r--app/workers/object_pool/join_worker.rb2
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb1
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_metrics_worker.rb2
-rw-r--r--app/workers/pipeline_notification_worker.rb3
-rw-r--r--app/workers/pipeline_process_worker.rb1
-rw-r--r--app/workers/pipeline_schedule_worker.rb1
-rw-r--r--app/workers/pipeline_success_worker.rb1
-rw-r--r--app/workers/pipeline_update_worker.rb1
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/process_commit_worker.rb1
-rw-r--r--app/workers/project_cache_worker.rb3
-rw-r--r--app/workers/project_export_worker.rb1
-rw-r--r--app/workers/project_service_worker.rb1
-rw-r--r--app/workers/reactive_caching_worker.rb8
-rw-r--r--app/workers/remove_expired_group_links_worker.rb4
-rw-r--r--app/workers/remove_expired_members_worker.rb1
-rw-r--r--app/workers/repository_import_worker.rb1
-rw-r--r--app/workers/repository_update_remote_mirror_worker.rb2
-rw-r--r--app/workers/stage_update_worker.rb1
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb15
-rw-r--r--app/workers/stuck_import_jobs_worker.rb1
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb2
-rw-r--r--app/workers/update_merge_requests_worker.rb2
-rw-r--r--app/workers/wait_for_cluster_creation_worker.rb8
-rw-r--r--app/workers/web_hook_worker.rb2
-rwxr-xr-xbin/secpick2
-rw-r--r--changelogs/unreleased/10242-move-old-vulns-api-to-vuln-findings.yml5
-rw-r--r--changelogs/unreleased/11137-propagate-all-env-vars-to-sast-containers.yml5
-rw-r--r--changelogs/unreleased/11930-handle-multiple-entries-dast-report.yml5
-rw-r--r--changelogs/unreleased/12769-milestone-blank-name-causes-issues.yml5
-rw-r--r--changelogs/unreleased/12881-security-dashboard-leveraging-sparklines-to-show-vulnerability-tren.yml5
-rw-r--r--changelogs/unreleased/13055-show-the-sync-information-for-design-repositories.yml5
-rw-r--r--changelogs/unreleased/13135-call-to-validate_query-in-custom-metrics-form-is-not-retried.yml5
-rw-r--r--changelogs/unreleased/13401-close-gitlab-issue-on-recovery-alerts-from-prometheus.yml5
-rw-r--r--changelogs/unreleased/13492-design-comments-width.yml5
-rw-r--r--changelogs/unreleased/13539-license-compliance-approval-required.yml5
-rw-r--r--changelogs/unreleased/14707-add-modsec-logging-sidecar-to-ingress-controller.yml5
-rw-r--r--changelogs/unreleased/14770-ignore-deprecated-column-on-projects.yml5
-rw-r--r--changelogs/unreleased/18126-change-tag-url-for-tag-push-events-in-chat-msg-integration.yaml5
-rw-r--r--changelogs/unreleased/18797-support-for-crossplane-as-a-managed-app.yml5
-rw-r--r--changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml5
-rw-r--r--changelogs/unreleased/19054-upgrade-helm.yml5
-rw-r--r--changelogs/unreleased/19445-native-group-milestone-page-needs-same-info-as-project-milestones.yml5
-rw-r--r--changelogs/unreleased/20081-add-mb-2-class-to-global-alerts.yml5
-rw-r--r--changelogs/unreleased/22392-add-eks-clusters-to-usage-data.yml5
-rw-r--r--changelogs/unreleased/22392-capture-aws-role-details.yml5
-rw-r--r--changelogs/unreleased/22392-eks-create-cluster-fe.yml5
-rw-r--r--changelogs/unreleased/24082-double-escaping-in-tableflip-quick-action.yml5
-rw-r--r--changelogs/unreleased/24146-query-string-params.yml5
-rw-r--r--changelogs/unreleased/24172-group-vars.yml5
-rw-r--r--changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml6
-rw-r--r--changelogs/unreleased/25188-unable-to-expand-collapse-files-in-merge-request-by-clicking-caret.yml5
-rw-r--r--changelogs/unreleased/26138-replace-raven-js-with-sentry-browser.yml5
-rw-r--r--changelogs/unreleased/26207-issue-board-loading-infinite-if-closing-the-closed-row.yml5
-rw-r--r--changelogs/unreleased/26380-personal-snippets.yml5
-rw-r--r--changelogs/unreleased/27034-new-branch-length.yml5
-rw-r--r--changelogs/unreleased/27433-fix-vulnerable-code-copied-from-devise.yml5
-rw-r--r--changelogs/unreleased/27464-misleading-input-controller-for-dates.yml5
-rw-r--r--changelogs/unreleased/28258-fix-project-clone-dropdrown-button-width.yml5
-rw-r--r--changelogs/unreleased/28302-move-add-license-button.yml5
-rw-r--r--changelogs/unreleased/28336-dropdown-icon-missing-on-compare-page.yml5
-rw-r--r--changelogs/unreleased/28338-different-dropdown-styles-on-settings-page.yml5
-rw-r--r--changelogs/unreleased/28350-manifest-error-file-attach.yml5
-rw-r--r--changelogs/unreleased/28801-fix-canary-inconsistency.yml5
-rw-r--r--changelogs/unreleased/28985-links-to-notes-in-collapsed-discussions-dont-work.yml5
-rw-r--r--changelogs/unreleased/29121-rename-trace.yml5
-rw-r--r--changelogs/unreleased/29451-webide-currentsha.yml5
-rw-r--r--changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml5
-rw-r--r--changelogs/unreleased/29986-remove-leaky-401-responses.yml5
-rw-r--r--changelogs/unreleased/30131-breadcrumb-back-to-integrations-not-correct.yml5
-rw-r--r--changelogs/unreleased/30161-expose-merge-request-status-in-api.yml5
-rw-r--r--changelogs/unreleased/30182-checkbox-with-no-text-causing-following-checkboxes-not-to-be-saved.yml5
-rw-r--r--changelogs/unreleased/30229-gitlab-backgroundmigration-pruneorphanedgeoevents-did-you-mean-prun.yml5
-rw-r--r--changelogs/unreleased/30533-asciidoc-image-link-lazy-loading.yml5
-rw-r--r--changelogs/unreleased/30616-improve-merge-request-description-placeholder.yml5
-rw-r--r--changelogs/unreleased/30660-allow-to-enable-disable-auto-ssl-letsencrypt-support-via-api.yml5
-rw-r--r--changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml5
-rw-r--r--changelogs/unreleased/30810-blob-view-buttons.yml5
-rw-r--r--changelogs/unreleased/31134-add-usage-ping-data-for-project-services.yml5
-rw-r--r--changelogs/unreleased/31184-refactor-disabled-sidebar-notification-to-vue.yml5
-rw-r--r--changelogs/unreleased/31222-follow-up-from-adjusts-snowplow-to-use-cookies-for-sessions.yml5
-rw-r--r--changelogs/unreleased/31309-add-ability-for-users-to-organize-projects-on-the-operations-dashbo.yml5
-rw-r--r--changelogs/unreleased/31364-error-when-attempting-to-view-gitlab-com-group-billing-page.yml5
-rw-r--r--changelogs/unreleased/31390-remove-pointer-cursor-from-memory-usage-chart.yml5
-rw-r--r--changelogs/unreleased/31411-add-loading-indicator-when-connecting-to-error-tracking-server.yml5
-rw-r--r--changelogs/unreleased/31658-add-rollback-dialog-environment.yml5
-rw-r--r--changelogs/unreleased/31843-contextual-documentation-to-help-users-download-npm-packages.yml5
-rw-r--r--changelogs/unreleased/31868-make-name-optional-parameter-of-release-entity.yml5
-rw-r--r--changelogs/unreleased/31912-epic-labels.yml5
-rw-r--r--changelogs/unreleased/31914-graphql-todos-mark-todo-as-done-pd.yml5
-rw-r--r--changelogs/unreleased/31919-graphql-MR-assignee-mutation.yml5
-rw-r--r--changelogs/unreleased/31919-graphql-MR-label-mutation.yml5
-rw-r--r--changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml5
-rw-r--r--changelogs/unreleased/31964-make-snippet-list-easier-to-scan.yml5
-rw-r--r--changelogs/unreleased/32052-add-pa-start-date-limit.yml5
-rw-r--r--changelogs/unreleased/32198-banner-alerting-of-project-move-is-showing-up-everywhere.yml5
-rw-r--r--changelogs/unreleased/32358-add-modsec-feature-flag-to-usage-ping.yml5
-rw-r--r--changelogs/unreleased/32367-hashed-storage-migration-handle-failed-attachment-migrations-wth-ex.yml6
-rw-r--r--changelogs/unreleased/32398-geo-rake-gitlab-geo-check-on-the-primary-is-cluttered.yml5
-rw-r--r--changelogs/unreleased/32419-growth-conversion-experiment-test-new-admin-upgrade-design-copy-for.yml5
-rw-r--r--changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml5
-rw-r--r--changelogs/unreleased/32464-backend-sentry-error-details.yml5
-rw-r--r--changelogs/unreleased/32464-detail-view-of-sentry-error.yml5
-rw-r--r--changelogs/unreleased/32534-gitlab-rake-gitlab-cleanup-orphan_job_artifact_files-dry_run-false-is-not-removing-artifacts.yml5
-rw-r--r--changelogs/unreleased/32562-dynamic-db-pool-size-in-puma.yml5
-rw-r--r--changelogs/unreleased/32685-remove-epics-tree-feature-flag.yml5
-rw-r--r--changelogs/unreleased/32935-preventing-accidental-project-deletion-db-changes.yml5
-rw-r--r--changelogs/unreleased/32935-preventing-accidental-project-deletion-index.yml5
-rw-r--r--changelogs/unreleased/32951-secure-modal-mobile-issue.yml5
-rw-r--r--changelogs/unreleased/32962-update-gcp-credit-url.yml5
-rw-r--r--changelogs/unreleased/33042-persist-zoom-meetings-added-to-issues-in-the-database-2.yml5
-rw-r--r--changelogs/unreleased/33054-share_groups_with_groups.yml5
-rw-r--r--changelogs/unreleased/33101-add-deployments-api-updated-after-param.yml5
-rw-r--r--changelogs/unreleased/33121-refactor-user-counts.yml5
-rw-r--r--changelogs/unreleased/33182-fix-productivity-analytics-multiple-labels-bug.yml5
-rw-r--r--changelogs/unreleased/33268-updated-operations-metrics-charts-do-not-load-properly.yml5
-rw-r--r--changelogs/unreleased/33306-missing-field-discussions.yml5
-rw-r--r--changelogs/unreleased/33460-webide-line-endings.yml5
-rw-r--r--changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml6
-rw-r--r--changelogs/unreleased/33557-make-pages-settings-e-g-enablement-access-control-more-visible.yml5
-rw-r--r--changelogs/unreleased/33672-add-enable-checkbox-for-grafana-authentication-settings.yml5
-rw-r--r--changelogs/unreleased/33805-add_serverless_framework_template.yml5
-rw-r--r--changelogs/unreleased/33896-security-dashboard-projects.yml5
-rw-r--r--changelogs/unreleased/33897-make-arrow-buttons-work-within-search-box.yml5
-rw-r--r--changelogs/unreleased/33902-fix-private-group-todo-mentions.yml5
-rw-r--r--changelogs/unreleased/34078-extend-audit-events-api-for-gitlab-com.yml5
-rw-r--r--changelogs/unreleased/34132-graphql-epic-subscribtions.yml5
-rw-r--r--changelogs/unreleased/34149-hide-delete-selected-in-designs-when-viewing-an-old-version.yml5
-rw-r--r--changelogs/unreleased/34230-fix-popover-image.yml5
-rw-r--r--changelogs/unreleased/34258-embedding-sentry-stacktrace.yml5
-rw-r--r--changelogs/unreleased/34299-enable-color-chip-asciidoc.yml5
-rw-r--r--changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml5
-rw-r--r--changelogs/unreleased/34335-move-subscribed-field-to-issue-type.yml5
-rw-r--r--changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml5
-rw-r--r--changelogs/unreleased/34416-subscribed-notification-header.yml5
-rw-r--r--changelogs/unreleased/34423-user-popover-immediately-closed-when-hovering-over-certain-areas.yml5
-rw-r--r--changelogs/unreleased/34426-use-new-list-task-icon-in-text-editor.yml6
-rw-r--r--changelogs/unreleased/34431-add-report-type-vulnerabilities.yml5
-rw-r--r--changelogs/unreleased/34443-fix-template-bug.yml5
-rw-r--r--changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml5
-rw-r--r--changelogs/unreleased/34564-vulnerability-issue-links.yml5
-rw-r--r--changelogs/unreleased/34577-add-dep-scanner-var-maven.yml5
-rw-r--r--changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml5
-rw-r--r--changelogs/unreleased/34607-Remove-IIFEs-from-branch_graph-js.yml5
-rw-r--r--changelogs/unreleased/34610-Remove-IIFEs-from-new_branch_form-js.yml5
-rw-r--r--changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml5
-rw-r--r--changelogs/unreleased/34717-update-expired-trial-copy.yml5
-rw-r--r--changelogs/unreleased/34755-refactor-getdateinpast-to-return-date-object.yml5
-rw-r--r--changelogs/unreleased/34757-bugfix-graphql-missing-todo-types.yml5
-rw-r--r--changelogs/unreleased/34770-error-500-for-api-v4-projects-id-services-jira-nomethoderror-undefi.yml5
-rw-r--r--changelogs/unreleased/34779-editing-metric-dashboard-using-yml-file.yml5
-rw-r--r--changelogs/unreleased/34827-delete-container-registry-tag-undefined-method-success-for-nil.yml5
-rw-r--r--changelogs/unreleased/34850-fix-graphql-todo-ids.yml5
-rw-r--r--changelogs/unreleased/34855-dependency-list-is-not-up-to-date-frontend.yml5
-rw-r--r--changelogs/unreleased/34944-fix-kubernetes-help-text-link.yml5
-rw-r--r--changelogs/unreleased/35217-add_fields_to_all_dashboards.yml5
-rw-r--r--changelogs/unreleased/35440-Hide-start-trial-buttons-for-expired-namespaces.yml5
-rw-r--r--changelogs/unreleased/35440-for-com-namespaces-with-expired-trials-remove-start-trial-ctas.yml5
-rw-r--r--changelogs/unreleased/35528-add-ui-event-tracking-for-container-registry.yml5
-rw-r--r--changelogs/unreleased/35534-broken-scroll-to-bottom.yml5
-rw-r--r--changelogs/unreleased/35537-button-regression-fix.yml5
-rw-r--r--changelogs/unreleased/35547-add-documentation-for-sign-in-application-setting.yml5
-rw-r--r--changelogs/unreleased/35618-add-clipboard-button-to-package-registry-information.yml5
-rw-r--r--changelogs/unreleased/35637-add-start-a-trial-option-in-top-right-drop-down.yml5
-rw-r--r--changelogs/unreleased/35709-update-squash-commit-sha-only-on-successful-merge.yml5
-rw-r--r--changelogs/unreleased/35731-fix-snippets-with-emoji-import.yml5
-rw-r--r--changelogs/unreleased/35749-improve-performance-of-lfs_object-queries.yml5
-rw-r--r--changelogs/unreleased/35751-new-trial-flow-for-logged-in-user-should-not-take-them-to-the-custo.yml5
-rw-r--r--changelogs/unreleased/35908-embedded-videos-not-scaled-correctly.yml5
-rw-r--r--changelogs/unreleased/36113-visual-design-of-edit-and-web-ide-button-in-blob-view.yml5
-rw-r--r--changelogs/unreleased/36141-update-start-a-trial-option-in-top-right-drop-down-to-include-gold.yml5
-rw-r--r--changelogs/unreleased/36142-update-saas-trial-header-to-include-the-tier-gold.yml5
-rw-r--r--changelogs/unreleased/36213-update-codequality-to-12-5.yml5
-rw-r--r--changelogs/unreleased/36262-monitor-cluster-health-charts-does-not-load.yml5
-rw-r--r--changelogs/unreleased/36453-flashcontainer-causing-page-header-to-be-off-center.yml5
-rw-r--r--changelogs/unreleased/36460-metrics-dashboard-fails-to-load-script-doesn-t-stop.yml5
-rw-r--r--changelogs/unreleased/48-add-company-question-to-profile-information.yml5
-rw-r--r--changelogs/unreleased/5366-display-anomaly-deviation-boundaries-on-dashboard-ce.yml5
-rw-r--r--changelogs/unreleased/63778-graphql-add-issue-due-date-sortring.yml5
-rw-r--r--changelogs/unreleased/7104-show-timeframe-dates-in-epics-list.yml5
-rw-r--r--changelogs/unreleased/7816-commits-diff-total-api.yml5
-rw-r--r--changelogs/unreleased/8199-epic-quick-actions-preview.yml5
-rw-r--r--changelogs/unreleased/8558-bump-ado-image-for-modsec-secruleengine.yml5
-rw-r--r--changelogs/unreleased/8558-use-custom-modsecurity-ingress-config.yml5
-rw-r--r--changelogs/unreleased/CauhxMilloy-gitlab-addStartEndMarkersForTagsSearch.yml5
-rw-r--r--changelogs/unreleased/Remove-IIFEs-from-image_file-js.yml5
-rw-r--r--changelogs/unreleased/Remove-IIFEs-from-network-js.yml5
-rw-r--r--changelogs/unreleased/Update-boards-components-board_form-vue.yml5
-rw-r--r--changelogs/unreleased/Update-boards-components-models-index-vue.yml5
-rw-r--r--changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml5
-rw-r--r--changelogs/unreleased/ab-projects-api-indexes-authenticated.yml5
-rw-r--r--changelogs/unreleased/ab-projects-api-indexes.yml5
-rw-r--r--changelogs/unreleased/ab-projects-id-filter.yml5
-rw-r--r--changelogs/unreleased/add-dead-jobs-to-api-sidekiq-metrics.yml5
-rw-r--r--changelogs/unreleased/add-default-plan.yml5
-rw-r--r--changelogs/unreleased/add-inheritable-mixin.yml5
-rw-r--r--changelogs/unreleased/add-missing-bottom-padding-in-settings.yml5
-rw-r--r--changelogs/unreleased/add-pendo-snippet.yml5
-rw-r--r--changelogs/unreleased/add-rails-parser-for-new-cs-report-format.yml5
-rw-r--r--changelogs/unreleased/add-slack-slash-command-issue-comment.yml5
-rw-r--r--changelogs/unreleased/add-snowplow-iglu-registry-application-setting.yml5
-rw-r--r--changelogs/unreleased/ak-add-elastic-cluster-app.yml4
-rw-r--r--changelogs/unreleased/ak-fix-undefined-value.yml5
-rw-r--r--changelogs/unreleased/allow-adding-requests-to-performance-bar-manually.yml5
-rw-r--r--changelogs/unreleased/allow-container-scanning-to-run-offline.yml5
-rw-r--r--changelogs/unreleased/an-mark-jobs-as-latency-sensitive.yml5
-rw-r--r--changelogs/unreleased/an-sidekiq-records-failure-durations.yml5
-rw-r--r--changelogs/unreleased/andr3-fix-designmanagement-upload-limit.yml5
-rw-r--r--changelogs/unreleased/auto-deploy-image-0-7-0.yml5
-rw-r--r--changelogs/unreleased/backstage-remove-export-designs-feature-flag.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-make-sidekiq-testing-fake-default.yml5
-rw-r--r--changelogs/unreleased/bug-fix-27164-Image-cannot-be-collapsed-on-merge-request-changes-tab.yml5
-rw-r--r--changelogs/unreleased/bvl-robust-gpg-homedir-cleanup.yml5
-rw-r--r--changelogs/unreleased/change-default-factor-on-merge-train.yml5
-rw-r--r--changelogs/unreleased/chore-slugify-duplication-removal.yml5
-rw-r--r--changelogs/unreleased/cluster_management_projects_updating.yml5
-rw-r--r--changelogs/unreleased/consider-location-fingerprint-in-mr-widget.yml5
-rw-r--r--changelogs/unreleased/defect-diff-file-size.yml5
-rw-r--r--changelogs/unreleased/deployment-commit-tracking.yml5
-rw-r--r--changelogs/unreleased/do-not-abort-merge-trains-on-ff.yml5
-rw-r--r--changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml5
-rw-r--r--changelogs/unreleased/dz-abuse-reports-filter.yml5
-rw-r--r--changelogs/unreleased/dz-add-indices-to-abuse-reports.yml5
-rw-r--r--changelogs/unreleased/dz-fix-clusters-api-doc-2.yml5
-rw-r--r--changelogs/unreleased/dz-improve-admin-features.yml5
-rw-r--r--changelogs/unreleased/dz-move-project-routes.yml5
-rw-r--r--changelogs/unreleased/enable-environment-dashboard-on-prod.yml5
-rw-r--r--changelogs/unreleased/enable-group-events.yml5
-rw-r--r--changelogs/unreleased/environments-dashboard-ux-tweaks.yml5
-rw-r--r--changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml5
-rw-r--r--changelogs/unreleased/fe-cluster-management-project.yml5
-rw-r--r--changelogs/unreleased/feat-unify-html-email-layouts.yml5
-rw-r--r--changelogs/unreleased/feature-cluster-cleanup-state-machine.yml5
-rw-r--r--changelogs/unreleased/feature-reduce-cluster-ip-size.yml5
-rw-r--r--changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml5
-rw-r--r--changelogs/unreleased/fix-dropzone-no-element-exception.yml5
-rw-r--r--changelogs/unreleased/fix-job-log-style-reset.yml5
-rw-r--r--changelogs/unreleased/fix-merge-train-is-not-refreshed-when-aborted.yml5
-rw-r--r--changelogs/unreleased/fix-stuck-ci-jobs-worker.yml5
-rw-r--r--changelogs/unreleased/fix_group_container_repositories_n_1.yml5
-rw-r--r--changelogs/unreleased/fj-24837-codesanbox-usage-ping.yml5
-rw-r--r--changelogs/unreleased/fj-32057-web-ide-preview-markdown-image-bug.yml5
-rw-r--r--changelogs/unreleased/gdk-672-bump-puma-killer-limits-for-dev.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-fix-group-export-descendants.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-group-structure-export-api-endp.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.71.0.yml5
-rw-r--r--changelogs/unreleased/gitlab_ci_path.yml5
-rw-r--r--changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml5
-rw-r--r--changelogs/unreleased/harishsr-emoticon-commit-links.yml5
-rw-r--r--changelogs/unreleased/helm-v2-16-1.yml5
-rw-r--r--changelogs/unreleased/hide-projects-when-admin-mode-is-disabled.yml5
-rw-r--r--changelogs/unreleased/id-avoid-preloading-merge-request-commits.yml5
-rw-r--r--changelogs/unreleased/id-conditional-check-mergeability.yml5
-rw-r--r--changelogs/unreleased/id-fix-any-approver-rule-for-projects.yml5
-rw-r--r--changelogs/unreleased/id-nil-short-commit-sha.yml5
-rw-r--r--changelogs/unreleased/id-optimize-mergeable-discussions-state.yml5
-rw-r--r--changelogs/unreleased/increase-certmanager-install-timeout.yml6
-rw-r--r--changelogs/unreleased/infinite-scroll.yml5
-rw-r--r--changelogs/unreleased/introduce-feature-flag-api-enable-disable.yml5
-rw-r--r--changelogs/unreleased/issue-63160.yml5
-rw-r--r--changelogs/unreleased/jc-add-internal-socket-dir-to-setup.yml5
-rw-r--r--changelogs/unreleased/jc-dont-render-commit-links.yml5
-rw-r--r--changelogs/unreleased/jc-dont-try-to-movedirs-unless-legacy.yml5
-rw-r--r--changelogs/unreleased/jej-group-saml-test-button-shows-response.yml5
-rw-r--r--changelogs/unreleased/jej-prevent-ldap-sign-in.yml5
-rw-r--r--changelogs/unreleased/jh_flash_messages_styling_22992.yml5
-rw-r--r--changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml5
-rw-r--r--changelogs/unreleased/jj-ramirez-master-patch-71805.yml5
-rw-r--r--changelogs/unreleased/jlenny-master-patch-21737.yml5
-rw-r--r--changelogs/unreleased/job-log-support-mac-line-break.yml5
-rw-r--r--changelogs/unreleased/jramsay-admin-mirror-helptext.yml5
-rw-r--r--changelogs/unreleased/jramsay-configure-default-branch-deletion.yml5
-rw-r--r--changelogs/unreleased/lm-grafana-auth-checkbox.yml5
-rw-r--r--changelogs/unreleased/lm-search-list-of-sentry-errors.yml5
-rw-r--r--changelogs/unreleased/maintenance-move-branch-selector-to-top.yml6
-rw-r--r--changelogs/unreleased/make-register-job-service-to-be-resillient.yml5
-rw-r--r--changelogs/unreleased/make_cluster_management_project_ff_default_enabled.yml5
-rw-r--r--changelogs/unreleased/mc-bug-omips-not-blocking-on-skipped-pipelines.yml5
-rw-r--r--changelogs/unreleased/mc-feature-flatten-ci-scripts.yml5
-rw-r--r--changelogs/unreleased/mermaid-update.yml5
-rw-r--r--changelogs/unreleased/most-affected-projects.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-release-links-to-milestone-detail-page.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-release-to-milestone-list-page.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-releases-filter-for-issues.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-releases-filter-for-merge-requests.yml5
-rw-r--r--changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml5
-rw-r--r--changelogs/unreleased/nfriend-fix-edit_url-property.yml5
-rw-r--r--changelogs/unreleased/nfriend-move-release-data-into-footer.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-confirm-email-before-ci-usage.yml5
-rw-r--r--changelogs/unreleased/osw-remove-n-plus-ones-from-branches-api-call.yml5
-rw-r--r--changelogs/unreleased/patch-35.yml5
-rw-r--r--changelogs/unreleased/ph-fixAdminGeoSidebarFlyOut.yml5
-rw-r--r--changelogs/unreleased/ph-fixProtectedBranchesFlash.yml5
-rw-r--r--changelogs/unreleased/pokstad1-gitaly-1-70-0.yml5
-rw-r--r--changelogs/unreleased/record-sidekiq-queuing-latency.yml5
-rw-r--r--changelogs/unreleased/remove-dind-for-ds.yml5
-rw-r--r--changelogs/unreleased/remove-domain-details.yml5
-rw-r--r--changelogs/unreleased/remove-empty-github-service-templates.yml5
-rw-r--r--changelogs/unreleased/remove_local_qualifier_from_geo_sync_indicators.yml5
-rw-r--r--changelogs/unreleased/remove_unused_image_screenshot.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_bootstrap_jquery_spec_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_issue_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_labels_select_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_line_highlighter_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_merge_request_tabs_spec_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_new_branch_Form_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_new_commit_form_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_preview_markdown_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_project_select_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_syntax_highlight_spec_js.yml5
-rw-r--r--changelogs/unreleased/remove_var_from_tree_js.yml5
-rw-r--r--changelogs/unreleased/render-html-tags-in-job-log.yml5
-rw-r--r--changelogs/unreleased/retrigger-license-compliance.yml5
-rw-r--r--changelogs/unreleased/rs-change-service-failure-reasons.yml5
-rw-r--r--changelogs/unreleased/security-2914-labels-visible-despite-no-access-to-issues-repositories.yml5
-rw-r--r--changelogs/unreleased/security-2920-fix-notes-with-label-cross-reference.yml5
-rw-r--r--changelogs/unreleased/security-64519-nested-graphql-query-can-cause-denial-of-service.yml5
-rw-r--r--changelogs/unreleased/security-65756-ex-admin-attacker-can-comment-in-internal.yml5
-rw-r--r--changelogs/unreleased/security-bvl-validate-force-remove-branch-on-mrs.yml6
-rw-r--r--changelogs/unreleased/security-developer-transfer-project.yml5
-rw-r--r--changelogs/unreleased/security-hide-private-members-in-project-member-autocomplete.yml3
-rw-r--r--changelogs/unreleased/security-id-fix-disclosure-of-private-repo-names.yml5
-rw-r--r--changelogs/unreleased/security-mask-sentry-token-ce.yml4
-rw-r--r--changelogs/unreleased/security-remove-deploy-access-levels-on-project-group-link-deletion.yml5
-rw-r--r--changelogs/unreleased/security-stored-xss-using-find-file.yml5
-rw-r--r--changelogs/unreleased/security-wiki-rdoc-content.yml5
-rw-r--r--changelogs/unreleased/sh-add-api-exception-to-logs.yml5
-rw-r--r--changelogs/unreleased/sh-add-exception-backtrace-production-log.yml5
-rw-r--r--changelogs/unreleased/sh-bitbucket-importer-handle-superseded.yml5
-rw-r--r--changelogs/unreleased/sh-ensure-short-ttl-sessions.yml5
-rw-r--r--changelogs/unreleased/sh-fix-bitbucket-importer-pr-state.yml5
-rw-r--r--changelogs/unreleased/sh-fix-protected-paths.yml5
-rw-r--r--changelogs/unreleased/sh-gitaly-duration-measurement.yml5
-rw-r--r--changelogs/unreleased/sh-guard-repository-mirrors-read-only.yml5
-rw-r--r--changelogs/unreleased/sh-set-admin-default-visibilities.yml5
-rw-r--r--changelogs/unreleased/sh-set-httponly-experimentation-subject-id.yml5
-rw-r--r--changelogs/unreleased/sh-support-project-template-id-in-api.yml5
-rw-r--r--changelogs/unreleased/sh-time-limit-merge-rebase-lock.yml5
-rw-r--r--changelogs/unreleased/sh-update-aws-sdk.yml5
-rw-r--r--changelogs/unreleased/sh-update-openid-connect.yml5
-rw-r--r--changelogs/unreleased/sh-upgrade-grpc.yml5
-rw-r--r--changelogs/unreleased/sh-use-rails-redis-store.yml5
-rw-r--r--changelogs/unreleased/sh-use-template-project-id.yml5
-rw-r--r--changelogs/unreleased/show-prometheus-is-updating.yml5
-rw-r--r--changelogs/unreleased/sort-vulnerabilities-for-pipeline-dashboard.yml5
-rw-r--r--changelogs/unreleased/stein_ma-gitlab-patch-32.yml5
-rw-r--r--changelogs/unreleased/tr-remove-grafana-ff.yml5
-rw-r--r--changelogs/unreleased/tracking-experimental-signup-flow.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-0.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-1.yml5
-rw-r--r--changelogs/unreleased/update-pages-1-12.yml5
-rw-r--r--changelogs/unreleased/visual-review-api.yml5
-rw-r--r--changelogs/unreleased/zj-pg-connection-ff-gitaly.yml5
-rw-r--r--config/application.rb11
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/gitlab.yml.example24
-rw-r--r--config/initializers/0_inflections.rb17
-rw-r--r--config/initializers/1_settings.rb15
-rw-r--r--config/initializers/7_prometheus_metrics.rb11
-rw-r--r--config/initializers/database_config.rb18
-rw-r--r--config/initializers/health_check.rb12
-rw-r--r--config/initializers/lograge.rb19
-rw-r--r--config/initializers/rack_attack_git_basic_auth.rb22
-rw-r--r--config/initializers/rack_attack_logging.rb6
-rw-r--r--config/initializers/rack_attack_new.rb62
-rw-r--r--config/initializers/validate_puma.rb5
-rw-r--r--config/locales/en.yml1
-rw-r--r--config/puma.example.development.rb10
-rw-r--r--config/routes.rb9
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/routes/project.rb83
-rw-r--r--config/routes/user.rb3
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--config/webpack.config.js7
-rw-r--r--danger/commit_messages/Dangerfile10
-rw-r--r--db/fixtures/development/02_users.rb77
-rw-r--r--db/fixtures/development/03_project.rb297
-rw-r--r--db/fixtures/development/04_labels.rb2
-rw-r--r--db/fixtures/development/05_users.rb34
-rw-r--r--db/fixtures/development/06_teams.rb6
-rw-r--r--db/fixtures/development/07_milestones.rb2
-rw-r--r--db/fixtures/development/10_merge_requests.rb8
-rw-r--r--db/fixtures/development/11_keys.rb2
-rw-r--r--db/fixtures/development/12_snippets.rb2
-rw-r--r--db/fixtures/development/14_pipelines.rb2
-rw-r--r--db/fixtures/development/16_protected_branches.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/fixtures/development/19_environments.rb2
-rw-r--r--db/fixtures/development/23_spam_logs.rb2
-rw-r--r--db/fixtures/development/24_forks.rb4
-rw-r--r--db/migrate/20180215181245_users_name_lower_index.rb6
-rw-r--r--db/migrate/20180504195842_project_name_lower_index.rb6
-rw-r--r--db/migrate/20180902070406_create_group_group_links.rb32
-rw-r--r--db/migrate/20190703171157_add_sourcing_epic_dates.rb10
-rw-r--r--db/migrate/20190703171555_add_sourcing_epic_dates_fks.rb25
-rw-r--r--db/migrate/20190805140353_remove_rendundant_index_from_releases.rb7
-rw-r--r--db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb21
-rw-r--r--db/migrate/20190910211526_create_packages_conan_file_metadata.rb18
-rw-r--r--db/migrate/20190918104731_add_cleanup_status_to_cluster.rb21
-rw-r--r--db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb12
-rw-r--r--db/migrate/20190930153535_create_zoom_meetings.rb24
-rw-r--r--db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb22
-rw-r--r--db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb1
-rw-r--r--db/migrate/20191003161031_add_mark_for_deletion_to_projects.rb11
-rw-r--r--db/migrate/20191003161032_add_mark_for_deletion_indexes_to_projects.rb19
-rw-r--r--db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb15
-rw-r--r--db/migrate/20191003195620_add_pendo_url_to_application_settings.rb9
-rw-r--r--db/migrate/20191004080818_add_productivity_analytics_start_date.rb11
-rw-r--r--db/migrate/20191004081520_fill_productivity_analytics_start_date.rb31
-rw-r--r--db/migrate/20191009100244_add_geo_design_repository_counters.rb16
-rw-r--r--db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb13
-rw-r--r--db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb17
-rw-r--r--db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb9
-rw-r--r--db/migrate/20191011084019_add_project_deletion_adjourned_period_to_application_settings.rb11
-rw-r--r--db/migrate/20191013100213_add_squash_commit_sha_to_merge_requests.rb9
-rw-r--r--db/migrate/20191014025629_rename_design_management_version_user_to_author.rb17
-rw-r--r--db/migrate/20191014030730_add_author_index_to_design_management_versions.rb17
-rw-r--r--db/migrate/20191016133352_create_ci_subscriptions_projects.rb21
-rw-r--r--db/migrate/20191017001326_create_users_security_dashboard_projects.rb15
-rw-r--r--db/migrate/20191017094449_add_remove_source_branch_after_merge_to_projects.rb17
-rw-r--r--db/migrate/20191017134513_add_deployment_merge_requests.rb33
-rw-r--r--db/migrate/20191017191341_create_clusters_applications_crossplane.rb19
-rw-r--r--db/migrate/20191023132005_add_merge_requests_index_on_target_project_and_branch.rb19
-rw-r--r--db/migrate/20191023152913_add_default_and_free_plans.rb29
-rw-r--r--db/migrate/20191024134020_add_index_to_zoom_meetings.rb17
-rw-r--r--db/migrate/20191026124116_set_application_settings_default_project_and_snippet_visibility.rb15
-rw-r--r--db/migrate/20191028162543_add_setup_for_company_to_user_preferences.rb9
-rw-r--r--db/migrate/20191028184740_rename_snowplow_site_id_to_snowplow_app_id.rb17
-rw-r--r--db/migrate/20191029125305_create_packages_conan_metadata.rb16
-rw-r--r--db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb23
-rw-r--r--db/migrate/20191030135044_create_plan_limits.rb14
-rw-r--r--db/migrate/20191030152934_move_limits_from_plans.rb17
-rw-r--r--db/migrate/20191101092917_replace_index_on_metrics_merged_at.rb19
-rw-r--r--db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb14
-rw-r--r--db/migrate/20191104205020_add_license_details_to_application_settings.rb11
-rw-r--r--db/migrate/20191105094558_add_report_type_to_vulnerabilities.rb9
-rw-r--r--db/migrate/20191105193652_add_index_on_deployments_updated_at.rb18
-rw-r--r--db/migrate/20191107173446_add_sourcegraph_admin_and_user_preferences.rb17
-rw-r--r--db/migrate/20191107220314_add_index_to_projects_on_marked_for_deletion.rb17
-rw-r--r--db/migrate/20191111115229_add_group_id_to_import_export_uploads.rb9
-rw-r--r--db/migrate/20191111115431_add_group_fk_to_import_export_uploads.rb19
-rw-r--r--db/migrate/20191111121500_default_ci_config_path.rb13
-rw-r--r--db/migrate/20191112115247_add_cached_markdown_version_to_vulnerabilities.rb9
-rw-r--r--db/migrate/20191112214305_add_indexes_for_projects_api_default_params.rb19
-rw-r--r--db/migrate/20191112221821_add_indexes_for_projects_api_default_params_authenticated.rb19
-rw-r--r--db/migrate/20191112232338_ensure_no_empty_milestone_titles.rb18
-rw-r--r--db/migrate/20191114173508_add_resolved_attributes_to_vulnerabilities.rb15
-rw-r--r--db/migrate/20191114173602_add_foreign_key_on_resolved_by_id_to_vulnerabilities.rb19
-rw-r--r--db/migrate/20191115091425_create_vulnerability_issue_links.rb23
-rw-r--r--db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb68
-rw-r--r--db/post_migrate/20190918104222_schedule_productivity_analytics_backfill.rb2
-rw-r--r--db/post_migrate/20190926180443_schedule_epic_issues_after_epics_move.rb35
-rw-r--r--db/post_migrate/20191008143850_fix_any_approver_rule_for_projects.rb51
-rw-r--r--db/post_migrate/20191014030134_cleanup_design_management_version_user_to_author_rename.rb17
-rw-r--r--db/post_migrate/20191017045817_schedule_fix_gitlab_com_pages_access_level.rb17
-rw-r--r--db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb19
-rw-r--r--db/post_migrate/20191021101942_remove_empty_github_service_templates.rb28
-rw-r--r--db/post_migrate/20191022113635_nullify_feature_flag_plaintext_tokens.rb39
-rw-r--r--db/post_migrate/20191029095537_cleanup_application_settings_snowplow_site_id_rename.rb17
-rw-r--r--db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb19
-rw-r--r--db/post_migrate/20191031112603_remove_limits_from_plans.rb17
-rw-r--r--db/post_migrate/20191105094625_set_report_type_for_vulnerabilities.rb26
-rw-r--r--db/post_migrate/20191105140942_add_indices_to_abuse_reports.rb17
-rw-r--r--db/post_migrate/20191112115317_change_vulnerabilities_title_html_to_nullable.rb9
-rw-r--r--db/post_migrate/20191114173624_set_resolved_state_on_vulnerabilities.rb31
-rw-r--r--db/schema.rb199
-rw-r--r--doc/README.md33
-rw-r--r--doc/administration/audit_events.md4
-rw-r--r--doc/administration/auth/ldap.md34
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md5
-rw-r--r--doc/administration/geo/replication/configuration.md12
-rw-r--r--doc/administration/geo/replication/database.md5
-rw-r--r--doc/administration/geo/replication/object_storage.md2
-rw-r--r--doc/administration/geo/replication/troubleshooting.md143
-rw-r--r--doc/administration/geo/replication/using_a_geo_server.md8
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md18
-rw-r--r--doc/administration/git_annex.md242
-rw-r--r--doc/administration/gitaly/index.md56
-rw-r--r--doc/administration/gitaly/praefect.md171
-rw-r--r--doc/administration/high_availability/README.md224
-rw-r--r--doc/administration/high_availability/consul.md17
-rw-r--r--doc/administration/high_availability/database.md87
-rw-r--r--doc/administration/high_availability/pgbouncer.md49
-rw-r--r--doc/administration/high_availability/redis.md2
-rw-r--r--doc/administration/incoming_email.md4
-rw-r--r--doc/administration/index.md20
-rw-r--r--doc/administration/integration/plantuml.md2
-rw-r--r--doc/administration/job_logs.md25
-rw-r--r--doc/administration/lfs/img/git-annex-branches.png (renamed from doc/workflow/lfs/images/git-annex-branches.png)bin32164 -> 32164 bytes
-rw-r--r--doc/administration/lfs/img/lfs-icon.png (renamed from doc/workflow/lfs/img/lfs-icon.png)bin4317 -> 4317 bytes
-rw-r--r--doc/administration/lfs/lfs_administration.md273
-rw-r--r--doc/administration/lfs/manage_large_binaries_with_git_lfs.md266
-rw-r--r--doc/administration/lfs/migrate_from_git_annex_to_git_lfs.md254
-rw-r--r--doc/administration/logs.md42
-rw-r--r--doc/administration/monitoring/gitlab_instance_administration_project/index.md3
-rw-r--r--doc/administration/monitoring/performance/img/performance_bar.pngbin33642 -> 71317 bytes
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md13
-rw-r--r--doc/administration/monitoring/prometheus/index.md25
-rw-r--r--doc/administration/operations/index.md3
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md4
-rw-r--r--doc/administration/packages/container_registry.md8
-rw-r--r--doc/administration/pages/index.md119
-rw-r--r--doc/administration/repository_storage_paths.md4
-rw-r--r--doc/administration/repository_storage_types.md14
-rw-r--r--doc/administration/timezone.md37
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md4
-rw-r--r--doc/administration/uploads.md4
-rw-r--r--doc/api/api_resources.md6
-rw-r--r--doc/api/audit_events.md116
-rw-r--r--doc/api/branches.md2
-rw-r--r--doc/api/commits.md29
-rw-r--r--doc/api/deployments.md29
-rw-r--r--doc/api/epic_links.md32
-rw-r--r--doc/api/epics.md40
-rw-r--r--doc/api/feature_flag_specs.md291
-rw-r--r--doc/api/feature_flags.md308
-rw-r--r--doc/api/geo_nodes.md12
-rw-r--r--doc/api/graphql/index.md5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5422
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json18749
-rw-r--r--doc/api/graphql/reference/index.md490
-rw-r--r--doc/api/group_clusters.md34
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/issues.md21
-rw-r--r--doc/api/license.md5
-rw-r--r--doc/api/merge_requests.md24
-rw-r--r--doc/api/packages.md45
-rw-r--r--doc/api/pages_domains.md71
-rw-r--r--doc/api/project_clusters.md39
-rw-r--r--doc/api/projects.md20
-rw-r--r--doc/api/releases/index.md2
-rw-r--r--doc/api/scim.md5
-rw-r--r--doc/api/search.md22
-rw-r--r--doc/api/services.md5
-rw-r--r--doc/api/settings.md16
-rw-r--r--doc/api/sidekiq_metrics.md6
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/users.md4
-rw-r--r--doc/api/visual_review_discussions.md40
-rw-r--r--doc/api/vulnerabilities.md114
-rw-r--r--doc/api/vulnerability_findings.md128
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/chatops/README.md5
-rw-r--r--doc/ci/ci_cd_for_external_repos/bitbucket_integration.md2
-rw-r--r--doc/ci/ci_cd_for_external_repos/github_integration.md4
-rw-r--r--doc/ci/ci_cd_for_external_repos/index.md4
-rw-r--r--doc/ci/docker/using_docker_build.md25
-rw-r--r--doc/ci/environments.md6
-rw-r--r--doc/ci/examples/deployment/README.md4
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md4
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/ci/img/pipelines_junit_test_report_ui_v12_5.pngbin0 -> 15957 bytes
-rw-r--r--doc/ci/interactive_web_terminal/index.md2
-rw-r--r--doc/ci/introduction/index.md14
-rw-r--r--doc/ci/junit_test_reports.md24
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md4
-rw-r--r--doc/ci/multi_project_pipelines.md7
-rw-r--r--doc/ci/pipelines.md34
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/variables/README.md33
-rw-r--r--doc/ci/variables/deprecated_variables.md12
-rw-r--r--doc/ci/variables/img/inherited_group_variables_v12_5.pngbin0 -> 73965 bytes
-rw-r--r--doc/ci/variables/predefined_variables.md210
-rw-r--r--doc/ci/yaml/README.md229
-rw-r--r--doc/development/README.md3
-rw-r--r--doc/development/api_graphql_styleguide.md94
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/changelog.md5
-rw-r--r--doc/development/chatops_on_gitlabcom.md2
-rw-r--r--doc/development/code_review.md6
-rw-r--r--doc/development/contributing/index.md4
-rw-r--r--doc/development/contributing/issue_workflow.md2
-rw-r--r--doc/development/contributing/merge_request_workflow.md6
-rw-r--r--doc/development/creating_enums.md15
-rw-r--r--doc/development/database_debugging.md2
-rw-r--r--doc/development/database_review.md7
-rw-r--r--doc/development/documentation/index.md4
-rw-r--r--doc/development/documentation/site_architecture/index.md145
-rw-r--r--doc/development/documentation/site_architecture/release_process.md241
-rw-r--r--doc/development/documentation/styleguide.md19
-rw-r--r--doc/development/documentation/workflow.md2
-rw-r--r--doc/development/ee_features.md40
-rw-r--r--doc/development/event_tracking/index.md4
-rw-r--r--doc/development/fe_guide/development_process.md2
-rw-r--r--doc/development/fe_guide/graphql.md8
-rw-r--r--doc/development/fe_guide/style_guide_js.md12
-rw-r--r--doc/development/fe_guide/style_guide_scss.md2
-rw-r--r--doc/development/feature_flags/controls.md8
-rw-r--r--doc/development/feature_flags/development.md19
-rw-r--r--doc/development/geo.md29
-rw-r--r--doc/development/git_object_deduplication.md2
-rw-r--r--doc/development/gitaly.md7
-rw-r--r--doc/development/gotchas.md70
-rw-r--r--doc/development/i18n/externalization.md5
-rw-r--r--doc/development/i18n/merging_translations.md30
-rw-r--r--doc/development/i18n/translation.md2
-rw-r--r--doc/development/kubernetes.md51
-rw-r--r--doc/development/lfs.md2
-rw-r--r--doc/development/merge_request_performance_guidelines.md179
-rw-r--r--doc/development/migration_style_guide.md2
-rw-r--r--doc/development/packages.md108
-rw-r--r--doc/development/pipelines.md71
-rw-r--r--doc/development/policies.md15
-rw-r--r--doc/development/profiling.md4
-rw-r--r--doc/development/rake_tasks.md20
-rw-r--r--doc/development/repository_mirroring.md2
-rw-r--r--doc/development/sidekiq_style_guide.md228
-rw-r--r--doc/development/testing_guide/best_practices.md29
-rw-r--r--doc/development/testing_guide/end_to_end/best_practices.md28
-rw-r--r--doc/development/testing_guide/end_to_end/feature_flags.md27
-rw-r--r--doc/development/testing_guide/end_to_end/flows.md56
-rw-r--r--doc/development/testing_guide/end_to_end/index.md2
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md59
-rw-r--r--doc/development/testing_guide/flaky_tests.md1
-rw-r--r--doc/development/testing_guide/frontend_testing.md77
-rw-r--r--doc/development/testing_guide/review_apps.md8
-rw-r--r--doc/development/understanding_explain_plans.md37
-rw-r--r--doc/development/utilities.md114
-rw-r--r--doc/gitlab-basics/README.md3
-rw-r--r--doc/gitlab-basics/feature_branch_workflow.md35
-rw-r--r--doc/gitlab-basics/fork-project.md2
-rw-r--r--doc/gitlab-basics/start-using-git.md2
-rw-r--r--doc/install/README.md2
-rw-r--r--doc/install/aws/index.md2
-rw-r--r--doc/install/installation.md28
-rw-r--r--doc/install/openshift_and_gitlab/index.md2
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/integration/README.md96
-rw-r--r--doc/integration/bitbucket.md7
-rw-r--r--doc/integration/elasticsearch.md9
-rw-r--r--doc/integration/img/sourcegraph_admin_v12_5.pngbin0 -> 61520 bytes
-rw-r--r--doc/integration/img/sourcegraph_demo_v12_5.pngbin0 -> 97025 bytes
-rw-r--r--doc/integration/img/sourcegraph_popover_v12_5.pngbin0 -> 27925 bytes
-rw-r--r--doc/integration/img/sourcegraph_user_preferences_v12_5.pngbin0 -> 37710 bytes
-rw-r--r--doc/integration/saml.md2
-rw-r--r--doc/integration/slash_commands.md1
-rw-r--r--doc/integration/sourcegraph.md128
-rw-r--r--doc/integration/ultra_auth.md2
-rw-r--r--doc/intro/README.md4
-rw-r--r--doc/policy/maintenance.md67
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/raketasks/cleanup.md2
-rw-r--r--doc/security/webhooks.md19
-rw-r--r--doc/ssh/README.md2
-rw-r--r--doc/topics/autodevops/index.md271
-rw-r--r--doc/topics/autodevops/quick_start_guide.md6
-rw-r--r--doc/topics/git/index.md8
-rw-r--r--doc/topics/git/migrate_to_git_lfs/index.md6
-rw-r--r--doc/topics/git/partial_clone.md20
-rw-r--r--doc/topics/gitlab_flow.md330
-rw-r--r--doc/topics/img/gitlab_flow.png (renamed from doc/workflow/img/gitlab_flow.png)bin47430 -> 47430 bytes
-rw-r--r--doc/topics/img/gitlab_flow_ci_mr.png (renamed from doc/workflow/img/ci_mr.png)bin12024 -> 12024 bytes
-rw-r--r--doc/topics/img/gitlab_flow_close_issue_mr.png (renamed from doc/workflow/img/close_issue_mr.png)bin42108 -> 42108 bytes
-rw-r--r--doc/topics/img/gitlab_flow_environment_branches.png (renamed from doc/workflow/img/environment_branches.png)bin12354 -> 12354 bytes
-rw-r--r--doc/topics/img/gitlab_flow_four_stages.png (renamed from doc/workflow/img/four_stages.png)bin7124 -> 7124 bytes
-rw-r--r--doc/topics/img/gitlab_flow_git_pull.png (renamed from doc/workflow/img/git_pull.png)bin28701 -> 28701 bytes
-rw-r--r--doc/topics/img/gitlab_flow_gitdashflow.png (renamed from doc/workflow/img/gitdashflow.png)bin68177 -> 68177 bytes
-rw-r--r--doc/topics/img/gitlab_flow_github_flow.png (renamed from doc/workflow/img/github_flow.png)bin6173 -> 6173 bytes
-rw-r--r--doc/topics/img/gitlab_flow_good_commit.png (renamed from doc/workflow/img/good_commit.png)bin8740 -> 8740 bytes
-rw-r--r--doc/topics/img/gitlab_flow_merge_commits.png (renamed from doc/workflow/img/merge_commits.png)bin7564 -> 7564 bytes
-rw-r--r--doc/topics/img/gitlab_flow_merge_request.png (renamed from doc/workflow/img/merge_request.png)bin47225 -> 47225 bytes
-rw-r--r--doc/topics/img/gitlab_flow_messy_flow.png (renamed from doc/workflow/img/messy_flow.png)bin11663 -> 11663 bytes
-rw-r--r--doc/topics/img/gitlab_flow_mr_inline_comments.png (renamed from doc/workflow/img/mr_inline_comments.png)bin52503 -> 52503 bytes
-rw-r--r--doc/topics/img/gitlab_flow_production_branch.png (renamed from doc/workflow/img/production_branch.png)bin7262 -> 7262 bytes
-rw-r--r--doc/topics/img/gitlab_flow_rebase.png (renamed from doc/workflow/img/rebase.png)bin28939 -> 28939 bytes
-rw-r--r--doc/topics/img/gitlab_flow_release_branches.png (renamed from doc/workflow/img/release_branches.png)bin12736 -> 12736 bytes
-rw-r--r--doc/topics/img/gitlab_flow_remove_checkbox.png (renamed from doc/workflow/img/remove_checkbox.png)bin6904 -> 6904 bytes
-rw-r--r--doc/topics/index.md1
-rw-r--r--doc/university/README.md2
-rw-r--r--doc/university/support/README.md2
-rw-r--r--doc/university/training/gitlab_flow.md2
-rw-r--r--doc/update/README.md8
-rw-r--r--doc/user/admin_area/activating_deactivating_users.md66
-rw-r--r--doc/user/admin_area/blocking_unblocking_users.md48
-rw-r--r--doc/user/admin_area/diff_limits.md2
-rw-r--r--doc/user/admin_area/index.md4
-rw-r--r--doc/user/admin_area/monitoring/health_check.md63
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md2
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md13
-rw-r--r--doc/user/admin_area/settings/email.md2
-rw-r--r--doc/user/admin_area/settings/img/two_factor_grace_period.pngbin0 -> 17591 bytes
-rw-r--r--doc/user/admin_area/settings/index.md1
-rw-r--r--doc/user/admin_area/settings/sign_in_restrictions.md56
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md4
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md2
-rw-r--r--doc/user/analytics/cycle_analytics.md54
-rw-r--r--doc/user/analytics/productivity_analytics.md11
-rw-r--r--doc/user/application_security/container_scanning/index.md71
-rw-r--r--doc/user/application_security/dast/index.md30
-rw-r--r--doc/user/application_security/dependency_list/img/dependency_list_v12_4.pngbin0 -> 137591 bytes
-rw-r--r--doc/user/application_security/dependency_list/index.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md71
-rw-r--r--doc/user/application_security/index.md38
-rw-r--r--doc/user/application_security/license_compliance/index.md16
-rw-r--r--doc/user/application_security/sast/analyzers.md5
-rw-r--r--doc/user/application_security/sast/index.md44
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.pngbin61667 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.pngbin0 -> 62965 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md4
-rw-r--r--doc/user/clusters/applications.md195
-rw-r--r--doc/user/clusters/crossplane.md292
-rw-r--r--doc/user/clusters/img/advanced-settings-cluster-management-project-v12_5.pngbin0 -> 66251 bytes
-rw-r--r--doc/user/clusters/management_project.md46
-rw-r--r--doc/user/group/clusters/index.md17
-rw-r--r--[-rwxr-xr-x]doc/user/group/epics/img/epic_view_roadmap_v12.3.pngbin50491 -> 50491 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/group/epics/img/epic_view_v12.3.pngbin61402 -> 61402 bytes
-rwxr-xr-xdoc/user/group/epics/img/epics_list_view_v12.3.pngbin39450 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/epics_list_view_v12.5.pngbin0 -> 442541 bytes
-rw-r--r--doc/user/group/epics/index.md44
-rw-r--r--doc/user/group/index.md21
-rw-r--r--doc/user/group/roadmap/index.md2
-rw-r--r--doc/user/group/saml_sso/index.md57
-rw-r--r--doc/user/group/saml_sso/scim_setup.md31
-rw-r--r--doc/user/group/subgroups/index.md2
-rw-r--r--doc/user/img/todos_add_todo_sidebar.png (renamed from doc/workflow/img/todos_add_todo_sidebar.png)bin17524 -> 17524 bytes
-rw-r--r--doc/user/img/todos_icon.png (renamed from doc/workflow/img/todos_icon.png)bin4910 -> 4910 bytes
-rw-r--r--doc/user/img/todos_index.png (renamed from doc/workflow/img/todos_index.png)bin98239 -> 98239 bytes
-rw-r--r--doc/user/img/todos_mark_done_sidebar.png (renamed from doc/workflow/img/todos_mark_done_sidebar.png)bin17619 -> 17619 bytes
-rw-r--r--doc/user/img/todos_todo_list_item.png (renamed from doc/workflow/img/todo_list_item.png)bin18776 -> 18776 bytes
-rw-r--r--doc/user/incident_management/img/incident_management_settings.pngbin0 -> 45533 bytes
-rw-r--r--doc/user/incident_management/index.md131
-rw-r--r--doc/user/index.md19
-rw-r--r--doc/user/markdown.md8
-rw-r--r--doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.pngbin3922 -> 0 bytes
-rw-r--r--doc/user/operations_dashboard/index.md9
-rw-r--r--doc/user/packages/conan_repository/index.md2
-rw-r--r--doc/user/packages/dependency_proxy/index.md2
-rw-r--r--doc/user/packages/npm_registry/index.md18
-rw-r--r--doc/user/permissions.md10
-rw-r--r--doc/user/profile/account/delete_account.md57
-rw-r--r--doc/user/profile/img/notification_global_settings.png (renamed from doc/workflow/img/notification_global_settings.png)bin67652 -> 67652 bytes
-rw-r--r--doc/user/profile/img/notification_group_settings.png (renamed from doc/workflow/img/notification_group_settings.png)bin54362 -> 54362 bytes
-rw-r--r--doc/user/profile/img/notification_project_settings.png (renamed from doc/workflow/img/notification_project_settings.png)bin58864 -> 58864 bytes
-rw-r--r--doc/user/profile/index.md2
-rw-r--r--doc/user/profile/notifications.md231
-rw-r--r--doc/user/profile/preferences.md15
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md730
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/create_dns.pngbin23923 -> 0 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/create_project.pngbin30568 -> 0 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.pngbin82157 -> 0 bytes
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md281
-rw-r--r--doc/user/project/clusters/img/add_cluster.png (renamed from doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png)bin59516 -> 59516 bytes
-rw-r--r--doc/user/project/clusters/img/environment.png (renamed from doc/user/project/clusters/eks_and_gitlab/img/environment.png)bin20339 -> 20339 bytes
-rw-r--r--doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.pngbin393690 -> 0 bytes
-rw-r--r--doc/user/project/clusters/img/kubernetes_pod_logs_v12_5.pngbin0 -> 183707 bytes
-rw-r--r--doc/user/project/clusters/img/pipeline.png (renamed from doc/user/project/clusters/eks_and_gitlab/img/pipeline.png)bin15288 -> 15288 bytes
-rw-r--r--doc/user/project/clusters/img/rbac.png (renamed from doc/user/project/clusters/eks_and_gitlab/img/rbac.png)bin15960 -> 15960 bytes
-rw-r--r--doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_5.pngbin0 -> 13681 bytes
-rw-r--r--doc/user/project/clusters/index.md442
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md32
-rw-r--r--doc/user/project/clusters/runbooks/index.md4
-rw-r--r--doc/user/project/clusters/serverless/index.md140
-rw-r--r--[-rwxr-xr-x]doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.pngbin141341 -> 141341 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/img/code_owners_approval_protected_branch_v12_4.pngbin16195 -> 16195 bytes
-rw-r--r--doc/user/project/img/time_tracking_example_v12_2.png (renamed from doc/workflow/time_tracking/img/time_tracking_example_v12_2.png)bin16362 -> 16362 bytes
-rw-r--r--doc/user/project/img/time_tracking_sidebar_v8_16.png (renamed from doc/workflow/time_tracking/img/time_tracking_sidebar_v8_16.png)bin9068 -> 9068 bytes
-rw-r--r--doc/user/project/import/gitea.md2
-rw-r--r--doc/user/project/import/github.md2
-rw-r--r--doc/user/project/index.md3
-rw-r--r--doc/user/project/integrations/generic_alerts.md8
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md3
-rw-r--r--doc/user/project/integrations/img/embed_metrics_issue_template.pngbin0 -> 146220 bytes
-rw-r--r--doc/user/project/integrations/img/grafana_panel_v12_5.pngbin0 -> 44193 bytes
-rw-r--r--doc/user/project/integrations/img/grafana_sharing_dialog_v12_5.pngbin0 -> 41203 bytes
-rw-r--r--doc/user/project/integrations/img/heatmap_panel_type.pngbin0 -> 8272 bytes
-rw-r--r--doc/user/project/integrations/img/http_proxy_access_v12_5.pngbin0 -> 47813 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_dashboard_anomaly_panel_type.pngbin0 -> 41015 bytes
-rw-r--r--doc/user/project/integrations/img/rendered_grafana_embed_v12_5.pngbin0 -> 61194 bytes
-rw-r--r--doc/user/project/integrations/img/select_query_variables_v12_5.pngbin0 -> 7368 bytes
-rw-r--r--doc/user/project/integrations/jira.md3
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/integrations/prometheus.md156
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md2
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/issues/associate_zoom_meeting.md42
-rw-r--r--doc/user/project/issues/csv_export.md4
-rw-r--r--doc/user/project/issues/design_management.md22
-rw-r--r--doc/user/project/issues/due_dates.md2
-rw-r--r--doc/user/project/issues/img/issue_weight.png (renamed from doc/workflow/issue_weight/issue.png)bin69564 -> 69564 bytes
-rw-r--r--doc/user/project/issues/img/select_all_designs_v12_4.pngbin1325569 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/zoom-quickaction-button.pngbin117097 -> 53418 bytes
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md16
-rw-r--r--doc/user/project/issues/issue_weight.md25
-rw-r--r--doc/user/project/labels.md4
-rw-r--r--doc/user/project/merge_requests/code_quality.md14
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md156
-rw-r--r--doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.pngbin21908 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_5.pngbin0 -> 21293 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.pngbin26902 -> 26902 bytes
-rw-r--r--doc/user/project/merge_requests/index.md688
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md6
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md5
-rw-r--r--doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md251
-rw-r--r--doc/user/project/merge_requests/versions.md11
-rw-r--r--doc/user/project/milestones/index.md61
-rw-r--r--doc/user/project/operations/error_tracking.md19
-rw-r--r--doc/user/project/operations/feature_flags.md31
-rw-r--r--doc/user/project/operations/img/error_details_v12_5.pngbin0 -> 522760 bytes
-rw-r--r--doc/user/project/operations/img/error_tracking_list.pngbin60762 -> 760603 bytes
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/index.md16
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md19
-rw-r--r--doc/user/project/pages/getting_started/fork_sample_project.md61
-rw-r--r--doc/user/project/pages/getting_started/new_or_existing_website.md45
-rw-r--r--doc/user/project/pages/getting_started/pages_bundled_template.md29
-rw-r--r--doc/user/project/pages/getting_started_part_one.md53
-rw-r--r--doc/user/project/pages/getting_started_part_two.md171
-rw-r--r--doc/user/project/pages/img/new_project_for_pages_v12_5.pngbin0 -> 71618 bytes
-rw-r--r--doc/user/project/pages/img/pages_workflow_v12_5.pngbin0 -> 29541 bytes
-rw-r--r--doc/user/project/pages/index.md85
-rw-r--r--doc/user/project/pages/introduction.md35
-rw-r--r--doc/user/project/pages/lets_encrypt_for_gitlab_pages.md4
-rw-r--r--doc/user/project/pages/pages_access_control.md48
-rw-r--r--doc/user/project/protected_branches.md11
-rw-r--r--doc/user/project/push_options.md30
-rw-r--r--doc/user/project/releases/img/edit_release_page_v12_5.pngbin0 -> 150927 bytes
-rw-r--r--doc/user/project/releases/img/milestone_list_with_releases_v12_5.pngbin0 -> 45454 bytes
-rw-r--r--doc/user/project/releases/img/milestone_with_releases_v12_5.pngbin0 -> 67529 bytes
-rw-r--r--doc/user/project/releases/img/new_tag_12_5.png (renamed from doc/workflow/releases/new_tag.png)bin42439 -> 42439 bytes
-rw-r--r--doc/user/project/releases/img/release_edit_button_v12_5.pngbin0 -> 87472 bytes
-rw-r--r--doc/user/project/releases/img/release_with_milestone_v12_5.pngbin0 -> 20197 bytes
-rw-r--r--doc/user/project/releases/img/tags_12_5.png (renamed from doc/workflow/releases/tags.png)bin44666 -> 44666 bytes
-rw-r--r--doc/user/project/releases/index.md131
-rw-r--r--doc/user/project/repository/file_finder.md45
-rw-r--r--doc/user/project/repository/forking_workflow.md55
-rw-r--r--doc/user/project/repository/img/file_finder_find_button.png (renamed from doc/workflow/img/file_finder_find_button.png)bin14565 -> 14565 bytes
-rw-r--r--doc/user/project/repository/img/file_finder_find_file.png (renamed from doc/workflow/img/file_finder_find_file.png)bin19478 -> 19478 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_branch_select.png (renamed from doc/workflow/forking/branch_select.png)bin18042 -> 18042 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_choose_namespace.png (renamed from doc/workflow/img/forking_workflow_choose_namespace.png)bin35084 -> 35084 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_fork_button.png (renamed from doc/workflow/img/forking_workflow_fork_button.png)bin25754 -> 25754 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_merge_request.png (renamed from doc/workflow/forking/merge_request.png)bin24625 -> 24625 bytes
-rw-r--r--doc/user/project/repository/img/forking_workflow_path_taken_error.png (renamed from doc/workflow/img/forking_workflow_path_taken_error.png)bin21497 -> 21497 bytes
-rw-r--r--doc/user/project/repository/img/repository_mirroring_copy_ssh_public_key_button.png (renamed from doc/workflow/img/copy_ssh_public_key_button.png)bin11225 -> 11225 bytes
-rw-r--r--doc/user/project/repository/img/repository_mirroring_force_update.png (renamed from doc/workflow/img/repository_mirroring_force_update.png)bin13586 -> 13586 bytes
-rw-r--r--doc/user/project/repository/img/repository_mirroring_pull_settings_lower.png (renamed from doc/workflow/img/repository_mirroring_pull_settings_lower.png)bin58056 -> 58056 bytes
-rw-r--r--doc/user/project/repository/img/repository_mirroring_pull_settings_upper.png (renamed from doc/workflow/img/repository_mirroring_pull_settings_upper.png)bin50084 -> 50084 bytes
-rw-r--r--doc/user/project/repository/img/repository_mirroring_push_settings.png (renamed from doc/workflow/img/repository_mirroring_push_settings.png)bin72515 -> 72515 bytes
-rw-r--r--doc/user/project/repository/index.md5
-rw-r--r--doc/user/project/repository/repository_mirroring.md430
-rw-r--r--doc/user/project/settings/index.md7
-rw-r--r--doc/user/project/time_tracking.md92
-rw-r--r--doc/user/search/advanced_search_syntax.md2
-rw-r--r--doc/user/search/img/issue_search_filter_v12_5.pngbin0 -> 504590 bytes
-rw-r--r--doc/user/search/index.md11
-rw-r--r--doc/user/shortcuts.md135
-rw-r--r--doc/user/todos.md142
-rw-r--r--doc/workflow/README.md55
-rw-r--r--doc/workflow/file_finder.md44
-rw-r--r--doc/workflow/forking_workflow.md54
-rw-r--r--doc/workflow/git_annex.md239
-rw-r--r--doc/workflow/git_lfs.md4
-rw-r--r--doc/workflow/gitlab_flow.md329
-rw-r--r--doc/workflow/issue_weight.md24
-rw-r--r--doc/workflow/lfs/lfs_administration.md272
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md265
-rw-r--r--doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md255
-rw-r--r--doc/workflow/notifications.md175
-rw-r--r--doc/workflow/releases.md25
-rw-r--r--doc/workflow/repository_mirroring.md427
-rw-r--r--doc/workflow/shortcuts.md133
-rw-r--r--doc/workflow/time_tracking.md90
-rw-r--r--doc/workflow/timezone.md40
-rw-r--r--doc/workflow/todos.md141
-rw-r--r--doc/workflow/workflow.md34
-rw-r--r--jest.config.js39
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/commits.rb6
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb66
-rw-r--r--lib/api/group_clusters.rb1
-rw-r--r--lib/api/group_container_repositories.rb4
-rw-r--r--lib/api/group_export.rb34
-rw-r--r--lib/api/helpers.rb17
-rw-r--r--lib/api/helpers/internal_helpers.rb3
-rw-r--r--lib/api/helpers/pagination.rb249
-rw-r--r--lib/api/helpers/projects_helpers.rb3
-rw-r--r--lib/api/internal/base.rb2
-rw-r--r--lib/api/merge_requests.rb12
-rw-r--r--lib/api/pages_domains.rb14
-rw-r--r--lib/api/project_clusters.rb1
-rw-r--r--lib/api/project_container_repositories.rb11
-rw-r--r--lib/api/projects.rb5
-rw-r--r--lib/api/releases.rb2
-rw-r--r--lib/api/settings.rb15
-rw-r--r--lib/api/sidekiq_metrics.rb3
-rw-r--r--lib/banzai/filter/inline_grafana_metrics_filter.rb76
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb89
-rw-r--r--lib/banzai/filter/video_link_filter.rb2
-rw-r--r--lib/banzai/pipeline/ascii_doc_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/bitbucket/representation/pull_request.rb5
-rw-r--r--lib/container_registry/client.rb2
-rw-r--r--lib/declarative_policy.rb9
-rw-r--r--lib/feature/gitaly.rb3
-rw-r--r--lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb2
-rw-r--r--lib/gitlab.rb12
-rw-r--r--lib/gitlab/analytics/cycle_analytics/base_query_builder.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/data_collector.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events.rb18
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb4
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb17
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb5
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb2
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb13
-rw-r--r--lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb4
-rw-r--r--lib/gitlab/auth/ip_rate_limiter.rb19
-rw-r--r--lib/gitlab/auth/ldap/config.rb8
-rw-r--r--lib/gitlab/background_migration/legacy_upload_mover.rb1
-rw-r--r--lib/gitlab/background_migration/legacy_uploads_migrator.rb2
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb101
-rw-r--r--lib/gitlab/ci/ansi2json/line.rb7
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb4
-rw-r--r--lib/gitlab/ci/ansi2json/style.rb21
-rw-r--r--lib/gitlab/ci/build/context/base.rb35
-rw-r--r--lib/gitlab/ci/build/context/build.rb41
-rw-r--r--lib/gitlab/ci/build/context/global.rb41
-rw-r--r--lib/gitlab/ci/build/policy/changes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb4
-rw-r--r--lib/gitlab/ci/build/rules.rb14
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb4
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/exists.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb7
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb17
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb20
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb4
-rw-r--r--lib/gitlab/ci/config/entry/default.rb34
-rw-r--r--lib/gitlab/ci/config/entry/files.rb26
-rw-r--r--lib/gitlab/ci/config/entry/job.rb67
-rw-r--r--lib/gitlab/ci/config/entry/key.rb45
-rw-r--r--lib/gitlab/ci/config/entry/need.rb44
-rw-r--r--lib/gitlab/ci/config/entry/needs.rb55
-rw-r--r--lib/gitlab/ci/config/entry/prefix.rb20
-rw-r--r--lib/gitlab/ci/config/entry/root.rb5
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb15
-rw-r--r--lib/gitlab/ci/config/entry/script.rb6
-rw-r--r--lib/gitlab/ci/config/entry/workflow.rb25
-rw-r--r--lib/gitlab/ci/config/normalizer.rb20
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content.rb59
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/process.rb41
-rw-r--r--lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb50
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb21
-rw-r--r--lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/seed.rb64
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb33
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb47
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/cache.rb77
-rw-r--r--lib/gitlab/ci/status/build/failed.rb4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml10
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml67
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml69
-rw-r--r--lib/gitlab/ci/yaml_processor.rb46
-rw-r--r--lib/gitlab/cleanup/orphan_job_artifact_files.rb11
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb67
-rw-r--r--lib/gitlab/cluster/mixins/puma_cluster.rb6
-rw-r--r--lib/gitlab/cluster/mixins/unicorn_http_server.rb19
-rw-r--r--lib/gitlab/cluster/puma_worker_killer_initializer.rb13
-rw-r--r--lib/gitlab/config/entry/configurable.rb29
-rw-r--r--lib/gitlab/config/entry/factory.rb20
-rw-r--r--lib/gitlab/config/entry/inheritable.rb40
-rw-r--r--lib/gitlab/config/entry/node.rb4
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb11
-rw-r--r--lib/gitlab/config/entry/validatable.rb21
-rw-r--r--lib/gitlab/config/entry/validators.rb39
-rw-r--r--lib/gitlab/cycle_analytics/group_stage_summary.rb7
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/base.rb5
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/deploy.rb3
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/issue.rb16
-rw-r--r--lib/gitlab/daemon.rb9
-rw-r--r--lib/gitlab/danger/helper.rb16
-rw-r--r--lib/gitlab/danger/teammate.rb5
-rw-r--r--lib/gitlab/data_builder/deployment.rb8
-rw-r--r--lib/gitlab/data_builder/push.rb30
-rw-r--r--lib/gitlab/database/migration_helpers.rb17
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb10
-rw-r--r--lib/gitlab/database_importers/self_monitoring/project/create_service.rb53
-rw-r--r--lib/gitlab/devise_failure.rb8
-rw-r--r--lib/gitlab/error_tracking/detailed_error.rb31
-rw-r--r--lib/gitlab/error_tracking/error_event.rb11
-rw-r--r--lib/gitlab/etag_caching/router.rb4
-rw-r--r--lib/gitlab/experimentation.rb69
-rw-r--r--lib/gitlab/favicon.rb2
-rw-r--r--lib/gitlab/file_finder.rb17
-rw-r--r--lib/gitlab/git/commit.rb4
-rw-r--r--lib/gitlab/git/repository.rb11
-rw-r--r--lib/gitlab/git/wiki.rb8
-rw-r--r--lib/gitlab/git_access_result/custom_action.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb27
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb45
-rw-r--r--lib/gitlab/gitaly_client/namespace_service.rb19
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb12
-rw-r--r--lib/gitlab/gon_helper.rb3
-rw-r--r--lib/gitlab/gpg.rb49
-rw-r--r--lib/gitlab/grape_logging/loggers/exception_logger.rb33
-rw-r--r--lib/gitlab/graphql/authorize/instrumentation.rb6
-rw-r--r--lib/gitlab/graphql/connections.rb6
-rw-r--r--lib/gitlab/graphql/connections/filterable_array_connection.rb17
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb40
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb57
-rw-r--r--lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb41
-rw-r--r--lib/gitlab/graphql/connections/keyset/connection.rb153
-rw-r--r--lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb66
-rw-r--r--lib/gitlab/graphql/connections/keyset/order_info.rb78
-rw-r--r--lib/gitlab/graphql/connections/keyset/query_builder.rb68
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb85
-rw-r--r--lib/gitlab/graphql/filterable_array.rb14
-rw-r--r--lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb25
-rw-r--r--lib/gitlab/health_checks/master_check.rb66
-rw-r--r--lib/gitlab/import_export.rb14
-rw-r--r--lib/gitlab/import_export/config.rb5
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/group_import_export.yml36
-rw-r--r--lib/gitlab/import_export/group_project_object_builder.rb11
-rw-r--r--lib/gitlab/import_export/group_tree_saver.rb55
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb206
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb31
-rw-r--r--lib/gitlab/import_export/reader.rb27
-rw-r--r--lib/gitlab/import_export/relation_factory.rb15
-rw-r--r--lib/gitlab/import_export/relation_rename_service.rb2
-rw-r--r--lib/gitlab/import_export/relation_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/saver.rb18
-rw-r--r--lib/gitlab/import_export/shared.rb40
-rw-r--r--lib/gitlab/instrumentation_helper.rb44
-rw-r--r--lib/gitlab/kubernetes/config_maps/aws_node_auth.rb46
-rw-r--r--lib/gitlab/kubernetes/helm.rb4
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/errors.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/service_selector.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb224
-rw-r--r--lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb2
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb46
-rw-r--r--lib/gitlab/metrics/exporter/web_exporter.rb25
-rw-r--r--lib/gitlab/metrics/requests_rack_middleware.rb4
-rw-r--r--lib/gitlab/pagination/base.rb32
-rw-r--r--lib/gitlab/pagination/offset_pagination.rb77
-rw-r--r--lib/gitlab/project_authorizations.rb30
-rw-r--r--lib/gitlab/project_template.rb3
-rw-r--r--lib/gitlab/prometheus/internal.rb45
-rw-r--r--lib/gitlab/prometheus/metric_group.rb16
-rw-r--r--lib/gitlab/prometheus/queries/knative_invocation_query.rb13
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb2
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb22
-rw-r--r--lib/gitlab/redis/wrapper.rb2
-rw-r--r--lib/gitlab/regex.rb15
-rw-r--r--lib/gitlab/search/found_blob.rb54
-rw-r--r--lib/gitlab/seeder.rb65
-rw-r--r--lib/gitlab/serializer/pagination.rb5
-rw-r--r--lib/gitlab/setup_helper.rb5
-rw-r--r--lib/gitlab/shell.rb12
-rw-r--r--lib/gitlab/sidekiq_daemon/monitor.rb6
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb47
-rw-r--r--lib/gitlab/slash_commands/command.rb1
-rw-r--r--lib/gitlab/slash_commands/issue_comment.rb55
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb4
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_comment.rb43
-rw-r--r--lib/gitlab/slash_commands/presenters/note_base.rb48
-rw-r--r--lib/gitlab/sourcegraph.rb26
-rw-r--r--lib/gitlab/sql/union.rb2
-rw-r--r--lib/gitlab/task_helpers.rb18
-rw-r--r--lib/gitlab/tracking.rb7
-rw-r--r--lib/gitlab/usage_data.rb24
-rw-r--r--lib/gitlab/usage_data_counters/web_ide_counter.rb14
-rw-r--r--lib/gitlab/utils/deep_size.rb4
-rw-r--r--lib/gitlab/wiki_file_finder.rb6
-rw-r--r--lib/gitlab/workhorse.rb1
-rw-r--r--lib/google_api/cloud_platform/client.rb4
-rw-r--r--lib/grafana/client.rb14
-rw-r--r--lib/prometheus/pid_provider.rb10
-rw-r--r--lib/quality/kubernetes_client.rb14
-rw-r--r--lib/sentry/client.rb122
-rw-r--r--lib/tasks/dev.rake4
-rw-r--r--lib/tasks/gitlab/graphql.rake42
-rw-r--r--lib/tasks/gitlab/seed.rake2
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/uploads/legacy.rake2
-rw-r--r--locale/ar_SA/gitlab.po4
-rw-r--r--locale/bg/gitlab.po4
-rw-r--r--locale/bn_BD/gitlab.po4
-rw-r--r--locale/bn_IN/gitlab.po4
-rw-r--r--locale/ca_ES/gitlab.po4
-rw-r--r--locale/cs_CZ/gitlab.po4
-rw-r--r--locale/cy_GB/gitlab.po4
-rw-r--r--locale/da_DK/gitlab.po4
-rw-r--r--locale/de/gitlab.po4
-rw-r--r--locale/el_GR/gitlab.po4
-rw-r--r--locale/eo/gitlab.po4
-rw-r--r--locale/es/gitlab.po4
-rw-r--r--locale/et_EE/gitlab.po4
-rw-r--r--locale/fa_IR/gitlab.po4
-rw-r--r--locale/fil_PH/gitlab.po4
-rw-r--r--locale/fr/gitlab.po4
-rw-r--r--locale/gitlab.pot1138
-rw-r--r--locale/gl_ES/gitlab.po4
-rw-r--r--locale/he_IL/gitlab.po4
-rw-r--r--locale/hi_IN/gitlab.po4
-rw-r--r--locale/hr_HR/gitlab.po4
-rw-r--r--locale/hu_HU/gitlab.po4
-rw-r--r--locale/id_ID/gitlab.po4
-rw-r--r--locale/it/gitlab.po4
-rw-r--r--locale/ja/gitlab.po4
-rw-r--r--locale/ka_GE/gitlab.po4
-rw-r--r--locale/ko/gitlab.po4
-rw-r--r--locale/mn_MN/gitlab.po4
-rw-r--r--locale/nb_NO/gitlab.po4
-rw-r--r--locale/nl_NL/gitlab.po4
-rw-r--r--locale/pa_IN/gitlab.po4
-rw-r--r--locale/pl_PL/gitlab.po4
-rw-r--r--locale/pt_BR/gitlab.po4
-rw-r--r--locale/pt_PT/gitlab.po4
-rw-r--r--locale/ro_RO/gitlab.po4
-rw-r--r--locale/ru/gitlab.po4
-rw-r--r--locale/sk_SK/gitlab.po4
-rw-r--r--locale/sq_AL/gitlab.po4
-rw-r--r--locale/sr_CS/gitlab.po4
-rw-r--r--locale/sr_SP/gitlab.po4
-rw-r--r--locale/sv_SE/gitlab.po4
-rw-r--r--locale/sw_KE/gitlab.po4
-rw-r--r--locale/tr_TR/gitlab.po4
-rw-r--r--locale/uk/gitlab.po4
-rw-r--r--locale/vi_VN/gitlab.po4
-rw-r--r--locale/zh_CN/gitlab.po4
-rw-r--r--locale/zh_HK/gitlab.po4
-rw-r--r--locale/zh_TW/gitlab.po4
-rw-r--r--package.json20
-rw-r--r--qa/Gemfile9
-rw-r--r--qa/Gemfile.lock13
-rw-r--r--qa/qa.rb24
-rw-r--r--qa/qa/flow/login.rb38
-rw-r--r--qa/qa/page/admin/overview/users/show.rb10
-rw-r--r--qa/qa/page/admin/settings/component/outbound_requests.rb33
-rw-r--r--qa/qa/page/admin/settings/network.rb7
-rw-r--r--qa/qa/page/base.rb52
-rw-r--r--qa/qa/page/component/select2.rb8
-rw-r--r--qa/qa/page/dashboard/projects.rb4
-rw-r--r--qa/qa/page/dashboard/welcome.rb17
-rw-r--r--qa/qa/page/element.rb10
-rw-r--r--qa/qa/page/file/shared/commit_button.rb4
-rw-r--r--qa/qa/page/group/show.rb10
-rw-r--r--qa/qa/page/main/menu.rb2
-rw-r--r--qa/qa/page/project/issue/index.rb4
-rw-r--r--qa/qa/page/project/issue/show.rb14
-rw-r--r--qa/qa/page/project/pipeline/index.rb12
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb2
-rw-r--r--qa/qa/page/project/sub_menus/settings.rb9
-rw-r--r--qa/qa/resource/base.rb11
-rw-r--r--qa/qa/resource/group.rb5
-rw-r--r--qa/qa/resource/issue.rb4
-rw-r--r--qa/qa/resource/members.rb4
-rw-r--r--qa/qa/resource/merge_request.rb2
-rw-r--r--qa/qa/resource/project.rb5
-rw-r--r--qa/qa/resource/runner.rb1
-rw-r--r--qa/qa/resource/sandbox.rb2
-rw-r--r--qa/qa/resource/user.rb19
-rw-r--r--qa/qa/runtime/api/client.rb19
-rw-r--r--qa/qa/runtime/browser.rb20
-rw-r--r--qa/qa/runtime/env.rb4
-rw-r--r--qa/qa/runtime/feature.rb40
-rw-r--r--qa/qa/runtime/fixtures.rb2
-rw-r--r--qa/qa/runtime/user.rb4
-rw-r--r--qa/qa/scenario/shared_attributes.rb1
-rw-r--r--qa/qa/service/docker_run/jenkins.rb43
-rw-r--r--qa/qa/service/docker_run/maven.rb44
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb57
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb11
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb22
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb19
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb2
-rw-r--r--qa/qa/specs/loop_runner.rb21
-rw-r--r--qa/qa/specs/runner.rb2
-rw-r--r--qa/qa/support/api.rb16
-rw-r--r--qa/qa/vendor/github/page/login.rb10
-rw-r--r--qa/qa/vendor/jenkins/page/base.rb24
-rw-r--r--qa/qa/vendor/jenkins/page/configure.rb48
-rw-r--r--qa/qa/vendor/jenkins/page/configure_job.rb62
-rw-r--r--qa/qa/vendor/jenkins/page/login.rb31
-rw-r--r--qa/qa/vendor/jenkins/page/new_credentials.rb50
-rw-r--r--qa/qa/vendor/jenkins/page/new_job.rb38
-rw-r--r--qa/qa/vendor/saml_idp/page/login.rb16
-rw-r--r--qa/spec/page/element_spec.rb18
-rw-r--r--qa/spec/resource/base_spec.rb2
-rw-r--r--qa/spec/spec_helper.rb20
-rw-r--r--rubocop/cop/rspec/any_instance_of.rb78
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/lint-doc.sh3
-rwxr-xr-xscripts/notify-slack14
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb36
-rw-r--r--scripts/review_apps/base-config.yaml70
-rwxr-xr-xscripts/review_apps/review-apps.sh98
-rwxr-xr-xscripts/static-analysis38
-rw-r--r--scripts/sync-stable-branch.sh32
-rwxr-xr-xscripts/trigger-build35
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb4
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb156
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb8
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb6
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb34
-rw-r--r--spec/controllers/concerns/confirm_email_warning_spec.rb2
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb47
-rw-r--r--spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb33
-rw-r--r--spec/controllers/concerns/renders_commits_spec.rb60
-rw-r--r--spec/controllers/concerns/sourcegraph_gon_spec.rb118
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb5
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb146
-rw-r--r--spec/controllers/groups/group_links_controller_spec.rb114
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb18
-rw-r--r--spec/controllers/groups_controller_spec.rb8
-rw-r--r--spec/controllers/health_controller_spec.rb134
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb5
-rw-r--r--spec/controllers/import/phabricator_controller_spec.rb2
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb8
-rw-r--r--spec/controllers/metrics_controller_spec.rb4
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb19
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb15
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb146
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb16
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb4
-rw-r--r--spec/controllers/projects/error_tracking_controller_spec.rb202
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb71
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb18
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb18
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb13
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb22
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb246
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb4
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb10
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb62
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb6
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb8
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb4
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb159
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb94
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb8
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb3
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb12
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb20
-rw-r--r--spec/controllers/projects/usage_ping_controller_spec.rb64
-rw-r--r--spec/controllers/projects_controller_spec.rb28
-rw-r--r--spec/controllers/registrations_controller_spec.rb94
-rw-r--r--spec/controllers/sessions_controller_spec.rb58
-rw-r--r--spec/controllers/snippets_controller_spec.rb12
-rw-r--r--spec/controllers/users_controller_spec.rb46
-rw-r--r--spec/db/schema_spec.rb48
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb4
-rw-r--r--spec/factories/ci/pipelines.rb44
-rw-r--r--spec/factories/clusters/applications/helm.rb9
-rw-r--r--spec/factories/clusters/clusters.rb20
-rw-r--r--spec/factories/clusters/platforms/kubernetes.rb2
-rw-r--r--spec/factories/clusters/providers/aws.rb3
-rw-r--r--spec/factories/clusters/providers/gcp.rb2
-rw-r--r--spec/factories/commit_statuses.rb2
-rw-r--r--spec/factories/deployments.rb4
-rw-r--r--spec/factories/error_tracking/detailed_error.rb29
-rw-r--r--spec/factories/error_tracking/error_event.rb18
-rw-r--r--spec/factories/grafana_integrations.rb3
-rw-r--r--spec/factories/group_group_links.rb9
-rw-r--r--spec/factories/issues.rb1
-rw-r--r--spec/factories/merge_requests.rb13
-rw-r--r--spec/factories/projects.rb5
-rw-r--r--spec/factories/zoom_meetings.rb18
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb24
-rw-r--r--spec/features/admin/admin_projects_spec.rb5
-rw-r--r--spec/features/admin/admin_settings_spec.rb31
-rw-r--r--spec/features/admin/admin_users_spec.rb4
-rw-r--r--spec/features/admin/clusters/eks_spec.rb29
-rw-r--r--spec/features/calendar_spec.rb10
-rw-r--r--spec/features/clusters/installing_applications_shared_examples.rb31
-rw-r--r--spec/features/commits_spec.rb37
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/cycle_analytics_spec.rb10
-rw-r--r--spec/features/dashboard/projects_spec.rb3
-rw-r--r--spec/features/explore/groups_spec.rb4
-rw-r--r--spec/features/global_search_spec.rb4
-rw-r--r--spec/features/groups/clusters/eks_spec.rb35
-rw-r--r--spec/features/groups/clusters/user_spec.rb8
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb4
-rw-r--r--spec/features/groups/issues_spec.rb4
-rw-r--r--spec/features/groups/milestone_spec.rb141
-rw-r--r--spec/features/groups_spec.rb20
-rw-r--r--spec/features/import/manifest_import_spec.rb2
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb4
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb6
-rw-r--r--spec/features/issuables/sorting_list_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb11
-rw-r--r--spec/features/issues/filtered_search/dropdown_release_spec.rb55
-rw-r--r--spec/features/issues/notes_on_issues_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb4
-rw-r--r--spec/features/issues/user_creates_confidential_merge_request_spec.rb2
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb13
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb1
-rw-r--r--spec/features/markdown/metrics_spec.rb66
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb6
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb6
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb3
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb7
-rw-r--r--spec/features/merge_request/user_edits_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_expands_diff_spec.rb4
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb3
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb5
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb6
-rw-r--r--spec/features/merge_request/user_reverts_merge_request_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb7
-rw-r--r--spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb63
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb3
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_pipelines_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb4
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb3
-rw-r--r--spec/features/merge_request/user_toggles_whitespace_changes_spec.rb3
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb3
-rw-r--r--spec/features/merge_requests/user_squashes_merge_request_spec.rb8
-rw-r--r--spec/features/milestones/user_views_milestones_spec.rb27
-rw-r--r--spec/features/populate_new_pipeline_vars_with_params_spec.rb32
-rw-r--r--spec/features/profile_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb4
-rw-r--r--spec/features/project_group_variables_spec.rb60
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb8
-rw-r--r--spec/features/projects/blobs/edit_spec.rb21
-rw-r--r--spec/features/projects/clusters/eks_spec.rb3
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb6
-rw-r--r--spec/features/projects/clusters/user_spec.rb8
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb6
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb10
-rw-r--r--spec/features/projects/compare_spec.rb8
-rw-r--r--spec/features/projects/environments/environment_spec.rb5
-rw-r--r--spec/features/projects/environments/environments_spec.rb4
-rw-r--r--spec/features/projects/features_visibility_spec.rb8
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb3
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb24
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb4
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb20
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb4
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb9
-rw-r--r--spec/features/projects/files/user_reads_pipeline_status_spec.rb4
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb4
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/fork_spec.rb4
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb2
-rw-r--r--spec/features/projects/graph_spec.rb6
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb6
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb8
-rw-r--r--spec/features/projects/jobs_spec.rb4
-rw-r--r--spec/features/projects/labels/search_labels_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb106
-rw-r--r--spec/features/projects/pages_lets_encrypt_spec.rb49
-rw-r--r--spec/features/projects/pages_spec.rb115
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb30
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb26
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb25
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb23
-rw-r--r--spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb8
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb6
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb1
-rw-r--r--spec/features/projects/view_on_env_spec.rb10
-rw-r--r--spec/features/projects_spec.rb20
-rw-r--r--spec/features/raven_js_spec.rb27
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb26
-rw-r--r--spec/features/security/project/internal_access_spec.rb8
-rw-r--r--spec/features/security/project/private_access_spec.rb8
-rw-r--r--spec/features/security/project/public_access_spec.rb8
-rw-r--r--spec/features/sentry_js_spec.rb28
-rw-r--r--spec/features/signed_commits_spec.rb26
-rw-r--r--spec/features/tags/developer_deletes_tag_spec.rb6
-rw-r--r--spec/features/unsubscribe_links_spec.rb2
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb2
-rw-r--r--spec/features/users/anonymous_sessions_spec.rb41
-rw-r--r--spec/features/users/login_spec.rb2
-rw-r--r--spec/features/users/signup_spec.rb10
-rw-r--r--spec/finders/abuse_reports_finder_spec.rb27
-rw-r--r--spec/finders/branches_finder_spec.rb56
-rw-r--r--spec/finders/container_repositories_finder_spec.rb50
-rw-r--r--spec/finders/issues_finder_spec.rb14
-rw-r--r--spec/finders/merge_requests_finder_spec.rb12
-rw-r--r--spec/finders/projects_finder_spec.rb45
-rw-r--r--spec/finders/prometheus_metrics_finder_spec.rb144
-rw-r--r--spec/finders/releases_finder_spec.rb33
-rw-r--r--spec/finders/tags_finder_spec.rb38
-rw-r--r--spec/finders/todos_finder_spec.rb88
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar_extras.json2
-rw-r--r--spec/fixtures/api/schemas/error_tracking/error.json20
-rw-r--r--spec/fixtures/api/schemas/error_tracking/error_detailed.json45
-rw-r--r--spec/fixtures/api/schemas/error_tracking/error_stack_trace.json14
-rw-r--r--spec/fixtures/api/schemas/error_tracking/issue_detailed.json11
-rw-r--r--spec/fixtures/api/schemas/error_tracking/issue_stack_trace.json11
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/blobs.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json5
-rw-r--r--spec/fixtures/api/schemas/release.json3
-rw-r--r--spec/fixtures/grafana/dashboard_response.json764
-rw-r--r--spec/fixtures/grafana/datasource_response.json21
-rw-r--r--spec/fixtures/grafana/expected_grafana_embed.json27
-rw-r--r--spec/fixtures/grafana/proxy_response.json459
-rw-r--r--spec/fixtures/grafana/simplified_dashboard_response.json40
-rw-r--r--spec/fixtures/group_export.tar.gzbin0 -> 4551 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.json)66
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.group.json)0
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.light.json)0
-rw-r--r--spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json)0
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json1
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json1
-rw-r--r--spec/frontend/api_spec.js20
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js81
-rw-r--r--spec/frontend/boards/issue_card_spec.js307
-rw-r--r--spec/frontend/boards/stores/getters_spec.js21
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js26
-rw-r--r--spec/frontend/clusters/components/applications_spec.js157
-rw-r--r--spec/frontend/clusters/services/crossplane_provider_stack_spec.js78
-rw-r--r--spec/frontend/clusters/services/mock_data.js27
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js43
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap4
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap47
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js69
-rw-r--r--spec/frontend/contributors/store/actions_spec.js60
-rw-r--r--spec/frontend/contributors/store/getters_spec.js73
-rw-r--r--spec/frontend/contributors/store/mutations_spec.js40
-rw-r--r--spec/frontend/contributors/utils_spec.js21
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js44
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js91
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js181
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js55
-rw-r--r--spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js117
-rw-r--r--spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js152
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js248
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js113
-rw-r--r--spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js (renamed from spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js)4
-rw-r--r--spec/frontend/create_cluster/init_create_cluster_spec.js73
-rw-r--r--spec/frontend/cycle_analytics/stage_nav_item_spec.js44
-rw-r--r--spec/frontend/environment.js6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js105
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js15
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js49
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_spec.js45
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js94
-rw-r--r--spec/frontend/error_tracking/store/details/getters_spec.js13
-rw-r--r--spec/frontend/error_tracking/store/list/getters_spec.js33
-rw-r--r--spec/frontend/error_tracking/store/list/mutation_spec.js (renamed from spec/frontend/error_tracking/store/mutation_spec.js)4
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js25
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js16
-rw-r--r--spec/frontend/fixtures/merge_requests.rb18
-rw-r--r--spec/frontend/fixtures/static/environments_logs.html4
-rw-r--r--spec/frontend/fixtures/static/signin_tabs.html3
-rw-r--r--spec/frontend/fixtures/u2f.rb4
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap101
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js125
-rw-r--r--spec/frontend/grafana_integration/store/mutations_spec.js35
-rw-r--r--spec/frontend/helpers/monitor_helper_spec.js82
-rw-r--r--spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap61
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js86
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js20
-rw-r--r--spec/frontend/ide/services/index_spec.js83
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js39
-rw-r--r--spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap15
-rw-r--r--spec/frontend/issuables_list/components/issuable_spec.js345
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js410
-rw-r--r--spec/frontend/issuables_list/issuable_list_test_data.js72
-rw-r--r--spec/frontend/issue_show/helpers.js10
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js4
-rw-r--r--spec/frontend/jobs/store/utils_spec.js20
-rw-r--r--spec/frontend/lib/utils/chart_utils_spec.js11
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js47
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js40
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js13
-rw-r--r--spec/frontend/monitoring/charts/time_series_spec.js (renamed from spec/javascripts/monitoring/charts/time_series_spec.js)154
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js303
-rw-r--r--spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js10
-rw-r--r--spec/frontend/monitoring/embed/embed_spec.js4
-rw-r--r--spec/frontend/monitoring/embed/mock_data.js4
-rw-r--r--spec/frontend/monitoring/mock_data.js465
-rw-r--r--spec/frontend/monitoring/panel_type_spec.js166
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js (renamed from spec/javascripts/monitoring/store/actions_spec.js)277
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js (renamed from spec/javascripts/monitoring/store/mutations_spec.js)127
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js (renamed from spec/javascripts/monitoring/store/utils_spec.js)0
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js331
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js141
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js6
-rw-r--r--spec/frontend/notes/components/note_app_spec.js26
-rw-r--r--spec/frontend/notes/mock_data.js1255
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js62
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js75
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js (renamed from spec/javascripts/pipelines/pipeline_triggerer_spec.js)5
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js (renamed from spec/javascripts/pipelines/pipelines_table_row_spec.js)138
-rw-r--r--spec/frontend/pipelines/test_reports/mock_data.js123
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js109
-rw-r--r--spec/frontend/pipelines/test_reports/stores/getters_spec.js54
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js63
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js64
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js77
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js78
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js54
-rw-r--r--spec/frontend/project_find_file_spec.js37
-rw-r--r--spec/frontend/registry/components/collapsible_container_spec.js53
-rw-r--r--spec/frontend/registry/components/table_registry_spec.js119
-rw-r--r--spec/frontend/releases/detail/components/app_spec.js19
-rw-r--r--spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap332
-rw-r--r--spec/frontend/releases/list/components/release_block_footer_spec.js163
-rw-r--r--spec/frontend/releases/list/components/release_block_spec.js33
-rw-r--r--spec/frontend/releases/mock_data.js4
-rw-r--r--spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap75
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap56
-rw-r--r--spec/frontend/repository/components/directory_download_links_spec.js29
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js4
-rw-r--r--spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap36
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js49
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap2
-rw-r--r--spec/frontend/repository/components/table/index_spec.js74
-rw-r--r--spec/frontend/repository/components/table/row_spec.js25
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js71
-rw-r--r--spec/frontend/repository/log_tree_spec.js27
-rw-r--r--spec/frontend/repository/pages/index_spec.js42
-rw-r--r--spec/frontend/repository/pages/tree_spec.js60
-rw-r--r--spec/frontend/repository/utils/commit_spec.js30
-rw-r--r--spec/frontend/repository/utils/dom_spec.js20
-rw-r--r--spec/frontend/repository/utils/readme_spec.js33
-rw-r--r--spec/frontend/repository/utils/title_spec.js4
-rw-r--r--spec/frontend/sentry/index_spec.js (renamed from spec/javascripts/raven/index_spec.js)20
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js214
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js1
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js1
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap37
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js (renamed from spec/javascripts/vue_shared/components/commit_spec.js)115
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js189
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/slot_switch_spec.js56
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js (renamed from spec/javascripts/vue_shared/components/table_pagination_spec.js)175
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js186
-rw-r--r--spec/graphql/features/authorization_spec.rb3
-rw-r--r--spec/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_assignees_spec.rb106
-rw-r--r--spec/graphql/mutations/merge_requests/set_labels_spec.rb77
-rw-r--r--spec/graphql/mutations/merge_requests/set_locked_spec.rb49
-rw-r--r--spec/graphql/mutations/merge_requests/set_milestone_spec.rb53
-rw-r--r--spec/graphql/mutations/merge_requests/set_subscription_spec.rb42
-rw-r--r--spec/graphql/mutations/todos/mark_done_spec.rb66
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb24
-rw-r--r--spec/graphql/resolvers/commit_pipelines_resolver_spec.rb53
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb42
-rw-r--r--spec/graphql/types/base_enum_spec.rb24
-rw-r--r--spec/graphql/types/commit_type_spec.rb2
-rw-r--r--spec/graphql/types/extended_issue_type_spec.rb21
-rw-r--r--spec/graphql/types/issue_sort_enum_spec.rb13
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/graphql/types/label_type_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb3
-rw-r--r--spec/graphql/types/tree/blob_type_spec.rb2
-rw-r--r--spec/graphql/types/tree/submodule_type_spec.rb2
-rw-r--r--spec/graphql/types/tree/tree_entry_type_spec.rb2
-rw-r--r--spec/helpers/application_helper_spec.rb4
-rw-r--r--spec/helpers/application_settings_helper_spec.rb23
-rw-r--r--spec/helpers/auth_helper_spec.rb17
-rw-r--r--spec/helpers/clusters_helper_spec.rb56
-rw-r--r--spec/helpers/dashboard_helper_spec.rb61
-rw-r--r--spec/helpers/environments_helper_spec.rb1
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb6
-rw-r--r--spec/helpers/issuables_helper_spec.rb55
-rw-r--r--spec/helpers/markup_helper_spec.rb37
-rw-r--r--spec/helpers/projects_helper_spec.rb18
-rw-r--r--spec/helpers/releases_helper_spec.rb14
-rw-r--r--spec/helpers/search_helper_spec.rb47
-rw-r--r--spec/helpers/snippets_helper_spec.rb206
-rw-r--r--spec/helpers/sourcegraph_helper_spec.rb64
-rw-r--r--spec/helpers/users_helper_spec.rb4
-rw-r--r--spec/initializers/6_validations_spec.rb2
-rw-r--r--spec/initializers/action_mailer_hooks_spec.rb2
-rw-r--r--spec/initializers/asset_proxy_setting_spec.rb2
-rw-r--r--spec/initializers/attr_encrypted_no_db_connection_spec.rb2
-rw-r--r--spec/initializers/database_config_spec.rb73
-rw-r--r--spec/initializers/direct_upload_support_spec.rb2
-rw-r--r--spec/initializers/doorkeeper_spec.rb2
-rw-r--r--spec/initializers/fog_google_https_private_urls_spec.rb2
-rw-r--r--spec/initializers/lograge_spec.rb48
-rw-r--r--spec/initializers/rest-client-hostname_override_spec.rb2
-rw-r--r--spec/initializers/secret_token_spec.rb2
-rw-r--r--spec/initializers/settings_spec.rb2
-rw-r--r--spec/initializers/trusted_proxies_spec.rb2
-rw-r--r--spec/initializers/zz_metrics_spec.rb2
-rw-r--r--spec/javascripts/boards/board_card_spec.js2
-rw-r--r--spec/javascripts/boards/board_list_common_spec.js15
-rw-r--r--spec/javascripts/boards/board_list_spec.js250
-rw-r--r--spec/javascripts/boards/components/boards_selector_spec.js7
-rw-r--r--spec/javascripts/boards/components/issue_time_estimate_spec.js70
-rw-r--r--spec/javascripts/boards/issue_card_spec.js292
-rw-r--r--spec/javascripts/bootstrap_jquery_spec.js14
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js2
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js49
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file_unreadable.js244
-rw-r--r--spec/javascripts/dropzone_input_spec.js86
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js2
-rw-r--r--spec/javascripts/frequent_items/mock_data.js4
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js7
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js152
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_spec.js28
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js298
-rw-r--r--spec/javascripts/ide/components/jobs/stage_spec.js95
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js4
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js56
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js28
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js18
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js55
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js6
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js30
-rw-r--r--spec/javascripts/issue_show/helpers.js11
-rw-r--r--spec/javascripts/lib/utils/tick_formats_spec.js40
-rw-r--r--spec/javascripts/merge_request_spec.js32
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js14
-rw-r--r--spec/javascripts/monitoring/charts/heatmap_spec.js69
-rw-r--r--spec/javascripts/monitoring/components/dashboard_spec.js432
-rw-r--r--spec/javascripts/monitoring/mock_data.js1097
-rw-r--r--spec/javascripts/monitoring/panel_type_spec.js79
-rw-r--r--spec/javascripts/monitoring/shared/prometheus_header_spec.js26
-rw-r--r--spec/javascripts/monitoring/utils_spec.js38
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js301
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js114
-rw-r--r--spec/javascripts/notes/mock_data.js1256
-rw-r--r--spec/javascripts/notes/stores/collapse_utils_spec.js10
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js81
-rw-r--r--spec/javascripts/raven/raven_config_spec.js254
-rw-r--r--spec/javascripts/search_autocomplete_spec.js68
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js21
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js46
-rw-r--r--spec/javascripts/syntax_highlight_spec.js8
-rw-r--r--spec/javascripts/test_bundle.js33
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js18
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js26
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js74
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js45
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js9
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js120
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js167
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb399
-rw-r--r--spec/lib/api/helpers_spec.rb14
-rw-r--r--spec/lib/backup/repository_spec.rb2
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb71
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb48
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb2
-rw-r--r--spec/lib/bitbucket/representation/pull_request_spec.rb1
-rw-r--r--spec/lib/container_registry/client_spec.rb12
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb92
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb19
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb15
-rw-r--r--spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb12
-rw-r--r--spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb2
-rw-r--r--spec/lib/gitlab/badge/pipeline/status_spec.rb2
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb71
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/build/context/global_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/default_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/entry/files_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb31
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb94
-rw-r--r--spec/lib/gitlab/ci/config/entry/need_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/config/entry/needs_spec.rb84
-rw-r--r--spec/lib/gitlab/ci/config/entry/prefix_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb120
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/workflow_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/config/normalizer_spec.rb104
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb161
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb148
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb261
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb104
-rw-r--r--spec/lib/gitlab/ci/status/composite_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb338
-rw-r--r--spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb9
-rw-r--r--spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb29
-rw-r--r--spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb37
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb6
-rw-r--r--spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb16
-rw-r--r--spec/lib/gitlab/cycle_analytics/usage_data_spec.rb2
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb13
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb18
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb7
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb26
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb1
-rw-r--r--spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb9
-rw-r--r--spec/lib/gitlab/devise_failure_spec.rb35
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb2
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb2
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb2
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb261
-rw-r--r--spec/lib/gitlab/external_authorization/access_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/logger_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization/response_spec.rb2
-rw-r--r--spec/lib/gitlab/external_authorization_spec.rb2
-rw-r--r--spec/lib/gitlab/fake_application_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/file_detector_spec.rb2
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb6
-rw-r--r--spec/lib/gitlab/fogbugz_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb18
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb2
-rw-r--r--spec/lib/gitlab/git_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/blob_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/health_check_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb8
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/util_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/caching_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/issues_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/labels_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/note_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/notes_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/releases_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/issuable_finder_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/label_finder_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/markdown_text_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/milestone_finder_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/page_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/parallel_scheduling_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/note_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/to_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation/user_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/representation_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/sequential_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/user_finder_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import_spec.rb2
-rw-r--r--spec/lib/gitlab/gl_repository_spec.rb2
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb10
-rw-r--r--spec/lib/gitlab/gpg_spec.rb98
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb51
-rw-r--r--spec/lib/gitlab/graphql/connections/filterable_array_connection_spec.rb26
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition_spec.rb56
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/conditions/null_condition_spec.rb42
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb281
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb127
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/order_info_spec.rb81
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset/query_builder_spec.rb108
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb117
-rw-r--r--spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb20
-rw-r--r--spec/lib/gitlab/group_search_results_spec.rb2
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb8
-rw-r--r--spec/lib/gitlab/health_checks/master_check_spec.rb49
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/http_io_spec.rb2
-rw-r--r--spec/lib/gitlab/http_spec.rb2
-rw-r--r--spec/lib/gitlab/i18n_spec.rb2
-rw-r--r--spec/lib/gitlab/identifier_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml13
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group_project_object_builder_spec.rb36
-rw-r--r--spec/lib/gitlab/import_export/group_tree_saver_spec.rb180
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb114
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb17
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_saver_spec.rb42
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml13
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/shared_spec.rb2
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb2
-rw-r--r--spec/lib/gitlab/insecure_key_fingerprint_spec.rb2
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb37
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/issuable_sorter_spec.rb2
-rw-r--r--spec/lib/gitlab/issuables_count_for_state_spec.rb2
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb2
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb33
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb27
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb2
-rw-r--r--spec/lib/gitlab/language_detection_spec.rb2
-rw-r--r--spec/lib/gitlab/lazy_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb11
-rw-r--r--spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb106
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb85
-rw-r--r--spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb76
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb4
-rw-r--r--spec/lib/gitlab/pagination/offset_pagination_spec.rb215
-rw-r--r--spec/lib/gitlab/phabricator_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/project_authorizations_spec.rb167
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb14
-rw-r--r--spec/lib/gitlab/project_template_spec.rb3
-rw-r--r--spec/lib/gitlab/prometheus/internal_spec.rb108
-rw-r--r--spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb13
-rw-r--r--spec/lib/gitlab/regex_spec.rb19
-rw-r--r--spec/lib/gitlab/search/found_blob_spec.rb24
-rw-r--r--spec/lib/gitlab/shell_spec.rb76
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb113
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb5
-rw-r--r--spec/lib/gitlab/slash_commands/issue_comment_spec.rb117
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/access_spec.rb10
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb37
-rw-r--r--spec/lib/gitlab/sourcegraph_spec.rb66
-rw-r--r--spec/lib/gitlab/sql/recursive_cte_spec.rb2
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb4
-rw-r--r--spec/lib/gitlab/tracking_spec.rb19
-rw-r--r--spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb34
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb54
-rw-r--r--spec/lib/gitlab/user_access_spec.rb2
-rw-r--r--spec/lib/gitlab/utils/deep_size_spec.rb6
-rw-r--r--spec/lib/gitlab/visibility_level_checker_spec.rb2
-rw-r--r--spec/lib/gitlab/wiki_file_finder_spec.rb2
-rw-r--r--spec/lib/gitlab_spec.rb42
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb3
-rw-r--r--spec/lib/grafana/client_spec.rb26
-rw-r--r--spec/lib/omni_auth/strategies/saml_spec.rb2
-rw-r--r--spec/lib/prometheus/pid_provider_spec.rb12
-rw-r--r--spec/lib/quality/helm_client_spec.rb20
-rw-r--r--spec/lib/quality/kubernetes_client_spec.rb52
-rw-r--r--spec/lib/sentry/client_spec.rb9
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb2
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb2
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb2
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/emails/releases_spec.rb1
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/mailers/repository_check_mailer_spec.rb2
-rw-r--r--spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb4
-rw-r--r--spec/migrations/active_record/schema_spec.rb2
-rw-r--r--spec/migrations/add_default_and_free_plans_spec.rb34
-rw-r--r--spec/migrations/add_foreign_keys_to_todos_spec.rb2
-rw-r--r--spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb2
-rw-r--r--spec/migrations/add_pages_access_level_to_project_feature_spec.rb2
-rw-r--r--spec/migrations/add_pipeline_build_foreign_key_spec.rb2
-rw-r--r--spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb2
-rw-r--r--spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb2
-rw-r--r--spec/migrations/backfill_store_project_full_path_in_repo_spec.rb8
-rw-r--r--spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb2
-rw-r--r--spec/migrations/cleanup_build_stage_migration_spec.rb2
-rw-r--r--spec/migrations/cleanup_environments_external_url_spec.rb2
-rw-r--r--spec/migrations/cleanup_stages_position_migration_spec.rb2
-rw-r--r--spec/migrations/create_missing_namespace_for_internal_users_spec.rb2
-rw-r--r--spec/migrations/drop_duplicate_protected_tags_spec.rb2
-rw-r--r--spec/migrations/enqueue_verify_pages_domain_workers_spec.rb2
-rw-r--r--spec/migrations/fill_empty_finished_at_in_deployments_spec.rb2
-rw-r--r--spec/migrations/fill_file_store_spec.rb4
-rw-r--r--spec/migrations/fill_productivity_analytics_start_date_spec.rb39
-rw-r--r--spec/migrations/fix_wrong_pages_access_level_spec.rb4
-rw-r--r--spec/migrations/generate_lets_encrypt_private_key_spec.rb2
-rw-r--r--spec/migrations/generate_missing_routes_spec.rb2
-rw-r--r--spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_legacy_artifacts_to_job_artifacts_spec.rb4
-rw-r--r--spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb2
-rw-r--r--spec/migrations/move_limits_from_plans_spec.rb37
-rw-r--r--spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb2
-rw-r--r--spec/migrations/remove_empty_github_service_templates_spec.rb55
-rw-r--r--spec/migrations/remove_redundant_pipeline_stages_spec.rb2
-rw-r--r--spec/migrations/reschedule_builds_stages_migration_spec.rb2
-rw-r--r--spec/migrations/reschedule_commits_count_for_merge_request_diff_spec.rb2
-rw-r--r--spec/migrations/schedule_digest_personal_access_tokens_spec.rb4
-rw-r--r--spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb4
-rw-r--r--spec/migrations/schedule_runners_token_encryption_spec.rb2
-rw-r--r--spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb4
-rw-r--r--spec/migrations/schedule_stages_index_migration_spec.rb2
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_spec.rb4
-rw-r--r--spec/migrations/schedule_to_archive_legacy_traces_spec.rb4
-rw-r--r--spec/migrations/truncate_user_fullname_spec.rb2
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb10
-rw-r--r--spec/models/application_setting_spec.rb79
-rw-r--r--spec/models/aws/role_spec.rb52
-rw-r--r--spec/models/ci/build_spec.rb139
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb54
-rw-r--r--spec/models/ci/pipeline_spec.rb319
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb2
-rw-r--r--spec/models/clusters/applications/crossplane_spec.rb57
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb179
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb38
-rw-r--r--spec/models/clusters/cluster_spec.rb160
-rw-r--r--spec/models/clusters/clusters_hierarchy_spec.rb40
-rw-r--r--spec/models/clusters/providers/aws_spec.rb62
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb16
-rw-r--r--spec/models/commit_status_spec.rb4
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb20
-rw-r--r--spec/models/concerns/from_union_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb28
-rw-r--r--spec/models/concerns/noteable_spec.rb44
-rw-r--r--spec/models/concerns/redactable_spec.rb38
-rw-r--r--spec/models/concerns/subscribable_spec.rb56
-rw-r--r--spec/models/container_repository_spec.rb32
-rw-r--r--spec/models/deployment_merge_request_spec.rb14
-rw-r--r--spec/models/deployment_spec.rb80
-rw-r--r--spec/models/environment_spec.rb189
-rw-r--r--spec/models/environment_status_spec.rb2
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb22
-rw-r--r--spec/models/evidence_spec.rb2
-rw-r--r--spec/models/grafana_integration_spec.rb31
-rw-r--r--spec/models/group_group_link_spec.rb36
-rw-r--r--spec/models/group_spec.rb122
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/issue_spec.rb13
-rw-r--r--spec/models/lfs_object_spec.rb12
-rw-r--r--spec/models/merge_request_diff_spec.rb8
-rw-r--r--spec/models/merge_request_spec.rb262
-rw-r--r--spec/models/milestone_spec.rb11
-rw-r--r--spec/models/namespace_spec.rb38
-rw-r--r--spec/models/personal_snippet_spec.rb19
-rw-r--r--spec/models/project_import_state_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb39
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb6
-rw-r--r--spec/models/project_services/data_fields_spec.rb18
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb31
-rw-r--r--spec/models/project_snippet_spec.rb21
-rw-r--r--spec/models/project_spec.rb82
-rw-r--r--spec/models/release_spec.rb20
-rw-r--r--spec/models/releases/source_spec.rb2
-rw-r--r--spec/models/remote_mirror_spec.rb2
-rw-r--r--spec/models/service_spec.rb20
-rw-r--r--spec/models/shard_spec.rb3
-rw-r--r--spec/models/snippet_spec.rb37
-rw-r--r--spec/models/spam_log_spec.rb2
-rw-r--r--spec/models/todo_spec.rb47
-rw-r--r--spec/models/user_spec.rb24
-rw-r--r--spec/models/wiki_page_spec.rb11
-rw-r--r--spec/models/zoom_meeting_spec.rb154
-rw-r--r--spec/policies/application_setting/term_policy_spec.rb2
-rw-r--r--spec/policies/base_policy_spec.rb43
-rw-r--r--spec/policies/ci/build_policy_spec.rb2
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb2
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb2
-rw-r--r--spec/policies/ci/trigger_policy_spec.rb2
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb2
-rw-r--r--spec/policies/deploy_key_policy_spec.rb2
-rw-r--r--spec/policies/deploy_token_policy_spec.rb2
-rw-r--r--spec/policies/environment_policy_spec.rb2
-rw-r--r--spec/policies/global_policy_spec.rb2
-rw-r--r--spec/policies/group_policy_spec.rb2
-rw-r--r--spec/policies/issuable_policy_spec.rb2
-rw-r--r--spec/policies/issue_policy_spec.rb2
-rw-r--r--spec/policies/merge_request_policy_spec.rb2
-rw-r--r--spec/policies/namespace_policy_spec.rb2
-rw-r--r--spec/policies/note_policy_spec.rb2
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb32
-rw-r--r--spec/policies/project_policy_spec.rb27
-rw-r--r--spec/policies/project_snippet_policy_spec.rb2
-rw-r--r--spec/policies/protected_branch_policy_spec.rb2
-rw-r--r--spec/policies/resource_label_event_policy_spec.rb2
-rw-r--r--spec/policies/user_policy_spec.rb2
-rw-r--r--spec/presenters/ci/bridge_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb4
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/group_variable_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/trigger_presenter_spec.rb2
-rw-r--r--spec/presenters/ci/variable_presenter_spec.rb2
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb2
-rw-r--r--spec/presenters/commit_status_presenter_spec.rb2
-rw-r--r--spec/presenters/conversational_development_index/metric_presenter_spec.rb2
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb6
-rw-r--r--spec/presenters/group_member_presenter_spec.rb2
-rw-r--r--spec/presenters/instance_clusterable_presenter_spec.rb37
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb2
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb6
-rw-r--r--spec/presenters/project_member_presenter_spec.rb2
-rw-r--r--spec/presenters/project_presenter_spec.rb62
-rw-r--r--spec/presenters/projects/settings/deploy_keys_presenter_spec.rb2
-rw-r--r--spec/presenters/release_presenter_spec.rb101
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/applications_spec.rb2
-rw-r--r--spec/requests/api/avatar_spec.rb2
-rw-r--r--spec/requests/api/award_emoji_spec.rb2
-rw-r--r--spec/requests/api/badges_spec.rb2
-rw-r--r--spec/requests/api/boards_spec.rb2
-rw-r--r--spec/requests/api/branches_spec.rb23
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb2
-rw-r--r--spec/requests/api/commit_statuses_spec.rb6
-rw-r--r--spec/requests/api/commits_spec.rb45
-rw-r--r--spec/requests/api/deploy_keys_spec.rb2
-rw-r--r--spec/requests/api/deployments_spec.rb91
-rw-r--r--spec/requests/api/discussions_spec.rb2
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb2
-rw-r--r--spec/requests/api/environments_spec.rb2
-rw-r--r--spec/requests/api/events_spec.rb2
-rw-r--r--spec/requests/api/features_spec.rb17
-rw-r--r--spec/requests/api/files_spec.rb2
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb48
-rw-r--r--spec/requests/api/graphql/current_user_query_spec.rb33
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb134
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb108
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb79
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb66
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb63
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/todos/mark_done_spec.rb97
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb141
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb2
-rw-r--r--spec/requests/api/group_boards_spec.rb2
-rw-r--r--spec/requests/api/group_clusters_spec.rb21
-rw-r--r--spec/requests/api/group_container_repositories_spec.rb10
-rw-r--r--spec/requests/api/group_export_spec.rb94
-rw-r--r--spec/requests/api/group_milestones_spec.rb2
-rw-r--r--spec/requests/api/group_variables_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb2
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/import_github_spec.rb2
-rw-r--r--spec/requests/api/internal/base_spec.rb12
-rw-r--r--spec/requests/api/jobs_spec.rb10
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/labels_spec.rb2
-rw-r--r--spec/requests/api/lint_spec.rb2
-rw-r--r--spec/requests/api/markdown_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb16
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb75
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/notes_spec.rb2
-rw-r--r--spec/requests/api/notification_settings_spec.rb2
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb2
-rw-r--r--spec/requests/api/pages/internal_access_spec.rb2
-rw-r--r--spec/requests/api/pages/private_access_spec.rb2
-rw-r--r--spec/requests/api/pages/public_access_spec.rb2
-rw-r--r--spec/requests/api/pages_domains_spec.rb96
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb2
-rw-r--r--spec/requests/api/project_clusters_spec.rb21
-rw-r--r--spec/requests/api/project_container_repositories_spec.rb9
-rw-r--r--spec/requests/api/project_events_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb4
-rw-r--r--spec/requests/api/project_hooks_spec.rb2
-rw-r--r--spec/requests/api/project_import_spec.rb4
-rw-r--r--spec/requests/api/project_milestones_spec.rb2
-rw-r--r--spec/requests/api/project_snapshots_spec.rb2
-rw-r--r--spec/requests/api/project_snippets_spec.rb2
-rw-r--r--spec/requests/api/project_templates_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb119
-rw-r--r--spec/requests/api/protected_branches_spec.rb2
-rw-r--r--spec/requests/api/protected_tags_spec.rb2
-rw-r--r--spec/requests/api/releases_spec.rb2
-rw-r--r--spec/requests/api/repositories_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb6
-rw-r--r--spec/requests/api/runners_spec.rb2
-rw-r--r--spec/requests/api/search_spec.rb3
-rw-r--r--spec/requests/api/services_spec.rb6
-rw-r--r--spec/requests/api/settings_spec.rb82
-rw-r--r--spec/requests/api/sidekiq_metrics_spec.rb6
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/api/system_hooks_spec.rb2
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/templates_spec.rb2
-rw-r--r--spec/requests/api/todos_spec.rb2
-rw-r--r--spec/requests/api/triggers_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb34
-rw-r--r--spec/requests/api/variables_spec.rb2
-rw-r--r--spec/requests/api/version_spec.rb2
-rw-r--r--spec/requests/api/wikis_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb36
-rw-r--r--spec/requests/groups/milestones_controller_spec.rb2
-rw-r--r--spec/requests/groups/registry/repositories_controller_spec.rb36
-rw-r--r--spec/requests/health_controller_spec.rb227
-rw-r--r--spec/requests/jwt_controller_spec.rb2
-rw-r--r--spec/requests/lfs_locks_api_spec.rb2
-rw-r--r--spec/requests/oauth_tokens_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb10
-rw-r--r--spec/requests/rack_attack_global_spec.rb64
-rw-r--r--spec/requests/request_profiler_spec.rb2
-rw-r--r--spec/routing/admin_routing_spec.rb2
-rw-r--r--spec/routing/environments_spec.rb2
-rw-r--r--spec/routing/group_routing_spec.rb2
-rw-r--r--spec/routing/import_routing_spec.rb2
-rw-r--r--spec/routing/notifications_routing_spec.rb2
-rw-r--r--spec/routing/openid_connect_spec.rb2
-rw-r--r--spec/routing/project_routing_spec.rb8
-rw-r--r--spec/routing/routing_spec.rb29
-rw-r--r--spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb2
-rw-r--r--spec/rubocop/cop/avoid_return_from_blocks_spec.rb2
-rw-r--r--spec/rubocop/cop/destroy_all_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/httparty_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/predicate_memoization_spec.rb2
-rw-r--r--spec/rubocop/cop/group_public_or_visible_to_user_spec.rb2
-rw-r--r--spec/rubocop/cop/include_sidekiq_worker_spec.rb2
-rw-r--r--spec/rubocop/cop/line_break_around_conditional_block_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_reference_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/add_timestamps_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/datetime_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/hash_index_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/remove_column_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/remove_concurrent_index_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/remove_index_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/safer_boolean_column_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/timestamps_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/update_column_in_batches_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/update_large_table_spec.rb2
-rw-r--r--spec/rubocop/cop/project_path_helper_spec.rb2
-rw-r--r--spec/rubocop/cop/rspec/any_instance_of_spec.rb61
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb2
-rw-r--r--spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb2
-rw-r--r--spec/rubocop/cop/sidekiq_options_queue_spec.rb2
-rw-r--r--spec/serializers/blob_entity_spec.rb12
-rw-r--r--spec/serializers/diff_file_base_entity_spec.rb15
-rw-r--r--spec/serializers/diff_file_entity_spec.rb7
-rw-r--r--spec/serializers/issuable_sidebar_extras_entity_spec.rb20
-rw-r--r--spec/serializers/job_artifact_report_entity_spec.rb2
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb19
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb22
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb5
-rw-r--r--spec/services/ci/cancel_user_pipelines_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service/cache_spec.rb168
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb272
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb114
-rw-r--r--spec/services/ci/find_exposed_artifacts_service_spec.rb147
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb22
-rw-r--r--spec/services/ci/register_job_service_spec.rb51
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb28
-rw-r--r--spec/services/clusters/aws/fetch_credentials_service_spec.rb68
-rw-r--r--spec/services/clusters/aws/finalize_creation_service_spec.rb124
-rw-r--r--spec/services/clusters/aws/provision_service_spec.rb131
-rw-r--r--spec/services/clusters/aws/proxy_service_spec.rb210
-rw-r--r--spec/services/clusters/aws/verify_provision_status_service_spec.rb76
-rw-r--r--spec/services/clusters/destroy_service_spec.rb56
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb2
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb45
-rw-r--r--spec/services/clusters/update_service_spec.rb127
-rw-r--r--spec/services/concerns/merge_requests/assigns_merge_params_spec.rb2
-rw-r--r--spec/services/create_branch_service_spec.rb15
-rw-r--r--spec/services/deployments/after_create_service_spec.rb34
-rw-r--r--spec/services/deployments/link_merge_requests_service_spec.rb121
-rw-r--r--spec/services/deployments/update_service_spec.rb52
-rw-r--r--spec/services/error_tracking/issue_details_service_spec.rb48
-rw-r--r--spec/services/error_tracking/issue_latest_event_service_spec.rb48
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb91
-rw-r--r--spec/services/error_tracking/list_projects_service_spec.rb2
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb2
-rw-r--r--spec/services/git/branch_push_service_spec.rb20
-rw-r--r--spec/services/groups/destroy_service_spec.rb18
-rw-r--r--spec/services/groups/group_links/create_service_spec.rb119
-rw-r--r--spec/services/groups/group_links/destroy_service_spec.rb63
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb55
-rw-r--r--spec/services/groups/transfer_service_spec.rb30
-rw-r--r--spec/services/groups/update_service_spec.rb61
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb6
-rw-r--r--spec/services/issues/update_service_spec.rb40
-rw-r--r--spec/services/issues/zoom_link_service_spec.rb162
-rw-r--r--spec/services/members/destroy_service_spec.rb2
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb4
-rw-r--r--spec/services/merge_requests/build_service_spec.rb34
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb24
-rw-r--r--spec/services/merge_requests/create_service_spec.rb6
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb57
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb106
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb8
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb2
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb80
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb20
-rw-r--r--spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb177
-rw-r--r--spec/services/metrics/dashboard/project_dashboard_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/system_dashboard_service_spec.rb3
-rw-r--r--spec/services/namespaces/statistics_refresher_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb25
-rw-r--r--spec/services/projects/after_rename_service_spec.rb2
-rw-r--r--spec/services/projects/container_repository/delete_tags_service_spec.rb47
-rw-r--r--spec/services/projects/destroy_service_spec.rb18
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/base_attachment_service_spec.rb56
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb32
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migration_service_spec.rb22
-rw-r--r--spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_service_spec.rb17
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb2
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb30
-rw-r--r--spec/services/projects/update_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb252
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb2
-rw-r--r--spec/services/system_notes/merge_requests_service_spec.rb243
-rw-r--r--spec/services/users/signup_service_spec.rb64
-rw-r--r--spec/services/zoom_notes_service_spec.rb81
-rw-r--r--spec/sidekiq/cron/job_gem_dependency_spec.rb2
-rw-r--r--spec/spec_helper.rb5
-rw-r--r--spec/support/capybara.rb6
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb8
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb2
-rwxr-xr-xspec/support/generate-seed-repo-rb7
-rw-r--r--spec/support/helpers/access_matchers_helpers.rb95
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb4
-rw-r--r--spec/support/helpers/grafana_api_helpers.rb41
-rw-r--r--spec/support/helpers/graphql_helpers.rb21
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb220
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/smime_helper.rb2
-rw-r--r--spec/support/helpers/stub_experiments.rb14
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb9
-rw-r--r--spec/support/helpers/test_env.rb41
-rw-r--r--spec/support/import_export/common_util.rb7
-rw-r--r--spec/support/matchers/access_matchers_for_request.rb53
-rw-r--r--spec/support/matchers/access_matchers_generic.rb66
-rw-r--r--spec/support/matchers/db_schema_matchers.rb32
-rwxr-xr-xspec/support/prepare-gitlab-git-test-for-commit1
-rw-r--r--spec/support/shared_examples/ci/auto_merge_merge_requests_examples.rb40
-rw-r--r--spec/support/shared_examples/container_repositories_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/cycle_analytics_event_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb137
-rw-r--r--spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/file_finder.rb10
-rw-r--r--spec/support/shared_examples/graphql/connection_paged_nodes.rb28
-rw-r--r--spec/support/shared_examples/graphql/sort_enum_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb105
-rw-r--r--spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb17
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/issuable_shared_examples.rb (renamed from spec/support/shared_examples/models/concern/issuable_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/models/concerns/redactable_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb23
-rw-r--r--spec/support/shared_examples/requests/api/notes.rb2
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb118
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_examples.rb33
-rw-r--r--spec/support/shared_examples/services/error_tracking_service_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/updating_mentions_shared_examples.rb26
-rw-r--r--spec/support/sidekiq.rb21
-rwxr-xr-xspec/support/unpack-gitlab-git-test6
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb27
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb8
-rw-r--r--spec/views/admin/application_settings/integrations.html.haml_spec.rb34
-rw-r--r--spec/views/devise/sessions/new.html.haml_spec.rb71
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb2
-rw-r--r--spec/views/profiles/preferences/show.html.haml_spec.rb72
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb1
-rw-r--r--spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb38
-rw-r--r--spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pages_domains/show.html.haml_spec.rb34
-rw-r--r--spec/views/projects/show.html.haml_spec.rb41
-rw-r--r--spec/views/projects/tree/_tree_header.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb48
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb13
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb37
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb51
-rw-r--r--spec/workers/group_export_worker_spec.rb29
-rw-r--r--spec/workers/hashed_storage/migrator_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/rollbacker_worker_spec.rb2
-rw-r--r--spec/workers/merge_worker_spec.rb1
-rw-r--r--spec/workers/new_note_worker_spec.rb19
-rw-r--r--spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb3
-rw-r--r--spec/workers/project_cache_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb57
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb21
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb2
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb15
-rw-r--r--vendor/aws/cloudformation/eks_cluster.yaml340
-rw-r--r--vendor/crossplane/values.yaml0
-rw-r--r--vendor/elastic_stack/values.yaml47
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/C++.gitignore0
-rw-r--r--[-rwxr-xr-x]vendor/gitignore/Java.gitignore0
-rw-r--r--vendor/ingress/modsecurity.conf274
-rw-r--r--vendor/project_templates/serverless_framework.tar.gzbin0 -> 92193 bytes
-rw-r--r--yarn.lock1654
3225 files changed, 93706 insertions, 25748 deletions
diff --git a/.gitignore b/.gitignore
index 65befc20963..b8cbfe9966d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,7 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
+/spec/examples.txt
/rails_best_practices_output.html
/tags
/vendor/bundle/*
@@ -82,3 +83,4 @@ jsdoc/
**/tmp/rubocop_cache/**
.overcommit.yml
.projections.json
+/qa/.rakeTasks
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 630c82bcc5c..36108d04e9c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,7 @@
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33"
stages:
+ - sync
- prepare
- quick-test
- test
@@ -8,7 +9,6 @@ stages:
- review
- qa
- post-test
- - notification
- pages
variables:
@@ -33,7 +33,6 @@ include:
- local: .gitlab/ci/frontend.gitlab-ci.yml
- local: .gitlab/ci/global.gitlab-ci.yml
- local: .gitlab/ci/memory.gitlab-ci.yml
- - local: .gitlab/ci/notifications.gitlab-ci.yml
- local: .gitlab/ci/pages.gitlab-ci.yml
- local: .gitlab/ci/qa.gitlab-ci.yml
- local: .gitlab/ci/reports.gitlab-ci.yml
@@ -42,3 +41,4 @@ include:
- local: .gitlab/ci/setup.gitlab-ci.yml
- local: .gitlab/ci/test-metadata.gitlab-ci.yml
- local: .gitlab/ci/yaml.gitlab-ci.yml
+ - local: .gitlab/ci/releases.gitlab-ci.yml
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index a02740373da..c8283326533 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -3,11 +3,12 @@
*.rake @gitlab-org/maintainers/rails-backend
# Technical writing team are the default reviewers for everything in `doc/`
-/doc/ @axil @marcia @eread @mikelewis
+/doc/ @gl-docsteam
# Frontend maintainers should see everything in `app/assets/`
-app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
-*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
+app/assets/ @gitlab-org/maintainers/frontend
+*.scss @annabeldunstone @gitlab-org/maintainers/frontend
+/scripts/frontend/ @gitlab-org/maintainers/frontend
# Database maintainers should review changes in `db/`
db/ @gitlab-org/maintainers/database
@@ -32,4 +33,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database
/.gitlab/ci/ @gl-quality/eng-prod
Dangerfile @gl-quality/eng-prod
/danger/ @gl-quality/eng-prod
+/lib/gitlab/danger/ @gl-quality/eng-prod
/scripts/ @gl-quality/eng-prod
diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml
index 35859a1ab33..bd11042eb11 100644
--- a/.gitlab/ci/cng.gitlab-ci.yml
+++ b/.gitlab/ci/cng.gitlab-ci.yml
@@ -1,4 +1,5 @@
cloud-native-image:
+ extends: .only:variables-canonical-dot-com
image: ruby:2.6-alpine
dependencies: []
stage: post-test
@@ -12,5 +13,3 @@ cloud-native-image:
only:
refs:
- tags
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 14eeebb9db9..07375fca611 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -2,12 +2,11 @@
extends:
- .default-tags
- .default-retry
- - .only-docs-changes
+ - .only:variables-canonical-dot-com
+ - .only:changes-docs
only:
refs:
- merge_requests
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
image: ruby:2.6-alpine
stage: review
dependencies: []
@@ -50,7 +49,7 @@ docs lint:
- .default-tags
- .default-retry
- .default-only
- - .only-docs-changes
+ - .only:changes-docs
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-docs-lint"
stage: test
dependencies: []
@@ -68,7 +67,7 @@ docs lint:
# Check the internal anchor links
- bundle exec nanoc check internal_anchors
-graphql-docs-verify:
+graphql-reference-verify:
extends:
- .only-ee
- .default-tags
@@ -76,10 +75,10 @@ graphql-docs-verify:
- .default-cache
- .default-only
- .default-before_script
- - .only-graphql-changes
- variables:
- SETUP_DB: "false"
+ - .only:changes-code-backstage-qa
+ - .use-pg9
stage: test
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 2f457bc0ee2..0b72461a9fd 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -12,7 +12,7 @@
- .default-only
- .default-before_script
- .assets-compile-cache
- - .only-code-qa-changes
+ - .only:changes-code-backstage-qa
image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-git-2.22-chrome-73.0-node-12.x-yarn-1.16-graphicsmagick-1.3.33-docker-18.06.1
stage: test
dependencies: ["setup-test-env"]
@@ -73,7 +73,7 @@ gitlab:assets:compile pull-cache:
- .default-only
- .default-before_script
- .assets-compile-cache
- - .only-code-qa-changes
+ - .only:changes-code-backstage-qa
- .use-pg9
stage: prepare
script:
@@ -128,7 +128,7 @@ compile-assets pull-cache foss:
- .default-cache
- .default-only
- .default-before_script
- - .only-code-changes
+ - .only:changes-code-backstage
- .use-pg9
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
@@ -205,7 +205,7 @@ jest-foss:
- .default-retry
- .default-cache
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
stage: test
dependencies: []
cache:
@@ -238,7 +238,7 @@ webpack-dev-server:
- .default-retry
- .default-cache
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
stage: test
needs: ["setup-test-env", "compile-assets pull-cache"]
dependencies: ["setup-test-env", "compile-assets pull-cache"]
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index fc9b00b5d3c..d746d8fe030 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -40,14 +40,97 @@
- merge_requests
- tags
-.only-code-changes:
+.only:variables-canonical-dot-com:
+ only:
+ variables:
+ - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ # Matches the gitlab-org group or its subgroups
+
+.only:variables_refs-canonical-dot-com-schedules:
+ extends: .only:variables-canonical-dot-com
+ only:
+ refs:
+ - schedules
+
+.except:refs-deploy:
+ except:
+ refs:
+ - /^\d+-\d+-auto-deploy-\d+$/
+
+.except:refs-master-tags-stable-deploy:
+ except:
+ refs:
+ - master
+ - tags
+ - /^[\d-]+-stable(-ee)?$/
+ - /^\d+-\d+-auto-deploy-\d+$/
+
+.only:kubernetes:
+ only:
+ kubernetes: active
+
+.only-review:
+ extends:
+ - .only:variables-canonical-dot-com
+ - .only:kubernetes
+ - .except:refs-master-tags-stable-deploy
+
+.only-review-schedules:
+ extends:
+ - .only:variables_refs-canonical-dot-com-schedules
+ - .only:kubernetes
+ - .except:refs-deploy
+
+.code-patterns: &code-patterns
+ - ".gitlab/ci/**/*"
+ - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
+ - ".csscomb.json"
+ - "Dockerfile.assets"
+ - "*_VERSION"
+ - "Gemfile{,.lock}"
+ - "Rakefile"
+ - "{babel.config,jest.config}.js"
+ - "config.ru"
+ - "{package.json,yarn.lock}"
+ - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
+
+.backstage-patterns: &backstage-patterns
+ - "Dangerfile"
+ - "danger/**/*"
+ - "{,ee/}fixtures/**/*"
+ - "{,ee/}rubocop/**/*"
+ - "{,ee/}spec/**/*"
+ - "doc/README.md" # Some RSpec test rely on this file
+
+.qa-patterns: &qa-patterns
+ - ".dockerignore"
+ - "qa/**/*"
+
+.docs-patterns: &docs-patterns
+ - ".gitlab/route-map.yml"
+ - "doc/**/*"
+ - ".markdownlint.json"
+
+.only:changes-code:
+ only:
+ changes: *code-patterns
+
+.only:changes-qa:
+ only:
+ changes: *qa-patterns
+
+.only:changes-docs:
+ only:
+ changes: *docs-patterns
+
+.only:changes-code-backstage:
only:
changes:
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- - "Dangerfile"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
@@ -55,36 +138,43 @@
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- - "{app,bin,config,danger,db,ee,fixtures,haml_lint,lib,locale,public,rubocop,scripts,spec,symbol,vendor}/**/*"
+ - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
+ # Backstage changes
+ - "Dangerfile"
+ - "danger/**/*"
+ - "{,ee/}fixtures/**/*"
+ - "{,ee/}rubocop/**/*"
+ - "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
-.only-qa-changes:
+.only:changes-code-qa:
only:
changes:
+ - ".gitlab/ci/**/*"
+ - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
+ - ".csscomb.json"
+ - "Dockerfile.assets"
+ - "*_VERSION"
+ - "Gemfile{,.lock}"
+ - "Rakefile"
+ - "{babel.config,jest.config}.js"
+ - "config.ru"
+ - "{package.json,yarn.lock}"
+ - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
+ # QA changes
- ".dockerignore"
- "qa/**/*"
-.only-docs-changes:
- only:
- changes:
- - ".gitlab/route-map.yml"
- - "doc/**/*"
- - ".markdownlint.json"
-
-.only-graphql-changes:
- only:
- changes:
- - "{,ee/}app/graphql/**/*"
- - "{,ee/}lib/gitlab/graphql/**/*"
-
-.only-code-qa-changes:
+.only:changes-code-backstage-qa:
only:
changes:
- ".gitlab/ci/**/*"
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
- ".csscomb.json"
- - "Dangerfile"
- "Dockerfile.assets"
- "*_VERSION"
- "Gemfile{,.lock}"
@@ -92,36 +182,19 @@
- "{babel.config,jest.config}.js"
- "config.ru"
- "{package.json,yarn.lock}"
- - "{app,bin,config,danger,db,ee,fixtures,haml_lint,lib,locale,public,rubocop,scripts,spec,symbol,vendor}/**/*"
+ - "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
+ - "doc/api/graphql/**/*"
+ # Backstage changes
+ - "Dangerfile"
+ - "danger/**/*"
+ - "{,ee/}fixtures/**/*"
+ - "{,ee/}rubocop/**/*"
+ - "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
+ # QA changes
- ".dockerignore"
- "qa/**/*"
-.only-review:
- only:
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
- kubernetes: active
- except:
- refs:
- - master
- - /^\d+-\d+-auto-deploy-\d+$/
- - /^[\d-]+-stable(-ee)?$/
-
-.only-review-schedules:
- only:
- refs:
- - schedules
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
- kubernetes: active
-
-.only-canonical-schedules:
- only:
- refs:
- - schedules@gitlab-org/gitlab
- - schedules@gitlab-org/gitlab-foss
-
.use-pg9:
services:
- name: postgres:9.6
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml
index 93bf87b24b2..ba14024df34 100644
--- a/.gitlab/ci/memory.gitlab-ci.yml
+++ b/.gitlab/ci/memory.gitlab-ci.yml
@@ -5,7 +5,7 @@
- .default-cache
- .default-only
- .default-before_script
- - .only-code-changes
+ - .only:changes-code
memory-static:
extends: .only-code-memory-job-base
diff --git a/.gitlab/ci/notifications.gitlab-ci.yml b/.gitlab/ci/notifications.gitlab-ci.yml
deleted file mode 100644
index 8e00ba022d0..00000000000
--- a/.gitlab/ci/notifications.gitlab-ci.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-.notify:
- image: alpine
- stage: notification
- dependencies: []
- cache: {}
- before_script:
- - apk update && apk add git curl bash
-
-schedule:package-and-qa:notify-success:
- extends:
- - .only-canonical-schedules
- - .notify
- variables:
- COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
- script:
- - 'scripts/notify-slack qa-master ":tada: Scheduled QA against master passed! :tada: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_passing'
- needs: ["schedule:package-and-qa"]
- when: on_success
-
-schedule:package-and-qa:notify-failure:
- extends:
- - .only-canonical-schedules
- - .notify
- variables:
- COMMIT_NOTES_URL: "https://$CI_SERVER_HOST/$CI_PROJECT_PATH/commit/$CI_COMMIT_SHA#notes-list"
- script:
- - 'scripts/notify-slack qa-master ":skull_and_crossbones: Scheduled QA against master failed! :skull_and_crossbones: See $CI_PIPELINE_URL. For downstream pipelines, see $COMMIT_NOTES_URL" ci_failing'
- needs: ["schedule:package-and-qa"]
- when: on_failure
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index a30772d5664..6a2d3702bdd 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -4,12 +4,11 @@ pages:
- .default-retry
- .default-cache
- .default-only
- - .only-code-qa-changes
+ - .only:variables-canonical-dot-com
+ - .only:changes-code-backstage-qa
only:
refs:
- master
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
stage: pages
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
script:
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 1194948a76f..3cb5a40a8b5 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -3,7 +3,7 @@
- .default-tags
- .default-retry
- .default-only
- - .only-code-qa-changes
+ - .only:changes-code-qa
stage: test
dependencies: []
cache:
@@ -31,7 +31,6 @@ qa:selectors-foss:
- .only-ee-as-if-foss
.package-and-qa-base:
- extends: .default-only
image: ruby:2.6-alpine
stage: qa
dependencies: []
@@ -40,35 +39,31 @@ qa:selectors-foss:
- source scripts/utils.sh
- install_gitlab_gem
- ./scripts/trigger-build omnibus
- only:
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/)/ # Matches the gitlab-org group or its subgroups
package-and-qa-manual:
extends:
- .package-and-qa-base
- - .only-code-changes
- except:
- refs:
- - master
- - /^\d+-\d+-auto-deploy-\d+$/
+ - .default-only
+ - .only:variables-canonical-dot-com
+ - .except:refs-deploy
+ - .only:changes-code
when: manual
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
package-and-qa:
extends:
- .package-and-qa-base
- - .only-qa-changes
- except:
- refs:
- - master
- - /^\d+-\d+-auto-deploy-\d+$/
+ - .default-only
+ - .only:variables-canonical-dot-com
+ - .except:refs-master-tags-stable-deploy
+ - .only:changes-qa
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
allow_failure: true
schedule:package-and-qa:
extends:
- .package-and-qa-base
- - .only-code-qa-changes
- - .only-canonical-schedules
+ - .default-only
+ - .only:variables_refs-canonical-dot-com-schedules
needs: ["build-qa-image", "gitlab:assets:compile pull-cache"]
+ allow_failure: true
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index bf478b68765..acee30867d9 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -22,7 +22,7 @@
- .default-cache
- .default-only
- .default-before_script
- - .only-code-changes
+ - .only:changes-code-backstage
.only-code-qa-rails-job-base:
extends:
@@ -31,7 +31,7 @@
- .default-cache
- .default-only
- .default-before_script
- - .only-code-qa-changes
+ - .only:changes-code-backstage-qa
setup-test-env:
extends:
@@ -239,6 +239,7 @@ static-analysis:
dependencies: ["setup-test-env", "compile-assets pull-cache"]
variables:
SETUP_DB: "false"
+ parallel: 2
script:
- scripts/static-analysis
cache:
@@ -251,13 +252,8 @@ static-analysis:
downtime_check:
extends:
- .rake-exec
- - .only-code-changes
- except:
- refs:
- - master
- - tags
- variables:
- - $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
+ - .only:changes-code-backstage
+ - .except:refs-master-tags-stable-deploy
stage: test
needs: ["setup-test-env"]
dependencies: ["setup-test-env"]
diff --git a/.gitlab/ci/releases.gitlab-ci.yml b/.gitlab/ci/releases.gitlab-ci.yml
new file mode 100644
index 00000000000..1ddc4e90fcf
--- /dev/null
+++ b/.gitlab/ci/releases.gitlab-ci.yml
@@ -0,0 +1,22 @@
+---
+
+# Syncs any changes pushed to a stable branch to the corresponding CE stable
+# branch. We run this prior to any tests so that random failures don't prevent a
+# sync.
+sync-stable-branch:
+ # We don't need/want any global before/after commands, so we overwrite these
+ # settings.
+ image: alpine:edge
+ stage: sync
+ # This job should only run on EE stable branches on the canonical GitLab.com
+ # repository.
+ only:
+ variables:
+ - $CI_SERVER_HOST == "gitlab.com"
+ refs:
+ - /^[\d-]+-stable-ee$/@gitlab-org/gitlab
+ before_script:
+ - apk add --no-cache --update curl bash
+ after_script: []
+ script:
+ - bash scripts/sync-stable-branch.sh
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 16c3f0e4f8c..fbb7826b6f2 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -11,7 +11,7 @@ code_quality:
extends:
- .default-retry
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
stage: test
image: docker:stable
allow_failure: true
@@ -50,7 +50,7 @@ sast:
extends:
- .default-retry
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage-qa
stage: test
image: docker:stable
variables:
@@ -132,7 +132,7 @@ dependency_scanning:
extends:
- .default-retry
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage-qa
stage: test
image: docker:stable
variables:
@@ -195,7 +195,7 @@ dast:
extends:
- .default-retry
- .default-only
- - .only-code-qa-changes
+ - .only:changes-code-qa
- .only-review
stage: qa
needs: ["review-deploy"]
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index c78c6a82815..4ed9ac03d0c 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -1,14 +1,8 @@
-.except-deploys:
- except:
- refs:
- - /^\d+-\d+-auto-deploy-\d+$/
-
.review-docker:
extends:
- .default-tags
- .default-retry
- .default-only
- - .except-deploys
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
services:
- docker:19.03.0-dind
@@ -23,10 +17,9 @@
build-qa-image:
extends:
- .review-docker
- - .only-code-qa-changes
- only:
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
+ - .only:variables-canonical-dot-com
+ - .except:refs-deploy
+ - .only:changes-code-qa
stage: prepare
script:
- '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
@@ -35,14 +28,11 @@ build-qa-image:
- echo "${CI_JOB_TOKEN}" | docker login --username gitlab-ci-token --password-stdin ${CI_REGISTRY}
- time docker push ${QA_IMAGE}
-schedule:review-cleanup:
+.base-review-cleanup:
extends:
- .default-tags
- .default-retry
- .default-only
- - .only-code-qa-changes
- - .only-review-schedules
- - .except-deploys
stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true
@@ -55,11 +45,22 @@ schedule:review-cleanup:
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
+schedule:review-cleanup:
+ extends:
+ - .base-review-cleanup
+ - .only-review-schedules
+
+manual:review-cleanup:
+ extends:
+ - .base-review-cleanup
+ - .only:changes-code-qa
+ when: manual
+
.review-build-cng-base:
extends:
+ - .default-tags
+ - .default-retry
- .default-only
- - .only-code-qa-changes
- - .except-deploys
image: ruby:2.6-alpine
stage: review-prepare
before_script:
@@ -74,6 +75,7 @@ review-build-cng:
extends:
- .review-build-cng-base
- .only-review
+ - .only:changes-code-qa
needs: ["gitlab:assets:compile pull-cache"]
schedule:review-build-cng:
@@ -82,26 +84,30 @@ schedule:review-build-cng:
- .only-review-schedules
needs: ["gitlab:assets:compile pull-cache"]
-.review-deploy-base:
+.review-workflow-base:
extends:
- .default-tags
- .default-retry
- .default-only
- - .only-code-qa-changes
- - .except-deploys
- stage: review
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
dependencies: []
- allow_failure: true
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "v2.3.7"
+ # v2.4.4 + two improvements:
+ # - Allow to pass an EE license when installing the chart: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1008
+ # - Allow to customize the livenessProbe for `gitlab-shell`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/1021
+ GITLAB_HELM_CHART_REF: "6c655ed77e60f1f7f533afb97bef8c9cb7dc61eb"
GITLAB_EDITION: "ce"
environment:
name: review/${CI_COMMIT_REF_NAME}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop
+
+.review-deploy-base:
+ extends: .review-workflow-base
+ stage: review
+ allow_failure: true
before_script:
- '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
@@ -112,21 +118,13 @@ schedule:review-build-cng:
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
script:
- - date
- check_kube_domain
- - date
- ensure_namespace
- - date
- install_tiller
- - date
- install_external_dns
- - date
- download_chart
- date
- deploy || (display_deployment_debug && exit 1)
- - date
- - add_license
- - date
artifacts:
paths: [review_app_url.txt]
expire_in: 2 days
@@ -136,6 +134,7 @@ review-deploy:
extends:
- .review-deploy-base
- .only-review
+ - .only:changes-code-qa
needs: ["review-build-cng"]
schedule:review-deploy:
@@ -144,11 +143,11 @@ schedule:review-deploy:
- .only-review-schedules
needs: ["schedule:review-build-cng"]
-review-stop:
+.base-review-stop:
extends:
- - .review-deploy-base
+ - .review-workflow-base
- .only-review
- when: manual
+ - .only:changes-code-qa
environment:
action: stop
variables:
@@ -161,24 +160,26 @@ review-stop:
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/utils.sh
- source utils.sh
- source review-apps.sh
- script:
- - delete_release
- artifacts:
- paths: []
-review-cleanup-failed-deployment:
- extends: review-stop
+review-stop-failed-deployment:
+ extends: .base-review-stop
stage: prepare
- when: on_success
- allow_failure: false
script:
- delete_failed_release
+review-stop:
+ extends: .base-review-stop
+ stage: review
+ when: manual
+ allow_failure: true
+ script:
+ - delete_release
+
.review-qa-base:
extends:
- .review-docker
- .only-review
- - .only-code-qa-changes
+ - .only:changes-code-qa
stage: qa
allow_failure: true
variables:
@@ -223,9 +224,7 @@ review-qa-all:
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation
.review-performance-base:
- extends:
- - .review-docker
- - .only-code-qa-changes
+ extends: .review-docker
stage: qa
allow_failure: true
before_script:
@@ -248,6 +247,7 @@ review-performance:
extends:
- .review-performance-base
- .only-review
+ - .only:changes-code-qa
needs: ["review-deploy"]
dependencies: ["review-deploy"]
before_script:
@@ -277,9 +277,8 @@ parallel-spec-reports:
extends:
- .default-tags
- .default-only
- - .only-code-qa-changes
- .only-review
- - .except-deploys
+ - .only:changes-code-qa
image: ruby:2.6-alpine
stage: post-test
dependencies: ["review-qa-all"]
@@ -310,18 +309,13 @@ danger-review:
- .default-retry
- .default-cache
- .default-only
+ - .except:refs-master-tags-stable-deploy
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
dependencies: []
only:
variables:
- $DANGER_GITLAB_API_TOKEN
- except:
- refs:
- - master
- variables:
- - $CI_COMMIT_REF_NAME =~ /^\d+-\d+-auto-deploy-\d+$/
- - $CI_COMMIT_REF_NAME =~ /^[\d-]+-stable(-ee)?$/
script:
- git version
- node --version
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index 861f3f1af5b..24267584393 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -6,7 +6,8 @@ cache gems:
- .default-retry
- .default-cache
- .default-before_script
- - .only-code-qa-changes
+ - .only:variables-canonical-dot-com
+ - .only:changes-code-backstage-qa
stage: test
dependencies: ["setup-test-env"]
needs: ["setup-test-env"]
@@ -21,15 +22,13 @@ cache gems:
refs:
- master
- tags
- variables:
- - $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org"
.minimal-job:
extends:
- .default-tags
- .default-retry
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
dependencies: []
gitlab_git_test:
diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml
index 6a7f3157d59..21af0d373bc 100644
--- a/.gitlab/ci/test-metadata.gitlab-ci.yml
+++ b/.gitlab/ci/test-metadata.gitlab-ci.yml
@@ -1,7 +1,7 @@
.tests-metadata-state:
extends:
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
variables:
TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
before_script:
@@ -48,7 +48,7 @@ flaky-examples-check:
- .default-tags
- .default-retry
- .default-only
- - .only-code-changes
+ - .only:changes-code-backstage
image: ruby:2.6-alpine
stage: post-test
variables:
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 3e634de4f0c..e06a6fb0cff 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -29,7 +29,7 @@ Set the title to: `Description of the original issue`
#### Documentation and final details
-- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
+- [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] 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)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index a2dd79ed1ab..2a7da2a436f 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -34,7 +34,7 @@ All reviewers can help ensure accuracy, clarity, completeness, and adherence to
**3. Maintainer**
1. [ ] Review by assigned maintainer, who can always request/require the above reviews. Maintainer's review can occur before or after a technical writer review.
-1. [ ] Ensure a release milestone is set and that you merge the equivalent EE MR before the CE MR if both exist.
+1. [ ] Ensure a release milestone is set.
1. [ ] If there has not been a technical writer review, [create an issue for one using the Doc Review template](https://gitlab.com/gitlab-org/gitlab/issues/new?issuable_template=Doc%20Review).
/label ~documentation
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 211ad359951..232a87c1981 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -416,9 +416,6 @@ linters:
- '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/_elasticsearch_form.html.haml'
- - 'ee/app/views/admin/application_settings/_slack.html.haml'
- - 'ee/app/views/admin/application_settings/_snowplow.html.haml'
- 'ee/app/views/admin/application_settings/_templates.html.haml'
- 'ee/app/views/admin/audit_logs/index.html.haml'
- 'ee/app/views/admin/dashboard/stats.html.haml'
@@ -495,7 +492,6 @@ linters:
- 'ee/app/views/projects/services/prometheus/_metrics.html.haml'
- 'ee/app/views/projects/settings/slacks/edit.html.haml'
- 'ee/app/views/shared/_additional_email_text.html.haml'
- - 'ee/app/views/shared/_geo_info_modal.html.haml'
- 'ee/app/views/shared/_mirror_update_button.html.haml'
- 'ee/app/views/shared/_shared_runners_minutes_limit.html.haml'
- 'ee/app/views/shared/audit_events/_event_table.html.haml'
diff --git a/.overcommit.yml.example b/.overcommit.yml.example
index 25823b9a8b3..9cd04825bc2 100644
--- a/.overcommit.yml.example
+++ b/.overcommit.yml.example
@@ -16,10 +16,25 @@
# Uncomment the following lines to make the configuration take effect.
PreCommit:
+ AuthorName:
+ enabled: false
+ EsLint:
+ enabled: true
+ # https://github.com/sds/overcommit/issues/338
+ command: './node_modules/eslint/bin/eslint.js'
+ HamlLint:
+ enabled: true
+ MergeConflicts:
+ enabled: true
+ exclude:
+ - '**/conflict/file_spec.rb'
+ - '**/git/conflict/parser_spec.rb'
+ # prettier? https://github.com/sds/overcommit/issues/614 https://github.com/sds/overcommit/issues/390#issuecomment-495703284
RuboCop:
enabled: true
# on_warn: fail # Treat all warnings as failures
-#
+ ScssLint:
+ enabled: true
#PostCheckout:
# ALL: # Special hook name that customizes all hooks of this type
# quiet: true # Change all post-checkout hooks to only display output on failure
diff --git a/.rubocop.yml b/.rubocop.yml
index 049340f90d4..1d5cf7642c2 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -56,7 +56,7 @@ Style/FrozenStringLiteralComment:
- 'qa/**/*'
- 'rubocop/**/*'
- 'scripts/**/*'
- - 'spec/**/*'
+ - 'spec/lib/gitlab/**/*'
RSpec/FilePath:
Exclude:
@@ -297,3 +297,6 @@ Graphql/Descriptions:
Include:
- 'app/graphql/**/*'
- 'ee/app/graphql/**/*'
+
+RSpec/AnyInstanceOf:
+ Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 3ed7af71b4f..f0388ab79d2 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -401,13 +401,6 @@ Rails/FilePath:
Rails/HasManyOrHasOneDependent:
Enabled: false
-# Offense count: 40
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: numeric, symbolic
-Rails/HttpStatus:
- Enabled: false
-
# Offense count: 2
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index b6156f8d254..9fa018b9719 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -4181,7 +4181,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Show hook errors for fast-forward merges. !1375
- Allow all parameters of group webhooks to be set through the UI. !1376
- Fix Elasticsearch queries when a group_id is specified. !1423
-- Check the right index mapping based on Rails environment for rake gitlab:elastic:add_feature_visiblity_levels_to_project. !1473
+- Check the right index mapping based on Rails environment for rake gitlab:elastic:add_feature_visibility_levels_to_project. !1473
- Fix issues with another milestone that has a matching list label could not be added to a board.
- Only admins or group owners can set LDAP overrides.
- Add support for load balancing database queries.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4de0606589f..fda536ae157 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,6 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
-## 12.4.3
-
-- No changes.
-
## 12.4.2
### Fixed (10 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 65ee0959841..bf480b0ade8 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.67.0
+ba4abcb75c7b70df662c6274df199fa261f32e11
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index 88c5fb891dc..bc80560fad6 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-1.4.0
+1.5.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 1cac385c6cb..0eed1a29efd 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.11.0
+1.12.0
diff --git a/Gemfile b/Gemfile
index 920f778c053..d27bc276088 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,12 +8,12 @@ gem 'bootsnap', '~> 1.4'
gem 'nakayoshi_fork', '~> 0.0.4'
# Responders respond_to and respond_with
-gem 'responders', '~> 2.0'
+gem 'responders', '~> 3.0'
gem 'sprockets', '~> 3.7.0'
# Default values for AR models
-gem 'default_value_for', '~> 3.2.0'
+gem 'default_value_for', '~> 3.3.0'
# Supported DBs
gem 'pg', '~> 1.1'
@@ -42,7 +42,7 @@ gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3'
-gem 'omniauth_openid_connect', '~> 0.3.1'
+gem 'omniauth_openid_connect', '~> 0.3.3'
gem "omniauth-ultraauth", '~> 0.0.2'
gem 'omniauth-salesforce', '~> 1.0.5'
gem 'rack-oauth2', '~> 1.9.3'
@@ -64,7 +64,7 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
-gem 'rubyzip', '~> 1.2.2', require: 'zip'
+gem 'rubyzip', '~> 1.3.0', require: 'zip'
# GitLab Pages letsencrypt support
gem 'acme-client', '~> 2.0.2'
@@ -72,7 +72,7 @@ gem 'acme-client', '~> 2.0.2'
gem 'browser', '~> 2.5'
# GPG
-gem 'gpgme', '~> 2.0.18'
+gem 'gpgme', '~> 2.0.19'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -136,7 +136,7 @@ gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.8'
-gem 'deckar01-task_list', '2.2.0'
+gem 'deckar01-task_list', '2.2.1'
gem 'gitlab-markup', '~> 1.7.0'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.17'
@@ -151,7 +151,7 @@ gem 'asciidoctor-plantuml', '0.0.9'
gem 'rouge', '~> 3.11.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
-gem 'nokogiri', '~> 1.10.4'
+gem 'nokogiri', '~> 1.10.5'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
@@ -159,6 +159,7 @@ gem 'icalendar'
# Diffs
gem 'diffy', '~> 3.1.0'
+gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.7'
@@ -175,7 +176,7 @@ group :puma do
end
# State machine
-gem 'state_machines-activerecord', '~> 0.5.1'
+gem 'state_machines-activerecord', '~> 0.6.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 6.0'
@@ -259,9 +260,6 @@ gem 'loofah', '~> 2.2'
# Working with license
gem 'licensee', '~> 8.9'
-# Protect against bruteforcing
-gem 'rack-attack', '~> 4.4.1'
-
# Ace editor
gem 'ace-rails-ap', '~> 4.1.0'
@@ -293,10 +291,13 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0"
+# Protect against bruteforcing
+gem 'rack-attack', '~> 6.2.0'
+
# Sentry integration
gem 'sentry-raven', '~> 2.9'
-gem 'premailer-rails', '~> 1.9.7'
+gem 'premailer-rails', '~> 1.10.3'
# LabKit: Tracing and Correlation
gem 'gitlab-labkit', '~> 0.5'
@@ -331,7 +332,6 @@ group :metrics do
end
group :development do
- gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
gem 'danger', '~> 6.0', require: false
@@ -388,7 +388,6 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
- gem 'license_finder', '~> 5.4', require: false
gem 'knapsack', '~> 1.17'
gem 'stackprof', '~> 0.2.10', require: false
@@ -398,6 +397,11 @@ group :development, :test do
gem 'timecop', '~> 0.8.0'
end
+# Gems required in omnibus-gitlab pipeline
+group :development, :test, :omnibus do
+ gem 'license_finder', '~> 5.4', require: false
+end
+
group :test do
gem 'shoulda-matchers', '~> 4.0.1', require: false
gem 'email_spec', '~> 2.2.0'
@@ -407,6 +411,7 @@ group :test do
gem 'concurrent-ruby', '~> 1.1'
gem 'test-prof', '~> 0.10.0'
gem 'rspec_junit_formatter'
+ gem 'guard-rspec'
end
gem 'octokit', '~> 4.9'
@@ -446,18 +451,18 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 1.65.0'
+gem 'gitaly', '~> 1.70.0'
-gem 'grpc', '~> 1.19.0'
+gem 'grpc', '~> 1.24.0'
-gem 'google-protobuf', '~> 3.7.1'
+gem 'google-protobuf', '~> 3.8.0'
gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles
-gem 'flipper', '~> 0.13.0'
-gem 'flipper-active_record', '~> 0.13.0'
-gem 'flipper-active_support_cache_store', '~> 0.13.0'
+gem 'flipper', '~> 0.17.1'
+gem 'flipper-active_record', '~> 0.17.1'
+gem 'flipper-active_support_cache_store', '~> 0.17.1'
gem 'unleash', '~> 0.1.5'
# Structured logging
@@ -469,3 +474,5 @@ gem 'gitlab-net-dns', '~> 0.9.1'
# Countries list
gem 'countries', '~> 3.0'
+
+gem 'retriable', '~> 3.1.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 18160932c56..15465cd6b03 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -50,8 +50,8 @@ GEM
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- acts-as-taggable-on (6.0.0)
- activerecord (~> 5.0)
+ acts-as-taggable-on (6.5.0)
+ activerecord (>= 5.0, < 6.1)
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
@@ -80,14 +80,16 @@ GEM
encryptor (~> 3.0.0)
attr_required (1.0.1)
awesome_print (1.8.0)
- aws-sdk (2.9.32)
- aws-sdk-resources (= 2.9.32)
- aws-sdk-core (2.9.32)
+ aws-eventstream (1.0.3)
+ aws-sdk (2.11.374)
+ aws-sdk-resources (= 2.11.374)
+ aws-sdk-core (2.11.374)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
- aws-sdk-resources (2.9.32)
- aws-sdk-core (= 2.9.32)
- aws-sigv4 (1.0.0)
+ aws-sdk-resources (2.11.374)
+ aws-sdk-core (= 2.11.374)
+ aws-sigv4 (1.1.0)
+ aws-eventstream (~> 1.0, >= 1.0.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@@ -171,9 +173,9 @@ GEM
unicode_utils (~> 1.4)
crack (0.4.3)
safe_yaml (~> 1.0.0)
- crass (1.0.4)
+ crass (1.0.5)
creole (0.5.0)
- css_parser (1.5.0)
+ css_parser (1.7.0)
addressable
daemons (1.2.6)
danger (6.0.9)
@@ -192,12 +194,12 @@ GEM
database_cleaner (1.7.0)
debug_inspector (0.0.3)
debugger-ruby_core_source (1.3.8)
- deckar01-task_list (2.2.0)
+ deckar01-task_list (2.2.1)
html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
- default_value_for (3.2.0)
- activerecord (>= 3.2.0, < 6.0)
+ default_value_for (3.3.0)
+ activerecord (>= 3.2.0, < 6.1)
derailed_benchmarks (1.3.5)
benchmark-ips (~> 2)
get_process_mem (~> 0)
@@ -222,6 +224,7 @@ GEM
railties
rotp (~> 2.0)
diff-lcs (1.3)
+ diff_match_patch (0.1.0)
diffy (3.1.0)
discordrb-webhooks-blackst0ne (3.3.0)
rest-client (~> 2.0)
@@ -285,13 +288,13 @@ GEM
fast_gettext (1.6.0)
ffaker (2.10.0)
ffi (1.11.1)
- flipper (0.13.0)
- flipper-active_record (0.13.0)
- activerecord (>= 3.2, < 6)
- flipper (~> 0.13.0)
- flipper-active_support_cache_store (0.13.0)
- activesupport (>= 3.2, < 6)
- flipper (~> 0.13.0)
+ flipper (0.17.1)
+ flipper-active_record (0.17.1)
+ activerecord (>= 4.2, < 7)
+ flipper (~> 0.17.1)
+ flipper-active_support_cache_store (0.17.1)
+ activesupport (>= 4.2, < 7)
+ flipper (~> 0.17.1)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
@@ -332,10 +335,8 @@ GEM
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
- font-awesome-rails (4.7.0.4)
- railties (>= 3.2, < 6.0)
- foreman (0.84.0)
- thor (~> 0.19.1)
+ font-awesome-rails (4.7.0.5)
+ railties (>= 3.2, < 6.1)
formatador (0.2.5)
fugit (1.2.1)
et-orbi (~> 1.1, >= 1.1.8)
@@ -358,12 +359,12 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
- gitaly (1.65.0)
+ gitaly (1.70.0)
grpc (~> 1.0)
github-markup (1.7.0)
- gitlab-labkit (0.5.2)
- actionpack (~> 5)
- activesupport (~> 5)
+ gitlab-labkit (0.7.0)
+ actionpack (>= 5.0.0, < 6.1.0)
+ activesupport (>= 5.0.0, < 6.1.0)
grpc (~> 1.19)
jaeger-client (~> 0.10)
opentracing (~> 0.4)
@@ -400,7 +401,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.7.1)
+ google-protobuf (3.8.0)
googleapis-common-protos-types (1.0.4)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@@ -410,7 +411,7 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
- gpgme (2.0.18)
+ gpgme (2.0.19)
mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
@@ -440,11 +441,25 @@ GEM
graphql (~> 1.6)
html-pipeline (~> 2.8)
sass (~> 3.4)
- grpc (1.19.0)
- google-protobuf (~> 3.1)
- googleapis-common-protos-types (~> 1.0.0)
+ grpc (1.24.0)
+ google-protobuf (~> 3.8)
+ googleapis-common-protos-types (~> 1.0)
gssapi (1.2.0)
ffi (>= 1.0.1)
+ guard (2.15.1)
+ formatador (>= 0.2.4)
+ listen (>= 2.7, < 4.0)
+ lumberjack (>= 1.0.12, < 2.0)
+ nenv (~> 0.1)
+ notiffany (~> 0.0)
+ pry (>= 0.9.12)
+ shellany (~> 0.0)
+ thor (>= 0.18.1)
+ guard-compat (1.2.1)
+ guard-rspec (4.7.3)
+ guard (~> 2.1)
+ guard-compat (~> 1.1)
+ rspec (>= 2.99.0, < 4.0)
haml (5.0.4)
temple (>= 0.8.0)
tilt
@@ -508,7 +523,7 @@ GEM
atlassian-jwt
multipart-post
oauth (~> 0.5, >= 0.5.0)
- jmespath (1.3.1)
+ jmespath (1.4.0)
js_regex (3.1.1)
character_set (~> 1.1)
regexp_parser (~> 1.1)
@@ -560,15 +575,20 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
+ listen (3.1.5)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ ruby_dep (~> 1.2)
locale (2.1.2)
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.3.0)
+ loofah (2.3.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
+ lumberjack (1.0.13)
mail (2.7.1)
mini_mime (>= 0.1.1)
mail_room (0.9.1)
@@ -584,7 +604,7 @@ GEM
mime-types-data (3.2019.0331)
mimemagic (0.3.2)
mini_magick (4.9.5)
- mini_mime (1.0.1)
+ mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
msgpack (1.3.1)
@@ -597,16 +617,20 @@ GEM
mustermann (~> 1.0.0)
nakayoshi_fork (0.0.4)
nap (1.1.0)
+ nenv (0.3.0)
net-ldap (0.16.0)
net-ntp (2.1.3)
net-ssh (5.2.0)
netrc (0.11.0)
nio4r (2.3.1)
no_proxy_fix (0.1.2)
- nokogiri (1.10.4)
+ nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
nokogiri
+ notiffany (0.1.3)
+ nenv (~> 0.1)
+ shellany (~> 0.0)
numerizer (0.1.1)
oauth (0.5.4)
oauth2 (1.4.1)
@@ -675,12 +699,12 @@ GEM
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- omniauth_openid_connect (0.3.1)
+ omniauth_openid_connect (0.3.3)
addressable (~> 2.5)
- omniauth (~> 1.3)
+ omniauth (~> 1.9)
openid_connect (~> 1.1)
open4 (1.3.4)
- openid_connect (1.1.6)
+ openid_connect (1.1.8)
activemodel
attr_required (>= 1.0.0)
json-jwt (>= 1.5.0)
@@ -703,12 +727,12 @@ GEM
pg (1.1.4)
po_to_json (1.0.1)
json (>= 1.6.0)
- premailer (1.10.4)
+ premailer (1.11.1)
addressable
- css_parser (>= 1.4.10)
+ css_parser (>= 1.6.0)
htmlentities (>= 4.0.0)
- premailer-rails (1.9.7)
- actionmailer (>= 3, < 6)
+ premailer-rails (1.10.3)
+ actionmailer (>= 3)
premailer (~> 1.7, >= 1.7.9)
proc_to_ast (0.1.0)
coderay
@@ -724,7 +748,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.6)
pry (>= 0.10.4)
- public_suffix (3.1.0)
+ public_suffix (3.1.1)
puma (3.12.0)
puma_worker_killer (0.1.0)
get_process_mem (~> 0.2)
@@ -734,8 +758,8 @@ GEM
rack (2.0.7)
rack-accept (0.4.5)
rack (>= 0.4)
- rack-attack (4.4.1)
- rack
+ rack-attack (6.2.0)
+ rack (>= 1.0, < 3)
rack-cors (1.0.2)
rack-oauth2 (1.9.3)
activesupport
@@ -763,10 +787,10 @@ GEM
bundler (>= 1.3.0)
railties (= 5.2.3)
sprockets-rails (>= 2.0.0)
- rails-controller-testing (1.0.2)
- actionpack (~> 5.x, >= 5.0.1)
- actionview (~> 5.x, >= 5.0.1)
- activesupport (~> 5.x)
+ rails-controller-testing (1.0.4)
+ actionpack (>= 5.0.1.x)
+ actionview (>= 5.0.1.x)
+ activesupport (>= 5.0.1.x)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
@@ -798,25 +822,25 @@ GEM
recaptcha (4.13.1)
json
recursive-open-struct (1.1.0)
- redis (4.1.2)
- redis-actionpack (5.0.2)
- actionpack (>= 4.0, < 6)
+ redis (4.1.3)
+ redis-actionpack (5.1.0)
+ actionpack (>= 4.0, < 7)
redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 2)
- redis-activesupport (5.0.7)
- activesupport (>= 3, < 6)
+ redis-activesupport (5.2.0)
+ activesupport (>= 3, < 7)
redis-store (>= 1.3, < 2)
redis-namespace (1.6.0)
redis (>= 3.0.4)
- redis-rack (2.0.5)
+ redis-rack (2.0.6)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2)
- redis-store (1.6.0)
- redis (>= 2.2, < 5)
+ redis-store (1.8.1)
+ redis (>= 4, < 5)
regexp_parser (1.5.1)
regexp_property_values (0.3.4)
representable (3.0.4)
@@ -824,9 +848,9 @@ GEM
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.1)
- responders (2.4.1)
- actionpack (>= 4.2.0, < 6.0)
- railties (>= 4.2.0, < 6.0)
+ responders (3.0.0)
+ actionpack (>= 5.0)
+ railties (>= 5.0)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@@ -897,11 +921,12 @@ GEM
ruby-progressbar (1.10.1)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
+ ruby_dep (1.5.0)
ruby_parser (3.13.1)
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
- rubyzip (1.2.2)
+ rubyzip (1.3.0)
rugged (0.28.3.1)
safe_yaml (1.0.4)
sanitize (4.6.6)
@@ -938,6 +963,7 @@ GEM
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
sexp_processor (4.12.0)
+ shellany (0.0.1)
shoulda-matchers (4.0.1)
activesupport (>= 4.2.0)
sidekiq (5.2.7)
@@ -978,11 +1004,11 @@ GEM
sshkey (2.0.0)
stackprof (0.2.10)
state_machines (0.5.0)
- state_machines-activemodel (0.5.1)
- activemodel (>= 4.1, < 6.0)
+ state_machines-activemodel (0.7.1)
+ activemodel (>= 4.1)
state_machines (>= 0.5.0)
- state_machines-activerecord (0.5.1)
- activerecord (>= 4.1, < 6.0)
+ state_machines-activerecord (0.6.0)
+ activerecord (>= 4.1)
state_machines-activemodel (>= 0.5.0)
swd (1.1.2)
activesupport (>= 3)
@@ -1127,12 +1153,13 @@ DEPENDENCIES
creole (~> 0.5.0)
danger (~> 6.0)
database_cleaner (~> 1.7.0)
- deckar01-task_list (= 2.2.0)
- default_value_for (~> 3.2.0)
+ deckar01-task_list (= 2.2.1)
+ default_value_for (~> 3.3.0)
derailed_benchmarks
device_detector
devise (~> 4.6)
devise-two-factor (~> 3.0.0)
+ diff_match_patch (~> 0.1.0)
diffy (~> 3.1.0)
discordrb-webhooks-blackst0ne (~> 3.3)
doorkeeper (~> 4.3)
@@ -1149,9 +1176,9 @@ DEPENDENCIES
faraday_middleware-aws-signers-v4
fast_blank
ffaker (~> 2.10)
- flipper (~> 0.13.0)
- flipper-active_record (~> 0.13.0)
- flipper-active_support_cache_store (~> 0.13.0)
+ flipper (~> 0.17.1)
+ flipper-active_record (~> 0.17.1)
+ flipper-active_support_cache_store (~> 0.17.1)
flowdock (~> 0.7)
fog-aliyun (~> 0.3)
fog-aws (~> 3.5)
@@ -1161,14 +1188,13 @@ DEPENDENCIES
fog-openstack (~> 1.0)
fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7)
- foreman (~> 0.84.0)
fugit (~> 1.2.1)
fuubar (~> 2.2.0)
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly (~> 1.65.0)
+ gitaly (~> 1.70.0)
github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
@@ -1181,8 +1207,8 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
- google-protobuf (~> 3.7.1)
- gpgme (~> 2.0.18)
+ google-protobuf (~> 3.8.0)
+ gpgme (~> 2.0.19)
grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.1)
@@ -1190,8 +1216,9 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.9.11)
graphql-docs (~> 1.6.0)
- grpc (~> 1.19.0)
+ grpc (~> 1.24.0)
gssapi
+ guard-rspec
haml_lint (~> 0.31.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
@@ -1226,7 +1253,7 @@ DEPENDENCIES
net-ldap
net-ntp
net-ssh (~> 5.2)
- nokogiri (~> 1.10.4)
+ nokogiri (~> 1.10.5)
oauth2 (~> 1.4)
octokit (~> 4.9)
omniauth (~> 1.8)
@@ -1246,17 +1273,17 @@ DEPENDENCIES
omniauth-twitter (~> 1.4)
omniauth-ultraauth (~> 0.0.2)
omniauth_crowd (~> 2.2.0)
- omniauth_openid_connect (~> 0.3.1)
+ omniauth_openid_connect (~> 0.3.3)
org-ruby (~> 0.9.12)
pg (~> 1.1)
- premailer-rails (~> 1.9.7)
+ premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.9.10)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4)
puma (~> 3.12)
puma_worker_killer
rack (~> 2.0.7)
- rack-attack (~> 4.4.1)
+ rack-attack (~> 6.2.0)
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
@@ -1275,7 +1302,8 @@ DEPENDENCIES
redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
- responders (~> 2.0)
+ responders (~> 3.0)
+ retriable (~> 3.1.2)
rouge (~> 3.11.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
@@ -1291,7 +1319,7 @@ DEPENDENCIES
ruby-prof (~> 1.0.0)
ruby-progressbar
ruby_parser (~> 3.8)
- rubyzip (~> 1.2.2)
+ rubyzip (~> 1.3.0)
rugged (~> 0.28)
sanitize (~> 4.6)
sassc-rails (~> 2.1.0)
@@ -1312,7 +1340,7 @@ DEPENDENCIES
sprockets (~> 3.7.0)
sshkey (~> 2.0)
stackprof (~> 0.2.10)
- state_machines-activerecord (~> 0.5.1)
+ state_machines-activerecord (~> 0.6.0)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.10.0)
thin (~> 1.7.0)
diff --git a/Guardfile b/Guardfile
new file mode 100644
index 00000000000..8a43f414ca9
--- /dev/null
+++ b/Guardfile
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# More info at https://github.com/guard/guard#readme
+
+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 files
+ rspec = dsl.rspec
+ 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)
+
+ # 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)
+
+ watch(rails.controllers) do |m|
+ [
+ rspec.spec.call("routing/#{m[1]}_routing"),
+ rspec.spec.call("controllers/#{m[1]}_controller")
+ ]
+ end
+
+ # Rails config changes
+ watch(rails.spec_helper) { rspec.spec_dir }
+ watch(rails.routes) { "#{rspec.spec_dir}/routing" }
+ watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
+
+ # Capybara features specs
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
+ watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
+end
diff --git a/PROCESS.md b/PROCESS.md
index 6bff60bff0f..45f28b33a63 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -79,7 +79,7 @@ star, smile, etc.). Some good tips about code reviews can be found in our
Overview and details of feature flag processes in development of GitLab itself is described in [feature flags process documentation](https://docs.gitlab.com/ee/development/feature_flags/process.html).
-Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/developing.html).
+Guides on how to include feature flags in your backend/frontend code while developing GitLab are described in [developing with feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/development.html).
Getting access and how to expose the feature to users is detailed in [controlling feature flags documentation](https://docs.gitlab.com/ee/development/feature_flags/controls.html).
diff --git a/VERSION b/VERSION
index 9f3bdf87a9c..4dd2ed8f250 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-12.4.3
+12.5.0-pre
diff --git a/app/assets/images/cluster_app_logos/crossplane.png b/app/assets/images/cluster_app_logos/crossplane.png
new file mode 100644
index 00000000000..32d8175108c
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/crossplane.png
Binary files differ
diff --git a/app/assets/images/cluster_app_logos/elastic_stack.png b/app/assets/images/cluster_app_logos/elastic_stack.png
new file mode 100644
index 00000000000..69fbc6aacd0
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/elastic_stack.png
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 908dc730aa4..aee9990bc0b 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -2,6 +2,8 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
+import flash from '~/flash';
+import { __ } from '~/locale';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -29,6 +31,7 @@ const Api = {
usersPath: '/api/:version/users.json',
userPath: '/api/:version/users/:id',
userStatusPath: '/api/:version/users/:id/status',
+ userProjectsPath: '/api/:version/users/:id/projects',
userPostStatusPath: '/api/:version/user/status',
commitPath: '/api/:version/projects/:id/repository/commits',
applySuggestionPath: '/api/:version/suggestions/:id/apply',
@@ -110,10 +113,9 @@ const Api = {
.get(url, {
params: Object.assign(defaults, options),
})
- .then(({ data }) => {
+ .then(({ data, headers }) => {
callback(data);
-
- return data;
+ return { data, headers };
});
},
@@ -239,7 +241,8 @@ const Api = {
.get(url, {
params: Object.assign({}, defaults, options),
})
- .then(({ data }) => callback(data));
+ .then(({ data }) => callback(data))
+ .catch(() => flash(__('Something went wrong while fetching projects')));
},
commitMultiple(id, data) {
@@ -348,6 +351,20 @@ const Api = {
});
},
+ userProjects(userId, query, options, callback) {
+ const url = Api.buildUrl(Api.userProjectsPath).replace(':id', userId);
+ const defaults = {
+ search: query,
+ per_page: 20,
+ };
+ return axios
+ .get(url, {
+ params: Object.assign({}, defaults, options),
+ })
+ .then(({ data }) => callback(data))
+ .catch(() => flash(__('Something went wrong while fetching projects')));
+ },
+
branches(id, query = '', options = {}) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index a07942d87cb..ca91400eac7 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var */
+/* eslint-disable func-names */
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
@@ -12,11 +12,8 @@ import { __ } from '~/locale';
// more than `x` users are referenced.
//
-var lastTextareaPreviewed;
-var lastTextareaHeight = null;
-var markdownPreview;
-var previewButtonSelector;
-var writeButtonSelector;
+let lastTextareaHeight;
+let lastTextareaPreviewed;
function MarkdownPreview() {}
@@ -27,14 +24,13 @@ MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.');
MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function($form) {
- var mdText;
- var preview = $form.find('.js-md-preview');
- var url = preview.data('url');
+ const preview = $form.find('.js-md-preview');
+ const url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
- mdText = $form.find('textarea.markdown-area').val();
+ const mdText = $form.find('textarea.markdown-area').val();
if (mdText === undefined) {
return;
@@ -46,7 +42,7 @@ MarkdownPreview.prototype.showPreview = function($form) {
} else {
preview.addClass('md-preview-loading').text(__('Loading...'));
this.fetchMarkdownPreview(mdText, url, response => {
- var body;
+ let body;
if (response.body.length > 0) {
({ body } = response);
} else {
@@ -91,8 +87,7 @@ MarkdownPreview.prototype.hideReferencedUsers = function($form) {
};
MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) {
- var referencedUsers;
- referencedUsers = $form.find('.referenced-users');
+ const referencedUsers = $form.find('.referenced-users');
if (referencedUsers.length) {
if (users.length >= this.referenceThreshold) {
referencedUsers.show();
@@ -108,8 +103,7 @@ MarkdownPreview.prototype.hideReferencedCommands = function($form) {
};
MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
- var referencedCommands;
- referencedCommands = $form.find('.referenced-commands');
+ const referencedCommands = $form.find('.referenced-commands');
if (commands.length > 0) {
referencedCommands.html(commands);
referencedCommands.show();
@@ -119,15 +113,15 @@ MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) {
}
};
-markdownPreview = new MarkdownPreview();
+const markdownPreview = new MarkdownPreview();
-previewButtonSelector = '.js-md-preview-button';
-writeButtonSelector = '.js-md-write-button';
+const previewButtonSelector = '.js-md-preview-button';
+const writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function() {
- var $form = $(this);
+ const $form = $(this);
$form.find('textarea.markdown-area').on('input', () => {
markdownPreview.hideReferencedUsers($form);
});
@@ -188,7 +182,7 @@ $(document).on('markdown-preview:hide', (e, $form) => {
});
$(document).on('markdown-preview:toggle', (e, keyboardEvent) => {
- var $target;
+ let $target;
$target = $(keyboardEvent.target);
if ($target.is('textarea.markdown-area')) {
$(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
@@ -201,16 +195,14 @@ $(document).on('markdown-preview:toggle', (e, keyboardEvent) => {
});
$(document).on('click', previewButtonSelector, function(e) {
- var $form;
e.preventDefault();
- $form = $(this).closest('form');
+ const $form = $(this).closest('form');
$(document).triggerHandler('markdown-preview:show', [$form]);
});
$(document).on('click', writeButtonSelector, function(e) {
- var $form;
e.preventDefault();
- $form = $(this).closest('form');
+ const $form = $(this).closest('form');
$(document).triggerHandler('markdown-preview:hide', [$form]);
});
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index b371f6be268..aedd8004ea5 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -118,8 +118,6 @@ export default class FileTemplateMediator {
}
});
- this.setFilename(item.name);
-
if (this.editor.getValue() !== '') {
this.setTypeSelectorToggleText(item.name);
}
@@ -133,14 +131,16 @@ export default class FileTemplateMediator {
selectTemplateFile(selector, query, data) {
const self = this;
+ const { name } = selector.config;
selector.renderLoading();
this.fetchFileTemplate(selector.config.type, query, data)
.then(file => {
this.setEditorContent(file);
+ this.setFilename(name);
selector.renderLoaded();
- this.typeSelector.setToggleText(selector.config.name);
+ this.typeSelector.setToggleText(name);
toast(__(`${query} template applied`), {
action: {
text: __('Undo'),
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 34560560756..c0df8b72095 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -133,7 +133,7 @@ export default {
if (this.board.name.length === 0) return;
this.isLoading = true;
if (this.isDeleteForm) {
- gl.boardService
+ boardsStore
.deleteBoard(this.currentBoard)
.then(() => {
visitUrl(boardsStore.rootPath);
@@ -143,7 +143,7 @@ export default {
this.isLoading = false;
});
} else {
- gl.boardService
+ boardsStore
.createBoard(this.board)
.then(resp => resp.data)
.then(data => {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1273fcc6a91..b8439bc8741 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -84,7 +84,8 @@ export default {
this.$nextTick(() => {
if (
this.scrollHeight() <= this.listHeight() &&
- this.list.issuesSize > this.list.issues.length
+ this.list.issuesSize > this.list.issues.length &&
+ this.list.isExpanded
) {
this.list.page += 1;
this.list.getIssues(false).catch(() => {
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 334c162954e..32491dfbcb6 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -168,7 +168,7 @@ export default {
}
const recentBoardsPromise = new Promise((resolve, reject) =>
- gl.boardService
+ boardsStore
.recentBoards()
.then(resolve)
.catch(err => {
@@ -184,7 +184,7 @@ export default {
}),
);
- Promise.all([gl.boardService.allBoards(), recentBoardsPromise])
+ Promise.all([boardsStore.allBoards(), recentBoardsPromise])
.then(([allBoards, recentBoards]) => [allBoards.data, recentBoards.data])
.then(([allBoardsJson, recentBoardsJson]) => {
this.loading = false;
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 40d75d53f75..d37e49bab46 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,5 +1,6 @@
<script>
import _ from 'underscore';
+import { mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -63,6 +64,7 @@ export default {
};
},
computed: {
+ ...mapState(['isShowingLabels']),
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
@@ -92,7 +94,7 @@ export default {
return false;
},
showLabelFooter() {
- return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+ return this.isShowingLabels && this.issue.labels.find(this.showLabel);
},
issueReferencePath() {
const { referencePath, groupId } = this.issue;
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index defa1f75ba2..618c2ada1f8 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -1,6 +1,7 @@
<script>
/* global ListIssue */
import { urlParamsToObject } from '~/lib/utils/common_utils';
+import boardsStore from '~/boards/stores/boards_store';
import ModalHeader from './header.vue';
import ModalList from './list.vue';
import ModalFooter from './footer.vue';
@@ -109,7 +110,7 @@ export default {
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
- return gl.boardService
+ return boardsStore
.getBacklog({
...urlParamsToObject(this.filter.path),
page: this.page,
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index befca70eeae..e76e2341dfd 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -13,6 +13,7 @@ import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import '~/boards/models/milestone';
import '~/boards/models/project';
+import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import ModalStore from '~/boards/stores/modal_store';
import BoardService from 'ee_else_ce/boards/services/board_service';
@@ -29,6 +30,7 @@ import {
} from '~/lib/utils/common_utils';
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,
setWeigthFetchingState,
@@ -67,6 +69,7 @@ export default () => {
BoardSidebar,
BoardAddIssuesModal,
},
+ store,
data: {
state: boardsStore.state,
loading: true,
@@ -314,5 +317,6 @@ export default () => {
}
toggleFocusMode(ModalStore, boardsStore, $boardApp);
+ toggleLabels();
mountMultipleBoardsSwitcher();
};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 1e213c324eb..bb8c8e68297 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -50,8 +50,8 @@ class List {
this.page = 1;
this.loading = true;
this.loadingMore = false;
- this.issues = [];
- this.issuesSize = 0;
+ this.issues = obj.issues || [];
+ this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
this.defaultAvatar = defaultAvatar;
if (obj.label) {
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
new file mode 100644
index 00000000000..4de1576099d
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -0,0 +1,3 @@
+export default {
+ getLabelToggleState: state => (state.isShowingLabels ? 'on' : 'off'),
+};
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index f70395a3771..471b952a212 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -1,14 +1,18 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from 'ee_else_ce/boards/stores/state';
+import getters from 'ee_else_ce/boards/stores/getters';
import actions from 'ee_else_ce/boards/stores/actions';
import mutations from 'ee_else_ce/boards/stores/mutations';
Vue.use(Vuex);
-export default () =>
+export const createStore = () =>
new Vuex.Store({
state,
+ getters,
actions,
mutations,
});
+
+export default createStore();
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index dd16abb01a5..24f44dc5629 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -1,3 +1,3 @@
export default () => ({
- // ...
+ isShowingLabels: true,
});
diff --git a/app/assets/javascripts/boards/toggle_labels.js b/app/assets/javascripts/boards/toggle_labels.js
new file mode 100644
index 00000000000..2d1ec238274
--- /dev/null
+++ b/app/assets/javascripts/boards/toggle_labels.js
@@ -0,0 +1 @@
+export default () => {};
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 7ea8901ecbb..75909dd9d20 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -8,11 +8,12 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } from './constants';
+import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX, CROSSPLANE } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
+import initProjectSelectDropdown from '~/project_select';
const Environments = () => import('ee_component/clusters/components/environments.vue');
@@ -37,6 +38,8 @@ export default class Clusters {
installJupyterPath,
installKnativePath,
updateKnativePath,
+ installElasticStackPath,
+ installCrossplanePath,
installPrometheusPath,
managePrometheusPath,
clusterEnvironmentsPath,
@@ -81,11 +84,13 @@ export default class Clusters {
installHelmEndpoint: installHelmPath,
installIngressEndpoint: installIngressPath,
installCertManagerEndpoint: installCertManagerPath,
+ installCrossplaneEndpoint: installCrossplanePath,
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
+ installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
});
@@ -108,8 +113,10 @@ export default class Clusters {
this.ingressDomainHelpText &&
this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');
+ initProjectSelectDropdown();
Clusters.initDismissableCallout();
initSettingsPanels();
+
const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area');
if (toggleButtonsContainer) {
setupToggleButtons(toggleButtonsContainer);
@@ -222,6 +229,7 @@ export default class Clusters {
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
+ eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
@@ -233,6 +241,7 @@ export default class Clusters {
eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
+ eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication');
}
@@ -399,18 +408,33 @@ export default class Clusters {
}
installApplication({ id: appId, params }) {
- this.store.updateAppProperty(appId, 'requestReason', null);
- this.store.updateAppProperty(appId, 'statusReason', null);
+ return Clusters.validateInstallation(appId, params)
+ .then(() => {
+ this.store.updateAppProperty(appId, 'requestReason', null);
+ this.store.updateAppProperty(appId, 'statusReason', null);
+ this.store.installApplication(appId);
+
+ // eslint-disable-next-line promise/no-nesting
+ this.service.installApplication(appId, params).catch(() => {
+ this.store.notifyInstallFailure(appId);
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin installing failed'),
+ );
+ });
+ })
+ .catch(error => this.store.updateAppProperty(appId, 'validationError', error));
+ }
- this.store.installApplication(appId);
+ static validateInstallation(appId, params) {
+ return new Promise((resolve, reject) => {
+ if (appId === CROSSPLANE && !params.stack) {
+ reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
+ return;
+ }
- return this.service.installApplication(appId, params).catch(() => {
- this.store.notifyInstallFailure(appId);
- this.store.updateAppProperty(
- appId,
- 'requestReason',
- s__('ClusterIntegration|Request to begin installing failed'),
- );
+ resolve();
});
}
@@ -458,6 +482,12 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'hostname', data.hostname);
}
+ setCrossplaneProviderStack(data) {
+ const appId = data.id;
+ this.store.updateAppProperty(appId, 'stack', data.stack.code);
+ this.store.updateAppProperty(appId, 'validationError', null);
+ }
+
destroy() {
this.destroyed = true;
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index b95f97077f6..a951a6bfeea 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -9,9 +9,11 @@ import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
+import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
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 { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
@@ -19,6 +21,7 @@ import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
+import CrossplaneProviderStack from './crossplane_provider_stack.vue';
export default {
components: {
@@ -27,6 +30,7 @@ export default {
LoadingButton,
GlLoadingIcon,
KnativeDomainEditor,
+ CrossplaneProviderStack,
},
props: {
type: {
@@ -88,9 +92,11 @@ export default {
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
+ crossplaneLogo,
knativeLogo,
meltanoLogo,
prometheusLogo,
+ elasticStackLogo,
}),
computed: {
isProjectCluster() {
@@ -114,6 +120,15 @@ export default {
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
+ crossplaneInstalled() {
+ return this.applications.crossplane.status === APPLICATION_STATUS.INSTALLED;
+ },
+ enableClusterApplicationCrossplane() {
+ return gon.features && gon.features.enableClusterApplicationCrossplane;
+ },
+ enableClusterApplicationElasticStack() {
+ return gon.features && gon.features.enableClusterApplicationElasticStack;
+ },
ingressDescription() {
return sprintf(
_.escape(
@@ -146,6 +161,24 @@ export default {
false,
);
},
+ crossplaneDescription() {
+ return sprintf(
+ _.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.`,
+ ),
+ ),
+ {
+ gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/crossplane.html"
+ target="_blank" rel="noopener noreferrer">
+ ${_.escape(s__('ClusterIntegration|Gitlab Integration'))}</a>`,
+ kubectl: `<code>kubectl</code>`,
+ },
+ false,
+ );
+ },
+
prometheusDescription() {
return sprintf(
_.escape(
@@ -168,9 +201,18 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ elasticStackInstalled() {
+ return this.applications.elastic_stack.status === APPLICATION_STATUS.INSTALLED;
+ },
+ elasticStackKibanaHostname() {
+ return this.applications.elastic_stack.kibana_hostname;
+ },
knative() {
return this.applications.knative;
},
+ crossplane() {
+ return this.applications.crossplane;
+ },
cloudRun() {
return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative;
},
@@ -207,6 +249,12 @@ export default {
hostname,
});
},
+ setCrossplaneProviderStack(stack) {
+ eventHub.$emit('setCrossplaneProviderStack', {
+ id: 'crossplane',
+ stack,
+ });
+ },
},
};
</script>
@@ -217,7 +265,7 @@ export default {
<p class="append-bottom-0">
{{
s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
- Helm Tiller is required to install any of the following applications.`)
+ Helm Tiller is required to install any of the following applications.`)
}}
<a :href="helpPath">{{ __('More information') }}</a>
</p>
@@ -242,9 +290,9 @@ export default {
<div slot="description">
{{
s__(`ClusterIntegration|Helm streamlines installing
- and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster,
- and manages releases of your charts.`)
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`)
}}
</div>
</application-row>
@@ -252,7 +300,7 @@ export default {
<div class="svg-container" v-html="helmInstallIllustration"></div>
{{
s__(`ClusterIntegration|You must first install Helm Tiller before
- installing the applications below`)
+ installing the applications below`)
}}
</div>
<application-row
@@ -275,8 +323,8 @@ export default {
<p>
{{
s__(`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.`)
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`)
}}
</p>
@@ -308,8 +356,8 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
- generated endpoint in order to access
- your application after it has been deployed.`)
+ generated endpoint in order to access
+ your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -320,8 +368,8 @@ export default {
<p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
{{
s__(`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.`)
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -368,7 +416,7 @@ export default {
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
- You must provide an email address for your Issuer. `)
+ You must provide an email address for your Issuer. `)
}}
<a
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
@@ -424,13 +472,41 @@ export default {
<div slot="description">
{{
s__(`ClusterIntegration|GitLab Runner connects to the
- repository and executes CI/CD jobs,
- pushing results back and deploying
- applications to production.`)
+ repository and executes CI/CD jobs,
+ pushing results back and deploying
+ applications to production.`)
}}
</div>
</application-row>
<application-row
+ v-if="enableClusterApplicationCrossplane"
+ id="crossplane"
+ :logo-url="crossplaneLogo"
+ :title="applications.crossplane.title"
+ :status="applications.crossplane.status"
+ :status-reason="applications.crossplane.statusReason"
+ :request-status="applications.crossplane.requestStatus"
+ :request-reason="applications.crossplane.requestReason"
+ :installed="applications.crossplane.installed"
+ :install-failed="applications.crossplane.installFailed"
+ :uninstallable="applications.crossplane.uninstallable"
+ :uninstall-successful="applications.crossplane.uninstallSuccessful"
+ :uninstall-failed="applications.crossplane.uninstallFailed"
+ :install-application-request-params="{ stack: applications.crossplane.stack }"
+ :disabled="!helmInstalled"
+ title-link="https://crossplane.io"
+ >
+ <template>
+ <div slot="description">
+ <p v-html="crossplaneDescription"></p>
+ <div class="form-group">
+ <CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
+ </div>
+ </div>
+ </template>
+ </application-row>
+
+ <application-row
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
@@ -451,10 +527,10 @@ export default {
<p>
{{
s__(`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.`)
+ 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.`)
}}
</p>
@@ -481,7 +557,7 @@ export default {
<p v-if="ingressInstalled" class="form-text text-muted">
{{
s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
+ If you do so, point hostname to Ingress IP Address from above.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
@@ -527,9 +603,9 @@ export default {
<p>
{{
s__(`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.`)
+ 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.`)
}}
</p>
@@ -542,6 +618,75 @@ export default {
/>
</div>
</application-row>
+ <application-row
+ v-if="enableClusterApplicationElasticStack"
+ id="elastic_stack"
+ :logo-url="elasticStackLogo"
+ :title="applications.elastic_stack.title"
+ :status="applications.elastic_stack.status"
+ :status-reason="applications.elastic_stack.statusReason"
+ :request-status="applications.elastic_stack.requestStatus"
+ :request-reason="applications.elastic_stack.requestReason"
+ :version="applications.elastic_stack.version"
+ :chart-repo="applications.elastic_stack.chartRepo"
+ :update-available="applications.elastic_stack.updateAvailable"
+ :installed="applications.elastic_stack.installed"
+ :install-failed="applications.elastic_stack.installFailed"
+ :update-successful="applications.elastic_stack.updateSuccessful"
+ :update-failed="applications.elastic_stack.updateFailed"
+ :uninstallable="applications.elastic_stack.uninstallable"
+ :uninstall-successful="applications.elastic_stack.uninstallSuccessful"
+ :uninstall-failed="applications.elastic_stack.uninstallFailed"
+ :disabled="!helmInstalled"
+ :install-application-request-params="{
+ kibana_hostname: applications.elastic_stack.kibana_hostname,
+ }"
+ title-link="https://github.com/helm/charts/tree/master/stable/elastic-stack"
+ >
+ <div slot="description">
+ <p>
+ {{
+ s__(
+ `ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
+ )
+ }}
+ </p>
+
+ <template v-if="ingressExternalEndpoint">
+ <div class="form-group">
+ <label for="elastic-stack-kibana-hostname">{{
+ s__('ClusterIntegration|Kibana Hostname')
+ }}</label>
+
+ <div class="input-group">
+ <input
+ v-model="applications.elastic_stack.kibana_hostname"
+ :readonly="elasticStackInstalled"
+ type="text"
+ class="form-control js-hostname"
+ />
+ <span class="input-group-btn">
+ <clipboard-button
+ :text="elasticStackKibanaHostname"
+ :title="s__('ClusterIntegration|Copy Kibana Hostname')"
+ class="js-clipboard-btn"
+ />
+ </span>
+ </div>
+
+ <p v-if="ingressInstalled" class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
new file mode 100644
index 00000000000..966918ae636
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
@@ -0,0 +1,93 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { s__ } from '../../locale';
+
+export default {
+ name: 'CrossplaneProviderStack',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ Icon,
+ },
+ props: {
+ stacks: {
+ type: Array,
+ required: false,
+ default: () => [
+ {
+ name: s__('Google Cloud Platform'),
+ code: 'gcp',
+ },
+ {
+ name: s__('Amazon Web Services'),
+ code: 'aws',
+ },
+ {
+ name: s__('Microsoft Azure'),
+ code: 'azure',
+ },
+ {
+ name: s__('Rook'),
+ code: 'rook',
+ },
+ ],
+ },
+ crossplane: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownText() {
+ const result = this.stacks.reduce((map, obj) => {
+ // eslint-disable-next-line no-param-reassign
+ map[obj.code] = obj.name;
+ return map;
+ }, {});
+ const { stack } = this.crossplane;
+ if (stack !== '') {
+ return result[stack];
+ }
+ return s__('Select Stack');
+ },
+ validationError() {
+ return this.crossplane.validationError;
+ },
+ },
+ methods: {
+ selectStack(stack) {
+ this.$emit('set', stack);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <label>
+ {{ s__('ClusterIntegration|Enabled stack') }}
+ </label>
+ <gl-dropdown
+ :disabled="crossplane.installed"
+ :text="dropdownText"
+ toggle-class="dropdown-menu-toggle gl-field-error-outline"
+ class="w-100"
+ :class="{ 'gl-show-field-errors': validationError }"
+ >
+ <gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
+ <span class="ml-1">{{ stack.name }}</span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
+ <p class="form-text text-muted">
+ {{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
+ <a
+ href="https://crossplane.io/docs/master/stacks-guide.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >{{ __('Crossplane') }}</a
+ >
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index f1925c243f2..125bcaacc1c 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -2,7 +2,16 @@
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
-import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
+import {
+ HELM,
+ INGRESS,
+ CERT_MANAGER,
+ PROMETHEUS,
+ RUNNER,
+ KNATIVE,
+ JUPYTER,
+ ELASTIC_STACK,
+} from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
@@ -28,6 +37,7 @@ const CUSTOM_APP_WARNING_TEXT = {
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
+ [ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
};
export default {
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index c6e4b7951cf..9f98f170fb0 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -50,8 +50,19 @@ export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
+export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
+export const ELASTIC_STACK = 'elastic_stack';
-export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
+export const APPLICATIONS = [
+ HELM,
+ INGRESS,
+ JUPYTER,
+ KNATIVE,
+ RUNNER,
+ CERT_MANAGER,
+ PROMETHEUS,
+ ELASTIC_STACK,
+];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index fa12802b3de..333fb293a15 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -7,10 +7,12 @@ export default class ClusterService {
helm: this.options.installHelmEndpoint,
ingress: this.options.installIngressEndpoint,
cert_manager: this.options.installCertManagerEndpoint,
+ crossplane: this.options.installCrossplaneEndpoint,
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
+ elastic_stack: this.options.installElasticStackEndpoint,
};
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 6464461ea0c..35dbf951551 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -5,6 +5,8 @@ import {
JUPYTER,
KNATIVE,
CERT_MANAGER,
+ ELASTIC_STACK,
+ CROSSPLANE,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
@@ -25,6 +27,7 @@ const applicationInitialState = {
uninstallable: false,
uninstallFailed: false,
uninstallSuccessful: false,
+ validationError: null,
};
export default class ClusterStore {
@@ -57,6 +60,11 @@ export default class ClusterStore {
title: s__('ClusterIntegration|Cert-Manager'),
email: null,
},
+ crossplane: {
+ ...applicationInitialState,
+ title: s__('ClusterIntegration|Crossplane'),
+ stack: null,
+ },
runner: {
...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
@@ -85,6 +93,11 @@ export default class ClusterStore {
updateSuccessful: false,
updateFailed: false,
},
+ elastic_stack: {
+ ...applicationInitialState,
+ title: s__('ClusterIntegration|Elastic Stack'),
+ kibana_hostname: null,
+ },
},
environments: [],
fetchingEnvironments: false,
@@ -197,13 +210,15 @@ export default class ClusterStore {
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
+ } else if (appId === CROSSPLANE) {
+ this.state.applications.crossplane.stack =
+ this.state.applications.crossplane.stack || serverAppEntry.stack;
} else if (appId === JUPYTER) {
- this.state.applications.jupyter.hostname =
- this.state.applications.jupyter.hostname ||
- serverAppEntry.hostname ||
- (this.state.applications.ingress.externalIp
- ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
- : '');
+ this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
+ this.state.applications.jupyter.hostname,
+ serverAppEntry.hostname,
+ 'jupyter',
+ );
} else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
this.state.applications.knative.hostname =
@@ -216,10 +231,26 @@ export default class ClusterStore {
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.updateAvailable = updateAvailable;
+ } else if (appId === ELASTIC_STACK) {
+ this.state.applications.elastic_stack.kibana_hostname = this.updateHostnameIfUnset(
+ this.state.applications.elastic_stack.kibana_hostname,
+ serverAppEntry.kibana_hostname,
+ 'kibana',
+ );
}
});
}
+ updateHostnameIfUnset(current, updated, fallback) {
+ return (
+ current ||
+ updated ||
+ (this.state.applications.ingress.externalIp
+ ? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
+ : '')
+ );
+ }
+
toggleFetchEnvironments(isFetching) {
this.state.fetchingEnvironments = isFetching;
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 6c04e0beb4d..60c2059a876 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-var, no-else-return, consistent-return, one-var, no-return-assign, no-unused-expressions, no-sequences */
+/* eslint-disable func-names, no-var, no-else-return, consistent-return, one-var, no-return-assign */
import $ from 'jquery';
@@ -9,40 +9,29 @@ const viewModes = ['two-up', 'swipe'];
export default class ImageFile {
constructor(file) {
this.file = file;
- this.requestImageInfo(
- $('.two-up.view .frame.deleted img', this.file),
- (function(_this) {
- return function() {
- return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), () => {
- _this.initViewModes();
-
- // Load two-up view after images are loaded
- // so that we can display the correct width and height information
- const $images = $('.two-up.view img', _this.file);
-
- $images.waitForImages(() => {
- _this.initView('two-up');
- });
- });
- };
- })(this),
+ this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), () =>
+ this.requestImageInfo($('.two-up.view .frame.added img', this.file), () => {
+ this.initViewModes();
+
+ // Load two-up view after images are loaded
+ // so that we can display the correct width and height information
+ const $images = $('.two-up.view img', this.file);
+
+ $images.waitForImages(() => {
+ this.initView('two-up');
+ });
+ }),
);
}
initViewModes() {
const viewMode = viewModes[0];
$('.view-modes', this.file).removeClass('hide');
- $('.view-modes-menu', this.file).on(
- 'click',
- 'li',
- (function(_this) {
- return function(event) {
- if (!$(event.currentTarget).hasClass('active')) {
- return _this.activateViewMode(event.currentTarget.className);
- }
- };
- })(this),
- );
+ $('.view-modes-menu', this.file).on('click', 'li', event => {
+ if (!$(event.currentTarget).hasClass('active')) {
+ return this.activateViewMode(event.currentTarget.className);
+ }
+ });
return this.activateViewMode(viewMode);
}
@@ -51,15 +40,10 @@ export default class ImageFile {
.removeClass('active')
.filter(`.${viewMode}`)
.addClass('active');
- return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(
- 200,
- (function(_this) {
- return function() {
- $(`.view.${viewMode}`, _this.file).fadeIn(200);
- return _this.initView(viewMode);
- };
- })(this),
- );
+ return $(`.view:visible:not(.${viewMode})`, this.file).fadeOut(200, () => {
+ $(`.view.${viewMode}`, this.file).fadeIn(200);
+ return this.initView(viewMode);
+ });
}
initView(viewMode) {
@@ -103,22 +87,18 @@ export default class ImageFile {
.on('touchmove', dragMove);
}
- prepareFrames(view) {
+ static prepareFrames(view) {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
$('.frame', view)
- .each(
- (function() {
- return function(index, frame) {
- var height, width;
- width = $(frame).width();
- height = $(frame).height();
- maxWidth = width > maxWidth ? width : maxWidth;
- return (maxHeight = height > maxHeight ? height : maxHeight);
- };
- })(this),
- )
+ .each((index, frame) => {
+ var height, width;
+ width = $(frame).width();
+ height = $(frame).height();
+ maxWidth = width > maxWidth ? width : maxWidth;
+ return (maxHeight = height > maxHeight ? height : maxHeight);
+ })
.css({
width: maxWidth,
height: maxHeight,
@@ -128,104 +108,95 @@ export default class ImageFile {
views = {
'two-up': function() {
- return $('.two-up.view .wrap', this.file).each(
- (function(_this) {
- return function(index, wrap) {
- $('img', wrap).each(function() {
- var currentWidth;
- currentWidth = $(this).width();
- if (currentWidth > availWidth / 2) {
- return $(this).width(availWidth / 2);
- }
- });
- return _this.requestImageInfo($('img', wrap), (width, height) => {
- $('.image-info .meta-width', wrap).text(`${width}px`);
- $('.image-info .meta-height', wrap).text(`${height}px`);
- return $('.image-info', wrap).removeClass('hide');
- });
- };
- })(this),
- );
+ return $('.two-up.view .wrap', this.file).each((index, wrap) => {
+ $('img', wrap).each(function() {
+ var currentWidth;
+ currentWidth = $(this).width();
+ if (currentWidth > availWidth / 2) {
+ return $(this).width(availWidth / 2);
+ }
+ });
+ return this.requestImageInfo($('img', wrap), (width, height) => {
+ $('.image-info .meta-width', wrap).text(`${width}px`);
+ $('.image-info .meta-height', wrap).text(`${height}px`);
+ return $('.image-info', wrap).removeClass('hide');
+ });
+ });
},
swipe() {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
- return $('.swipe.view', this.file).each(
- (function(_this) {
- return function(index, view) {
- var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
- $swipeFrame = $('.swipe-frame', view);
- $swipeWrap = $('.swipe-wrap', view);
- $swipeBar = $('.swipe-bar', view);
-
- $swipeFrame.css({
- width: maxWidth + 16,
- height: maxHeight + 28,
- });
- $swipeWrap.css({
- width: maxWidth + 1,
- height: maxHeight + 2,
- });
- // Set swipeBar left position to match image frame
- $swipeBar.css({
- left: 1,
- });
-
- wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
-
- _this.initDraggable($swipeBar, wrapPadding, (e, left) => {
- if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
- $swipeWrap.width(maxWidth + 1 - left);
- $swipeBar.css('left', left);
- }
- });
- };
- })(this),
- );
+ return $('.swipe.view', this.file).each((index, view) => {
+ var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding;
+ const ref = ImageFile.prepareFrames(view);
+ [maxWidth, maxHeight] = ref;
+ $swipeFrame = $('.swipe-frame', view);
+ $swipeWrap = $('.swipe-wrap', view);
+ $swipeBar = $('.swipe-bar', view);
+
+ $swipeFrame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28,
+ });
+ $swipeWrap.css({
+ width: maxWidth + 1,
+ height: maxHeight + 2,
+ });
+ // Set swipeBar left position to match image frame
+ $swipeBar.css({
+ left: 1,
+ });
+
+ wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+ this.initDraggable($swipeBar, wrapPadding, (e, left) => {
+ if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
+ $swipeWrap.width(maxWidth + 1 - left);
+ $swipeBar.css('left', left);
+ }
+ });
+ });
},
'onion-skin': function() {
var dragTrackWidth, maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
- return $('.onion-skin.view', this.file).each(
- (function(_this) {
- return function(index, view) {
- var $frame, $track, $dragger, $frameAdded, framePadding, ref;
- (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
- $frame = $('.onion-skin-frame', view);
- $frameAdded = $('.frame.added', view);
- $track = $('.drag-track', view);
- $dragger = $('.dragger', $track);
-
- $frame.css({
- width: maxWidth + 16,
- height: maxHeight + 28,
- });
- $('.swipe-wrap', view).css({
- width: maxWidth + 1,
- height: maxHeight + 2,
- });
- $dragger.css({
- left: dragTrackWidth,
- });
-
- $frameAdded.css('opacity', 1);
- framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
-
- _this.initDraggable($dragger, framePadding, (e, left) => {
- var opacity = left / dragTrackWidth;
-
- if (opacity >= 0 && opacity <= 1) {
- $dragger.css('left', left);
- $frameAdded.css('opacity', opacity);
- }
- });
- };
- })(this),
- );
+ return $('.onion-skin.view', this.file).each((index, view) => {
+ var $frame, $track, $dragger, $frameAdded, framePadding;
+
+ const ref = ImageFile.prepareFrames(view);
+ [maxWidth, maxHeight] = ref;
+ $frame = $('.onion-skin-frame', view);
+ $frameAdded = $('.frame.added', view);
+ $track = $('.drag-track', view);
+ $dragger = $('.dragger', $track);
+
+ $frame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28,
+ });
+ $('.swipe-wrap', view).css({
+ width: maxWidth + 1,
+ height: maxHeight + 2,
+ });
+ $dragger.css({
+ left: dragTrackWidth,
+ });
+
+ $frameAdded.css('opacity', 1);
+ framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+ this.initDraggable($dragger, framePadding, (e, left) => {
+ var opacity = left / dragTrackWidth;
+
+ if (opacity >= 0 && opacity <= 1) {
+ $dragger.css('left', left);
+ $frameAdded.css('opacity', opacity);
+ }
+ });
+ });
},
};
@@ -235,14 +206,7 @@ export default class ImageFile {
if (domImg.complete) {
return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
} else {
- return img.on(
- 'load',
- (function(_this) {
- return function() {
- return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
- };
- })(this),
- );
+ return img.on('load', () => callback.call(this, domImg.naturalWidth, domImg.naturalHeight));
}
}
}
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 81ba15577fb..a23707209dc 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, no-else-return */
+/* eslint-disable func-names, no-else-return */
import $ from 'jquery';
import { __ } from './locale';
@@ -8,9 +8,8 @@ import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() {
- var $dropdown, selected;
- $dropdown = $(this);
- selected = $dropdown.data('selected');
+ const $dropdown = $(this);
+ const selected = $dropdown.data('selected');
const $dropdownContainer = $dropdown.closest('.dropdown');
const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer);
const $filterInput = $('input[type="search"]', $dropdownContainer);
@@ -44,17 +43,16 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
fieldName: $dropdown.data('fieldName'),
filterInput: 'input[type="search"]',
renderRow(ref) {
- var link;
+ const link = $('<a />')
+ .attr('href', '#')
+ .addClass(ref === selected ? 'is-active' : '')
+ .text(ref)
+ .attr('data-ref', ref);
if (ref.header != null) {
return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else {
- link = $('<a />')
- .attr('href', '#')
- .addClass(ref === selected ? 'is-active' : '')
- .text(ref)
- .attr('data-ref', ref);
return $('<li />').append(link);
}
},
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 197a0706062..4fa18b19556 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -41,7 +41,7 @@ export default {
noForkText() {
return sprintf(
__(
- "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private.",
+ "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private.",
),
{ link_start: `<a href="${this.newForkPath}" class="help-link">`, link_end: '</a>' },
false,
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
new file mode 100644
index 00000000000..7dd6b051cb4
--- /dev/null
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -0,0 +1,227 @@
+<script>
+import { __ } from '~/locale';
+import _ from 'underscore';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { getDatesInRange } from '~/lib/utils/datetime_utility';
+import { xAxisLabelFormatter, dateFormatter } from '../utils';
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLoadingIcon,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ masterChart: null,
+ individualCharts: [],
+ svgs: {},
+ masterChartHeight: 264,
+ individualChartHeight: 216,
+ };
+ },
+ computed: {
+ ...mapState(['chartData', 'loading']),
+ ...mapGetters(['showChart', 'parsedData']),
+ masterChartData() {
+ const data = {};
+ this.xAxisRange.forEach(date => {
+ data[date] = this.parsedData.total[date] || 0;
+ });
+ return [
+ {
+ name: __('Commits'),
+ data: Object.entries(data),
+ },
+ ];
+ },
+ masterChartOptions() {
+ return {
+ ...this.getCommonChartOptions(true),
+ yAxis: {
+ name: __('Number of commits'),
+ },
+ grid: {
+ bottom: 64,
+ left: 64,
+ right: 20,
+ top: 20,
+ },
+ };
+ },
+ individualChartsData() {
+ const maxNumberOfIndividualContributorsCharts = 100;
+
+ return Object.keys(this.parsedData.byAuthor)
+ .map(name => {
+ const author = this.parsedData.byAuthor[name];
+ return {
+ name,
+ email: author.email,
+ commits: author.commits,
+ dates: [
+ {
+ name: __('Commits'),
+ data: this.xAxisRange.map(date => [date, author.dates[date] || 0]),
+ },
+ ],
+ };
+ })
+ .sort((a, b) => b.commits - a.commits)
+ .slice(0, maxNumberOfIndividualContributorsCharts);
+ },
+ individualChartOptions() {
+ return {
+ ...this.getCommonChartOptions(false),
+ yAxis: {
+ name: __('Commits'),
+ max: this.individualChartYAxisMax,
+ },
+ grid: {
+ bottom: 27,
+ left: 64,
+ right: 20,
+ top: 8,
+ },
+ };
+ },
+ individualChartYAxisMax() {
+ return this.individualChartsData.reduce((acc, item) => {
+ const values = item.dates[0].data.map(value => value[1]);
+ return Math.max(acc, ...values);
+ }, 0);
+ },
+ xAxisRange() {
+ const dates = Object.keys(this.parsedData.total).sort((a, b) => new Date(a) - new Date(b));
+
+ const firstContributionDate = new Date(dates[0]);
+ const lastContributionDate = new Date(dates[dates.length - 1]);
+
+ return getDatesInRange(firstContributionDate, lastContributionDate, dateFormatter);
+ },
+ firstContributionDate() {
+ return this.xAxisRange[0];
+ },
+ lastContributionDate() {
+ return this.xAxisRange[this.xAxisRange.length - 1];
+ },
+ charts() {
+ return _.uniq(this.individualCharts);
+ },
+ },
+ mounted() {
+ this.fetchChartData(this.endpoint);
+ },
+ methods: {
+ ...mapActions(['fetchChartData']),
+ getCommonChartOptions(isMasterChart) {
+ return {
+ xAxis: {
+ type: 'time',
+ name: '',
+ data: this.xAxisRange,
+ axisLabel: {
+ formatter: xAxisLabelFormatter,
+ showMaxLabel: false,
+ showMinLabel: false,
+ },
+ boundaryGap: false,
+ splitNumber: isMasterChart ? 24 : 18,
+ // 28 days
+ minInterval: 28 * 86400 * 1000,
+ min: this.firstContributionDate,
+ max: this.lastContributionDate,
+ },
+ };
+ },
+ setSvg(name) {
+ return getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(() => {});
+ },
+ onMasterChartCreated(chart) {
+ this.masterChart = chart;
+ this.setSvg('scroll-handle')
+ .then(() => {
+ this.masterChart.setOption({
+ dataZoom: [
+ {
+ type: 'slider',
+ handleIcon: this.svgs['scroll-handle'],
+ },
+ ],
+ });
+ })
+ .catch(() => {});
+ this.masterChart.on('datazoom', _.debounce(this.setIndividualChartsZoom, 200));
+ },
+ onIndividualChartCreated(chart) {
+ this.individualCharts.push(chart);
+ },
+ setIndividualChartsZoom(options) {
+ this.charts.forEach(chart =>
+ chart.setOption(
+ {
+ dataZoom: {
+ start: options.start,
+ end: options.end,
+ show: false,
+ },
+ },
+ { lazyUpdate: true },
+ ),
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="loading" class="contributors-loader text-center">
+ <gl-loading-icon :inline="true" :size="4" />
+ </div>
+
+ <div v-else-if="showChart" class="contributors-charts">
+ <h4>{{ __('Commits to') }} {{ branch }}</h4>
+ <span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
+ <div>
+ <gl-area-chart
+ :data="masterChartData"
+ :option="masterChartOptions"
+ :height="masterChartHeight"
+ @created="onMasterChartCreated"
+ />
+ </div>
+
+ <div class="row">
+ <div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6">
+ <h4>{{ contributor.name }}</h4>
+ <p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
+ <gl-area-chart
+ :data="contributor.dates"
+ :option="individualChartOptions"
+ :height="individualChartHeight"
+ @created="onIndividualChartCreated"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js
new file mode 100644
index 00000000000..b6063589734
--- /dev/null
+++ b/app/assets/javascripts/contributors/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ContributorsGraphs from './components/contributors.vue';
+import store from './stores';
+
+export default () => {
+ const el = document.querySelector('.js-contributors-graph');
+
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+
+ render(createElement) {
+ return createElement(ContributorsGraphs, {
+ props: {
+ endpoint: el.dataset.projectGraphPath,
+ branch: el.dataset.projectBranch,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/contributors/services/contributors_service.js b/app/assets/javascripts/contributors/services/contributors_service.js
new file mode 100644
index 00000000000..5a8bbb66511
--- /dev/null
+++ b/app/assets/javascripts/contributors/services/contributors_service.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ fetchChartData(endpoint) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
new file mode 100644
index 00000000000..4138ff24f1d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -0,0 +1,20 @@
+import flash from '~/flash';
+import { __ } from '~/locale';
+import service from '../services/contributors_service';
+import * as types from './mutation_types';
+
+export const fetchChartData = ({ commit }, endpoint) => {
+ commit(types.SET_LOADING_STATE, true);
+
+ return service
+ .fetchChartData(endpoint)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_CHART_DATA, data);
+ commit(types.SET_LOADING_STATE, false);
+ })
+ .catch(() => flash(__('An error occurred while loading chart data')));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js
new file mode 100644
index 00000000000..9e02e3ed9e7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/getters.js
@@ -0,0 +1,33 @@
+export const showChart = state => Boolean(!state.loading && state.chartData);
+
+export const parsedData = state => {
+ const byAuthor = {};
+ const total = {};
+
+ state.chartData.forEach(({ date, author_name, author_email }) => {
+ total[date] = total[date] ? total[date] + 1 : 1;
+
+ const authorData = byAuthor[author_name];
+
+ if (!authorData) {
+ byAuthor[author_name] = {
+ email: author_email.toLowerCase(),
+ commits: 1,
+ dates: {
+ [date]: 1,
+ },
+ };
+ } else {
+ authorData.commits += 1;
+ authorData.dates[date] = authorData.dates[date] ? authorData.dates[date] + 1 : 1;
+ }
+ });
+
+ return {
+ total,
+ byAuthor,
+ };
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js
new file mode 100644
index 00000000000..bc739851aa7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import mutations from './mutations';
+import * as getters from './getters';
+import * as actions from './actions';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/contributors/stores/mutation_types.js b/app/assets/javascripts/contributors/stores/mutation_types.js
new file mode 100644
index 00000000000..62e0a51d5f8
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_CHART_DATA = 'SET_CHART_DATA';
+export const SET_LOADING_STATE = 'SET_LOADING_STATE';
+export const SET_ACTIVE_BRANCH = 'SET_ACTIVE_BRANCH';
diff --git a/app/assets/javascripts/contributors/stores/mutations.js b/app/assets/javascripts/contributors/stores/mutations.js
new file mode 100644
index 00000000000..f1f460d072d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutations.js
@@ -0,0 +1,17 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_LOADING_STATE](state, value) {
+ state.loading = value;
+ },
+ [types.SET_CHART_DATA](state, chartData) {
+ Object.assign(state, {
+ chartData,
+ });
+ },
+ [types.SET_ACTIVE_BRANCH](state, branch) {
+ Object.assign(state, {
+ branch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/state.js b/app/assets/javascripts/contributors/stores/state.js
new file mode 100644
index 00000000000..1dc1a3c7b75
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ loading: false,
+ chartData: null,
+ branch: 'master',
+});
diff --git a/app/assets/javascripts/contributors/utils.js b/app/assets/javascripts/contributors/utils.js
new file mode 100644
index 00000000000..7d8932ce495
--- /dev/null
+++ b/app/assets/javascripts/contributors/utils.js
@@ -0,0 +1,30 @@
+import { getMonthNames } from '~/lib/utils/datetime_utility';
+
+/**
+ * Converts provided string to date and returns formatted value as a year for date in January and month name for the rest
+ * @param {String}
+ * @returns {String} - formatted value
+ *
+ * xAxisLabelFormatter('01-12-2019') will return '2019'
+ * xAxisLabelFormatter('02-12-2019') will return 'Feb'
+ * xAxisLabelFormatter('07-12-2019') will return 'Jul'
+ */
+export const xAxisLabelFormatter = val => {
+ const date = new Date(val);
+ const month = date.getUTCMonth();
+ const year = date.getUTCFullYear();
+ return month === 0 ? `${year}` : getMonthNames(true)[month];
+};
+
+/**
+ * Formats provided date to YYYY-MM-DD format
+ * @param {Date}
+ * @returns {String} - formatted value
+ */
+export const dateFormatter = date => {
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth();
+ const day = date.getUTCDate();
+
+ return `${year}-${`0${month + 1}`.slice(-2)}-${`0${day}`.slice(-2)}`;
+};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
index 3c6da43c4c4..e6893c14cda 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/cluster_form_dropdown.vue
@@ -2,14 +2,19 @@
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+import { GlIcon } from '@gitlab/ui';
-const findItem = (items, valueProp, value) => items.find(item => item[valueProp] === value);
+const toArray = value => [].concat(value);
+const itemsProp = (items, prop) => items.map(item => item[prop]);
+const defaultSearchFn = (searchQuery, labelProp) => item =>
+ item[labelProp].toLowerCase().indexOf(searchQuery) > -1;
export default {
components: {
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
+ GlIcon,
},
props: {
fieldName: {
@@ -28,7 +33,7 @@ export default {
default: '',
},
value: {
- type: [Object, String],
+ type: [Object, Array, String],
required: false,
default: () => null,
},
@@ -72,6 +77,11 @@ export default {
required: false,
default: false,
},
+ multiple: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
errorMessage: {
type: String,
required: false,
@@ -90,12 +100,11 @@ export default {
searchFn: {
type: Function,
required: false,
- default: searchQuery => item => item.name.toLowerCase().indexOf(searchQuery) > -1,
+ default: defaultSearchFn,
},
},
data() {
return {
- selectedItem: findItem(this.items, this.value),
searchQuery: '',
};
},
@@ -109,36 +118,52 @@ export default {
return this.disabledText;
}
- if (!this.selectedItem) {
+ if (!this.selectedItems.length) {
return this.placeholder;
}
- return this.selectedItemLabel;
+ return this.selectedItemsLabels;
},
results() {
- if (!this.items) {
- return [];
- }
-
- return this.items.filter(this.searchFn(this.searchQuery));
+ return this.getItemsOrEmptyList().filter(this.searchFn(this.searchQuery, this.labelProperty));
},
- selectedItemLabel() {
- return this.selectedItem && this.selectedItem[this.labelProperty];
+ selectedItems() {
+ const valueProp = this.valueProperty;
+ const valueList = toArray(this.value);
+ const items = this.getItemsOrEmptyList();
+
+ return items.filter(item => valueList.some(value => item[valueProp] === value));
},
- selectedItemValue() {
- return (this.selectedItem && this.selectedItem[this.valueProperty]) || '';
+ selectedItemsLabels() {
+ return itemsProp(this.selectedItems, this.labelProperty).join(', ');
},
- },
- watch: {
- value(value) {
- this.selectedItem = findItem(this.items, this.valueProperty, value);
+ selectedItemsValues() {
+ return itemsProp(this.selectedItems, this.valueProperty).join(', ');
},
},
methods: {
- select(item) {
- this.selectedItem = item;
+ getItemsOrEmptyList() {
+ return this.items || [];
+ },
+ selectSingle(item) {
this.$emit('input', item[this.valueProperty]);
},
+ selectMultiple(item) {
+ const value = toArray(this.value);
+ const itemValue = item[this.valueProperty];
+ const itemValueIndex = value.indexOf(itemValue);
+
+ if (itemValueIndex > -1) {
+ value.splice(itemValueIndex, 1);
+ } else {
+ value.push(itemValue);
+ }
+
+ this.$emit('input', value);
+ },
+ isSelected(item) {
+ return this.selectedItems.includes(item);
+ },
},
};
</script>
@@ -146,7 +171,7 @@ export default {
<template>
<div>
<div class="js-gcp-machine-type-dropdown dropdown">
- <dropdown-hidden-input :name="fieldName" :value="selectedItemValue" />
+ <dropdown-hidden-input :name="fieldName" :value="selectedItemsValues" />
<dropdown-button
:class="{ 'border-danger': hasErrors }"
:is-disabled="disabled"
@@ -158,15 +183,28 @@ export default {
<div class="dropdown-content">
<ul>
<li v-if="!results.length">
- <span class="js-empty-text menu-item">
- {{ emptyText }}
- </span>
+ <span class="js-empty-text menu-item">{{ emptyText }}</span>
</li>
<li v-for="item in results" :key="item.id">
- <button class="js-dropdown-item" type="button" @click.prevent="select(item)">
- <slot name="item" :item="item">
- {{ item.name }}
- </slot>
+ <button
+ v-if="multiple"
+ class="js-dropdown-item d-flex align-items-center"
+ type="button"
+ @click.stop.prevent="selectMultiple(item)"
+ >
+ <gl-icon
+ :class="[{ invisible: !isSelected(item) }, 'mr-1']"
+ name="mobile-issue-close"
+ />
+ <slot name="item" :item="item">{{ item.name }}</slot>
+ </button>
+ <button
+ v-else
+ class="js-dropdown-item"
+ type="button"
+ @click.prevent="selectSingle(item)"
+ >
+ <slot name="item" :item="item">{{ item.name }}</slot>
</button>
</li>
</ul>
@@ -182,8 +220,7 @@ export default {
'text-muted': !hasErrors,
},
]"
+ >{{ errorMessage }}</span
>
- {{ errorMessage }}
- </span>
</div>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
index 22ee368b8e0..3f7c2204b9f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/create_eks_cluster.vue
@@ -1,4 +1,5 @@
<script>
+import { mapState } from 'vuex';
import ServiceCredentialsForm from './service_credentials_form.vue';
import EksClusterConfigurationForm from './eks_cluster_configuration_form.vue';
@@ -16,14 +17,37 @@ export default {
type: String,
required: true,
},
+ accountAndExternalIdsHelpPath: {
+ type: String,
+ required: true,
+ },
+ createRoleArnHelpPath: {
+ type: String,
+ required: true,
+ },
+ externalLinkIcon: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['hasCredentials']),
},
};
</script>
<template>
<div class="js-create-eks-cluster">
<eks-cluster-configuration-form
+ v-if="hasCredentials"
:gitlab-managed-cluster-help-path="gitlabManagedClusterHelpPath"
:kubernetes-integration-help-path="kubernetesIntegrationHelpPath"
+ :external-link-icon="externalLinkIcon"
+ />
+ <service-credentials-form
+ v-else
+ :create-role-arn-help-path="createRoleArnHelpPath"
+ :account-and-external-ids-help-path="accountAndExternalIdsHelpPath"
+ :external-link-icon="externalLinkIcon"
/>
</div>
</template>
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 1188cf08850..57d5f4f541b 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
@@ -4,8 +4,8 @@ import { sprintf, s__ } from '~/locale';
import _ from 'underscore';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import ClusterFormDropdown from './cluster_form_dropdown.vue';
-import RegionDropdown from './region_dropdown.vue';
import { KUBERNETES_VERSIONS } from '../constants';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
const { mapState: mapRegionsState, mapActions: mapRegionsActions } = createNamespacedHelpers(
@@ -22,13 +22,17 @@ const {
mapState: mapSecurityGroupsState,
mapActions: mapSecurityGroupsActions,
} = createNamespacedHelpers('securityGroups');
+const {
+ mapState: mapInstanceTypesState,
+ mapActions: mapInstanceTypesActions,
+} = createNamespacedHelpers('instanceTypes');
export default {
components: {
ClusterFormDropdown,
- RegionDropdown,
GlFormInput,
GlFormCheckbox,
+ LoadingButton,
},
props: {
gitlabManagedClusterHelpPath: {
@@ -39,6 +43,10 @@ export default {
type: String,
required: true,
},
+ externalLinkIcon: {
+ type: String,
+ required: true,
+ },
},
computed: {
...mapState([
@@ -51,7 +59,10 @@ export default {
'selectedSubnet',
'selectedRole',
'selectedSecurityGroup',
+ 'selectedInstanceType',
+ 'nodeCount',
'gitlabManagedCluster',
+ 'isCreatingCluster',
]),
...mapRolesState({
roles: 'items',
@@ -83,6 +94,11 @@ export default {
isLoadingSecurityGroups: 'isLoadingItems',
loadingSecurityGroupsError: 'loadingItemsError',
}),
+ ...mapInstanceTypesState({
+ instanceTypes: 'items',
+ isLoadingInstanceTypes: 'isLoadingItems',
+ loadingInstanceTypesError: 'loadingItemsError',
+ }),
kubernetesVersions() {
return KUBERNETES_VERSIONS;
},
@@ -98,6 +114,27 @@ export default {
securityGroupDropdownDisabled() {
return !this.selectedVpc;
},
+ createClusterButtonDisabled() {
+ return (
+ !this.clusterName ||
+ !this.environmentScope ||
+ !this.kubernetesVersion ||
+ !this.selectedRegion ||
+ !this.selectedKeyPair ||
+ !this.selectedVpc ||
+ !this.selectedSubnet ||
+ !this.selectedRole ||
+ !this.selectedSecurityGroup ||
+ !this.selectedInstanceType ||
+ !this.nodeCount ||
+ this.isCreatingCluster
+ );
+ },
+ createClusterButtonLabel() {
+ return this.isCreatingCluster
+ ? s__('ClusterIntegration|Creating Kubernetes cluster')
+ : s__('ClusterIntegration|Create Kubernetes cluster');
+ },
kubernetesIntegrationHelpText() {
const escapedUrl = _.escape(this.kubernetesIntegrationHelpPath);
@@ -115,11 +152,26 @@ export default {
roleDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}.',
+ 'ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}.',
+ ),
+ {
+ startLink:
+ '<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#role-create" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ regionsDropdownHelpText() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}.',
),
{
startLink:
- '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
+ '<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -128,11 +180,12 @@ export default {
keyPairDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services%{endLink}.',
+ 'ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}.',
),
{
startLink:
'<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -141,11 +194,12 @@ export default {
vpcDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services%{endLink}.',
+ '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}.',
),
{
startLink:
- '<a href="https://console.aws.amazon.com/vpc/home?#vpc" target="_blank" rel="noopener noreferrer">',
+ '<a href="https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.html#vpc-create" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -154,11 +208,12 @@ export default {
subnetDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Choose the %{startLink}subnets%{endLink} in your VPC where your worker nodes will run.',
+ 'ClusterIntegration|Choose the %{startLink}subnets %{externalLinkIcon} %{endLink} in your VPC where your worker nodes will run.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#subnets" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -167,11 +222,26 @@ export default {
securityGroupDropdownHelpText() {
return sprintf(
s__(
- 'ClusterIntegration|Choose the %{startLink}security groups%{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
+ 'ClusterIntegration|Choose the %{startLink}security group %{externalLinkIcon} %{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.',
),
{
startLink:
'<a href="https://console.aws.amazon.com/vpc/home?#securityGroups" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ instanceTypesDropdownHelpText() {
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Choose the worker node %{startLink}instance type %{externalLinkIcon} %{endLink}.',
+ ),
+ {
+ startLink:
+ '<a href="https://aws.amazon.com/ec2/instance-types" target="_blank" rel="noopener noreferrer">',
+ externalLinkIcon: this.externalLinkIcon,
endLink: '</a>',
},
false,
@@ -195,9 +265,12 @@ export default {
mounted() {
this.fetchRegions();
this.fetchRoles();
+ this.fetchInstanceTypes();
},
methods: {
...mapActions([
+ 'createCluster',
+ 'signOut',
'setClusterName',
'setEnvironmentScope',
'setKubernetesVersion',
@@ -207,6 +280,8 @@ export default {
'setRole',
'setKeyPair',
'setSecurityGroup',
+ 'setInstanceType',
+ 'setNodeCount',
'setGitlabManagedCluster',
]),
...mapRegionsActions({ fetchRegions: 'fetchItems' }),
@@ -215,15 +290,22 @@ export default {
...mapRolesActions({ fetchRoles: 'fetchItems' }),
...mapKeyPairsActions({ fetchKeyPairs: 'fetchItems' }),
...mapSecurityGroupsActions({ fetchSecurityGroups: 'fetchItems' }),
+ ...mapInstanceTypesActions({ fetchInstanceTypes: 'fetchItems' }),
setRegionAndFetchVpcsAndKeyPairs(region) {
this.setRegion({ region });
+ this.setVpc({ vpc: null });
+ this.setKeyPair({ keyPair: null });
+ this.setSubnet({ subnet: null });
+ this.setSecurityGroup({ securityGroup: null });
this.fetchVpcs({ region });
this.fetchKeyPairs({ region });
},
setVpcAndFetchSubnets(vpc) {
this.setVpc({ vpc });
- this.fetchSubnets({ vpc });
- this.fetchSecurityGroups({ vpc });
+ this.setSubnet({ subnet: null });
+ this.setSecurityGroup({ securityGroup: null });
+ this.fetchSubnets({ vpc, region: this.selectedRegion });
+ this.fetchSecurityGroups({ vpc, region: this.selectedRegion });
},
},
};
@@ -233,7 +315,12 @@ export default {
<h2>
{{ s__('ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster') }}
</h2>
- <p v-html="kubernetesIntegrationHelpText"></p>
+ <div class="mb-3" v-html="kubernetesIntegrationHelpText"></div>
+ <div class="mb-3">
+ <button class="btn btn-link js-sign-out" @click.prevent="signOut()">
+ {{ s__('ClusterIntegration|Select a different AWS role') }}
+ </button>
+ </div>
<div class="form-group">
<label class="label-bold" for="eks-cluster-name">{{
s__('ClusterIntegration|Kubernetes cluster name')
@@ -273,7 +360,7 @@ export default {
<cluster-form-dropdown
field-id="eks-role"
field-name="eks-role"
- :input="selectedRole"
+ :value="selectedRole"
:items="roles"
:loading="isLoadingRoles"
:loading-text="s__('ClusterIntegration|Loading IAM Roles')"
@@ -288,13 +375,21 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Region') }}</label>
- <region-dropdown
+ <cluster-form-dropdown
+ field-id="eks-region"
+ field-name="eks-region"
:value="selectedRegion"
- :regions="regions"
- :error="loadingRegionsError"
+ :items="regions"
:loading="isLoadingRegions"
+ :loading-text="s__('ClusterIntegration|Loading Regions')"
+ :placeholder="s__('ClusterIntergation|Select a region')"
+ :search-field-placeholder="s__('ClusterIntegration|Search regions')"
+ :empty-text="s__('ClusterIntegration|No region found')"
+ :has-errors="Boolean(loadingRegionsError)"
+ :error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
@input="setRegionAndFetchVpcsAndKeyPairs($event)"
/>
+ <p class="form-text text-muted" v-html="regionsDropdownHelpText"></p>
</div>
<div class="form-group">
<label class="label-bold" for="eks-key-pair">{{
@@ -303,7 +398,7 @@ export default {
<cluster-form-dropdown
field-id="eks-key-pair"
field-name="eks-key-pair"
- :input="selectedKeyPair"
+ :value="selectedKeyPair"
:items="keyPairs"
:disabled="keyPairDropdownDisabled"
:disabled-text="s__('ClusterIntegration|Select a region to choose a Key Pair')"
@@ -323,7 +418,7 @@ export default {
<cluster-form-dropdown
field-id="eks-vpc"
field-name="eks-vpc"
- :input="selectedVpc"
+ :value="selectedVpc"
:items="vpcs"
:loading="isLoadingVpcs"
:disabled="vpcDropdownDisabled"
@@ -339,11 +434,12 @@ export default {
<p class="form-text text-muted" v-html="vpcDropdownHelpText"></p>
</div>
<div class="form-group">
- <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnet') }}</label>
+ <label class="label-bold" for="eks-role">{{ s__('ClusterIntegration|Subnets') }}</label>
<cluster-form-dropdown
field-id="eks-subnet"
field-name="eks-subnet"
- :input="selectedSubnet"
+ multiple
+ :value="selectedSubnet"
:items="subnets"
:loading="isLoadingSubnets"
:disabled="subnetDropdownDisabled"
@@ -360,12 +456,12 @@ export default {
</div>
<div class="form-group">
<label class="label-bold" for="eks-security-group">{{
- s__('ClusterIntegration|Security groups')
+ s__('ClusterIntegration|Security group')
}}</label>
<cluster-form-dropdown
field-id="eks-security-group"
field-name="eks-security-group"
- :input="selectedSecurityGroup"
+ :value="selectedSecurityGroup"
:items="securityGroups"
:loading="isLoadingSecurityGroups"
:disabled="securityGroupDropdownDisabled"
@@ -383,6 +479,39 @@ export default {
<p class="form-text text-muted" v-html="securityGroupDropdownHelpText"></p>
</div>
<div class="form-group">
+ <label class="label-bold" for="eks-instance-type">{{
+ s__('ClusterIntegration|Instance type')
+ }}</label>
+ <cluster-form-dropdown
+ field-id="eks-instance-type"
+ field-name="eks-instance-type"
+ :value="selectedInstanceType"
+ :items="instanceTypes"
+ :loading="isLoadingInstanceTypes"
+ :loading-text="s__('ClusterIntegration|Loading instance types')"
+ :placeholder="s__('ClusterIntergation|Select an instance type')"
+ :search-field-placeholder="s__('ClusterIntegration|Search instance types')"
+ :empty-text="s__('ClusterIntegration|No instance type found')"
+ :has-errors="Boolean(loadingInstanceTypesError)"
+ :error-message="s__('ClusterIntegration|Could not load instance types')"
+ @input="setInstanceType({ instanceType: $event })"
+ />
+ <p class="form-text text-muted" v-html="instanceTypesDropdownHelpText"></p>
+ </div>
+ <div class="form-group">
+ <label class="label-bold" for="eks-node-count">{{
+ s__('ClusterIntegration|Number of nodes')
+ }}</label>
+ <gl-form-input
+ id="eks-node-count"
+ type="number"
+ min="1"
+ step="1"
+ :value="nodeCount"
+ @input="setNodeCount({ nodeCount: $event })"
+ />
+ </div>
+ <div class="form-group">
<gl-form-checkbox
:checked="gitlabManagedCluster"
@input="setGitlabManagedCluster({ gitlabManagedCluster: $event })"
@@ -390,5 +519,14 @@ export default {
>
<p class="form-text text-muted" v-html="gitlabManagedHelpText"></p>
</div>
+ <div class="form-group">
+ <loading-button
+ class="js-create-cluster btn-success"
+ :disabled="createClusterButtonDisabled"
+ :loading="isCreatingCluster"
+ :label="createClusterButtonLabel"
+ @click="createCluster()"
+ />
+ </div>
</form>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue
deleted file mode 100644
index 765955305c8..00000000000
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/region_dropdown.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { sprintf, s__ } from '~/locale';
-
-import ClusterFormDropdown from './cluster_form_dropdown.vue';
-
-export default {
- components: {
- ClusterFormDropdown,
- },
- props: {
- regions: {
- type: Array,
- required: false,
- default: () => [],
- },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- error: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- hasErrors() {
- return Boolean(this.error);
- },
- helpText() {
- return sprintf(
- s__('ClusterIntegration|Learn more about %{startLink}Regions%{endLink}.'),
- {
- startLink:
- '<a href="https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/" target="_blank" rel="noopener noreferrer">',
- endLink: '</a>',
- },
- false,
- );
- },
- },
-};
-</script>
-<template>
- <div>
- <cluster-form-dropdown
- field-id="eks-region"
- field-name="eks-region"
- :items="regions"
- :loading="loading"
- :loading-text="s__('ClusterIntegration|Loading Regions')"
- :placeholder="s__('ClusterIntergation|Select a region')"
- :search-field-placeholder="s__('ClusterIntegration|Search regions')"
- :empty-text="s__('ClusterIntegration|No region found')"
- :has-errors="hasErrors"
- :error-message="s__('ClusterIntegration|Could not load regions from your AWS account')"
- v-bind="$attrs"
- v-on="$listeners"
- />
- <p class="form-text text-muted" v-html="helpText"></p>
- </div>
-</template>
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 79029b8cfa8..ab33e9fbc95 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,3 +1,141 @@
+<script>
+import { GlFormInput } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import _ from 'underscore';
+import { mapState, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+
+export default {
+ components: {
+ GlFormInput,
+ LoadingButton,
+ ClipboardButton,
+ },
+ props: {
+ accountAndExternalIdsHelpPath: {
+ type: String,
+ required: true,
+ },
+ createRoleArnHelpPath: {
+ type: String,
+ required: true,
+ },
+ externalLinkIcon: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ roleArn: '',
+ };
+ },
+ computed: {
+ ...mapState(['accountId', 'externalId', 'isCreatingRole', 'createRoleError']),
+ submitButtonDisabled() {
+ return this.isCreatingRole || !this.roleArn;
+ },
+ submitButtonLabel() {
+ return this.isCreatingRole
+ ? __('Authenticating')
+ : s__('ClusterIntegration|Authenticate with AWS');
+ },
+ accountAndExternalIdsHelpText() {
+ const escapedUrl = _.escape(this.accountAndExternalIdsHelpPath);
+
+ return sprintf(
+ s__(
+ 'ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}',
+ ),
+ {
+ startAwsLink:
+ '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
+ startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ provisionRoleArnHelpText() {
+ const escapedUrl = _.escape(this.createRoleArnHelpPath);
+
+ return sprintf(
+ s__(
+ '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}',
+ ),
+ {
+ startAwsLink:
+ '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
+ startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
+ externalLinkIcon: this.externalLinkIcon,
+ endLink: '</a>',
+ },
+ false,
+ );
+ },
+ },
+ methods: {
+ ...mapActions(['createRole']),
+ },
+};
+</script>
<template>
- <form name="service-credentials-form"></form>
+ <form name="service-credentials-form" @submit.prevent="createRole({ roleArn, externalId })">
+ <h2>{{ s__('ClusterIntegration|Authenticate with Amazon Web Services') }}</h2>
+ <p>
+ {{
+ s__(
+ '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.',
+ )
+ }}
+ </p>
+ <div v-if="createRoleError" class="js-invalid-credentials bs-callout bs-callout-danger">
+ {{ createRoleError }}
+ </div>
+ <div class="form-row">
+ <div class="form-group col-md-6">
+ <label for="gitlab-account-id">{{ __('Account ID') }}</label>
+ <div class="input-group">
+ <gl-form-input id="gitlab-account-id" type="text" readonly :value="accountId" />
+ <div class="input-group-append">
+ <clipboard-button
+ :text="accountId"
+ :title="__('Copy Account ID to clipboard')"
+ class="input-group-text js-copy-account-id-button"
+ />
+ </div>
+ </div>
+ </div>
+ <div class="form-group col-md-6">
+ <label for="eks-external-id">{{ __('External ID') }}</label>
+ <div class="input-group">
+ <gl-form-input id="eks-external-id" type="text" readonly :value="externalId" />
+ <div class="input-group-append">
+ <clipboard-button
+ :text="externalId"
+ :title="__('Copy External ID to clipboard')"
+ class="input-group-text js-copy-external-id-button"
+ />
+ </div>
+ </div>
+ </div>
+ <div class="col-12 mb-3 mt-n3">
+ <p class="form-text text-muted" v-html="accountAndExternalIdsHelpText"></p>
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
+ <gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
+ <p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
+ </div>
+ <loading-button
+ class="js-submit-service-credentials btn-success"
+ type="submit"
+ :disabled="submitButtonDisabled"
+ :loading="isCreatingRole"
+ :label="submitButtonLabel"
+ />
+ </form>
</template>
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index 339642f991e..a850ba89818 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1,7 +1,2 @@
// eslint-disable-next-line import/prefer-default-export
-export const KUBERNETES_VERSIONS = [
- { name: '1.14', value: '1.14' },
- { name: '1.13', value: '1.13' },
- { name: '1.12', value: '1.12' },
- { name: '1.11', value: '1.11' },
-];
+export const KUBERNETES_VERSIONS = [{ name: '1.14', value: '1.14' }];
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/index.js b/app/assets/javascripts/create_cluster/eks_cluster/index.js
index 1f595e9b2df..27f859d8972 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/index.js
@@ -1,16 +1,54 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import { parseBoolean } from '~/lib/utils/common_utils';
import CreateEksCluster from './components/create_eks_cluster.vue';
import createStore from './store';
Vue.use(Vuex);
export default el => {
- const { gitlabManagedClusterHelpPath, kubernetesIntegrationHelpPath } = el.dataset;
+ const {
+ gitlabManagedClusterHelpPath,
+ kubernetesIntegrationHelpPath,
+ accountAndExternalIdsHelpPath,
+ createRoleArnHelpPath,
+ getRolesPath,
+ getRegionsPath,
+ getKeyPairsPath,
+ getVpcsPath,
+ getSubnetsPath,
+ getSecurityGroupsPath,
+ getInstanceTypesPath,
+ externalId,
+ accountId,
+ hasCredentials,
+ createRolePath,
+ createClusterPath,
+ signOutPath,
+ externalLinkIcon,
+ } = el.dataset;
return new Vue({
el,
- store: createStore(),
+ store: createStore({
+ initialState: {
+ hasCredentials: parseBoolean(hasCredentials),
+ externalId,
+ accountId,
+ createRolePath,
+ createClusterPath,
+ signOutPath,
+ },
+ apiPaths: {
+ getRolesPath,
+ getRegionsPath,
+ getKeyPairsPath,
+ getVpcsPath,
+ getSubnetsPath,
+ getSecurityGroupsPath,
+ getInstanceTypesPath,
+ },
+ }),
components: {
CreateEksCluster,
},
@@ -19,6 +57,9 @@ export default el => {
props: {
gitlabManagedClusterHelpPath,
kubernetesIntegrationHelpPath,
+ accountAndExternalIdsHelpPath,
+ createRoleArnHelpPath,
+ externalLinkIcon,
},
});
},
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
index d982e4db4c1..21b87d525cf 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/services/aws_services_facade.js
@@ -1,84 +1,58 @@
-import EC2 from 'aws-sdk/clients/ec2';
-import IAM from 'aws-sdk/clients/iam';
-
-export const fetchRoles = () => {
- const iam = new IAM();
-
- return iam
- .listRoles()
- .promise()
- .then(({ Roles: roles }) => roles.map(({ RoleName: name }) => ({ name })));
-};
-
-export const fetchKeyPairs = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeKeyPairs()
- .promise()
- .then(({ KeyPairs: keyPairs }) => keyPairs.map(({ RegionName: name }) => ({ name })));
-};
-
-export const fetchRegions = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeRegions()
- .promise()
- .then(({ Regions: regions }) =>
- regions.map(({ RegionName: name }) => ({
- name,
- value: name,
+import axios from '~/lib/utils/axios_utils';
+
+export default apiPaths => ({
+ fetchRoles() {
+ return axios
+ .get(apiPaths.getRolesPath)
+ .then(({ data: { roles } }) =>
+ roles.map(({ role_name: name, arn: value }) => ({ name, value })),
+ );
+ },
+ fetchKeyPairs({ region }) {
+ return axios
+ .get(apiPaths.getKeyPairsPath, { params: { region } })
+ .then(({ data: { key_pairs: keyPairs } }) =>
+ keyPairs.map(({ key_name }) => ({ name: key_name, value: key_name })),
+ );
+ },
+ fetchRegions() {
+ return axios.get(apiPaths.getRegionsPath).then(({ data: { regions } }) =>
+ regions.map(({ region_name }) => ({
+ name: region_name,
+ value: region_name,
})),
);
-};
-
-export const fetchVpcs = () => {
- const ec2 = new EC2();
-
- return ec2
- .describeVpcs()
- .promise()
- .then(({ Vpcs: vpcs }) =>
- vpcs.map(({ VpcId: id }) => ({
- value: id,
- name: id,
+ },
+ fetchVpcs({ region }) {
+ return axios.get(apiPaths.getVpcsPath, { params: { region } }).then(({ data: { vpcs } }) =>
+ vpcs.map(({ vpc_id }) => ({
+ value: vpc_id,
+ name: vpc_id,
})),
);
-};
-
-export const fetchSubnets = ({ vpc }) => {
- const ec2 = new EC2();
-
- return ec2
- .describeSubnets({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ Subnets: subnets }) => subnets.map(({ SubnetId: id }) => ({ id, name: id })));
-};
-
-export const fetchSecurityGroups = ({ vpc }) => {
- const ec2 = new EC2();
-
- return ec2
- .describeSecurityGroups({
- Filters: [
- {
- Name: 'vpc-id',
- Values: [vpc],
- },
- ],
- })
- .promise()
- .then(({ SecurityGroups: securityGroups }) =>
- securityGroups.map(({ GroupName: name, GroupId: value }) => ({ name, value })),
- );
-};
-
-export default () => {};
+ },
+ fetchSubnets({ vpc, region }) {
+ return axios
+ .get(apiPaths.getSubnetsPath, { params: { vpc_id: vpc, region } })
+ .then(({ data: { subnets } }) =>
+ subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id })),
+ );
+ },
+ fetchSecurityGroups({ vpc, region }) {
+ return axios
+ .get(apiPaths.getSecurityGroupsPath, { params: { vpc_id: vpc, region } })
+ .then(({ data: { security_groups: securityGroups } }) =>
+ securityGroups.map(({ group_name: name, group_id: value }) => ({ name, value })),
+ );
+ },
+ fetchInstanceTypes() {
+ return axios
+ .get(apiPaths.getInstanceTypesPath)
+ .then(({ data: { instance_types: instanceTypes } }) =>
+ instanceTypes.map(({ instance_type_name }) => ({
+ name: instance_type_name,
+ value: instance_type_name,
+ })),
+ );
+ },
+});
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index 917c8da6c3e..72f15263a8f 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,4 +1,12 @@
import * as types from './mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+
+const getErrorMessage = data => {
+ const errorKey = Object.keys(data)[0];
+
+ return data[errorKey][0];
+};
export const setClusterName = ({ commit }, payload) => {
commit(types.SET_CLUSTER_NAME, payload);
@@ -12,6 +20,68 @@ export const setKubernetesVersion = ({ commit }, payload) => {
commit(types.SET_KUBERNETES_VERSION, payload);
};
+export const createRole = ({ dispatch, state: { createRolePath } }, payload) => {
+ dispatch('requestCreateRole');
+
+ return axios
+ .post(createRolePath, {
+ role_arn: payload.roleArn,
+ role_external_id: payload.externalId,
+ })
+ .then(() => dispatch('createRoleSuccess'))
+ .catch(error => dispatch('createRoleError', { error }));
+};
+
+export const requestCreateRole = ({ commit }) => {
+ commit(types.REQUEST_CREATE_ROLE);
+};
+
+export const createRoleSuccess = ({ commit }) => {
+ commit(types.CREATE_ROLE_SUCCESS);
+};
+
+export const createRoleError = ({ commit }, payload) => {
+ commit(types.CREATE_ROLE_ERROR, payload);
+};
+
+export const createCluster = ({ dispatch, state }) => {
+ dispatch('requestCreateCluster');
+
+ return axios
+ .post(state.createClusterPath, {
+ name: state.clusterName,
+ environment_scope: state.environmentScope,
+ managed: state.gitlabManagedCluster,
+ provider_aws_attributes: {
+ region: state.selectedRegion,
+ vpc_id: state.selectedVpc,
+ subnet_ids: state.selectedSubnet,
+ role_arn: state.selectedRole,
+ key_name: state.selectedKeyPair,
+ security_group_id: state.selectedSecurityGroup,
+ instance_type: state.selectedInstanceType,
+ num_nodes: state.nodeCount,
+ },
+ })
+ .then(({ headers: { location } }) => dispatch('createClusterSuccess', location))
+ .catch(({ response: { data } }) => {
+ dispatch('createClusterError', data);
+ });
+};
+
+export const requestCreateCluster = ({ commit }) => {
+ commit(types.REQUEST_CREATE_CLUSTER);
+};
+
+export const createClusterSuccess = (_, location) => {
+ window.location.assign(location);
+};
+
+export const createClusterError = ({ commit }, error) => {
+ commit(types.CREATE_CLUSTER_ERROR, error);
+ createFlash(getErrorMessage(error));
+};
+
export const setRegion = ({ commit }, payload) => {
commit(types.SET_REGION, payload);
};
@@ -40,4 +110,16 @@ export const setGitlabManagedCluster = ({ commit }, payload) => {
commit(types.SET_GITLAB_MANAGED_CLUSTER, payload);
};
-export default () => {};
+export const setInstanceType = ({ commit }, payload) => {
+ commit(types.SET_INSTANCE_TYPE, payload);
+};
+
+export const setNodeCount = ({ commit }, payload) => {
+ commit(types.SET_NODE_COUNT, payload);
+};
+
+export const signOut = ({ commit, state: { signOutPath } }) =>
+ axios
+ .delete(signOutPath)
+ .then(() => commit(types.SIGN_OUT))
+ .catch(({ response: { data } }) => createFlash(getErrorMessage(data)));
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
index d575deafd19..5982fc8a2fd 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/index.js
@@ -6,14 +6,16 @@ import state from './state';
import clusterDropdownStore from './cluster_dropdown';
-import * as awsServices from '../services/aws_services_facade';
+import awsServicesFactory from '../services/aws_services_facade';
-const createStore = () =>
- new Vuex.Store({
+const createStore = ({ initialState, apiPaths }) => {
+ const awsServices = awsServicesFactory(apiPaths);
+
+ return new Vuex.Store({
actions,
getters,
mutations,
- state: state(),
+ state: Object.assign(state(), initialState),
modules: {
roles: {
namespaced: true,
@@ -39,7 +41,12 @@ const createStore = () =>
namespaced: true,
...clusterDropdownStore(awsServices.fetchSecurityGroups),
},
+ instanceTypes: {
+ namespaced: true,
+ ...clusterDropdownStore(awsServices.fetchInstanceTypes),
+ },
},
});
+};
export default createStore;
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
index 82eb512ac07..f9204cc2207 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutation_types.js
@@ -7,4 +7,13 @@ export const SET_KEY_PAIR = 'SET_KEY_PAIR';
export const SET_SUBNET = 'SET_SUBNET';
export const SET_ROLE = 'SET_ROLE';
export const SET_SECURITY_GROUP = 'SET_SECURITY_GROUP';
+export const SET_INSTANCE_TYPE = 'SET_INSTANCE_TYPE';
+export const SET_NODE_COUNT = 'SET_NODE_COUNT';
export const SET_GITLAB_MANAGED_CLUSTER = 'SET_GITLAB_MANAGED_CLUSTER';
+export const REQUEST_CREATE_ROLE = 'REQUEST_CREATE_ROLE';
+export const CREATE_ROLE_SUCCESS = 'CREATE_ROLE_SUCCESS';
+export const CREATE_ROLE_ERROR = 'CREATE_ROLE_ERROR';
+export const SIGN_OUT = 'SIGN_OUT';
+export const REQUEST_CREATE_CLUSTER = 'REQUEST_CREATE_CLUSTER';
+export const CREATE_CLUSTER_SUCCESS = 'CREATE_CLUSTER_SUCCESS';
+export const CREATE_CLUSTER_ERROR = 'CREATE_CLUSTER_ERROR';
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
index 79950ac7dce..aa04c8f7079 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/mutations.js
@@ -28,7 +28,39 @@ export default {
[types.SET_SECURITY_GROUP](state, { securityGroup }) {
state.selectedSecurityGroup = securityGroup;
},
+ [types.SET_INSTANCE_TYPE](state, { instanceType }) {
+ state.selectedInstanceType = instanceType;
+ },
+ [types.SET_NODE_COUNT](state, { nodeCount }) {
+ state.nodeCount = nodeCount;
+ },
[types.SET_GITLAB_MANAGED_CLUSTER](state, { gitlabManagedCluster }) {
state.gitlabManagedCluster = gitlabManagedCluster;
},
+ [types.REQUEST_CREATE_ROLE](state) {
+ state.isCreatingRole = true;
+ state.createRoleError = null;
+ state.hasCredentials = false;
+ },
+ [types.CREATE_ROLE_SUCCESS](state) {
+ state.isCreatingRole = false;
+ state.createRoleError = null;
+ state.hasCredentials = true;
+ },
+ [types.CREATE_ROLE_ERROR](state, { error }) {
+ state.isCreatingRole = false;
+ state.createRoleError = error;
+ state.hasCredentials = false;
+ },
+ [types.REQUEST_CREATE_CLUSTER](state) {
+ state.isCreatingCluster = true;
+ state.createClusterError = null;
+ },
+ [types.CREATE_CLUSTER_ERROR](state, { error }) {
+ state.isCreatingCluster = false;
+ state.createClusterError = error;
+ },
+ [types.SIGN_OUT](state) {
+ state.hasCredentials = false;
+ },
};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
index bf74213bdce..2e3a05a9187 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/state.js
@@ -1,18 +1,31 @@
import { KUBERNETES_VERSIONS } from '../constants';
+const [{ value: kubernetesVersion }] = KUBERNETES_VERSIONS;
+
export default () => ({
- isValidatingCredentials: false,
- validCredentials: false,
+ createRolePath: null,
+
+ isCreatingRole: false,
+ roleCreated: false,
+ createRoleError: false,
+
+ accountId: '',
+ externalId: '',
clusterName: '',
environmentScope: '*',
- kubernetesVersion: [KUBERNETES_VERSIONS].value,
+ kubernetesVersion,
selectedRegion: '',
selectedRole: '',
selectedKeyPair: '',
selectedVpc: '',
selectedSubnet: '',
selectedSecurityGroup: '',
+ selectedInstanceType: 'm5.large',
+ nodeCount: '3',
+
+ isCreatingCluster: false,
+ createClusterError: false,
gitlabManagedCluster: true,
});
diff --git a/app/assets/javascripts/projects/gke_cluster_namespace/index.js b/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js
index 0ec4d8807b0..0ec4d8807b0 100644
--- a/app/assets/javascripts/projects/gke_cluster_namespace/index.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster_namespace/index.js
diff --git a/app/assets/javascripts/create_cluster/init_create_cluster.js b/app/assets/javascripts/create_cluster/init_create_cluster.js
new file mode 100644
index 00000000000..7c984582fd8
--- /dev/null
+++ b/app/assets/javascripts/create_cluster/init_create_cluster.js
@@ -0,0 +1,37 @@
+import initGkeDropdowns from './gke_cluster';
+import initGkeNamespace from './gke_cluster_namespace';
+import PersistentUserCallout from '~/persistent_user_callout';
+
+const newClusterViews = [':clusters:new', ':clusters:create_gcp', ':clusters:create_user'];
+
+const isProjectLevelCluster = page => page.startsWith('project:clusters');
+
+export default (document, gon) => {
+ const { page } = document.body.dataset;
+ const isNewClusterView = newClusterViews.some(view => page.endsWith(view));
+
+ if (!isNewClusterView) {
+ return;
+ }
+
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
+
+ initGkeDropdowns();
+
+ if (gon.features.createEksClusters) {
+ import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
+ .then(({ default: initCreateEKSCluster }) => {
+ const el = document.querySelector('.js-create-eks-cluster-form-container');
+
+ if (el) {
+ initCreateEKSCluster(el);
+ }
+ })
+ .catch(() => {});
+ }
+
+ if (isProjectLevelCluster(page)) {
+ initGkeNamespace();
+ }
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
deleted file mode 100644
index fc6d83bf96c..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import { GlButton } from '@gitlab/ui';
-
-export default {
- name: 'StageCardListItem',
- components: {
- Icon,
- GlButton,
- },
- props: {
- isActive: {
- type: Boolean,
- required: true,
- },
- canEdit: {
- type: Boolean,
- default: false,
- required: false,
- },
- },
-};
-</script>
-
-<template>
- <div
- :class="{ active: isActive }"
- class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
- >
- <slot></slot>
- <div v-if="canEdit" class="dropdown">
- <gl-button
- :title="__('More actions')"
- class="more-actions-toggle btn btn-transparent p-0"
- data-toggle="dropdown"
- >
- <icon class="icon" name="ellipsis_v" />
- </gl-button>
- <ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
- <slot name="dropdown-options"></slot>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
index 004d335f572..1b09fe1b370 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue
@@ -1,11 +1,6 @@
<script>
-import StageCardListItem from './stage_card_list_item.vue';
-
export default {
name: 'StageNavItem',
- components: {
- StageCardListItem,
- },
props: {
isDefaultStage: {
type: Boolean,
@@ -40,16 +35,16 @@ export default {
hasValue() {
return this.value && this.value.length > 0;
},
- editable() {
- return this.isUserAllowed && this.canEdit;
- },
},
};
</script>
<template>
<li @click="$emit('select')">
- <stage-card-list-item :is-active="isActive" :can-edit="editable">
+ <div
+ :class="{ active: isActive }"
+ class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded border-color-default border-style-solid border-width-1px"
+ >
<div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
{{ title }}
</div>
@@ -62,27 +57,6 @@ export default {
<span class="not-available">{{ __('Not available') }}</span>
</template>
</div>
- <template v-slot:dropdown-options>
- <template v-if="isDefaultStage">
- <li>
- <button type="button" class="btn-default btn-transparent">
- {{ __('Hide stage') }}
- </button>
- </li>
- </template>
- <template v-else>
- <li>
- <button type="button" class="btn-default btn-transparent">
- {{ __('Edit stage') }}
- </button>
- </li>
- <li>
- <button type="button" class="btn-danger danger">
- {{ __('Remove stage') }}
- </button>
- </li>
- </template>
- </template>
- </stage-card-list-item>
+ </div>
</li>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 9a1e59ec045..a5ffa84e3fb 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -124,8 +124,10 @@ export default {
:diff-viewer-mode="diffViewerMode"
:new-path="diffFile.new_path"
:new-sha="diffFile.diff_refs.head_sha"
+ :new-size="diffFile.new_size"
:old-path="diffFile.old_path"
:old-sha="diffFile.diff_refs.base_sha"
+ :old-size="diffFile.old_size"
:file-hash="diffFileHash"
:project-path="projectPath"
:a-mode="diffFile.a_mode"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 2514274224d..9236f0d5349 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -54,11 +54,12 @@ export default {
showLoadingIcon() {
return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
- hasDiffLines() {
+ hasDiff() {
return (
- this.file.highlighted_diff_lines &&
- this.file.parallel_diff_lines &&
- this.file.parallel_diff_lines.length > 0
+ (this.file.highlighted_diff_lines &&
+ this.file.parallel_diff_lines &&
+ this.file.parallel_diff_lines.length > 0) ||
+ !this.file.blob.readable_text
);
},
isFileTooLarge() {
@@ -82,7 +83,7 @@ export default {
},
watch: {
isCollapsed: function fileCollapsedWatch(newVal, oldVal) {
- if (!newVal && oldVal && !this.hasDiffLines) {
+ if (!newVal && oldVal && !this.hasDiff) {
this.handleLoadCollapsedDiff();
}
@@ -103,7 +104,7 @@ export default {
'setFileCollapsed',
]),
handleToggle() {
- if (!this.hasDiffLines) {
+ if (!this.hasDiff) {
this.handleLoadCollapsedDiff();
} else {
this.isCollapsed = !this.isCollapsed;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 0ff26445a6a..62b390a46d7 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -290,5 +290,5 @@ export default function dropzoneInput(form) {
formTextarea.focus();
});
- return Dropzone.forElement($formDropzone.get(0));
+ return $formDropzone.get(0) ? Dropzone.forElement($formDropzone.get(0)) : null;
}
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
new file mode 100644
index 00000000000..37c9818f869
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -0,0 +1,141 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import dateFormat from 'dateformat';
+import { __, sprintf } from '~/locale';
+import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import Stacktrace from './stacktrace.vue';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { trackClickErrorLinkToSentryOptions } from '../utils';
+
+export default {
+ components: {
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ TooltipOnTruncate,
+ Icon,
+ Stacktrace,
+ },
+ directives: {
+ TrackEvent: TrackEventDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ issueDetailsPath: {
+ type: String,
+ required: true,
+ },
+ issueStackTracePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState('details', ['error', 'loading', 'loadingStacktrace', 'stacktraceData']),
+ ...mapGetters('details', ['stacktrace']),
+ reported() {
+ return sprintf(
+ __('Reported %{timeAgo} by %{reportedBy}'),
+ {
+ reportedBy: `<strong>${this.error.culprit}</strong>`,
+ timeAgo: this.timeFormated(this.stacktraceData.date_received),
+ },
+ false,
+ );
+ },
+ firstReleaseLink() {
+ return `${this.error.external_base_url}/releases/${this.error.first_release_short_version}`;
+ },
+ lastReleaseLink() {
+ return `${this.error.external_base_url}releases/${this.error.last_release_short_version}`;
+ },
+ showDetails() {
+ return Boolean(!this.loading && this.error && this.error.id);
+ },
+ showStacktrace() {
+ return Boolean(!this.loadingStacktrace && this.stacktrace && this.stacktrace.length);
+ },
+ },
+ mounted() {
+ this.startPollingDetails(this.issueDetailsPath);
+ this.startPollingStacktrace(this.issueStackTracePath);
+ },
+ methods: {
+ ...mapActions('details', ['startPollingDetails', 'startPollingStacktrace']),
+ trackClickErrorLinkToSentryOptions,
+ formatDate(date) {
+ return `${this.timeFormated(date)} (${dateFormat(date, 'UTC:yyyy-mm-dd h:MM:ssTT Z')})`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="loading" class="py-3">
+ <gl-loading-icon :size="3" />
+ </div>
+
+ <div v-else-if="showDetails" class="error-details">
+ <div class="top-area align-items-center justify-content-between py-3">
+ <span v-if="!loadingStacktrace && stacktrace" v-html="reported"></span>
+ <!-- <gl-button class="my-3 ml-auto" variant="success">
+ {{ __('Create Issue') }}
+ </gl-button>-->
+ </div>
+ <div>
+ <tooltip-on-truncate :title="error.title" truncate-target="child" placement="top">
+ <h2 class="text-truncate">{{ error.title }}</h2>
+ </tooltip-on-truncate>
+ <h3>{{ __('Error details') }}</h3>
+ <ul>
+ <li>
+ <span class="bold">{{ __('Sentry event') }}:</span>
+ <gl-link
+ v-track-event="trackClickErrorLinkToSentryOptions(error.external_url)"
+ :href="error.external_url"
+ target="_blank"
+ >
+ <span class="text-truncate">{{ error.external_url }}</span>
+ <icon name="external-link" class="ml-1 flex-shrink-0" />
+ </gl-link>
+ </li>
+ <li v-if="error.first_release_short_version">
+ <span class="bold">{{ __('First seen') }}:</span>
+ {{ formatDate(error.first_seen) }}
+ <gl-link :href="firstReleaseLink" target="_blank">
+ <span>{{ __('Release') }}: {{ error.first_release_short_version }}</span>
+ </gl-link>
+ </li>
+ <li v-if="error.last_release_short_version">
+ <span class="bold">{{ __('Last seen') }}:</span>
+ {{ formatDate(error.last_seen) }}
+ <gl-link :href="lastReleaseLink" target="_blank">
+ <span>{{ __('Release') }}: {{ error.last_release_short_version }}</span>
+ </gl-link>
+ </li>
+ <li>
+ <span class="bold">{{ __('Events') }}:</span>
+ <span>{{ error.count }}</span>
+ </li>
+ <li>
+ <span class="bold">{{ __('Users') }}:</span>
+ <span>{{ error.user_count }}</span>
+ </li>
+ </ul>
+
+ <div v-if="loadingStacktrace" class="py-3">
+ <gl-loading-icon :size="3" />
+ </div>
+
+ <template v-if="showStacktrace">
+ <h3 class="my-4">{{ __('Stack trace') }}</h3>
+ <stacktrace :entries="stacktrace" />
+ </template>
+ </div>
+ </div>
+ </div>
+</template>
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 cd298e2c692..88139ce7403 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -1,11 +1,19 @@
<script>
-import { mapActions, mapState } from 'vuex';
-import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import {
+ GlEmptyState,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTable,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { visitUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { trackViewInSentryOptions, trackClickErrorLinkToSentryOptions } from '../utils';
+import { trackViewInSentryOptions } from '../utils';
export default {
fields: [
@@ -20,6 +28,7 @@ export default {
GlLink,
GlLoadingIcon,
GlTable,
+ GlSearchBoxByType,
Icon,
TimeAgo,
},
@@ -48,8 +57,17 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ errorSearchQuery: '',
+ };
+ },
computed: {
- ...mapState(['errors', 'externalUrl', 'loading']),
+ ...mapState('list', ['errors', 'externalUrl', 'loading']),
+ ...mapGetters('list', ['filterErrorsByTitle']),
+ filteredErrors() {
+ return this.errorSearchQuery ? this.filterErrorsByTitle(this.errorSearchQuery) : this.errors;
+ },
},
created() {
if (this.errorTrackingEnabled) {
@@ -57,9 +75,11 @@ export default {
}
},
methods: {
- ...mapActions(['startPolling', 'restartPolling']),
+ ...mapActions('list', ['startPolling', 'restartPolling']),
trackViewInSentryOptions,
- trackClickErrorLinkToSentryOptions,
+ viewDetails(errorId) {
+ visitUrl(`error_tracking/${errorId}/details`);
+ },
},
};
</script>
@@ -71,10 +91,17 @@ export default {
<gl-loading-icon :size="3" />
</div>
<div v-else>
- <div class="d-flex justify-content-end">
+ <div class="d-flex flex-row justify-content-around bg-secondary border">
+ <gl-search-box-by-type
+ v-model="errorSearchQuery"
+ class="col-lg-10 m-3 p-0"
+ :placeholder="__('Search or filter results...')"
+ type="search"
+ autofocus
+ />
<gl-button
v-track-event="trackViewInSentryOptions(externalUrl)"
- class="my-3 ml-auto"
+ class="m-3"
variant="primary"
:href="externalUrl"
target="_blank"
@@ -84,7 +111,14 @@ export default {
</gl-button>
</div>
- <gl-table :items="errors" :fields="$options.fields" :show-empty="true" fixed stacked="sm">
+ <gl-table
+ class="mt-3"
+ :items="filteredErrors"
+ :fields="$options.fields"
+ :show-empty="true"
+ fixed
+ stacked="sm"
+ >
<template slot="HEAD_events" slot-scope="data">
<div class="text-md-right">{{ data.label }}</div>
</template>
@@ -94,13 +128,11 @@ export default {
<template slot="error" slot-scope="errors">
<div class="d-flex flex-column">
<gl-link
- v-track-event="trackClickErrorLinkToSentryOptions(errors.item.externalUrl)"
- :href="errors.item.externalUrl"
class="d-flex text-dark"
target="_blank"
+ @click="viewDetails(errors.item.id)"
>
<strong class="text-truncate">{{ errors.item.title.trim() }}</strong>
- <icon name="external-link" class="ml-1 flex-shrink-0" />
</gl-link>
<span class="text-secondary text-truncate">
{{ errors.item.culprit }}
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace.vue b/app/assets/javascripts/error_tracking/components/stacktrace.vue
new file mode 100644
index 00000000000..6b71967624f
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/stacktrace.vue
@@ -0,0 +1,33 @@
+<script>
+import StackTraceEntry from './stacktrace_entry.vue';
+
+export default {
+ components: {
+ StackTraceEntry,
+ },
+ props: {
+ entries: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ isFirstEntry(index) {
+ return index === 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="stacktrace">
+ <stack-trace-entry
+ v-for="(entry, index) in entries"
+ :key="`stacktrace-entry-${index}`"
+ :lines="entry.context"
+ :file-path="entry.filename"
+ :error-line="entry.lineNo"
+ :expanded="isFirstEntry(index)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
new file mode 100644
index 00000000000..ad542c579a9
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlTooltip } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ FileIcon,
+ Icon,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ lines: {
+ type: Array,
+ required: true,
+ },
+ filePath: {
+ type: String,
+ required: true,
+ },
+ errorLine: {
+ type: Number,
+ required: true,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isExpanded: this.expanded,
+ };
+ },
+ computed: {
+ linesLength() {
+ return this.lines.length;
+ },
+ collapseIcon() {
+ return this.isExpanded ? 'chevron-down' : 'chevron-right';
+ },
+ },
+ methods: {
+ isHighlighted(lineNum) {
+ return lineNum === this.errorLine;
+ },
+ toggle() {
+ this.isExpanded = !this.isExpanded;
+ },
+ lineNum(line) {
+ return line[0];
+ },
+ lineCode(line) {
+ return line[1];
+ },
+ },
+ userColorScheme: window.gon.user_color_scheme,
+};
+</script>
+
+<template>
+ <div class="file-holder">
+ <div ref="header" class="file-title file-title-flex-parent">
+ <div class="file-header-content ">
+ <div class="d-inline-block cursor-pointer" @click="toggle()">
+ <icon :name="collapseIcon" :size="16" aria-hidden="true" class="append-right-5" />
+ </div>
+ <div class="d-inline-block append-right-4">
+ <file-icon
+ :file-name="filePath"
+ :size="18"
+ aria-hidden="true"
+ css-classes="append-right-5"
+ />
+ <strong v-gl-tooltip :title="filePath" class="file-title-name" data-container="body">
+ {{ filePath }}
+ </strong>
+ </div>
+
+ <clipboard-button
+ :title="__('Copy file path')"
+ :text="filePath"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
+ </div>
+
+ <table v-if="isExpanded" :class="$options.userColorScheme" class="code js-syntax-highlight">
+ <tbody>
+ <template v-for="(line, index) in lines">
+ <tr :key="`stacktrace-line-${index}`" class="line_holder">
+ <td class="diff-line-num" :class="{ old: isHighlighted(lineNum(line)) }">
+ {{ lineNum(line) }}
+ </td>
+ <td
+ class="line_content"
+ :class="{ old: isHighlighted(lineNum(line)) }"
+ v-html="lineCode(line)"
+ ></td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js
new file mode 100644
index 00000000000..b9b51a6539f
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/details.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import store from './store';
+import ErrorDetails from './components/error_details.vue';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-error_details',
+ components: {
+ ErrorDetails,
+ },
+ store,
+ render(createElement) {
+ const domEl = document.querySelector(this.$options.el);
+ const { issueDetailsPath, issueStackTracePath } = domEl.dataset;
+
+ return createElement('error-details', {
+ props: {
+ issueDetailsPath,
+ issueStackTracePath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/list.js
index 073e2c8f1c7..073e2c8f1c7 100644
--- a/app/assets/javascripts/error_tracking/index.js
+++ b/app/assets/javascripts/error_tracking/list.js
diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js
index ab89521dc46..68988296cc2 100644
--- a/app/assets/javascripts/error_tracking/services/index.js
+++ b/app/assets/javascripts/error_tracking/services/index.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
export default {
- getErrorList({ endpoint }) {
+ getSentryData({ endpoint }) {
return axios.get(endpoint);
},
};
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
new file mode 100644
index 00000000000..0390bca7175
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -0,0 +1,63 @@
+import service from '../../services';
+import * as types from './mutation_types';
+import createFlash from '~/flash';
+import Poll from '~/lib/utils/poll';
+import { __ } from '~/locale';
+
+let stackTracePoll;
+let detailPoll;
+
+const stopPolling = poll => {
+ if (poll) poll.stop();
+};
+
+export function startPollingDetails({ commit }, endpoint) {
+ detailPoll = new Poll({
+ resource: service,
+ method: 'getSentryData',
+ data: { endpoint },
+ successCallback: ({ data }) => {
+ if (!data) {
+ detailPoll.restart();
+ return;
+ }
+
+ commit(types.SET_ERROR, data.error);
+ commit(types.SET_LOADING, false);
+
+ stopPolling(detailPoll);
+ },
+ errorCallback: () => {
+ commit(types.SET_LOADING, false);
+ createFlash(__('Failed to load error details from Sentry.'));
+ },
+ });
+
+ detailPoll.makeRequest();
+}
+
+export function startPollingStacktrace({ commit }, endpoint) {
+ stackTracePoll = new Poll({
+ resource: service,
+ method: 'getSentryData',
+ data: { endpoint },
+ successCallback: ({ data }) => {
+ if (!data) {
+ stackTracePoll.restart();
+ return;
+ }
+ commit(types.SET_STACKTRACE_DATA, data.error);
+ commit(types.SET_LOADING_STACKTRACE, false);
+
+ stopPolling(stackTracePoll);
+ },
+ errorCallback: () => {
+ commit(types.SET_LOADING_STACKTRACE, false);
+ createFlash(__('Failed to load stacktrace.'));
+ },
+ });
+
+ stackTracePoll.makeRequest();
+}
+
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
new file mode 100644
index 00000000000..7d13439d721
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -0,0 +1,3 @@
+export const stacktrace = state => state.stacktraceData.stack_trace_entries.reverse();
+
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/details/mutation_types.js b/app/assets/javascripts/error_tracking/store/details/mutation_types.js
new file mode 100644
index 00000000000..a2592253a2d
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/details/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_ERROR = 'SET_ERRORS';
+export const SET_LOADING = 'SET_LOADING';
+export const SET_LOADING_STACKTRACE = 'SET_LOADING_STACKTRACE';
+export const SET_STACKTRACE_DATA = 'SET_STACKTRACE_DATA';
diff --git a/app/assets/javascripts/error_tracking/store/details/mutations.js b/app/assets/javascripts/error_tracking/store/details/mutations.js
new file mode 100644
index 00000000000..6f4720444e0
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/details/mutations.js
@@ -0,0 +1,16 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ERROR](state, data) {
+ state.error = data;
+ },
+ [types.SET_LOADING](state, loading) {
+ state.loading = loading;
+ },
+ [types.SET_LOADING_STACKTRACE](state, data) {
+ state.loadingStacktrace = data;
+ },
+ [types.SET_STACKTRACE_DATA](state, data) {
+ state.stacktraceData = data;
+ },
+};
diff --git a/app/assets/javascripts/error_tracking/store/details/state.js b/app/assets/javascripts/error_tracking/store/details/state.js
new file mode 100644
index 00000000000..95fb0ba0558
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/details/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ error: {},
+ stacktraceData: {},
+ loading: true,
+ loadingStacktrace: true,
+});
diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js
index 3136682fb64..941c752e96a 100644
--- a/app/assets/javascripts/error_tracking/store/index.js
+++ b/app/assets/javascripts/error_tracking/store/index.js
@@ -1,19 +1,36 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import * as actions from './actions';
-import mutations from './mutations';
+
+import * as listActions from './list/actions';
+import listMutations from './list/mutations';
+import listState from './list/state';
+import * as listGetters from './list/getters';
+
+import * as detailsActions from './details/actions';
+import detailsMutations from './details/mutations';
+import detailsState from './details/state';
+import * as detailsGetters from './details/getters';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
- state: {
- errors: [],
- externalUrl: '',
- loading: true,
+ modules: {
+ list: {
+ namespaced: true,
+ state: listState(),
+ actions: listActions,
+ mutations: listMutations,
+ getters: listGetters,
+ },
+ details: {
+ namespaced: true,
+ state: detailsState(),
+ actions: detailsActions,
+ mutations: detailsMutations,
+ getters: detailsGetters,
+ },
},
- actions,
- mutations,
});
export default createStore();
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 1e754a4f54f..18c6e5e9695 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/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 Poll from '~/lib/utils/poll';
@@ -9,7 +9,7 @@ let eTagPoll;
export function startPolling({ commit, dispatch }, endpoint) {
eTagPoll = new Poll({
resource: Service,
- method: 'getErrorList',
+ method: 'getSentryData',
data: { endpoint },
successCallback: ({ data }) => {
if (!data) {
diff --git a/app/assets/javascripts/error_tracking/store/list/getters.js b/app/assets/javascripts/error_tracking/store/list/getters.js
new file mode 100644
index 00000000000..1a2ec62f79f
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/list/getters.js
@@ -0,0 +1,4 @@
+export const filterErrorsByTitle = state => errorQuery =>
+ state.errors.filter(error => error.title.match(new RegExp(`${errorQuery}`, 'i')));
+
+export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
index f9d77a6b08e..f9d77a6b08e 100644
--- a/app/assets/javascripts/error_tracking/store/mutation_types.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutation_types.js
diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index e4bd81db9c9..e4bd81db9c9 100644
--- a/app/assets/javascripts/error_tracking/store/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
diff --git a/app/assets/javascripts/error_tracking/store/list/state.js b/app/assets/javascripts/error_tracking/store/list/state.js
new file mode 100644
index 00000000000..d371350ef0e
--- /dev/null
+++ b/app/assets/javascripts/error_tracking/store/list/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ errors: [],
+ externalUrl: '',
+ loading: true,
+});
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 50eb3e63b7c..786abc8ce49 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -43,16 +43,7 @@ export default {
'isProjectInvalid',
'projectSelectionLabel',
]),
- ...mapState([
- 'apiHost',
- 'connectError',
- 'connectSuccessful',
- 'enabled',
- 'projects',
- 'selectedProject',
- 'settingsLoading',
- 'token',
- ]),
+ ...mapState(['enabled', 'projects', 'selectedProject', 'settingsLoading', 'token']),
},
created() {
this.setInitialState({
@@ -65,15 +56,7 @@ export default {
});
},
methods: {
- ...mapActions([
- 'fetchProjects',
- 'setInitialState',
- 'updateApiHost',
- 'updateEnabled',
- 'updateSelectedProject',
- 'updateSettings',
- 'updateToken',
- ]),
+ ...mapActions(['setInitialState', 'updateEnabled', 'updateSelectedProject', 'updateSettings']),
handleSubmit() {
this.updateSettings();
},
@@ -95,15 +78,7 @@ export default {
s__('ErrorTracking|Active')
}}</label>
</div>
- <error-tracking-form
- :api-host="apiHost"
- :connect-error="connectError"
- :connect-successful="connectSuccessful"
- :token="token"
- @handle-connect="fetchProjects"
- @update-api-host="updateApiHost"
- @update-token="updateToken"
- />
+ <error-tracking-form />
<div class="form-group">
<project-dropdown
:has-projects="hasProjects"
diff --git a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
index a734e8527dd..d86116aa315 100644
--- a/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/error_tracking_form.vue
@@ -1,32 +1,20 @@
<script>
-import { GlButton, GlFormInput } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { GlFormInput } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
- components: { GlButton, GlFormInput, Icon },
- props: {
- apiHost: {
- type: String,
- required: true,
- },
- connectError: {
- type: Boolean,
- required: true,
- },
- connectSuccessful: {
- type: Boolean,
- required: true,
- },
- token: {
- type: String,
- required: true,
- },
- },
+ components: { GlFormInput, Icon, LoadingButton },
computed: {
+ ...mapState(['apiHost', 'connectError', 'connectSuccessful', 'isLoadingProjects', 'token']),
tokenInputState() {
return this.connectError ? false : null;
},
},
+ methods: {
+ ...mapActions(['fetchProjects', 'updateApiHost', 'updateToken']),
+ },
};
</script>
@@ -40,8 +28,9 @@ export default {
<gl-form-input
id="error-tracking-api-host"
:value="apiHost"
+ :disabled="isLoadingProjects"
placeholder="https://mysentryserver.com"
- @input="$emit('update-api-host', $event)"
+ @input="updateApiHost"
/>
<!-- eslint-enable @gitlab/vue-i18n/no-bare-attribute-strings -->
</div>
@@ -60,15 +49,17 @@ export default {
id="error-tracking-token"
:value="token"
:state="tokenInputState"
- @input="$emit('update-token', $event)"
+ :disabled="isLoadingProjects"
+ @input="updateToken"
/>
</div>
<div class="col-4 col-md-3 gl-pl-0">
- <gl-button
- class="js-error-tracking-connect prepend-left-5"
- @click="$emit('handle-connect')"
- >{{ __('Connect') }}</gl-button
- >
+ <loading-button
+ class="js-error-tracking-connect prepend-left-5 d-inline-flex"
+ :label="isLoadingProjects ? __('Connecting') : __('Connect')"
+ :loading="isLoadingProjects"
+ @click="fetchProjects"
+ />
<icon
v-show="connectSuccessful"
class="js-error-tracking-connect-success prepend-left-5 text-success align-middle"
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index 95105797807..6b540ea7dfd 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -6,17 +6,20 @@ import { transformFrontendSettings } from '../utils';
import * as types from './mutation_types';
export const requestProjects = ({ commit }) => {
+ commit(types.SET_PROJECTS_LOADING, true);
commit(types.RESET_CONNECT);
};
export const receiveProjectsSuccess = ({ commit }, projects) => {
commit(types.UPDATE_CONNECT_SUCCESS);
commit(types.RECEIVE_PROJECTS, projects);
+ commit(types.SET_PROJECTS_LOADING, false);
};
export const receiveProjectsError = ({ commit }) => {
commit(types.UPDATE_CONNECT_ERROR);
commit(types.CLEAR_PROJECTS);
+ commit(types.SET_PROJECTS_LOADING, false);
};
export const fetchProjects = ({ dispatch, state }) => {
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
index b4f8a237947..bf3df383ddc 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
@@ -9,3 +9,4 @@ export const UPDATE_ENABLED = 'UPDATE_ENABLED';
export const UPDATE_SELECTED_PROJECT = 'UPDATE_SELECTED_PROJECT';
export const UPDATE_SETTINGS_LOADING = 'UPDATE_SETTINGS_LOADING';
export const UPDATE_TOKEN = 'UPDATE_TOKEN';
+export const SET_PROJECTS_LOADING = 'SET_PROJECTS_LOADING';
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
index 4089d1ee94e..133f25264b9 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutations.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -58,4 +58,7 @@ export default {
state.connectSuccessful = false;
state.connectError = true;
},
+ [types.SET_PROJECTS_LOADING](state, loading) {
+ state.isLoadingProjects = loading;
+ },
};
diff --git a/app/assets/javascripts/error_tracking_settings/store/state.js b/app/assets/javascripts/error_tracking_settings/store/state.js
index 98219d33f4d..ab616f11e83 100644
--- a/app/assets/javascripts/error_tracking_settings/store/state.js
+++ b/app/assets/javascripts/error_tracking_settings/store/state.js
@@ -3,6 +3,7 @@ export default () => ({
enabled: false,
token: '',
projects: [],
+ isLoadingProjects: false,
selectedProject: null,
settingsLoading: false,
connectSuccessful: false,
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index f280f3cd26c..5fa07045d5e 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -13,6 +13,7 @@ export default class AvailableDropdownMappings {
runnerTagsEndpoint,
labelsEndpoint,
milestonesEndpoint,
+ releasesEndpoint,
groupsOnly,
includeAncestorGroups,
includeDescendantGroups,
@@ -21,6 +22,7 @@ export default class AvailableDropdownMappings {
this.runnerTagsEndpoint = runnerTagsEndpoint;
this.labelsEndpoint = labelsEndpoint;
this.milestonesEndpoint = milestonesEndpoint;
+ this.releasesEndpoint = releasesEndpoint;
this.groupsOnly = groupsOnly;
this.includeAncestorGroups = includeAncestorGroups;
this.includeDescendantGroups = includeDescendantGroups;
@@ -70,6 +72,19 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
+ release: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getReleasesEndpoint(),
+ symbol: '',
+
+ // The DropdownNonUser class is hardcoded to look for and display a
+ // "title" property, so we need to add this property to each release object
+ preprocessing: releases => releases.map(r => ({ ...r, title: r.tag })),
+ },
+ element: this.container.querySelector('#js-dropdown-release'),
+ },
label: {
reference: null,
gl: DropdownNonUser,
@@ -130,6 +145,10 @@ export default class AvailableDropdownMappings {
return `${this.milestonesEndpoint}.json`;
}
+ getReleasesEndpoint() {
+ return `${this.releasesEndpoint}.json`;
+ }
+
getLabelsEndpoint() {
let endpoint = `${this.labelsEndpoint}.json?`;
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 835d3bf8a53..5ff95f45be4 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -11,6 +11,7 @@ export default class FilteredSearchDropdownManager {
runnerTagsEndpoint = '',
labelsEndpoint = '',
milestonesEndpoint = '',
+ releasesEndpoint = '',
tokenizer,
page,
isGroup,
@@ -18,10 +19,13 @@ export default class FilteredSearchDropdownManager {
isGroupDecendent,
filteredSearchTokenKeys,
}) {
+ const removeTrailingSlash = url => url.replace(/\/$/, '');
+
this.container = FilteredSearchContainer.container;
- this.runnerTagsEndpoint = runnerTagsEndpoint.replace(/\/$/, '');
- this.labelsEndpoint = labelsEndpoint.replace(/\/$/, '');
- this.milestonesEndpoint = milestonesEndpoint.replace(/\/$/, '');
+ this.runnerTagsEndpoint = removeTrailingSlash(runnerTagsEndpoint);
+ this.labelsEndpoint = removeTrailingSlash(labelsEndpoint);
+ this.milestonesEndpoint = removeTrailingSlash(milestonesEndpoint);
+ this.releasesEndpoint = removeTrailingSlash(releasesEndpoint);
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
@@ -54,6 +58,7 @@ export default class FilteredSearchDropdownManager {
this.runnerTagsEndpoint,
this.labelsEndpoint,
this.milestonesEndpoint,
+ this.releasesEndpoint,
this.groupsOnly,
this.includeAncestorGroups,
this.includeDescendantGroups,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index fd335362e5b..5c2d32f4e85 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -89,6 +89,7 @@ export default class FilteredSearchManager {
this.filteredSearchInput.getAttribute('data-runner-tags-endpoint') || '',
labelsEndpoint: this.filteredSearchInput.getAttribute('data-labels-endpoint') || '',
milestonesEndpoint: this.filteredSearchInput.getAttribute('data-milestones-endpoint') || '',
+ releasesEndpoint: this.filteredSearchInput.getAttribute('data-releases-endpoint') || '',
tokenizer: this.tokenizer,
page: this.page,
isGroup: this.isGroup,
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index 6c3d9e33420..414bcf186a3 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -1,7 +1,9 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys';
import { __ } from '~/locale';
-export const tokenKeys = [
+export const tokenKeys = [];
+
+tokenKeys.push(
{
key: 'author',
type: 'string',
@@ -26,15 +28,27 @@ export const tokenKeys = [
icon: 'clock',
tag: '%milestone',
},
- {
- key: 'label',
- type: 'array',
- param: 'name[]',
- symbol: '~',
- icon: 'labels',
- tag: '~label',
- },
-];
+);
+
+if (gon && gon.features && gon.features.releaseSearchFilter) {
+ tokenKeys.push({
+ key: 'release',
+ type: 'string',
+ param: 'tag',
+ symbol: '',
+ icon: 'rocket',
+ tag: __('tag name'),
+ });
+}
+
+tokenKeys.push({
+ key: 'label',
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ icon: 'labels',
+ tag: '~label',
+});
if (gon.current_user_id) {
// Appending tokenkeys only logged-in
@@ -89,6 +103,16 @@ export const conditions = [
value: __('Started'),
},
{
+ url: 'release_tag=None',
+ tokenKey: 'release',
+ value: __('None'),
+ },
+ {
+ url: 'release_tag=Any',
+ tokenKey: 'release',
+ value: __('Any'),
+ },
+ {
url: 'label_name[]=None',
tokenKey: 'label',
value: __('None'),
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index fc9c5827ed4..2c3320b5e79 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -37,7 +37,7 @@ const createAction = config => `
`;
const createFlashEl = (message, type) => `
- <div class="flash-content flash-${type} rounded">
+ <div class="flash-${type}">
<div class="flash-text">
${_.escape(message)}
<div class="close-icon-wrapper js-close-icon">
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
index 41b660a243f..92ac3a2c94d 100644
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -47,7 +47,8 @@ export default {
hasSearchQuery: true,
});
},
- [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) {
+ [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) {
+ const rawItems = results.data;
Object.assign(state, {
items: rawItems.map(rawItem => ({
id: rawItem.id,
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 4e1b4f2652c..045f77af7ea 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -617,7 +617,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
- this.removeArrayKeyEvent();
+ this.removeArrowKeyEvent();
$input = this.dropdown.find('.dropdown-input-field');
if (this.options.filterable) {
$input.blur();
@@ -900,7 +900,7 @@ GitLabDropdown = (function() {
);
};
- GitLabDropdown.prototype.removeArrayKeyEvent = function() {
+ GitLabDropdown.prototype.removeArrowKeyEvent = function() {
return $('body').off('keydown');
};
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
new file mode 100644
index 00000000000..bd504d95ee2
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { mapState, mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlButton,
+ GlFormCheckbox,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ Icon,
+ },
+ data() {
+ return { placeholderUrl: 'https://my-url.grafana.net/' };
+ },
+ computed: {
+ ...mapState(['operationsSettingsEndpoint', 'grafanaToken', 'grafanaUrl', 'grafanaEnabled']),
+ integrationEnabled: {
+ get() {
+ return this.grafanaEnabled;
+ },
+ set(grafanaEnabled) {
+ this.setGrafanaEnabled(grafanaEnabled);
+ },
+ },
+ localGrafanaToken: {
+ get() {
+ return this.grafanaToken;
+ },
+ set(token) {
+ this.setGrafanaToken(token);
+ },
+ },
+ localGrafanaUrl: {
+ get() {
+ return this.grafanaUrl;
+ },
+ set(url) {
+ this.setGrafanaUrl(url);
+ },
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setGrafanaUrl',
+ 'setGrafanaToken',
+ 'setGrafanaEnabled',
+ 'updateGrafanaIntegration',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <section id="grafana" class="settings no-animate js-grafana-integration">
+ <div class="settings-header">
+ <h4 class="js-section-header">
+ {{ s__('GrafanaIntegration|Grafana Authentication') }}
+ </h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
+ <p class="js-section-sub-header">
+ {{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
+ </p>
+ </div>
+ <div class="settings-content">
+ <form>
+ <gl-form-checkbox
+ id="grafana-integration-enabled"
+ v-model="integrationEnabled"
+ class="mb-4"
+ >
+ {{ s__('GrafanaIntegration|Active') }}
+ </gl-form-checkbox>
+ <gl-form-group
+ :label="s__('GrafanaIntegration|Grafana URL')"
+ label-for="grafana-url"
+ :description="s__('GrafanaIntegration|Enter the base URL of the Grafana instance.')"
+ >
+ <gl-form-input id="grafana-url" v-model="localGrafanaUrl" :placeholder="placeholderUrl" />
+ </gl-form-group>
+ <gl-form-group :label="s__('GrafanaIntegration|API Token')" label-for="grafana-token">
+ <gl-form-input id="grafana-token" v-model="localGrafanaToken" />
+ <p class="form-text text-muted">
+ {{ s__('GrafanaIntegration|Enter the Grafana API Token.') }}
+ <a
+ href="https://grafana.com/docs/http_api/auth/#create-api-token"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ <icon name="external-link" class="vertical-align-middle" />
+ </a>
+ </p>
+ </gl-form-group>
+ <gl-button variant="success" @click="updateGrafanaIntegration">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </form>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/grafana_integration/index.js b/app/assets/javascripts/grafana_integration/index.js
new file mode 100644
index 00000000000..a93edab4388
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import store from './store';
+import GrafanaIntegration from './components/grafana_integration.vue';
+
+export default () => {
+ const el = document.querySelector('.js-grafana-integration');
+ return new Vue({
+ el,
+ store: store(el.dataset),
+ render(createElement) {
+ return createElement(GrafanaIntegration);
+ },
+ });
+};
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
new file mode 100644
index 00000000000..d83f1e0831c
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -0,0 +1,42 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as mutationTypes from './mutation_types';
+
+export const setGrafanaUrl = ({ commit }, url) => commit(mutationTypes.SET_GRAFANA_URL, url);
+
+export const setGrafanaToken = ({ commit }, token) =>
+ commit(mutationTypes.SET_GRAFANA_TOKEN, token);
+
+export const setGrafanaEnabled = ({ commit }, enabled) =>
+ commit(mutationTypes.SET_GRAFANA_ENABLED, enabled);
+
+export const updateGrafanaIntegration = ({ state, dispatch }) =>
+ axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ grafana_integration_attributes: {
+ grafana_url: state.grafanaUrl,
+ token: state.grafanaToken,
+ enabled: state.grafanaEnabled,
+ },
+ },
+ })
+ .then(() => dispatch('receiveGrafanaIntegrationUpdateSuccess'))
+ .catch(error => dispatch('receiveGrafanaIntegrationUpdateError', error));
+
+export const receiveGrafanaIntegrationUpdateSuccess = () => {
+ /**
+ * The operations_controller currently handles successful requests
+ * by creating a flash banner messsage to notify the user.
+ */
+ refreshCurrentPage();
+};
+
+export const receiveGrafanaIntegrationUpdateError = (_, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+};
diff --git a/app/assets/javascripts/grafana_integration/store/index.js b/app/assets/javascripts/grafana_integration/store/index.js
new file mode 100644
index 00000000000..e96bb1e8aad
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: createState(initialState),
+ actions,
+ mutations,
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/grafana_integration/store/mutation_types.js b/app/assets/javascripts/grafana_integration/store/mutation_types.js
new file mode 100644
index 00000000000..314c3a4039a
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_GRAFANA_URL = 'SET_GRAFANA_URL';
+export const SET_GRAFANA_TOKEN = 'SET_GRAFANA_TOKEN';
+export const SET_GRAFANA_ENABLED = 'SET_GRAFANA_ENABLED';
diff --git a/app/assets/javascripts/grafana_integration/store/mutations.js b/app/assets/javascripts/grafana_integration/store/mutations.js
new file mode 100644
index 00000000000..0992030d404
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/mutations.js
@@ -0,0 +1,13 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_GRAFANA_URL](state, url) {
+ state.grafanaUrl = url;
+ },
+ [types.SET_GRAFANA_TOKEN](state, token) {
+ state.grafanaToken = token;
+ },
+ [types.SET_GRAFANA_ENABLED](state, enabled) {
+ state.grafanaEnabled = enabled;
+ },
+};
diff --git a/app/assets/javascripts/grafana_integration/store/state.js b/app/assets/javascripts/grafana_integration/store/state.js
new file mode 100644
index 00000000000..a912eb58327
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/state.js
@@ -0,0 +1,8 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
+export default (initialState = {}) => ({
+ operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
+ grafanaToken: initialState.grafanaIntegrationToken || '',
+ grafanaUrl: initialState.grafanaIntegrationUrl || '',
+ grafanaEnabled: parseBoolean(initialState.grafanaIntegrationEnabled) || false,
+});
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 460174caf4d..eda0f5d1d23 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,15 +1,23 @@
import $ from 'jquery';
import { slugify } from './lib/utils/text_utility';
+import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
+import flash from '~/flash';
+import { __ } from '~/locale';
export default class Group {
constructor() {
this.groupPath = $('#group_path');
this.groupName = $('#group_name');
+ this.parentId = $('#group_parent_id');
this.updateHandler = this.update.bind(this);
this.resetHandler = this.reset.bind(this);
+ this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this);
if (this.groupName.val() === '') {
this.groupName.on('keyup', this.updateHandler);
this.groupPath.on('keydown', this.resetHandler);
+ if (!this.parentId.val()) {
+ this.groupName.on('blur', this.updateGroupPathSlugHandler);
+ }
}
}
@@ -21,5 +29,21 @@ export default class Group {
reset() {
this.groupName.off('keyup', this.updateHandler);
this.groupPath.off('keydown', this.resetHandler);
+ this.groupName.off('blur', this.checkPathHandler);
+ }
+
+ updateGroupPathSlug() {
+ const slug = this.groupPath.val() || slugify(this.groupName.val());
+ if (!slug) return;
+
+ fetchGroupPathAvailability(slug)
+ .then(({ data }) => data)
+ .then(data => {
+ if (data.exists && data.suggests.length > 0) {
+ const suggestedSlug = data.suggests[0];
+ this.groupPath.val(suggestedSlug);
+ }
+ })
+ .catch(() => flash(__('An error occurred while checking group path')));
}
}
diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js
index 2c2a04d5b5e..d172aa8a444 100644
--- a/app/assets/javascripts/helpers/monitor_helper.js
+++ b/app/assets/javascripts/helpers/monitor_helper.js
@@ -1,17 +1,31 @@
-/* eslint-disable import/prefer-default-export */
-
+/**
+ * @param {Array} queryResults - Array of Result objects
+ * @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
+ * @returns {Array} The formatted values
+ */
+// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) =>
- queryResults.reduce((acc, result) => {
- const data = result.values.filter(([, value]) => !Number.isNaN(value));
- if (!data.length) {
- return acc;
- }
- const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
- const name = result.metric[relevantMetric];
- const series = { data };
- if (name) {
- series.name = `${defaultConfig.name}: ${name}`;
- }
+ queryResults
+ .map(result => {
+ const data = result.values.filter(([, value]) => !Number.isNaN(value));
+ if (!data.length) {
+ return null;
+ }
+ const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
+ const name = result.metric[relevantMetric];
+ const series = { data };
+ if (name) {
+ series.name = `${defaultConfig.name}: ${name}`;
+ } else {
+ series.name = defaultConfig.name;
+ Object.keys(result.metric).forEach(templateVar => {
+ const value = result.metric[templateVar];
+ const regex = new RegExp(`{{\\s*${templateVar}\\s*}}`, 'g');
+
+ series.name = series.name.replace(regex, value);
+ });
+ }
- return acc.concat({ ...defaultConfig, ...series });
- }, []);
+ return { ...defaultConfig, ...series };
+ })
+ .filter(series => series !== null);
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 9ad9d4455b5..52ca61c06b0 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -58,6 +58,7 @@ export default {
<template>
<div class="ide-stage card prepend-top-default">
<div
+ ref="cardHeader"
:class="{
'border-bottom-0': stage.isCollapsed,
}"
@@ -79,7 +80,7 @@ export default {
</div>
<icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
- <div v-show="!stage.isCollapsed" class="card-body">
+ <div v-show="!stage.isCollapsed" ref="jobList" class="card-body">
<gl-loading-icon v-if="showLoadingIcon" />
<template v-else>
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 6999746f115..beb179d0411 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -92,6 +92,7 @@ export default {
},
methods: {
...mapActions(['getFileData', 'getRawFileData']),
+ ...mapActions('clientside', ['pingUsage']),
loadFileContent(path) {
return this.getFileData({ path, makeFileActive: false }).then(() =>
this.getRawFileData({ path }),
@@ -100,6 +101,8 @@ export default {
initPreview() {
if (!this.mainEntry) return null;
+ this.pingUsage();
+
return this.loadFileContent(this.mainEntry)
.then(() => this.$nextTick())
.then(() => {
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 3bf8308ccea..08b3e8a34d6 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -301,6 +301,7 @@ export default {
v-if="showContentViewer"
:content="file.content || file.raw"
:path="file.rawPath || file.path"
+ :file-path="file.path"
:file-size="file.size"
:project-path="file.projectId"
:type="fileType"
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index ba33b6826d6..f6ad2f9c7d1 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,4 +1,6 @@
import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { escapeFileUrl } from '../stores/utils';
import Api from '~/api';
export default {
@@ -23,18 +25,25 @@ export default {
.then(({ data }) => data);
},
getBaseRawFileData(file, sha) {
- if (file.tempFile) {
- return Promise.resolve(file.baseRaw);
- }
+ if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw);
- if (file.baseRaw) {
- return Promise.resolve(file.baseRaw);
- }
+ // if files are renamed, their base path has changed
+ const filePath =
+ file.mrChange && file.mrChange.renamed_file ? file.mrChange.old_path : file.path;
return axios
- .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
- transformResponse: [f => f],
- })
+ .get(
+ joinPaths(
+ gon.relative_url_root || '/',
+ file.projectId,
+ 'raw',
+ sha,
+ escapeFileUrl(filePath),
+ ),
+ {
+ transformResponse: [f => f],
+ },
+ )
.then(({ data }) => data);
},
getProjectData(namespace, project) {
@@ -58,8 +67,8 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
- getFiles(projectUrl, branchId) {
- const url = `${projectUrl}/files/${branchId}`;
+ getFiles(projectUrl, ref) {
+ const url = `${projectUrl}/files/${ref}`;
return axios.get(url, { params: { format: 'json' } });
},
lastCommitPipelines({ getters }) {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 59445afc7a4..9af0b50d1a5 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,11 +1,10 @@
import { joinPaths } from '~/lib/utils/url_utility';
-import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
import router from '../../ide_router';
-import { setPageTitle, replaceFileUrl } from '../utils';
+import { escapeFileUrl, addFinalNewlineIfNeeded, setPageTitleForFile } from '../utils';
import { viewerTypes, stageKeys } from '../../constants';
export const closeFile = ({ commit, state, dispatch }, file) => {
@@ -58,7 +57,7 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
};
export const getFileData = (
- { state, commit, dispatch },
+ { state, commit, dispatch, getters },
{ path, makeFileActive = true, openFile = makeFileActive },
) => {
const file = state.entries[path];
@@ -67,15 +66,18 @@ export const getFileData = (
commit(types.TOGGLE_LOADING, { entry: file });
- const url = file.prevPath ? replaceFileUrl(file.url, file.path, file.prevPath) : file.url;
+ const url = joinPaths(
+ gon.relative_url_root || '/',
+ state.currentProjectId,
+ file.type,
+ getters.lastCommit && getters.lastCommit.id,
+ escapeFileUrl(file.prevPath || file.path),
+ );
return service
- .getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
- .then(({ data, headers }) => {
- const normalizedHeaders = normalizeHeaders(headers);
- let title = normalizedHeaders['PAGE-TITLE'];
- title = file.prevPath ? title.replace(file.prevPath, file.path) : title;
- setPageTitle(decodeURI(title));
+ .getFileData(url)
+ .then(({ data }) => {
+ setPageTitleForFile(state, file);
if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
@@ -140,7 +142,10 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path];
- commit(types.UPDATE_FILE_CONTENT, { path, content });
+ commit(types.UPDATE_FILE_CONTENT, {
+ path,
+ content: addFinalNewlineIfNeeded(content),
+ });
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 1273e375859..6790c0fbdaa 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -152,15 +152,17 @@ export const openMergeRequest = (
.then(mr => {
dispatch('setCurrentBranchId', mr.source_branch);
- dispatch('getBranchData', {
+ // getFiles needs to be called after getting the branch data
+ // since files are fetched using the last commit sha of the branch
+ return dispatch('getBranchData', {
projectId,
branchId: mr.source_branch,
- });
-
- return dispatch('getFiles', {
- projectId,
- branchId: mr.source_branch,
- });
+ }).then(() =>
+ dispatch('getFiles', {
+ projectId,
+ branchId: mr.source_branch,
+ }),
+ );
})
.then(() =>
dispatch('getMergeRequestVersions', {
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 75511574d3e..72cd099c5a5 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -46,7 +46,7 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
});
};
-export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+export const getFiles = ({ state, commit, dispatch, getters }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (
!state.trees[`${projectId}/${branchId}`] ||
@@ -54,10 +54,11 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
state.trees[`${projectId}/${branchId}`].tree.length === 0)
) {
const selectedProject = state.projects[projectId];
- commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+ const selectedBranch = getters.findBranch(projectId, branchId);
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
- .getFiles(selectedProject.web_url, branchId)
+ .getFiles(selectedProject.web_url, selectedBranch.commit.id)
.then(({ data }) => {
const { entries, treeList } = decorateFiles({
data,
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 85fd45358be..a176fd0aca8 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -34,7 +34,9 @@ export const currentMergeRequest = state => {
return null;
};
-export const currentProject = state => state.projects[state.currentProjectId];
+export const findProject = state => projectId => state.projects[projectId];
+
+export const currentProject = (state, getters) => getters.findProject(state.currentProjectId);
export const emptyRepo = state =>
state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
@@ -94,8 +96,14 @@ export const lastCommit = (state, getters) => {
return branch ? branch.commit : null;
};
+export const findBranch = (state, getters) => (projectId, branchId) => {
+ const project = getters.findProject(projectId);
+
+ return project && project.branches[branchId];
+};
+
export const currentBranch = (state, getters) =>
- getters.currentProject && getters.currentProject.branches[state.currentBranchId];
+ getters.findBranch(state.currentProjectId, state.currentBranchId);
export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name;
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index f1f544b52b2..85550578e94 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -10,6 +10,7 @@ import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
import fileTemplates from './modules/file_templates';
import paneModule from './modules/pane';
+import clientsideModule from './modules/clientside';
Vue.use(Vuex);
@@ -26,6 +27,7 @@ export const createStore = () =>
branches,
fileTemplates: fileTemplates(),
rightPane: paneModule(),
+ clientside: clientsideModule(),
},
});
diff --git a/app/assets/javascripts/ide/stores/modules/clientside/actions.js b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
new file mode 100644
index 00000000000..eb3bcdff2ae
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
@@ -0,0 +1,12 @@
+import axios from '~/lib/utils/axios_utils';
+
+export const pingUsage = ({ rootGetters }) => {
+ const { web_url: projectUrl } = rootGetters.currentProject;
+
+ const url = `${projectUrl}/usage_ping/web_ide_clientside_preview`;
+
+ return axios.post(url);
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/clientside/index.js b/app/assets/javascripts/ide/stores/modules/clientside/index.js
new file mode 100644
index 00000000000..b28f7b935a8
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/clientside/index.js
@@ -0,0 +1,6 @@
+import * as actions from './actions';
+
+export default () => ({
+ namespaced: true,
+ actions,
+});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index a8d8ff31afe..be7ee80656f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -113,6 +113,11 @@ export const setPageTitle = title => {
document.title = title;
};
+export const setPageTitleForFile = (state, file) => {
+ const title = [file.path, state.currentBranchId, state.currentProjectId, 'GitLab'].join(' · ');
+ setPageTitle(title);
+};
+
export const commitActionForFile = file => {
if (file.prevPath) {
return commitActionTypes.move;
@@ -269,3 +274,7 @@ export const pathsAreEqual = (a, b) => {
return cleanA === cleanB;
};
+
+// 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;
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index 74150ce3a8b..bd6e8433544 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,11 +1,13 @@
/* eslint-disable class-methods-use-this, no-new */
import $ from 'jquery';
+import { property } from 'underscore';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
import subscriptionSelect from './subscription_select';
import LabelsSelect from './labels_select';
+import issueableEventHub from './issuables_list/eventhub';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -14,6 +16,8 @@ const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-si
export default class IssuableBulkUpdateSidebar {
constructor() {
+ this.vueIssuablesListFeature = property(['gon', 'features', 'vueIssuablesList'])(window);
+
this.initDomElements();
this.bindEvents();
this.initDropdowns();
@@ -41,6 +45,17 @@ export default class IssuableBulkUpdateSidebar {
this.$issuesList.on('change', () => this.updateFormState());
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState());
+
+ if (this.vueIssuablesListFeature) {
+ issueableEventHub.$on('issuables:updateBulkEdit', () => {
+ // Danger! Strong coupling ahead!
+ // The bulk update sidebar and its dropdowns look for .selected-issuable checkboxes, and get data on which issue
+ // is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties
+ // explicitly, but this component is used in too many places right now to refactor straight away.
+
+ this.updateFormState();
+ });
+ }
}
initDropdowns() {
@@ -73,6 +88,8 @@ export default class IssuableBulkUpdateSidebar {
toggleBulkEdit(e, enable) {
e.preventDefault();
+ issueableEventHub.$emit('issuables:toggleBulkEdit', enable);
+
this.toggleSidebarDisplay(enable);
this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable);
@@ -106,7 +123,7 @@ export default class IssuableBulkUpdateSidebar {
}
toggleCheckboxDisplay(show) {
- this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
+ this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show || this.vueIssuablesListFeature);
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
}
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue
new file mode 100644
index 00000000000..eb924609a8a
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/components/issuable.vue
@@ -0,0 +1,335 @@
+<script>
+/*
+ * This is tightly coupled to projects/issues/_issue.html.haml,
+ * any changes done to the haml need to be reflected here.
+ */
+import { escape, isNumber } from 'underscore';
+import { GlLink, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import {
+ dateInWords,
+ formatDate,
+ getDayDifference,
+ getTimeago,
+ timeFor,
+ newDateAsLocaleTime,
+} from '~/lib/utils/datetime_utility';
+import { sprintf, __ } from '~/locale';
+import initUserPopovers from '~/user_popovers';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import Icon from '~/vue_shared/components/icon.vue';
+import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+
+const ISSUE_TOKEN = '#';
+
+export default {
+ components: {
+ Icon,
+ IssueAssignees,
+ GlLink,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ issuable: {
+ type: Object,
+ required: true,
+ },
+ isBulkEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ baseUrl: {
+ type: String,
+ required: false,
+ default() {
+ return window.location.href;
+ },
+ },
+ },
+ computed: {
+ milestoneLink() {
+ const { title } = this.issuable.milestone;
+
+ return this.issuableLink({ milestone_title: title });
+ },
+ hasLabels() {
+ return Boolean(this.issuable.labels && this.issuable.labels.length);
+ },
+ hasWeight() {
+ return isNumber(this.issuable.weight);
+ },
+ dueDate() {
+ return this.issuable.due_date ? newDateAsLocaleTime(this.issuable.due_date) : undefined;
+ },
+ dueDateWords() {
+ return this.dueDate ? dateInWords(this.dueDate, true) : undefined;
+ },
+ hasNoComments() {
+ return !this.userNotesCount;
+ },
+ isOverdue() {
+ return this.dueDate ? this.dueDate < new Date() : false;
+ },
+ isClosed() {
+ return this.issuable.state === 'closed';
+ },
+ issueCreatedToday() {
+ return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1;
+ },
+ labelIdsString() {
+ return JSON.stringify(this.issuable.labels.map(l => l.id));
+ },
+ milestoneDueDate() {
+ const { due_date: dueDate } = this.issuable.milestone || {};
+
+ return dueDate ? newDateAsLocaleTime(dueDate) : undefined;
+ },
+ milestoneTooltipText() {
+ if (this.milestoneDueDate) {
+ return sprintf(__('%{primary} (%{secondary})'), {
+ primary: formatDate(this.milestoneDueDate, 'mmm d, yyyy'),
+ secondary: timeFor(this.milestoneDueDate),
+ });
+ }
+ return __('Milestone');
+ },
+ openedAgoByString() {
+ const { author, created_at } = this.issuable;
+
+ return sprintf(
+ __('opened %{timeAgoString} by %{user}'),
+ {
+ timeAgoString: escape(getTimeago().format(created_at)),
+ user: `<a href="${escape(author.web_url)}"
+ data-user-id=${escape(author.id)}
+ data-username=${escape(author.username)}
+ data-name=${escape(author.name)}
+ data-avatar-url="${escape(author.avatar_url)}">
+ ${escape(author.name)}
+ </a>`,
+ },
+ false,
+ );
+ },
+ referencePath() {
+ // TODO: The API should return the reference path (it doesn't now) https://gitlab.com/gitlab-org/gitlab/issues/31301
+ return `${ISSUE_TOKEN}${this.issuable.iid}`;
+ },
+ updatedDateString() {
+ return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt');
+ },
+ updatedDateAgo() {
+ // snake_case because it's the same i18n string as the HAML view
+ return sprintf(__('updated %{time_ago}'), {
+ time_ago: escape(getTimeago().format(this.issuable.updated_at)),
+ });
+ },
+ userNotesCount() {
+ return this.issuable.user_notes_count;
+ },
+ issuableMeta() {
+ return [
+ {
+ key: 'merge-requests',
+ value: this.issuable.merge_requests_count,
+ title: __('Related merge requests'),
+ class: 'js-merge-requests',
+ icon: 'merge-request',
+ },
+ {
+ key: 'upvotes',
+ value: this.issuable.upvotes,
+ title: __('Upvotes'),
+ class: 'js-upvotes',
+ faicon: 'fa-thumbs-up',
+ },
+ {
+ key: 'downvotes',
+ value: this.issuable.downvotes,
+ title: __('Downvotes'),
+ class: 'js-downvotes',
+ faicon: 'fa-thumbs-down',
+ },
+ ];
+ },
+ },
+ mounted() {
+ // TODO: Refactor user popover to use its own component instead of
+ // spawning event listeners on Vue-rendered elements.
+ initUserPopovers([this.$refs.openedAgoByContainer.querySelector('a')]);
+ },
+ methods: {
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.text_color,
+ };
+ },
+ issuableLink(params) {
+ return mergeUrlParams(params, this.baseUrl);
+ },
+ labelHref({ name }) {
+ return this.issuableLink({ 'label_name[]': name });
+ },
+ onSelect(ev) {
+ this.$emit('select', {
+ issuable: this.issuable,
+ selected: ev.target.checked,
+ });
+ },
+ },
+
+ confidentialTooltipText: __('Confidential'),
+};
+</script>
+<template>
+ <li
+ :id="`issue_${issuable.id}`"
+ class="issue"
+ :class="{ today: issueCreatedToday, closed: isClosed }"
+ :data-id="issuable.id"
+ :data-labels="labelIdsString"
+ :data-url="issuable.web_url"
+ >
+ <div class="d-flex">
+ <!-- Bulk edit checkbox -->
+ <div v-if="isBulkEditing" class="mr-2">
+ <input
+ :checked="selected"
+ class="selected-issuable"
+ type="checkbox"
+ :data-id="issuable.id"
+ @input="onSelect"
+ />
+ </div>
+
+ <!-- Issuable info container -->
+ <!-- Issuable main info -->
+ <div class="flex-grow-1">
+ <div class="title">
+ <span class="issue-title-text">
+ <i
+ v-if="issuable.confidential"
+ v-gl-tooltip
+ class="fa fa-eye-slash"
+ :title="$options.confidentialTooltipText"
+ :aria-label="$options.confidentialTooltipText"
+ ></i>
+ <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link>
+ </span>
+ <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">{{
+ issuable.task_status
+ }}</span>
+ </div>
+
+ <div class="issuable-info">
+ <span>{{ referencePath }}</span>
+
+ <span class="d-none d-sm-inline-block mr-1">
+ &middot;
+ <span ref="openedAgoByContainer" v-html="openedAgoByString"></span>
+ </span>
+
+ <gl-link
+ v-if="issuable.milestone"
+ v-gl-tooltip
+ class="d-none d-sm-inline-block mr-1 js-milestone"
+ :href="milestoneLink"
+ :title="milestoneTooltipText"
+ >
+ <i class="fa fa-clock-o"></i>
+ {{ issuable.milestone.title }}
+ </gl-link>
+
+ <span
+ v-if="dueDate"
+ v-gl-tooltip
+ class="d-none d-sm-inline-block mr-1 js-due-date"
+ :class="{ cred: isOverdue }"
+ :title="__('Due date')"
+ >
+ <i class="fa fa-calendar"></i>
+ {{ dueDateWords }}
+ </span>
+
+ <span v-if="hasLabels" class="js-labels">
+ <gl-link
+ v-for="label in issuable.labels"
+ :key="label.id"
+ class="label-link mr-1"
+ :href="labelHref(label)"
+ >
+ <span
+ v-gl-tooltip
+ class="badge color-label"
+ :style="labelStyle(label)"
+ :title="label.description"
+ >{{ label.name }}</span
+ >
+ </gl-link>
+ </span>
+
+ <span
+ v-if="hasWeight"
+ v-gl-tooltip
+ :title="__('Weight')"
+ class="d-none d-sm-inline-block js-weight"
+ >
+ <icon name="weight" class="align-text-bottom" />
+ {{ issuable.weight }}
+ </span>
+ </div>
+ </div>
+
+ <!-- Issuable meta -->
+ <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center">
+ <div class="controls d-flex">
+ <span v-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
+
+ <issue-assignees
+ :assignees="issuable.assignees"
+ class="align-items-center d-flex ml-2"
+ :icon-size="16"
+ img-css-classes="mr-1"
+ :max-visible="4"
+ />
+
+ <template v-for="meta in issuableMeta">
+ <span
+ v-if="meta.value"
+ :key="meta.key"
+ v-gl-tooltip
+ :class="['d-none d-sm-inline-block ml-2', meta.class]"
+ :title="meta.title"
+ >
+ <icon v-if="meta.icon" :name="meta.icon" />
+ <i v-else :class="['fa', meta.faicon]"></i>
+ {{ meta.value }}
+ </span>
+ </template>
+
+ <gl-link
+ v-gl-tooltip
+ class="ml-2 js-notes"
+ :href="`${issuable.web_url}#notes`"
+ :title="__('Comments')"
+ :class="{ 'no-comments': hasNoComments }"
+ >
+ <i class="fa fa-comments"></i>
+ {{ userNotesCount }}
+ </gl-link>
+ </div>
+ <div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString">
+ {{ updatedDateAgo }}
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
new file mode 100644
index 00000000000..6b6a8bd4068
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
@@ -0,0 +1,277 @@
+<script>
+import { omit } from 'underscore';
+import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
+import flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { scrollToElement, urlParamsToObject } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import initManualOrdering from '~/manual_ordering';
+import Issuable from './issuable.vue';
+import {
+ sortOrderMap,
+ RELATIVE_POSITION,
+ PAGE_SIZE,
+ PAGE_SIZE_MANUAL,
+ LOADING_LIST_ITEMS_LENGTH,
+} from '../constants';
+import issueableEventHub from '../eventhub';
+
+export default {
+ LOADING_LIST_ITEMS_LENGTH,
+ components: {
+ GlEmptyState,
+ GlPagination,
+ GlSkeletonLoading,
+ Issuable,
+ },
+ props: {
+ canBulkEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ createIssuePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ emptySvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ sortKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ filters: {},
+ isBulkEditing: false,
+ issuables: [],
+ loading: false,
+ page: 1,
+ selection: {},
+ totalItems: 0,
+ };
+ },
+ computed: {
+ allIssuablesSelected() {
+ // WARNING: Because we are only keeping track of selected values
+ // this works, we will need to rethink this if we start tracking
+ // [id]: false for not selected values.
+ return this.issuables.length === Object.keys(this.selection).length;
+ },
+ emptyState() {
+ if (this.issuables.length) {
+ return {}; // Empty state shouldn't be shown here
+ } else if (this.hasFilters) {
+ return {
+ title: __('Sorry, your filter produced no results'),
+ description: __('To widen your search, change or remove filters above'),
+ };
+ } else if (this.filters.state === 'opened') {
+ return {
+ title: __('There are no open issues'),
+ description: __('To keep this project going, create a new issue'),
+ primaryLink: this.createIssuePath,
+ primaryText: __('New issue'),
+ };
+ } else if (this.filters.state === 'closed') {
+ return {
+ title: __('There are no closed issues'),
+ };
+ }
+
+ return {
+ title: __('There are no issues to show'),
+ description: __(
+ 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
+ ),
+ };
+ },
+ hasFilters() {
+ const ignored = ['utf8', 'state', 'scope', 'order_by', 'sort'];
+ return Object.keys(omit(this.filters, ignored)).length > 0;
+ },
+ isManualOrdering() {
+ return this.sortKey === RELATIVE_POSITION;
+ },
+ itemsPerPage() {
+ return this.isManualOrdering ? PAGE_SIZE_MANUAL : PAGE_SIZE;
+ },
+ baseUrl() {
+ return window.location.href.replace(/(\?.*)?(#.*)?$/, '');
+ },
+ },
+ watch: {
+ selection() {
+ // We need to call nextTick here to wait for all of the boxes to be checked and rendered
+ // before we query the dom in issuable_bulk_update_actions.js.
+ this.$nextTick(() => {
+ issueableEventHub.$emit('issuables:updateBulkEdit');
+ });
+ },
+ issuables() {
+ this.$nextTick(() => {
+ initManualOrdering();
+ });
+ },
+ },
+ mounted() {
+ if (this.canBulkEdit) {
+ this.unsubscribeToggleBulkEdit = issueableEventHub.$on('issuables:toggleBulkEdit', val => {
+ this.isBulkEditing = val;
+ });
+ }
+ this.fetchIssuables();
+ },
+ beforeDestroy() {
+ issueableEventHub.$off('issuables:toggleBulkEdit');
+ },
+ methods: {
+ isSelected(issuableId) {
+ return Boolean(this.selection[issuableId]);
+ },
+ setSelection(ids) {
+ ids.forEach(id => {
+ this.select(id, true);
+ });
+ },
+ clearSelection() {
+ this.selection = {};
+ },
+ select(id, isSelect = true) {
+ if (isSelect) {
+ this.$set(this.selection, id, true);
+ } else {
+ this.$delete(this.selection, id);
+ }
+ },
+ fetchIssuables(pageToFetch) {
+ this.loading = true;
+
+ this.clearSelection();
+
+ this.setFilters();
+
+ return axios
+ .get(this.endpoint, {
+ params: {
+ ...this.filters,
+
+ with_labels_details: true,
+ page: pageToFetch || this.page,
+ per_page: this.itemsPerPage,
+ },
+ })
+ .then(response => {
+ this.loading = false;
+ this.issuables = response.data;
+ this.totalItems = Number(response.headers['x-total']);
+ this.page = Number(response.headers['x-page']);
+ })
+ .catch(() => {
+ this.loading = false;
+ return flash(__('An error occurred while loading issues'));
+ });
+ },
+ getQueryObject() {
+ return urlParamsToObject(window.location.search);
+ },
+ onPaginate(newPage) {
+ if (newPage === this.page) return;
+
+ scrollToElement('#content-body');
+ this.fetchIssuables(newPage);
+ },
+ onSelectAll() {
+ if (this.allIssuablesSelected) {
+ this.selection = {};
+ } else {
+ this.setSelection(this.issuables.map(({ id }) => id));
+ }
+ },
+ onSelectIssuable({ issuable, selected }) {
+ if (!this.canBulkEdit) return;
+
+ this.select(issuable.id, selected);
+ },
+ setFilters() {
+ const {
+ label_name: labels,
+ milestone_title: milestoneTitle,
+ ...filters
+ } = this.getQueryObject();
+
+ if (milestoneTitle) {
+ filters.milestone = milestoneTitle;
+ }
+ if (Array.isArray(labels)) {
+ filters.labels = labels.join(',');
+ }
+ if (!filters.state) {
+ filters.state = 'opened';
+ }
+
+ Object.assign(filters, sortOrderMap[this.sortKey]);
+
+ this.filters = filters;
+ },
+ },
+};
+</script>
+
+<template>
+ <ul v-if="loading" class="content-list">
+ <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue">
+ <gl-skeleton-loading />
+ </li>
+ </ul>
+ <div v-else-if="issuables.length">
+ <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
+ <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" />
+ <strong>{{ __('Select all') }}</strong>
+ </div>
+ <ul
+ class="content-list issuable-list issues-list"
+ :class="{ 'manual-ordering': isManualOrdering }"
+ >
+ <issuable
+ v-for="issuable in issuables"
+ :key="issuable.id"
+ class="pr-3"
+ :class="{ 'user-can-drag': isManualOrdering }"
+ :issuable="issuable"
+ :is-bulk-editing="isBulkEditing"
+ :selected="isSelected(issuable.id)"
+ :base-url="baseUrl"
+ @select="onSelectIssuable"
+ />
+ </ul>
+ <div class="mt-3">
+ <gl-pagination
+ v-if="totalItems"
+ :value="page"
+ :per-page="itemsPerPage"
+ :total-items="totalItems"
+ class="justify-content-center"
+ @input="onPaginate"
+ />
+ </div>
+ </div>
+ <gl-empty-state
+ v-else
+ :title="emptyState.title"
+ :description="emptyState.description"
+ :svg-path="emptySvgPath"
+ :primary-button-link="emptyState.primaryLink"
+ :primary-button-text="emptyState.primaryText"
+ />
+</template>
diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js
new file mode 100644
index 00000000000..71b9c52c703
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/constants.js
@@ -0,0 +1,33 @@
+// Maps sort order as it appears in the URL query to API `order_by` and `sort` params.
+const PRIORITY = 'priority';
+const ASC = 'asc';
+const DESC = 'desc';
+const CREATED_AT = 'created_at';
+const UPDATED_AT = 'updated_at';
+const DUE_DATE = 'due_date';
+const MILESTONE_DUE = 'milestone_due';
+const POPULARITY = 'popularity';
+const WEIGHT = 'weight';
+const LABEL_PRIORITY = 'label_priority';
+export const RELATIVE_POSITION = 'relative_position';
+export const LOADING_LIST_ITEMS_LENGTH = 8;
+export const PAGE_SIZE = 20;
+export const PAGE_SIZE_MANUAL = 100;
+
+export const sortOrderMap = {
+ priority: { order_by: PRIORITY, sort: ASC }, // asc and desc are flipped for some reason
+ created_date: { order_by: CREATED_AT, sort: DESC },
+ created_asc: { order_by: CREATED_AT, sort: ASC },
+ updated_desc: { order_by: UPDATED_AT, sort: DESC },
+ updated_asc: { order_by: UPDATED_AT, sort: ASC },
+ milestone_due_desc: { order_by: MILESTONE_DUE, sort: DESC },
+ milestone: { order_by: MILESTONE_DUE, sort: ASC },
+ due_date_desc: { order_by: DUE_DATE, sort: DESC },
+ due_date: { order_by: DUE_DATE, sort: ASC },
+ popularity: { order_by: POPULARITY, sort: DESC },
+ popularity_asc: { order_by: POPULARITY, sort: ASC },
+ label_priority: { order_by: LABEL_PRIORITY, sort: ASC }, // asc and desc are flipped
+ relative_position: { order_by: RELATIVE_POSITION, sort: ASC },
+ weight_desc: { order_by: WEIGHT, sort: DESC },
+ weight: { order_by: WEIGHT, sort: ASC },
+};
diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issuables_list/eventhub.js
new file mode 100644
index 00000000000..d1601a7d8f3
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/eventhub.js
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+const issueablesEventBus = new Vue();
+
+export default issueablesEventBus;
diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js
new file mode 100644
index 00000000000..9fc7fa837ff
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/index.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import IssuablesListApp from './components/issuables_list_app.vue';
+
+export default function initIssuablesList() {
+ if (!gon.features || !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 });
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index a9e086fade8..9136a47d542 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, one-var, consistent-return */
+/* eslint-disable consistent-return */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
@@ -91,18 +91,17 @@ export default class Issue {
'click',
'.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
e => {
- var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
- $button = $(e.currentTarget);
- shouldSubmit = $button.hasClass('btn-comment');
+ const $button = $(e.currentTarget);
+ const shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
this.disableCloseReopenButton($button);
- url = $button.attr('href');
+ const url = $button.attr('href');
return axios
.put(url)
.then(({ data }) => {
@@ -139,16 +138,14 @@ export default class Issue {
}
static submitNoteForm(form) {
- var noteText;
- noteText = form.find('textarea.js-note-text').val();
+ const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) {
return form.submit();
}
}
static initRelatedBranches() {
- var $container;
- $container = $('#related-branches');
+ const $container = $('#related-branches');
return axios
.get($container.data('url'))
.then(({ data }) => {
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue
index ef126166e8b..03a697d11ed 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/jobs/components/log/log.vue
@@ -11,11 +11,35 @@ export default {
computed: {
...mapState(['traceEndpoint', 'trace', 'isTraceComplete']),
},
+ updated() {
+ this.$nextTick(() => {
+ this.handleScrollDown();
+ });
+ },
+ mounted() {
+ this.$nextTick(() => {
+ this.handleScrollDown();
+ });
+ },
methods: {
- ...mapActions(['toggleCollapsibleLine']),
+ ...mapActions(['toggleCollapsibleLine', 'scrollBottom']),
handleOnClickCollapsibleLine(section) {
this.toggleCollapsibleLine(section);
},
+ /**
+ * The job log is sent in HTML, which means we need to use `v-html` to render it
+ * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
+ * in this case because it runs before `v-html` has finished running, since there's no
+ * Vue binding.
+ * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
+ */
+ handleScrollDown() {
+ if (this.isScrolledToBottomBeforeReceivingTrace) {
+ setTimeout(() => {
+ this.scrollBottom();
+ }, 0);
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index 58e49f54d96..179d0bc4e0f 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -17,7 +17,7 @@ export const parseLine = (line = {}, lineNumber) => ({
* @param Number lineNumber
*/
export const parseHeaderLine = (line = {}, lineNumber) => ({
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: parseLine(line, lineNumber),
lines: [],
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 72de3b5d726..6abf723be9a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, one-var, no-new, consistent-return, no-shadow, no-param-reassign, vars-on-top, 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, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
@@ -15,63 +15,39 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
export default class LabelsSelect {
constructor(els, options = {}) {
- var _this, $els;
- _this = this;
+ const _this = this;
- $els = $(els);
+ let $els = $(els);
if (!els) {
$els = $('.js-label-select');
}
$els.each((i, dropdown) => {
- var $block,
- $dropdown,
- $form,
- $loading,
- $selectbox,
- $sidebarCollapsedValue,
- $value,
- $dropdownMenu,
- abilityName,
- defaultLabel,
- issueUpdateURL,
- labelUrl,
- namespacePath,
- projectPath,
- saveLabelData,
- selectedLabel,
- showAny,
- showNo,
- $sidebarLabelTooltip,
- initialSelected,
- fieldName,
- showMenuAbove,
- $dropdownContainer;
- $dropdown = $(dropdown);
- $dropdownContainer = $dropdown.closest('.labels-filter');
- namespacePath = $dropdown.data('namespacePath');
- projectPath = $dropdown.data('projectPath');
- issueUpdateURL = $dropdown.data('issueUpdate');
- selectedLabel = $dropdown.data('selected');
+ const $dropdown = $(dropdown);
+ const $dropdownContainer = $dropdown.closest('.labels-filter');
+ const namespacePath = $dropdown.data('namespacePath');
+ const projectPath = $dropdown.data('projectPath');
+ const issueUpdateURL = $dropdown.data('issueUpdate');
+ let selectedLabel = $dropdown.data('selected');
if (selectedLabel != null && !$dropdown.hasClass('js-multiselect')) {
selectedLabel = selectedLabel.split(',');
}
- showNo = $dropdown.data('showNo');
- showAny = $dropdown.data('showAny');
- showMenuAbove = $dropdown.data('showMenuAbove');
- defaultLabel = $dropdown.data('defaultLabel') || __('Label');
- abilityName = $dropdown.data('abilityName');
- $selectbox = $dropdown.closest('.selectbox');
- $block = $selectbox.closest('.block');
- $form = $dropdown.closest('form, .js-issuable-update');
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
- $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
- $value = $block.find('.value');
- $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
- $loading = $block.find('.block-loading').fadeOut();
- fieldName = $dropdown.data('fieldName');
- initialSelected = $selectbox
+ const showNo = $dropdown.data('showNo');
+ const showAny = $dropdown.data('showAny');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const defaultLabel = $dropdown.data('defaultLabel') || __('Label');
+ const abilityName = $dropdown.data('abilityName');
+ const $selectbox = $dropdown.closest('.selectbox');
+ const $block = $selectbox.closest('.block');
+ const $form = $dropdown.closest('form, .js-issuable-update');
+ const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ const $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ const $value = $block.find('.value');
+ const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
+ const $loading = $block.find('.block-loading').fadeOut();
+ const fieldName = $dropdown.data('fieldName');
+ let initialSelected = $selectbox
.find(`input[name="${$dropdown.data('fieldName')}"]`)
.map(function() {
return this.value;
@@ -90,9 +66,8 @@ export default class LabelsSelect {
);
}
- saveLabelData = function() {
- var data, selected;
- selected = $dropdown
+ const saveLabelData = function() {
+ const selected = $dropdown
.closest('.selectbox')
.find(`input[name='${fieldName}']`)
.map(function() {
@@ -103,7 +78,7 @@ export default class LabelsSelect {
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
- data = {};
+ const data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
if (!selected.length) {
@@ -114,12 +89,13 @@ export default class LabelsSelect {
axios
.put(issueUpdateURL, data)
.then(({ data }) => {
- var labelCount, template, labelTooltipTitle, labelTitles;
+ let labelTooltipTitle;
+ let template;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueUpdateURL = issueUpdateURL;
- labelCount = 0;
+ let labelCount = 0;
if (data.labels.length && issueUpdateURL) {
template = LabelsSelect.getLabelTemplate({
labels: _.sortBy(data.labels, 'title'),
@@ -174,7 +150,7 @@ export default class LabelsSelect {
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
- labelTitles = data.labels.map(label => label.title);
+ let labelTitles = data.labels.map(label => label.title);
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
@@ -199,13 +175,13 @@ export default class LabelsSelect {
$dropdown.glDropdown({
showMenuAbove,
data(term, callback) {
- labelUrl = $dropdown.attr('data-labels');
+ const labelUrl = $dropdown.attr('data-labels');
axios
.get(labelUrl)
.then(res => {
let { data } = res;
if ($dropdown.hasClass('js-extra-options')) {
- var extraData = [];
+ const extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
@@ -232,22 +208,14 @@ export default class LabelsSelect {
.catch(() => flash(__('Error fetching labels.')));
},
renderRow(label) {
- var linkEl,
- listItemEl,
- colorEl,
- indeterminate,
- removesAll,
- selectedClass,
- i,
- marked,
- dropdownValue;
-
- selectedClass = [];
- removesAll = label.id <= 0 || label.id == null;
+ let colorEl;
+
+ const selectedClass = [];
+ const removesAll = label.id <= 0 || label.id == null;
if ($dropdown.hasClass('js-filter-bulk-update')) {
- indeterminate = $dropdown.data('indeterminate') || [];
- marked = $dropdown.data('marked') || [];
+ const indeterminate = $dropdown.data('indeterminate') || [];
+ const marked = $dropdown.data('marked') || [];
if (indeterminate.indexOf(label.id) !== -1) {
selectedClass.push('is-indeterminate');
@@ -255,7 +223,7 @@ export default class LabelsSelect {
if (marked.indexOf(label.id) !== -1) {
// Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf('is-indeterminate');
+ const i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
@@ -263,7 +231,7 @@ export default class LabelsSelect {
}
} else {
if (this.id(label)) {
- dropdownValue = this.id(label)
+ const dropdownValue = this.id(label)
.toString()
.replace(/'/g, "\\'");
@@ -287,7 +255,7 @@ export default class LabelsSelect {
colorEl = '';
}
- linkEl = document.createElement('a');
+ const linkEl = document.createElement('a');
linkEl.href = '#';
// We need to identify which items are actually labels
@@ -300,7 +268,7 @@ export default class LabelsSelect {
linkEl.className = selectedClass.join(' ');
linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
- listItemEl = document.createElement('li');
+ const listItemEl = document.createElement('li');
listItemEl.appendChild(linkEl);
return listItemEl;
@@ -312,12 +280,12 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel(selected, el) {
- var $dropdownParent = $dropdown.parent();
- var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
- var isSelected = el !== null ? el.hasClass('is-active') : false;
+ const $dropdownParent = $dropdown.parent();
+ const $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
+ const isSelected = el !== null ? el.hasClass('is-active') : false;
- var title = selected ? selected.title : null;
- var selectedLabels = this.selected;
+ const title = selected ? selected.title : null;
+ const selectedLabels = this.selected;
if ($dropdownInputField.length && $dropdownInputField.val().length) {
$dropdownParent.find('.dropdown-input-clear').trigger('click');
@@ -329,7 +297,7 @@ export default class LabelsSelect {
} else if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
- var index = this.selected.indexOf(title);
+ const index = this.selected.indexOf(title);
this.selected.splice(index, 1);
}
@@ -359,10 +327,9 @@ export default class LabelsSelect {
}
},
hidden() {
- var isIssueIndex, isMRIndex, page;
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
@@ -393,14 +360,13 @@ export default class LabelsSelect {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
- var isIssueIndex, isMRIndex, page, boardsModel;
- var fadeOutLoader = () => {
+ const fadeOutLoader = () => {
$loading.fadeOut();
};
- page = $('body').attr('data-page');
- isIssueIndex = page === 'projects:issues:index';
- isMRIndex = page === 'projects:merge_requests:index';
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === 'projects:merge_requests:index';
if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
$dropdown
@@ -419,6 +385,7 @@ export default class LabelsSelect {
return;
}
+ let boardsModel;
if ($dropdown.closest('.add-issues-modal').length) {
boardsModel = ModalStore.store.filter;
}
@@ -450,7 +417,7 @@ export default class LabelsSelect {
}),
);
} else {
- var { labels } = boardsStore.detail.issue;
+ let { labels } = boardsStore.detail.issue;
labels = labels.filter(selectedLabel => selectedLabel.id !== label.id);
boardsStore.detail.issue.labels = labels;
}
@@ -578,16 +545,14 @@ export default class LabelsSelect {
}
// eslint-disable-next-line class-methods-use-this
setDropdownData($dropdown, isMarking, value) {
- var i, markedIds, unmarkedIds, indeterminateIds;
-
- markedIds = $dropdown.data('marked') || [];
- unmarkedIds = $dropdown.data('unmarked') || [];
- indeterminateIds = $dropdown.data('indeterminate') || [];
+ const markedIds = $dropdown.data('marked') || [];
+ const unmarkedIds = $dropdown.data('unmarked') || [];
+ const indeterminateIds = $dropdown.data('indeterminate') || [];
if (isMarking) {
markedIds.push(value);
- i = indeterminateIds.indexOf(value);
+ let i = indeterminateIds.indexOf(value);
if (i > -1) {
indeterminateIds.splice(i, 1);
}
@@ -598,7 +563,7 @@ export default class LabelsSelect {
}
} else {
// If marked item (not common) is unmarked
- i = markedIds.indexOf(value);
+ const i = markedIds.indexOf(value);
if (i > -1) {
markedIds.splice(i, 1);
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index c05db4a5c71..2c5278d16ae 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -26,7 +26,11 @@ export default (resolvers = {}, config = {}) => {
createUploadLink(httpOptions),
new BatchHttpLink(httpOptions),
),
- cache: new InMemoryCache(config.cacheConfig),
+ cache: new InMemoryCache({
+ ...config.cacheConfig,
+ freezeResults: config.assumeImmutableResults,
+ }),
resolvers,
+ assumeImmutableResults: config.assumeImmutableResults,
});
};
diff --git a/app/assets/javascripts/lib/utils/chart_utils.js b/app/assets/javascripts/lib/utils/chart_utils.js
index 0f78756aac8..4a1e6c5d68c 100644
--- a/app/assets/javascripts/lib/utils/chart_utils.js
+++ b/app/assets/javascripts/lib/utils/chart_utils.js
@@ -81,3 +81,20 @@ export const lineChartOptions = ({ width, numberOfPoints, shouldAdjustFontSize }
},
},
});
+
+/**
+ * Takes a dataset and returns an array containing the y-values of it's first and last entry.
+ * (e.g., [['xValue1', 'yValue1'], ['xValue2', 'yValue2'], ['xValue3', 'yValue3']] will yield ['yValue1', 'yValue3'])
+ *
+ * @param {Array} data
+ * @returns {[*, *]}
+ */
+export const firstAndLastY = data => {
+ const [firstEntry] = data;
+ const [lastEntry] = data.slice(-1);
+
+ const firstY = firstEntry[1];
+ const lastY = lastEntry[1];
+
+ return [firstY, lastY];
+};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 37b0215f6f9..28143859e4c 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -78,11 +78,11 @@ export const getDayName = date =>
* @param {date} datetime
* @returns {String}
*/
-export const formatDate = datetime => {
+export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => {
if (_.isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
throw new Error(__('Invalid date'));
}
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ return dateFormat(datetime, format);
};
/**
@@ -541,7 +541,7 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
* The result cannot become negative.
*
* @param endDate date string that the time difference is calculated for
- * @return {number} number of milliseconds remaining until the given date
+ * @return {Number} number of milliseconds remaining until the given date
*/
export const calculateRemainingMilliseconds = endDate => {
const remainingMilliseconds = new Date(endDate).getTime() - Date.now();
@@ -552,15 +552,53 @@ export const calculateRemainingMilliseconds = endDate => {
* Subtracts a given number of days from a given date and returns the new date.
*
* @param {Date} date the date that we will substract days from
- * @param {number} daysInPast number of days that are subtracted from a given date
- * @returns {String} Date string in ISO format
+ * @param {Number} daysInPast number of days that are subtracted from a given date
+ * @returns {Date} Date in past as Date object
*/
-export const getDateInPast = (date, daysInPast) => {
- const dateClone = newDate(date);
- return new Date(
- dateClone.setTime(dateClone.getTime() - daysInPast * 24 * 60 * 60 * 1000),
- ).toISOString();
+export const getDateInPast = (date, daysInPast) =>
+ new Date(newDate(date).setDate(date.getDate() - daysInPast));
+
+/*
+ * Appending T00:00:00 makes JS assume local time and prevents it from shifting the date
+ * to match the user's time zone. We want to display the date in server time for now, to
+ * be consistent with the "edit issue -> due date" UI.
+ */
+
+export const newDateAsLocaleTime = date => {
+ const suffix = 'T00:00:00';
+ return new Date(`${date}${suffix}`);
};
export const beginOfDayTime = 'T00:00:00Z';
export const endOfDayTime = 'T23:59:59Z';
+
+/**
+ * @param {Date} d1
+ * @param {Date} d2
+ * @param {Function} formatter
+ * @return {Any[]} an array of formatted dates between 2 given dates (including start&end date)
+ */
+export const getDatesInRange = (d1, d2, formatter = x => x) => {
+ if (!(d1 instanceof Date) || !(d2 instanceof Date)) {
+ return [];
+ }
+ let startDate = d1.getTime();
+ const endDate = d2.getTime();
+ const oneDay = 24 * 3600 * 1000;
+ const range = [d1];
+
+ while (startDate < endDate) {
+ startDate += oneDay;
+ range.push(new Date(startDate));
+ }
+
+ return range.map(formatter);
+};
+
+/**
+ * Converts the supplied number of seconds to milliseconds.
+ *
+ * @param {Number} seconds
+ * @return {Number} number of milliseconds
+ */
+export const secondsToMilliseconds = seconds => seconds * 1000;
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index cd509a13193..8db08099b3f 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,8 +1,7 @@
-/* eslint-disable no-var, consistent-return, no-return-assign */
+/* eslint-disable consistent-return, no-return-assign */
function notificationGranted(message, opts, onclick) {
- var notification;
- notification = new Notification(message, opts);
+ const notification = new Notification(message, opts);
setTimeout(
() =>
// Hide the notification after X amount of seconds
@@ -21,8 +20,7 @@ function notifyPermissions() {
}
function notifyMe(message, body, icon, onclick) {
- var opts;
- opts = {
+ const opts = {
body,
icon,
};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 0f2cc57b1f9..bc87232f40b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -117,3 +117,36 @@ export const median = arr => {
const sorted = arr.sort((a, b) => a - b);
return arr.length % 2 !== 0 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
};
+
+/**
+ * Computes the change from one value to the other as a percentage.
+ * @param {Number} firstY
+ * @param {Number} lastY
+ * @returns {Number}
+ */
+export const changeInPercent = (firstY, lastY) => {
+ if (firstY === lastY) {
+ return 0;
+ }
+
+ return Math.round(((lastY - firstY) / Math.abs(firstY)) * 100);
+};
+
+/**
+ * Computes and formats the change from one value to the other as a percentage.
+ * Prepends the computed percentage with either "+" or "-" to indicate an in- or decrease and
+ * returns a given string if the result is not finite (for example, if the first value is "0").
+ * @param firstY
+ * @param lastY
+ * @param nonFiniteResult
+ * @returns {String}
+ */
+export const formattedChangeInPercent = (firstY, lastY, { nonFiniteResult = '-' } = {}) => {
+ const change = changeInPercent(firstY, lastY);
+
+ if (!Number.isFinite(change)) {
+ return nonFiniteResult;
+ }
+
+ return `${change >= 0 ? '+' : ''}${change}%`;
+};
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index d13fbeb5fc7..0c194d67bce 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -36,25 +36,26 @@ export const humanize = string =>
export const dasherize = str => str.replace(/[_\s]+/g, '-');
/**
- * Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters
- * @param {String} str
+ * Replaces whitespace and non-sluggish characters with a given separator
+ * @param {String} str - The string to slugify
+ * @param {String=} separator - The separator used to separate words (defaults to "-")
* @returns {String}
*/
-export const slugify = str => {
+export const slugify = (str, separator = '-') => {
const slug = str
.trim()
.toLowerCase()
- .replace(/[^a-zA-Z0-9_.-]+/g, '-');
+ .replace(/[^a-zA-Z0-9_.-]+/g, separator);
- return slug === '-' ? '' : slug;
+ return slug === separator ? '' : slug;
};
/**
- * Replaces whitespaces with underscore and converts to lower case
+ * Replaces whitespace and non-sluggish characters with underscores
* @param {String} str
* @returns {String}
*/
-export const slugifyWithUnderscore = str => str.toLowerCase().replace(/\s+/g, '_');
+export const slugifyWithUnderscore = str => slugify(str, '_');
/**
* Truncates given text
@@ -139,6 +140,14 @@ export const stripHtml = (string, replace = '') => {
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase());
/**
+ * Converts camelCase string to snake_case
+ *
+ * @param {*} string
+ */
+export const convertToSnakeCase = string =>
+ slugifyWithUnderscore(string.match(/([a-zA-Z][^A-Z]*)/g).join(' '));
+
+/**
* Converts a sentence to lower case from the second word onwards
* e.g. Hello World => Hello world
*
diff --git a/app/assets/javascripts/lib/utils/tick_formats.js b/app/assets/javascripts/lib/utils/tick_formats.js
deleted file mode 100644
index af3ca714400..00000000000
--- a/app/assets/javascripts/lib/utils/tick_formats.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { createDateTimeFormat } from '../../locale';
-
-let dateTimeFormats;
-
-export const initDateFormats = () => {
- const dayFormat = createDateTimeFormat({ month: 'short', day: 'numeric' });
- const monthFormat = createDateTimeFormat({ month: 'long' });
- const yearFormat = createDateTimeFormat({ year: 'numeric' });
-
- dateTimeFormats = {
- dayFormat,
- monthFormat,
- yearFormat,
- };
-};
-
-initDateFormats();
-
-/**
- Formats a localized date in way that it can be used for d3.js axis.tickFormat().
-
- That is, it displays
- - 4-digit for first of January
- - full month name for first of every month
- - day and abbreviated month otherwise
-
- see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat
- */
-export const dateTickFormat = date => {
- if (date.getDate() !== 1) {
- return dateTimeFormats.dayFormat.format(date);
- }
-
- if (date.getMonth() > 0) {
- return dateTimeFormats.monthFormat.format(date);
- }
-
- return dateTimeFormats.yearFormat.format(date);
-};
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index b6b96fe7bd5..dd868bb9f4c 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-underscore-dangle, no-param-reassign, consistent-return, one-var, no-else-return */
+/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return, no-else-return */
import $ from 'jquery';
@@ -82,13 +82,13 @@ LineHighlighter.prototype.highlightHash = function(newHash) {
};
LineHighlighter.prototype.clickHandler = function(event) {
- var current, lineNumber, range;
+ let range;
event.preventDefault();
this.clearHighlight();
- lineNumber = $(event.target)
+ const lineNumber = $(event.target)
.closest('a')
.data('lineNumber');
- current = this.hashToRange(this._hash);
+ const current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
// If there's no current selection, or there is but Shift wasn't held,
// treat this like a single-line selection.
@@ -121,12 +121,11 @@ LineHighlighter.prototype.clearHighlight = function() {
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
- var first, last, matches;
// ?L(\d+)(?:-(\d+))?$/)
- matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
+ const matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
- first = parseInt(matches[1], 10);
- last = matches[2] ? parseInt(matches[2], 10) : null;
+ const first = parseInt(matches[1], 10);
+ const last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
} else {
return [null, null];
@@ -160,7 +159,7 @@ LineHighlighter.prototype.highlightRange = function(range) {
// Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
- var hash;
+ let hash;
if (lastLineNumber) {
hash = `#L${firstLineNumber}-${lastLineNumber}`;
} else {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c19a845eb69..465c9a362ba 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -37,7 +37,6 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import { initUserTracking } from './tracking';
import { __ } from './locale';
-import initPrivacyPolicyUpdateCallout from './privacy_policy_update_callout';
import 'ee_else_ce/main_ee';
@@ -97,7 +96,6 @@ function deferredInitialisation() {
initUsagePingConsent();
initUserPopovers();
initUserTracking();
- initPrivacyPolicyUpdateCallout();
if (document.querySelector('.search')) initSearchAutocomplete();
@@ -162,24 +160,6 @@ function deferredInitialisation() {
});
loadAwardsHandler();
-
- /**
- * Toggle Canary Badge
- *
- * For GitLab.com only, when the user is using canary
- * we render a Next badge and hide the option to switch
- * to canay
- */
- if (Cookies.get('gitlab_canary') && Cookies.get('gitlab_canary') === 'true') {
- const canaryBadge = document.querySelector('.js-canary-badge');
- const canaryLink = document.querySelector('.js-canary-link');
- if (canaryBadge) {
- canaryBadge.classList.remove('hidden');
- }
- if (canaryLink) {
- canaryLink.classList.add('hidden');
- }
- }
}
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
index 29a0e5a904a..f93dbcd4c47 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/manual_ordering.js
@@ -18,7 +18,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
});
-const initManualOrdering = () => {
+const initManualOrdering = (draggableSelector = 'li.issue') => {
const issueList = document.querySelector('.manual-ordering');
if (!issueList || !(gon.current_user_id > 0)) {
@@ -34,14 +34,14 @@ const initManualOrdering = () => {
group: {
name: 'issues',
},
- draggable: 'li.issue',
+ draggable: draggableSelector,
onStart: () => {
sortableStart();
},
onUpdate: event => {
const el = event.item;
- const url = el.getAttribute('url');
+ const url = el.getAttribute('url') || el.dataset.url;
const prev = el.previousElementSibling;
const next = el.nextElementSibling;
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 7223b5c0d43..3a7ade5ad94 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-underscore-dangle, one-var, consistent-return */
+/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
import { __ } from '~/locale';
@@ -17,14 +17,7 @@ function MergeRequest(opts) {
this.opts = opts != null ? opts : {};
this.submitNoteForm = this.submitNoteForm.bind(this);
this.$el = $('.merge-request');
- this.$('.show-all-commits').on(
- 'click',
- (function(_this) {
- return function() {
- return _this.showAllCommits();
- };
- })(this),
- );
+ this.$('.show-all-commits').on('click', () => this.showAllCommits());
this.initTabs();
this.initMRBtnListeners();
@@ -71,12 +64,10 @@ MergeRequest.prototype.showAllCommits = function() {
};
MergeRequest.prototype.initMRBtnListeners = function() {
- var _this;
- _this = this;
+ const _this = this;
return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, shouldSubmit;
- $this = $(this);
- shouldSubmit = $this.hasClass('btn-comment');
+ const $this = $(this);
+ const shouldSubmit = $this.hasClass('btn-comment');
if (shouldSubmit && $this.data('submitted')) {
return;
}
@@ -95,8 +86,7 @@ MergeRequest.prototype.initMRBtnListeners = function() {
};
MergeRequest.prototype.submitNoteForm = function(form, $button) {
- var noteText;
- noteText = form.find('textarea.js-note-text').val();
+ const noteText = form.find('textarea.js-note-text').val();
if (noteText.trim().length > 0) {
form.submit();
$button.data('submitted', true);
@@ -106,7 +96,7 @@ MergeRequest.prototype.submitNoteForm = function(form, $button) {
MergeRequest.prototype.initCommitMessageListeners = function() {
$(document).on('click', 'a.js-with-description-link', e => {
- var textarea = $('textarea.js-commit-message');
+ const textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
@@ -115,7 +105,7 @@ MergeRequest.prototype.initCommitMessageListeners = function() {
});
$(document).on('click', 'a.js-without-description-link', e => {
- var textarea = $('textarea.js-commit-message');
+ const textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
new file mode 100644
index 00000000000..8eeac737a11
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -0,0 +1,227 @@
+<script>
+import { flatten, isNumber } from 'underscore';
+import { GlLineChart, 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 { graphDataValidatorForAnomalyValues } from '../../utils';
+import MonitorTimeSeriesChart from './time_series.vue';
+
+/**
+ * Series indexes
+ */
+const METRIC = 0;
+const UPPER = 1;
+const LOWER = 2;
+
+/**
+ * Boundary area appearance
+ */
+const AREA_COLOR = colorValues.anomalyAreaColor;
+const AREA_OPACITY = areaOpacityValues.default;
+const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})`;
+
+/**
+ * The anomaly component highlights when a metric shows
+ * some anomalous behavior.
+ *
+ * It shows both a metric line and a boundary band in a
+ * time series chart, the boundary band shows the normal
+ * range of values the metric should take.
+ *
+ * This component accepts 3 queries, which contain the
+ * "metric", "upper" limit and "lower" limit.
+ *
+ * The upper and lower series are "stacked areas" visually
+ * to create the boundary band, and if any "metric" value
+ * is outside this band, it is highlighted to warn users.
+ *
+ * The boundary band stack must be painted above the 0 line
+ * so the area is shown correctly. If any of the values of
+ * the data are negative, the chart data is shifted to be
+ * above 0 line.
+ *
+ * The data passed to the time series is will always be
+ * positive, but reformatted to show the original values of
+ * data.
+ *
+ */
+export default {
+ components: {
+ GlLineChart,
+ GlChartSeriesLabel,
+ MonitorTimeSeriesChart,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForAnomalyValues,
+ },
+ },
+ computed: {
+ series() {
+ return this.graphData.queries.map(query => {
+ const values = query.result[0] ? query.result[0].values : [];
+ return {
+ label: query.label,
+ data: values.filter(([, value]) => !Number.isNaN(value)),
+ };
+ });
+ },
+ /**
+ * If any of the values of the data is negative, the
+ * chart data is shifted to the lowest value
+ *
+ * This offset is the lowest value.
+ */
+ yOffset() {
+ const values = flatten(this.series.map(ser => ser.data.map(([, y]) => y)));
+ const min = values.length ? Math.floor(Math.min(...values)) : 0;
+ return min < 0 ? -min : 0;
+ },
+ metricData() {
+ const originalMetricQuery = this.graphData.queries[0];
+
+ const metricQuery = { ...originalMetricQuery };
+ metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
+ x,
+ y + this.yOffset,
+ ]);
+ return {
+ ...this.graphData,
+ type: 'line-chart',
+ queries: [metricQuery],
+ };
+ },
+ metricSeriesConfig() {
+ return {
+ type: 'line',
+ symbol: 'circle',
+ symbolSize: (val, params) => {
+ if (this.isDatapointAnomaly(params.dataIndex)) {
+ return symbolSizes.anomaly;
+ }
+ // 0 causes echarts to throw an error, use small number instead
+ // see https://gitlab.com/gitlab-org/gitlab-ui/issues/423
+ return 0.001;
+ },
+ showSymbol: true,
+ itemStyle: {
+ color: params => {
+ if (this.isDatapointAnomaly(params.dataIndex)) {
+ return colorValues.anomalySymbol;
+ }
+ return colorValues.primaryColor;
+ },
+ },
+ };
+ },
+ chartOptions() {
+ const [, upperSeries, lowerSeries] = this.series;
+ const calcOffsetY = (data, offsetCallback) =>
+ data.map((value, dataIndex) => {
+ const [x, y] = value;
+ return [x, y + offsetCallback(dataIndex)];
+ });
+
+ const yAxisWithOffset = {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num - this.yOffset, 3).toString(),
+ },
+ };
+
+ /**
+ * Boundary is rendered by 2 series: An invisible
+ * series (opacity: 0) stacked on a visible one.
+ *
+ * Order is important, lower boundary is stacked
+ * *below* the upper boundary.
+ */
+ const boundarySeries = [];
+
+ if (upperSeries.data.length && lowerSeries.data.length) {
+ // Lower boundary, plus the offset if negative values
+ boundarySeries.push(
+ this.makeBoundarySeries({
+ name: this.formatLegendLabel(lowerSeries),
+ data: calcOffsetY(lowerSeries.data, () => this.yOffset),
+ }),
+ );
+ // Upper boundary, minus the lower boundary
+ boundarySeries.push(
+ this.makeBoundarySeries({
+ name: this.formatLegendLabel(upperSeries),
+ data: calcOffsetY(upperSeries.data, i => -this.yValue(LOWER, i)),
+ areaStyle: {
+ color: AREA_COLOR,
+ opacity: AREA_OPACITY,
+ },
+ }),
+ );
+ }
+ return { yAxis: yAxisWithOffset, series: boundarySeries };
+ },
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return query.label;
+ },
+ yValue(seriesIndex, dataIndex) {
+ const d = this.series[seriesIndex].data[dataIndex];
+ return d && d[1];
+ },
+ yValueFormatted(seriesIndex, dataIndex) {
+ const y = this.yValue(seriesIndex, dataIndex);
+ return isNumber(y) ? y.toFixed(3) : '';
+ },
+ isDatapointAnomaly(dataIndex) {
+ const yVal = this.yValue(METRIC, dataIndex);
+ const yUpper = this.yValue(UPPER, dataIndex);
+ const yLower = this.yValue(LOWER, dataIndex);
+ return (isNumber(yUpper) && yVal > yUpper) || (isNumber(yLower) && yVal < yLower);
+ },
+ makeBoundarySeries(series) {
+ const stackKey = 'anomaly-boundary-series-stack';
+ return {
+ type: 'line',
+ stack: stackKey,
+ lineStyle: {
+ width: 0,
+ color: AREA_COLOR_RGBA, // legend color
+ },
+ color: AREA_COLOR_RGBA, // tooltip color
+ symbol: 'none',
+ ...series,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <monitor-time-series-chart
+ v-bind="$attrs"
+ :graph-data="metricData"
+ :option="chartOptions"
+ :series-config="metricSeriesConfig"
+ >
+ <slot></slot>
+ <template v-slot:tooltipContent="slotProps">
+ <div
+ v-for="(content, seriesIndex) in slotProps.tooltip.content"
+ :key="seriesIndex"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="content.color">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ yValueFormatted(seriesIndex, content.dataIndex) }}
+ </div>
+ </div>
+ </template>
+ </monitor-time-series-chart>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
new file mode 100644
index 00000000000..b8158247e49
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlHeatmap } from '@gitlab/ui/dist/charts';
+import dateformat from 'dateformat';
+import PrometheusHeader from '../shared/prometheus_header.vue';
+import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import { graphDataValidatorForValues } from '../../utils';
+
+export default {
+ components: {
+ GlHeatmap,
+ ResizableChartContainer,
+ PrometheusHeader,
+ },
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ chartData() {
+ return this.queries.result.reduce(
+ (acc, result, i) => [...acc, ...result.values.map((value, j) => [i, j, value[1]])],
+ [],
+ );
+ },
+ xAxisName() {
+ return this.graphData.x_label || '';
+ },
+ yAxisName() {
+ return this.graphData.y_label || '';
+ },
+ xAxisLabels() {
+ return this.queries.result.map(res => Object.values(res.metric)[0]);
+ },
+ yAxisLabels() {
+ return this.result.values.map(val => {
+ const [yLabel] = val;
+
+ return dateformat(new Date(yLabel), 'HH:MM:ss');
+ });
+ },
+ result() {
+ return this.queries.result[0];
+ },
+ queries() {
+ return this.graphData.queries[0];
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph col-12 col-lg-6">
+ <prometheus-header :graph-title="graphData.title" />
+ <resizable-chart-container>
+ <gl-heatmap
+ ref="heatmapChart"
+ v-bind="$attrs"
+ :data-series="chartData"
+ :x-axis-name="xAxisName"
+ :y-axis-name="yAxisName"
+ :x-axis-labels="xAxisLabels"
+ :y-axis-labels="yAxisLabels"
+ :width="containerWidth"
+ />
+ </resizable-chart-container>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 78fe575717a..6a88c8a5ee3 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,17 +1,23 @@
<script>
import { s__, __ } from '~/locale';
-import { GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import _ from 'underscore';
+import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
-import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
-import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import {
+ chartHeight,
+ graphTypes,
+ lineTypes,
+ lineWidths,
+ symbolSizes,
+ dateFormats,
+} from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
-let debouncedResize;
-
export default {
components: {
GlAreaChart,
@@ -22,6 +28,9 @@ export default {
GlLink,
Icon,
},
+ directives: {
+ GlResizeObserverDirective,
+ },
inheritAttrs: false,
props: {
graphData: {
@@ -29,9 +38,15 @@ export default {
required: true,
validator: graphDataValidatorForValues.bind(null, false),
},
- containerWidth: {
- type: Number,
- required: true,
+ option: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ seriesConfig: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
deploymentData: {
type: Array,
@@ -99,29 +114,35 @@ export default {
const lineWidth =
appearance && appearance.line && appearance.line.width
? appearance.line.width
- : undefined;
+ : lineWidths.default;
const areaStyle = {
opacity:
appearance && appearance.area && typeof appearance.area.opacity === 'number'
? appearance.area.opacity
: undefined,
};
-
const series = makeDataSeries(query.result, {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
width: lineWidth,
+ color: this.primaryColor,
},
showSymbol: false,
areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ ...this.seriesConfig,
});
return acc.concat(series);
}, []);
},
+ chartOptionSeries() {
+ return (this.option.series || []).concat(this.scatterSeries ? [this.scatterSeries] : []);
+ },
chartOptions() {
+ const option = _.omit(this.option, 'series');
return {
+ series: this.chartOptionSeries,
xAxis: {
name: __('Time'),
type: 'time',
@@ -138,8 +159,8 @@ export default {
formatter: num => roundOffFloat(num, 3).toString(),
},
},
- series: this.scatterSeries,
dataZoom: [this.dataZoomConfig],
+ ...option,
};
},
dataZoomConfig() {
@@ -147,6 +168,14 @@ export default {
return handleIcon ? { handleIcon } : {};
},
+ /**
+ * This method returns the earliest time value in all series of a chart.
+ * Takes a chart data with data to populate a timeseries.
+ * data should be an array of data points [t, y] where t is a ISO formatted date,
+ * and is sorted by t (time).
+ * @returns {(String|null)} earliest x value from all series, or null when the
+ * chart series data is empty.
+ */
earliestDatapoint() {
return this.chartData.reduce((acc, series) => {
const { data } = series;
@@ -206,21 +235,13 @@ export default {
return `${this.graphData.y_label}`;
},
},
- watch: {
- containerWidth: 'onResize',
- },
mounted() {
const graphTitleEl = this.$refs.graphTitle;
if (graphTitleEl && graphTitleEl.scrollWidth > graphTitleEl.offsetWidth) {
this.showTitleTooltip = true;
}
},
- beforeDestroy() {
- window.removeEventListener('resize', debouncedResize);
- },
created() {
- debouncedResize = debounceByAnimationFrame(this.onResize);
- window.addEventListener('resize', debouncedResize);
this.setSvg('rocket');
this.setSvg('scroll-handle');
},
@@ -241,10 +262,11 @@ export default {
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else {
- const { seriesName, color } = dataPoint;
+ const { seriesName, color, dataIndex } = dataPoint;
const value = yVal.toFixed(3);
this.tooltip.content.push({
name: seriesName,
+ dataIndex,
value,
color,
});
@@ -276,7 +298,7 @@ export default {
</script>
<template>
- <div class="prometheus-graph">
+ <div v-gl-resize-observer-directive="onResize" class="prometheus-graph">
<div class="prometheus-graph-header">
<h5
ref="graphTitle"
@@ -317,23 +339,27 @@ export default {
</template>
<template v-else>
<template slot="tooltipTitle">
- <div class="text-nowrap">
- {{ tooltip.title }}
- </div>
+ <slot name="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </slot>
</template>
<template slot="tooltipContent">
- <div
- v-for="(content, key) in tooltip.content"
- :key="key"
- class="d-flex justify-content-between"
- >
- <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
- {{ content.name }}
- </gl-chart-series-label>
- <div class="prepend-left-32">
- {{ content.value }}
+ <slot name="tooltipContent" :tooltip="tooltip">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
</div>
- </div>
+ </slot>
</template>
</template>
</component>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index b4ea415bb51..26e2c2568c1 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,7 +11,7 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
@@ -22,12 +22,9 @@ import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
-import { sidebarAnimationDuration } from '../constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-let sidebarMutationObserver;
-
export default {
components: {
VueDraggable,
@@ -167,10 +164,10 @@ export default {
data() {
return {
state: 'gettingStarted',
- elWidth: 0,
formIsValid: null,
selectedTimeWindow: {},
isRearrangingPanels: false,
+ hasValidDates: true,
};
},
computed: {
@@ -178,7 +175,7 @@ export default {
return this.customMetricsAvailable && this.customMetricsPath.length;
},
...mapState('monitoringDashboard', [
- 'groups',
+ 'dashboard',
'emptyState',
'showEmptyState',
'environments',
@@ -189,10 +186,15 @@ export default {
'additionalPanelTypesEnabled',
]),
firstDashboard() {
- return this.allDashboards[0] || {};
+ return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
+ ? this.allDashboards[0]
+ : {};
+ },
+ selectedDashboard() {
+ return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
},
selectedDashboardText() {
- return this.currentDashboard || this.firstDashboard.display_name;
+ return this.selectedDashboard.display_name;
},
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
@@ -200,8 +202,13 @@ export default {
addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState;
},
- alertWidgetAvailable() {
- return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint;
+ hasHeaderButtons() {
+ return (
+ this.addingMetricsAvailable ||
+ this.showRearrangePanelsBtn ||
+ this.selectedDashboard.can_edit ||
+ this.externalDashboardUrl.length
+ );
},
},
created() {
@@ -214,11 +221,6 @@ export default {
projectPath: this.projectPath,
});
},
- beforeDestroy() {
- if (sidebarMutationObserver) {
- sidebarMutationObserver.disconnect();
- }
- },
mounted() {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
@@ -235,17 +237,12 @@ export default {
this.selectedTimeWindow = range;
if (!isValidDate(start) || !isValidDate(end)) {
+ this.hasValidDates = false;
this.showInvalidDateError();
} else {
+ this.hasValidDates = true;
this.fetchData(range);
}
-
- sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
- sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
- attributes: true,
- childList: false,
- subtree: false,
- });
}
},
methods: {
@@ -253,43 +250,25 @@ export default {
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
- 'setDashboardEnabled',
+ 'setPanelGroupMetrics',
]),
chartsWithData(charts) {
- if (!this.useDashboardEndpoint) {
- return charts;
- }
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
- csvText(graphData) {
- const chartData = graphData.queries[0].result[0].values;
- const yLabel = graphData.y_label;
- const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
- },
- downloadCsv(graphData) {
- const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
- // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
- // Issue number: https://gitlab.com/gitlab-org/gitlab-foss/issues/63845
- getGraphAlerts(queries) {
- if (!this.allAlerts) return {};
- const metricIdsForChart = queries.map(q => q.metricId);
- return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
- },
- getGraphAlertValues(queries) {
- return Object.values(this.getGraphAlerts(queries));
- },
- showToast() {
- this.$toast.show(__('Link copied'));
- },
- // TODO: END
+ updateMetrics(key, metrics) {
+ this.setPanelGroupMetrics({
+ metrics,
+ key,
+ });
+ },
+ removeMetric(key, metrics, graphIndex) {
+ this.setPanelGroupMetrics({
+ metrics: metrics.filter((v, i) => i !== graphIndex),
+ key,
+ });
+ },
removeGraph(metrics, graphIndex) {
// At present graphs will not be removed, they should removed using the vuex store
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
@@ -306,11 +285,6 @@ export default {
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
- onSidebarMutation() {
- setTimeout(() => {
- this.elWidth = this.$el.clientWidth;
- }, sidebarAnimationDuration);
- },
toggleRearrangingPanels() {
this.isRearrangingPanels = !this.isRearrangingPanels;
},
@@ -389,7 +363,7 @@ export default {
</gl-form-group>
<gl-form-group
- v-if="!showEmptyState"
+ v-if="hasValidDates"
:label="s__('Metrics|Show last')"
label-size="sm"
label-for="monitor-time-window-dropdown"
@@ -403,7 +377,7 @@ export default {
</template>
<gl-form-group
- v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length"
+ v-if="hasHeaderButtons"
label-for="prometheus-graphs-dropdown-buttons"
class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end"
>
@@ -451,6 +425,14 @@ export default {
</gl-modal>
<gl-button
+ v-if="selectedDashboard.can_edit"
+ class="mt-1 js-edit-link"
+ :href="selectedDashboard.project_blob_path"
+ >
+ {{ __('Edit dashboard') }}
+ </gl-button>
+
+ <gl-button
v-if="externalDashboardUrl.length"
class="mt-1 js-external-dashboard-link"
variant="primary"
@@ -468,116 +450,46 @@ export default {
<div v-if="!showEmptyState">
<graph-group
- v-for="(groupData, index) in groups"
+ v-for="(groupData, index) in dashboard.panel_groups"
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
:collapse-group="groupHasData(groupData)"
>
- <template v-if="additionalPanelTypesEnabled">
- <vue-draggable
- :list="groupData.metrics"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
+ <vue-draggable
+ :value="groupData.metrics"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updateMetrics(groupData.key, $event)"
+ >
+ <div
+ v-for="(graphData, graphIndex) in groupData.metrics"
+ :key="`panel-type-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
>
- <div
- v-for="(graphData, graphIndex) in groupData.metrics"
- :key="`panel-type-${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="removeGraph(groupData.metrics, 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"
- :dashboard-width="elWidth"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- />
+ <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="removeGraph(groupData.metrics, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
+ ><icon name="close"
+ /></a>
</div>
- </div>
- </vue-draggable>
- </template>
- <template v-else>
- <monitor-time-series-chart
- v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
- :key="graphIndex"
- class="col-12 col-lg-6 pb-3"
- :graph-data="graphData"
- :deployment-data="deploymentData"
- :thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="elWidth"
- :project-path="projectPath"
- group-id="monitor-time-series-chart"
- >
- <div
- class="d-flex align-items-center"
- :class="alertWidgetAvailable ? 'justify-content-between' : 'justify-content-end'"
- >
- <alert-widget
- v-if="alertWidgetAvailable && graphData"
- :modal-id="`alert-modal-${index}-${graphIndex}`"
+
+ <panel-type
+ :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
+ :graph-data="graphData"
:alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
- @setAlerts="setAlerts"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ :index="`${index}-${graphIndex}`"
/>
- <gl-dropdown
- v-gl-tooltip
- class="ml-2 mr-3"
- toggle-class="btn btn-transparent border-0"
- :right="true"
- :no-caret="true"
- :title="__('More actions')"
- >
- <template slot="button-content">
- <icon name="ellipsis_v" class="text-secondary" />
- </template>
- <gl-dropdown-item
- v-track-event="downloadCSVOptions(graphData.title)"
- :href="downloadCsv(graphData)"
- download="chart_metrics.csv"
- >
- {{ __('Download CSV') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-track-event="
- generateLinkToChartOptions(
- generateLink(groupData.group, graphData.title, graphData.y_label),
- )
- "
- class="js-chart-link"
- :data-clipboard-text="
- generateLink(groupData.group, graphData.title, graphData.y_label)
- "
- @click="showToast"
- >
- {{ __('Generate link to chart') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="alertWidgetAvailable"
- v-gl-modal="`alert-modal-${index}-${graphIndex}`"
- >
- {{ __('Alerts') }}
- </gl-dropdown-item>
- </gl-dropdown>
</div>
- </monitor-time-series-chart>
- </template>
+ </div>
+ </vue-draggable>
</graph-group>
</div>
<empty-state
diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
index 4616a767295..8749019c5cd 100644
--- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue
@@ -55,17 +55,13 @@ export default {
};
},
},
+ watch: {
+ selectedTimeWindow() {
+ this.verifyTimeRange();
+ },
+ },
mounted() {
- const range = getTimeWindow(this.selectedTimeWindow);
- if (range) {
- this.selectedTimeWindowText = this.timeWindows[range];
- } else {
- this.customTime = {
- from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
- to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
- };
- this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
- }
+ this.verifyTimeRange();
},
methods: {
activeTimeWindow(key) {
@@ -87,6 +83,18 @@ export default {
closeDropdown() {
this.$refs.dropdown.hide();
},
+ verifyTimeRange() {
+ const range = getTimeWindow(this.selectedTimeWindow);
+ if (range) {
+ this.selectedTimeWindowText = this.timeWindows[range];
+ } else {
+ this.customTime = {
+ from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
+ to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
+ };
+ this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
index 7857aaa6ecc..f75839c7c6b 100644
--- a/app/assets/javascripts/monitoring/components/embed.vue
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -35,9 +35,9 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['groups', 'metricsWithData']),
+ ...mapState('monitoringDashboard', ['dashboard', 'metricsWithData']),
charts() {
- const groupWithMetrics = this.groups.find(group =>
+ const groupWithMetrics = this.dashboard.panel_groups.find(group =>
group.metrics.find(chart => this.chartHasData(chart)),
) || { metrics: [] };
@@ -78,9 +78,6 @@ export default {
}, sidebarAnimationDuration);
},
setInitialState() {
- this.setFeatureFlags({
- prometheusEndpointEnabled: true,
- });
this.setEndpoints({
dashboardEndpoint: removeParams(['start', 'end'], this.dashboardUrl),
});
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index ee3a2bae79b..3cb6ccb64b1 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -45,7 +45,7 @@ export default {
<div v-if="showPanels" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
- <a role="button" @click="collapse">
+ <a role="button" class="js-graph-group-toggle" @click="collapse">
<icon :size="16" :aria-label="__('Toggle collapse')" :name="caretIcon" />
</a>
</div>
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 1a14d06f4c8..cafb4b0b479 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -11,7 +11,9 @@ import {
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.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 MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
@@ -19,7 +21,7 @@ import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
MonitorSingleStatChart,
- MonitorTimeSeriesChart,
+ MonitorHeatmapChart,
MonitorEmptyChart,
Icon,
GlDropdown,
@@ -40,10 +42,6 @@ export default {
type: Object,
required: true,
},
- dashboardWidth: {
- type: Number,
- required: true,
- },
index: {
type: String,
required: false,
@@ -71,6 +69,12 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
+ monitorChartComponent() {
+ if (this.isPanelType('anomaly-chart')) {
+ return MonitorAnomalyChart;
+ }
+ return MonitorTimeSeriesChart;
+ },
},
methods: {
getGraphAlerts(queries) {
@@ -97,14 +101,19 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
- <monitor-time-series-chart
+ <monitor-heatmap-chart
+ v-else-if="isPanelType('heatmap') && graphDataHasMetrics"
+ :graph-data="graphData"
+ :container-width="dashboardWidth"
+ />
+ <component
+ :is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="dashboardWidth"
- group-id="monitor-area-chart"
+ group-id="panel-type-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -146,6 +155,6 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-time-series-chart>
+ </component>
<monitor-empty-chart v-else :graph-title="graphData.title" />
</template>
diff --git a/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue b/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue
new file mode 100644
index 00000000000..153c8f389db
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/shared/prometheus_header.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+ props: {
+ graphTitle: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphTitle }}</h5>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 2836fe4fc26..1a1fcdd0e66 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -14,13 +14,28 @@ export const graphTypes = {
};
export const symbolSizes = {
+ anomaly: 8,
default: 14,
};
+export const areaOpacityValues = {
+ default: 0.2,
+};
+
+export const colorValues = {
+ primaryColor: '#1f78d1', // $blue-500 (see variables.scss)
+ anomalySymbol: '#db3b21',
+ anomalyAreaColor: '#1f78d1',
+};
+
export const lineTypes = {
default: 'solid',
};
+export const lineWidths = {
+ default: 2,
+};
+
export const timeWindows = {
thirtyMinutes: __('30 minutes'),
threeHours: __('3 hours'),
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 6aa1fb5e9c6..a14145d480b 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -11,13 +11,6 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
- if (gon.features) {
- store.dispatch('monitoringDashboard/setFeatureFlags', {
- prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
- additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
- });
- }
-
const [currentDashboard] = getParameterValues('dashboard');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 2cf34ddb45b..6a8e3cc82f5 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -7,7 +7,7 @@ import { s__, __ } from '../../locale';
const MAX_REQUESTS = 3;
-function backOffRequest(makeRequestCallback) {
+export function backOffRequest(makeRequestCallback) {
let requestCounter = 0;
return backOff((next, stop) => {
makeRequestCallback()
@@ -35,14 +35,6 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
-export const setFeatureFlags = (
- { commit },
- { prometheusEndpointEnabled, additionalPanelTypesEnabled },
-) => {
- commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
- commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
-};
-
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
@@ -79,29 +71,7 @@ export const fetchData = ({ dispatch }, params) => {
dispatch('fetchEnvironmentsData');
};
-export const fetchMetricsData = ({ state, dispatch }, params) => {
- if (state.useDashboardEndpoint) {
- return dispatch('fetchDashboard', params);
- }
-
- dispatch('requestMetricsData');
-
- return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
- .then(resp => resp.data)
- .then(response => {
- if (!response || !response.data || !response.success) {
- dispatch('receiveMetricsDataFailure', null);
- createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
- }
- dispatch('receiveMetricsDataSuccess', response.data);
- })
- .catch(error => {
- dispatch('receiveMetricsDataFailure', error);
- if (state.setShowErrorBanner) {
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
- }
- });
-};
+export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params);
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard');
@@ -111,11 +81,13 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
params.dashboard = state.currentDashboard;
}
- return axios
- .get(state.dashboardEndpoint, { params })
+ return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
.then(response => {
- dispatch('receiveMetricsDashboardSuccess', { response, params });
+ dispatch('receiveMetricsDashboardSuccess', {
+ response,
+ params,
+ });
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
@@ -166,7 +138,7 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
commit(types.REQUEST_METRICS_DATA);
const promises = [];
- state.groups.forEach(group => {
+ state.dashboard.panel_groups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
promises.push(dispatch('fetchPrometheusMetric', { metric, params }));
@@ -221,5 +193,15 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
});
};
+/**
+ * Set a new array of metrics to a panel group
+ * @param {*} data An object containing
+ * - `key` with a unique panel key
+ * - `metrics` with the metrics array
+ */
+export const setPanelGroupMetrics = ({ commit }, data) => {
+ commit(types.SET_PANEL_GROUP_METRICS, data);
+};
+
// 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 9c546427c6e..fa15a2ba800 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -9,10 +9,9 @@ export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCC
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
-export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
-export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
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';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 320b33d3d69..696af5aed75 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
+import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
-import { normalizeMetrics, sortMetrics, normalizeMetric, normalizeQueryResult } from './utils';
+import { normalizeMetrics, normalizeMetric, normalizeQueryResult } from './utils';
const normalizePanel = panel => panel.metrics.map(normalizeMetric);
@@ -10,10 +11,12 @@ export default {
state.showEmptyState = true;
},
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
- state.groups = groupData.map(group => {
+ state.dashboard.panel_groups = groupData.map((group, i) => {
+ const key = `${slugify(group.group || 'default')}-${i}`;
let { metrics = [], panels = [] } = group;
// each panel has metric information that needs to be normalized
+
panels = panels.map(panel => ({
...panel,
metrics: normalizePanel(panel),
@@ -22,24 +25,21 @@ export default {
// for backwards compatibility, and to limit Vue template changes:
// for each group alias panels to metrics
// for each panel alias metrics to queries
- if (state.useDashboardEndpoint) {
- metrics = panels.map(panel => ({
- ...panel,
- queries: panel.metrics,
- }));
- }
+ metrics = panels.map(panel => ({
+ ...panel,
+ queries: panel.metrics,
+ }));
return {
...group,
panels,
- metrics: normalizeMetrics(sortMetrics(metrics)),
+ key,
+ metrics: normalizeMetrics(metrics),
};
});
- if (!state.groups.length) {
+ if (!state.dashboard.panel_groups.length) {
state.emptyState = 'noData';
- } else {
- state.showEmptyState = false;
}
},
[types.RECEIVE_METRICS_DATA_FAILURE](state, error) {
@@ -65,7 +65,7 @@ export default {
state.showEmptyState = false;
- state.groups.forEach(group => {
+ state.dashboard.panel_groups.forEach(group => {
group.metrics.forEach(metric => {
metric.queries.forEach(query => {
if (query.metric_id === metricId) {
@@ -86,9 +86,6 @@ export default {
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
- [types.SET_DASHBOARD_ENABLED](state, enabled) {
- state.useDashboardEndpoint = enabled;
- },
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
},
@@ -97,12 +94,13 @@ export default {
state.emptyState = 'noData';
},
[types.SET_ALL_DASHBOARDS](state, dashboards) {
- state.allDashboards = dashboards;
- },
- [types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
- state.additionalPanelTypesEnabled = enabled;
+ state.allDashboards = dashboards || [];
},
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
+ [types.SET_PANEL_GROUP_METRICS](state, payload) {
+ const panelGroup = state.dashboard.panel_groups.find(pg => payload.key === pg.key);
+ panelGroup.metrics = payload.metrics;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e894e988f6a..87e94311176 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -7,12 +7,12 @@ export default () => ({
environmentsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
- useDashboardEndpoint: false,
- additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
- groups: [],
+ dashboard: {
+ panel_groups: [],
+ },
deploymentData: [],
environments: [],
metricsWithData: [],
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index a19829f0c65..8a396b15a31 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -82,12 +82,6 @@ export const normalizeMetric = (metric = {}) =>
'id',
);
-export const sortMetrics = metrics =>
- _.chain(metrics)
- .sortBy('title')
- .sortBy('weight')
- .value();
-
export const normalizeQueryResult = timeSeries => {
let normalizedResult = {};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 00f188c1d5a..2ae1647011d 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,7 +1,6 @@
import dateformat from 'dateformat';
import { secondsIn, dateTimePickerRegex, dateFormats } from './constants';
-
-const secondsToMilliseconds = seconds => seconds * 1000;
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
export const getTimeDiff = timeWindow => {
const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds
@@ -131,4 +130,20 @@ export const downloadCSVOptions = title => {
return { category, action, label: 'Chart title', property: title };
};
+/**
+ * This function validates the graph data contains exactly 3 queries plus
+ * value validations from graphDataValidatorForValues.
+ * @param {Object} isValues
+ * @param {Object} graphData the graph data response from a prometheus request
+ * @returns {boolean} true if the data is valid
+ */
+export const graphDataValidatorForAnomalyValues = graphData => {
+ const anomalySeriesCount = 3; // metric, upper, lower
+ return (
+ graphData.queries &&
+ graphData.queries.length === anomalySeriesCount &&
+ graphDataValidatorForValues(false, graphData)
+ );
+};
+
export default {};
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index a0ba2193d90..c301c304409 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,12 +1,12 @@
-/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, camelcase */
+/* eslint-disable func-names, consistent-return, camelcase */
import $ from 'jquery';
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import Raphael from './raphael';
-export default (function() {
- function BranchGraph(element1, options1) {
+export default class BranchGraph {
+ constructor(element1, options1) {
this.element = element1;
this.options = options1;
this.scrollTop = this.scrollTop.bind(this);
@@ -28,7 +28,7 @@ export default (function() {
this.load();
}
- BranchGraph.prototype.load = function() {
+ load() {
axios
.get(this.options.url)
.then(({ data }) => {
@@ -37,21 +37,23 @@ export default (function() {
this.buildGraph();
})
.catch(() => __('Error fetching network graph.'));
- };
+ }
- BranchGraph.prototype.prepareData = function(days, commits) {
- var c, ch, cw, j, len, ref;
+ prepareData(days, commits) {
+ let c = 0;
+ let j = 0;
+ let len = 0;
this.days = days;
this.commits = commits;
this.collectParents();
this.graphHeight = $(this.element).height();
this.graphWidth = $(this.element).width();
- ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
- cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
+ const ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
+ const cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
this.r = Raphael(this.element.get(0), cw, ch);
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
- ref = this.commits;
+ const ref = this.commits;
for (j = 0, len = ref.length; j < len; j += 1) {
c = ref[j];
if (c.id in this.parents) {
@@ -61,37 +63,34 @@ export default (function() {
this.markCommit(c);
}
return this.collectColors();
- };
+ }
- BranchGraph.prototype.collectParents = function() {
- var c, j, len, p, ref, results;
- ref = this.commits;
- results = [];
+ collectParents() {
+ let j = 0;
+ let l = 0;
+ let len = 0;
+ let len1 = 0;
+ const ref = this.commits;
+ const results = [];
for (j = 0, len = ref.length; j < len; j += 1) {
- c = ref[j];
+ const c = ref[j];
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
- results.push(
- function() {
- var l, len1, ref1, results1;
- ref1 = c.parents;
- results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l += 1) {
- p = ref1[l];
- this.parents[p[0]] = true;
- results1.push((this.mspace = Math.max(this.mspace, p[1])));
- }
- return results1;
- }.call(this),
- );
+ const ref1 = c.parents;
+ const results1 = [];
+ for (l = 0, len1 = ref1.length; l < len1; l += 1) {
+ const p = ref1[l];
+ this.parents[p[0]] = true;
+ results1.push((this.mspace = Math.max(this.mspace, p[1])));
+ }
+ results.push(results1);
}
return results;
- };
+ }
- BranchGraph.prototype.collectColors = function() {
- var k, results;
- k = 0;
- results = [];
+ collectColors() {
+ let k = 0;
+ const results = [];
while (k < this.mspace) {
this.colors.push(Raphael.getColor(0.8));
// Skipping a few colors in the spectrum to get more contrast between colors
@@ -100,23 +99,24 @@ export default (function() {
results.push((k += 1));
}
return results;
- };
+ }
- BranchGraph.prototype.buildGraph = function() {
- var cuday, cumonth, day, len, mm, ref;
+ buildGraph() {
+ let mm = 0;
+ let len = 0;
+ let cuday = 0;
+ let cumonth = '';
const { r } = this;
- cuday = 0;
- cumonth = '';
r.rect(0, 0, 40, this.barHeight).attr({
fill: '#222',
});
r.rect(40, 0, 30, this.barHeight).attr({
fill: '#444',
});
- ref = this.days;
+ const ref = this.days;
for (mm = 0, len = ref.length; mm < len; mm += 1) {
- day = ref[mm];
+ const day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
// Dates
r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
@@ -138,29 +138,28 @@ export default (function() {
}
this.renderPartialGraph();
return this.bindEvents();
- };
+ }
- BranchGraph.prototype.renderPartialGraph = function() {
- var commit, end, i, isGraphEdge, start, x, y;
- start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
+ renderPartialGraph() {
+ const isGraphEdge = true;
+ let i = 0;
+ let start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
if (start < 0) {
- isGraphEdge = true;
start = 0;
}
- end = start + 40;
+ let end = start + 40;
if (this.commits.length < end) {
- isGraphEdge = true;
end = this.commits.length;
}
if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
i = start;
this.prev_start = start;
while (i < end) {
- commit = this.commits[i];
+ const commit = this.commits[i];
i += 1;
if (commit.hasDrawn !== true) {
- x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
- y = this.offsetY + this.unitTime * commit.time;
+ const x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ const y = this.offsetY + this.unitTime * commit.time;
this.drawDot(x, y, commit);
this.drawLines(x, y, commit);
this.appendLabel(x, y, commit);
@@ -170,70 +169,62 @@ export default (function() {
}
return this.top.toFront();
}
- };
+ }
- BranchGraph.prototype.bindEvents = function() {
+ bindEvents() {
const { element } = this;
- return $(element).scroll(
- (function(_this) {
- return function() {
- return _this.renderPartialGraph();
- };
- })(this),
- );
- };
+ return $(element).scroll(() => this.renderPartialGraph());
+ }
- BranchGraph.prototype.scrollDown = function() {
+ scrollDown() {
this.element.scrollTop(this.element.scrollTop() + 50);
return this.renderPartialGraph();
- };
+ }
- BranchGraph.prototype.scrollUp = function() {
+ scrollUp() {
this.element.scrollTop(this.element.scrollTop() - 50);
return this.renderPartialGraph();
- };
+ }
- BranchGraph.prototype.scrollLeft = function() {
+ scrollLeft() {
this.element.scrollLeft(this.element.scrollLeft() - 50);
return this.renderPartialGraph();
- };
+ }
- BranchGraph.prototype.scrollRight = function() {
+ scrollRight() {
this.element.scrollLeft(this.element.scrollLeft() + 50);
return this.renderPartialGraph();
- };
+ }
- BranchGraph.prototype.scrollBottom = function() {
+ scrollBottom() {
return this.element.scrollTop(this.element.find('svg').height());
- };
+ }
- BranchGraph.prototype.scrollTop = function() {
+ scrollTop() {
return this.element.scrollTop(0);
- };
-
- BranchGraph.prototype.appendLabel = function(x, y, commit) {
- var label, rect, shortrefs, text, textbox;
+ }
+ appendLabel(x, y, commit) {
if (!commit.refs) {
return;
}
const { r } = this;
- shortrefs = commit.refs;
+ let shortrefs = commit.refs;
// Truncate if longer than 15 chars
if (shortrefs.length > 17) {
shortrefs = `${shortrefs.substr(0, 15)}…`;
}
- text = r.text(x + 4, y, shortrefs).attr({
+ const text = r.text(x + 4, y, shortrefs).attr({
'text-anchor': 'start',
font: '10px Monaco, monospace',
fill: '#FFF',
title: commit.refs,
});
- textbox = text.getBBox();
+ const textbox = text.getBBox();
// Create rectangle based on the size of the textbox
- rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
+ const rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
fill: '#000',
'fill-opacity': 0.5,
stroke: 'none',
@@ -244,13 +235,13 @@ export default (function() {
'fill-opacity': 0.5,
stroke: 'none',
});
- label = r.set(rect, text);
+ const label = r.set(rect, text);
label.transform(['t', -rect.getBBox().width - 15, 0]);
// Set text to front
return text.toFront();
- };
+ }
- BranchGraph.prototype.appendAnchor = function(x, y, commit) {
+ appendAnchor(x, y, commit) {
const { r, top, options } = this;
const anchor = r
.circle(x, y, 10)
@@ -270,9 +261,9 @@ export default (function() {
},
);
return top.push(anchor);
- };
+ }
- BranchGraph.prototype.drawDot = function(x, y, commit) {
+ drawDot(x, y, commit) {
const { r } = this;
r.circle(x, y, 3).attr({
fill: this.colors[commit.space],
@@ -293,20 +284,24 @@ export default (function() {
'text-anchor': 'start',
font: '14px Monaco, monospace',
});
- };
+ }
- BranchGraph.prototype.drawLines = function(x, y, commit) {
- var arrow, color, i, len, offset, parent, parentCommit, parentX1, parentX2, parentY, route;
+ drawLines(x, y, commit) {
+ let i = 0;
+ let len = 0;
+ let arrow = '';
+ let offset = [];
+ let color = [];
const { r } = this;
const ref = commit.parents;
const results = [];
for (i = 0, len = ref.length; i < len; i += 1) {
- parent = ref[i];
- parentCommit = this.preparedCommits[parent[0]];
- parentY = this.offsetY + this.unitTime * parentCommit.time;
- parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
- parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+ const parent = ref[i];
+ const parentCommit = this.preparedCommits[parent[0]];
+ const parentY = this.offsetY + this.unitTime * parentCommit.time;
+ const parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
+ const parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
// Set line color
if (parentCommit.space <= commit.space) {
color = this.colors[commit.space];
@@ -325,7 +320,7 @@ export default (function() {
arrow = 'l-5,0,2,4,3,-4,-4,2';
}
// Start point
- route = ['M', x + offset[0], y + offset[1]];
+ const route = ['M', x + offset[0], y + offset[1]];
// Add arrow if not first parent
if (i > 0) {
route.push(arrow);
@@ -344,9 +339,9 @@ export default (function() {
);
}
return results;
- };
+ }
- BranchGraph.prototype.markCommit = function(commit) {
+ markCommit(commit) {
if (commit.id === this.options.commit_id) {
const { r } = this;
const x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
@@ -359,7 +354,5 @@ export default (function() {
// Displayed in the center
return this.element.scrollTop(y - this.graphHeight / 2);
}
- };
-
- return BranchGraph;
-})();
+ }
+}
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 9f9db21d65b..918c6e408a2 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, no-var, one-var, consistent-return, no-return-assign, no-shadow, no-else-return, @gitlab/i18n/no-non-i18n-strings */
+/* eslint-disable func-names, consistent-return, no-return-assign, no-else-return, @gitlab/i18n/no-non-i18n-strings */
import $ from 'jquery';
import RefSelectDropdown from './ref_select_dropdown';
@@ -26,23 +26,22 @@ export default class NewBranchForm {
}
setupRestrictions() {
- var endsWith, invalid, single, startsWith;
- startsWith = {
+ const startsWith = {
pattern: /^(\/|\.)/g,
prefix: "can't start with",
conjunction: 'or',
};
- endsWith = {
+ const endsWith = {
pattern: /(\/|\.|\.lock)$/g,
prefix: "can't end in",
conjunction: 'or',
};
- invalid = {
+ const invalid = {
pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
prefix: "can't contain",
conjunction: ', ',
};
- single = {
+ const single = {
pattern: /^@+$/g,
prefix: "can't be",
conjunction: 'or',
@@ -51,19 +50,17 @@ export default class NewBranchForm {
}
validate() {
- var errorMessage, errors, formatter, unique, validator;
const { indexOf } = [];
this.branchNameError.empty();
- unique = function(values, value) {
+ const unique = function(values, value) {
if (indexOf.call(values, value) === -1) {
values.push(value);
}
return values;
};
- formatter = function(values, restriction) {
- var formatted;
- formatted = values.map(value => {
+ const formatter = function(values, restriction) {
+ const formatted = values.map(value => {
switch (false) {
case !/\s/.test(value):
return 'spaces';
@@ -75,20 +72,17 @@ export default class NewBranchForm {
});
return `${restriction.prefix} ${formatted.join(restriction.conjunction)}`;
};
- validator = (function(_this) {
- return function(errors, restriction) {
- var matched;
- matched = _this.name.val().match(restriction.pattern);
- if (matched) {
- return errors.concat(formatter(matched.reduce(unique, []), restriction));
- } else {
- return errors;
- }
- };
- })(this);
- errors = this.restrictions.reduce(validator, []);
+ const validator = (errors, restriction) => {
+ const matched = this.name.val().match(restriction.pattern);
+ if (matched) {
+ return errors.concat(formatter(matched.reduce(unique, []), restriction));
+ } else {
+ return errors;
+ }
+ };
+ const errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
- errorMessage = $('<span/>').text(errors.join(', '));
+ const errorMessage = $('<span/>').text(errors.join(', '));
return this.branchNameError.append(errorMessage);
}
}
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index b142f212eb0..037be8467cb 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, no-return-assign */
+/* eslint-disable no-return-assign */
export default class NewCommitForm {
constructor(form) {
this.form = form;
@@ -11,8 +11,7 @@ export default class NewCommitForm {
this.renderDestination();
}
renderDestination() {
- var different;
- different = this.branchName.val() !== this.originalBranch.val();
+ const different = this.branchName.val() !== this.originalBranch.val();
if (different) {
this.createMergeRequestContainer.show();
if (!this.wasDifferent) {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 3715a91d599..defa278c089 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,8 +1,8 @@
-/* eslint-disable no-restricted-properties, func-names, no-var, camelcase,
+/* eslint-disable no-restricted-properties, no-var, camelcase,
no-unused-expressions, one-var, default-case,
-consistent-return, no-alert, no-return-assign,
-no-param-reassign, no-else-return, vars-on-top,
-no-shadow, no-useless-escape, class-methods-use-this */
+consistent-return, no-alert, no-param-reassign, no-else-return,
+vars-on-top, no-shadow, no-useless-escape,
+class-methods-use-this */
/* global ResolveService */
@@ -281,14 +281,7 @@ export default class Notes {
if (Notes.interval) {
clearInterval(Notes.interval);
}
- return (Notes.interval = setInterval(
- (function(_this) {
- return function() {
- return _this.refresh();
- };
- })(this),
- this.pollingInterval,
- ));
+ Notes.interval = setInterval(() => this.refresh(), this.pollingInterval);
}
refresh() {
@@ -847,57 +840,52 @@ export default class Notes {
var noteElId, $note;
$note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id');
- $(`.note[id="${noteElId}"]`).each(
- (function() {
- // A same note appears in the "Discussion" and in the "Changes" tab, we have
- // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
- // where $('#noteId') would return only one.
- return function(i, el) {
- var $note, $notes;
- $note = $(el);
- $notes = $note.closest('.discussion-notes');
- const discussionId = $('.notes', $notes).data('discussionId');
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- if (gl.diffNoteApps[noteElId]) {
- gl.diffNoteApps[noteElId].$destroy();
- }
- }
-
- $note.remove();
+ $(`.note[id="${noteElId}"]`).each((i, el) => {
+ // A same note appears in the "Discussion" and in the "Changes" tab, we have
+ // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
+ // where $('#noteId') would return only one.
+ const $note = $(el);
+ const $notes = $note.closest('.discussion-notes');
+ const discussionId = $('.notes', $notes).data('discussionId');
+
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (gl.diffNoteApps[noteElId]) {
+ gl.diffNoteApps[noteElId].$destroy();
+ }
+ }
- // check if this is the last note for this line
- if ($notes.find('.note').length === 0) {
- var notesTr = $notes.closest('tr');
+ $note.remove();
- // "Discussions" tab
- $notes.closest('.timeline-entry').remove();
+ // check if this is the last note for this line
+ if ($notes.find('.note').length === 0) {
+ const notesTr = $notes.closest('tr');
- $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
+ // "Discussions" tab
+ $notes.closest('.timeline-entry').remove();
- // The notes tr can contain multiple lists of notes, like on the parallel diff
- // notesTr does not exist for image diffs
- if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
- const $diffFile = $notes.closest('.diff-file');
- if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
- });
+ $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
- $diffFile[0].dispatchEvent(removeBadgeEvent);
- }
+ // The notes tr can contain multiple lists of notes, like on the parallel diff
+ // notesTr does not exist for image diffs
+ if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
+ const $diffFile = $notes.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
+ },
+ });
- $notes.remove();
- } else if (notesTr.length > 0) {
- notesTr.remove();
- }
+ $diffFile[0].dispatchEvent(removeBadgeEvent);
}
- };
- })(this),
- );
+
+ $notes.remove();
+ } else if (notesTr.length > 0) {
+ notesTr.remove();
+ }
+ }
+ });
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
new file mode 100644
index 00000000000..4c9075912ee
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -0,0 +1,133 @@
+<script>
+import { mapActions } from 'vuex';
+import _ from 'underscore';
+
+import { s__, __, sprintf } from '~/locale';
+import { truncateSha } from '~/lib/utils/text_utility';
+
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import noteEditedText from './note_edited_text.vue';
+import noteHeader from './note_header.vue';
+
+export default {
+ name: 'DiffDiscussionHeader',
+ components: {
+ userAvatarLink,
+ noteEditedText,
+ noteHeader,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ notes() {
+ return this.discussion.notes;
+ },
+ firstNote() {
+ return this.notes[0];
+ },
+ lastNote() {
+ return this.notes[this.notes.length - 1];
+ },
+ author() {
+ return this.firstNote.author;
+ },
+ resolvedText() {
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
+ },
+ lastUpdatedBy() {
+ return this.notes.length > 1 ? this.lastNote.author : null;
+ },
+ lastUpdatedAt() {
+ return this.notes.length > 1 ? this.lastNote.created_at : null;
+ },
+ headerText() {
+ const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
+ const linkEnd = '</a>';
+
+ const { commit_id: commitId } = this.discussion;
+ let commitDisplay = commitId;
+
+ if (commitId) {
+ commitDisplay = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
+ }
+
+ const {
+ for_commit: isForCommit,
+ diff_discussion: isDiffDiscussion,
+ active: isActive,
+ } = this.discussion;
+
+ let text = s__('MergeRequests|started a thread');
+ if (isForCommit) {
+ text = s__(
+ 'MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}',
+ );
+ } else if (isDiffDiscussion && commitId) {
+ text = isActive
+ ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}')
+ : s__(
+ 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitDisplay}%{linkEnd}',
+ );
+ } else if (isDiffDiscussion) {
+ text = isActive
+ ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}')
+ : s__(
+ 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}',
+ );
+ }
+
+ return sprintf(text, { commitDisplay, linkStart, linkEnd }, false);
+ },
+ },
+ methods: {
+ ...mapActions(['toggleDiscussion']),
+ toggleDiscussionHandler() {
+ this.toggleDiscussion({ discussionId: this.discussion.id });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-header note-wrapper">
+ <div v-once class="timeline-icon align-self-start flex-shrink-0">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content w-100">
+ <note-header
+ :author="author"
+ :created-at="firstNote.created_at"
+ :note-id="firstNote.id"
+ :include-toggle="true"
+ :expanded="discussion.expanded"
+ @toggleHandler="toggleDiscussionHandler"
+ >
+ <span v-html="headerText"></span>
+ </note-header>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ <note-edited-text
+ v-else-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ :action-text="__('Last updated')"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 3158e086f6c..e4f09492d9c 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -101,6 +101,7 @@ export default {
<time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
</a>
</template>
+ <slot name="extra-controls"></slot>
<i
class="fa fa-spinner fa-spin editing-spinner"
:aria-label="__('Comment is being updated')"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cb1975a8962..47ec740b63a 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,18 +1,15 @@
<script>
-import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
-import { truncateSha } from '~/lib/utils/text_utility';
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import icon from '~/vue_shared/components/icon.vue';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteHeader from './note_header.vue';
+import diffDiscussionHeader from './diff_discussion_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
-import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import noteable from '../mixins/noteable';
@@ -27,9 +24,8 @@ export default {
components: {
icon,
userAvatarLink,
- noteHeader,
+ diffDiscussionHeader,
noteSignedOutWidget,
- noteEditedText,
noteForm,
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
TimelineEntryItem,
@@ -92,9 +88,6 @@ export default {
currentUser() {
return this.getUserData;
},
- author() {
- return this.firstNote.author;
- },
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
@@ -104,27 +97,6 @@ export default {
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
- lastUpdatedBy() {
- const { notes } = this.discussion;
-
- if (notes.length > 1) {
- return notes[notes.length - 1].author;
- }
-
- return null;
- },
- lastUpdatedAt() {
- const { notes } = this.discussion;
-
- if (notes.length > 1) {
- return notes[notes.length - 1].created_at;
- }
-
- return null;
- },
- resolvedText() {
- return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
- },
shouldShowJumpToNextDiscussion() {
return this.showJumpToNextDiscussion(this.discussionsByDiffOrder ? 'diff' : 'discussion');
},
@@ -150,40 +122,6 @@ export default {
shouldHideDiscussionBody() {
return this.shouldRenderDiffs && !this.isExpanded;
},
- actionText() {
- const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
- const linkEnd = '</a>';
-
- let { commit_id: commitId } = this.discussion;
- if (commitId) {
- commitId = `<span class="commit-sha">${truncateSha(commitId)}</span>`;
- }
-
- const {
- for_commit: isForCommit,
- diff_discussion: isDiffDiscussion,
- active: isActive,
- } = this.discussion;
-
- let text = s__('MergeRequests|started a thread');
- if (isForCommit) {
- text = s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}');
- } else if (isDiffDiscussion && commitId) {
- text = isActive
- ? s__('MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}')
- : s__(
- 'MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}',
- );
- } else if (isDiffDiscussion) {
- text = isActive
- ? s__('MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}')
- : s__(
- 'MergeRequests|started a thread on %{linkStart}an old version of the diff%{linkEnd}',
- );
- }
-
- return sprintf(text, { commitId, linkStart, linkEnd }, false);
- },
diffLine() {
if (this.line) {
return this.line;
@@ -208,16 +146,11 @@ export default {
methods: {
...mapActions([
'saveNote',
- 'toggleDiscussion',
'removePlaceholderNotes',
'toggleResolveNote',
'expandDiscussion',
'removeConvertedDiscussion',
]),
- truncateSha,
- toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.discussion.id });
- },
showReplyForm() {
this.isReplying = true;
},
@@ -311,43 +244,7 @@ export default {
class="discussion js-discussion-container"
data-qa-selector="discussion_content"
>
- <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
- <div v-once class="timeline-icon align-self-start flex-shrink-0">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <div class="timeline-content w-100">
- <note-header
- :author="author"
- :created-at="firstNote.created_at"
- :note-id="firstNote.id"
- :include-toggle="true"
- :expanded="discussion.expanded"
- @toggleHandler="toggleDiscussionHandler"
- >
- <span v-html="actionText"></span>
- </note-header>
- <note-edited-text
- v-if="discussion.resolved"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline"
- />
- <note-edited-text
- v-else-if="lastUpdatedAt"
- :edited-at="lastUpdatedAt"
- :edited-by="lastUpdatedBy"
- action-text="Last updated"
- class-name="discussion-headline-light js-discussion-headline"
- />
- </div>
- </div>
+ <diff-discussion-header v-if="shouldRenderDiffs" :discussion="discussion" />
<div v-if="!shouldHideDiscussionBody" class="discussion-body">
<component
:is="wrapperComponent"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index c6c97489e5e..9d1de4ef8a0 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -122,6 +122,8 @@ export default {
this.toggleAward({ awardName, noteId });
});
}
+
+ window.addEventListener('hashchange', this.handleHashChanged);
},
updated() {
this.$nextTick(() => {
@@ -131,6 +133,7 @@ export default {
},
beforeDestroy() {
this.stopPolling();
+ window.removeEventListener('hashchange', this.handleHashChanged);
},
methods: {
...mapActions([
@@ -138,7 +141,6 @@ export default {
'fetchDiscussions',
'poll',
'toggleAward',
- 'scrollToNoteIfNeeded',
'setNotesData',
'setNoteableData',
'setUserData',
@@ -151,6 +153,13 @@ export default {
'convertToDiscussion',
'stopPolling',
]),
+ handleHashChanged() {
+ const noteId = this.checkLocationHash();
+
+ if (noteId) {
+ this.setTargetNoteHash(getLocationHash());
+ }
+ },
fetchNotes() {
if (this.isFetching) return null;
@@ -194,6 +203,8 @@ export default {
this.expandDiscussion({ discussionId: discussion.id });
}
}
+
+ return noteId;
},
startReplying(discussionId) {
return this.convertToDiscussion(discussionId)
diff --git a/app/assets/javascripts/notes/mixins/description_version_history.js b/app/assets/javascripts/notes/mixins/description_version_history.js
new file mode 100644
index 00000000000..12d80f3faa2
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/description_version_history.js
@@ -0,0 +1,12 @@
+// Placeholder for GitLab FOSS
+// Actual implementation: ee/app/assets/javascripts/notes/mixins/description_version_history.js
+export default {
+ computed: {
+ canSeeDescriptionVersion() {},
+ shouldShowDescriptionVersion() {},
+ descriptionVersionToggleIcon() {},
+ },
+ methods: {
+ toggleDescriptionVersion() {},
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 004035ea1d4..82c291379ec 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -12,6 +12,7 @@ import service from '../services/notes_service';
import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
+import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import { __ } from '~/locale';
import Api from '~/api';
@@ -475,5 +476,20 @@ export const convertToDiscussion = ({ commit }, noteId) =>
export const removeConvertedDiscussion = ({ commit }, noteId) =>
commit(types.REMOVE_CONVERTED_DISCUSSION, noteId);
+export const fetchDescriptionVersion = (_, { endpoint, startingVersion }) => {
+ let requestUrl = endpoint;
+
+ if (startingVersion) {
+ requestUrl = mergeUrlParams({ start_version_id: startingVersion }, requestUrl);
+ }
+
+ return axios
+ .get(requestUrl)
+ .then(res => res.data)
+ .catch(() => {
+ Flash(__('Something went wrong while fetching description changes. Please try again.'));
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index bee6d4f0329..3cdcc7a05b8 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -1,34 +1,9 @@
-import { n__, s__, sprintf } from '~/locale';
import { DESCRIPTION_TYPE } from '../constants';
/**
- * Changes the description from a note, returns 'changed the description n number of times'
- */
-export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => {
- const descriptionNote = Object.assign({}, note);
-
- descriptionNote.note_html = sprintf(
- s__(`MergeRequest|
- %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`),
- {
- paragraphStart: '<p dir="auto">',
- paragraphEnd: '</p>',
- descriptionChangedTimes,
- timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes),
- },
- false,
- );
-
- descriptionNote.times_updated = descriptionChangedTimes;
-
- return descriptionNote;
-};
-
-/**
* Checks the time difference between two notes from their 'created_at' dates
* returns an integer
*/
-
export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => {
const descriptionNoteBegin = new Date(noteBeggining.created_at);
const descriptionNoteEnd = new Date(noteEnd.created_at);
@@ -57,7 +32,6 @@ export const isDescriptionSystemNote = note => note.system && note.note === DESC
export const collapseSystemNotes = notes => {
let lastDescriptionSystemNote = null;
let lastDescriptionSystemNoteIndex = -1;
- let descriptionChangedTimes = 1;
return notes.slice(0).reduce((acc, currentNote) => {
const note = currentNote.notes[0];
@@ -70,32 +44,24 @@ export const collapseSystemNotes = notes => {
} else if (lastDescriptionSystemNote) {
const timeDifferenceMinutes = getTimeDifferenceMinutes(lastDescriptionSystemNote, note);
- // are they less than 10 minutes apart?
- if (timeDifferenceMinutes > 10) {
- // reset counter
- descriptionChangedTimes = 1;
+ // are they less than 10 minutes apart from the same user?
+ if (timeDifferenceMinutes > 10 || note.author.id !== lastDescriptionSystemNote.author.id) {
// update the previous system note
lastDescriptionSystemNote = note;
lastDescriptionSystemNoteIndex = acc.length;
} else {
- // increase counter
- descriptionChangedTimes += 1;
+ // set the first version to fetch grouped system note versions
+ note.start_description_version_id = lastDescriptionSystemNote.description_version_id;
// delete the previous one
acc.splice(lastDescriptionSystemNoteIndex, 1);
- // replace the text of the current system note with the collapsed note.
- currentNote.notes.splice(
- 0,
- 1,
- changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes),
- );
-
// update the previous system note index
lastDescriptionSystemNoteIndex = acc.length;
}
}
}
+
acc.push(currentNote);
return acc;
}, []);
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index d76b1f174fc..d97e24d9e0b 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,3 +1,8 @@
+/* eslint-disable no-new */
import AbuseReports from './abuse_reports';
+import UsersSelect from '~/users_select';
-document.addEventListener('DOMContentLoaded', () => new AbuseReports());
+document.addEventListener('DOMContentLoaded', () => {
+ new AbuseReports();
+ new UsersSelect();
+});
diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js
index 43992938d07..4d04c37caa7 100644
--- a/app/assets/javascripts/pages/admin/clusters/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/index.js
@@ -1,21 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
-import initGkeDropdowns from '~/create_cluster/gke_cluster';
-
-function initGcpSignupCallout() {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
-}
+import initCreateCluster from '~/create_cluster/init_create_cluster';
document.addEventListener('DOMContentLoaded', () => {
- const { page } = document.body.dataset;
- const newClusterViews = [
- 'admin:clusters:new',
- 'admin:clusters:create_gcp',
- 'admin:clusters:create_user',
- ];
-
- if (newClusterViews.indexOf(page) > -1) {
- initGcpSignupCallout();
- initGkeDropdowns();
- }
+ initCreateCluster(document, gon);
});
diff --git a/app/assets/javascripts/pages/groups/index.js b/app/assets/javascripts/pages/groups/index.js
index a33d242908b..4d04c37caa7 100644
--- a/app/assets/javascripts/pages/groups/index.js
+++ b/app/assets/javascripts/pages/groups/index.js
@@ -1,21 +1,5 @@
-import PersistentUserCallout from '~/persistent_user_callout';
-import initGkeDropdowns from '~/create_cluster/gke_cluster';
-
-function initGcpSignupCallout() {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
-}
+import initCreateCluster from '~/create_cluster/init_create_cluster';
document.addEventListener('DOMContentLoaded', () => {
- const { page } = document.body.dataset;
- const newClusterViews = [
- 'groups:clusters:new',
- 'groups:clusters:create_gcp',
- 'groups:clusters:create_user',
- ];
-
- if (newClusterViews.indexOf(page) > -1) {
- initGcpSignupCallout();
- initGkeDropdowns();
- }
+ initCreateCluster(document, gon);
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index dcdee77a8ab..090e1a2bc6d 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,3 +1,4 @@
+import initIssuablesList from '~/issuables_list';
import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
@@ -11,6 +12,8 @@ document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
+ initIssuablesList();
+
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
new file mode 100644
index 00000000000..1d68ccd724d
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+const rootUrl = gon.relative_url_root;
+
+export default function fetchGroupPathAvailability(groupPath) {
+ return axios.get(`${rootUrl}/users/${groupPath}/suggests`);
+}
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
new file mode 100644
index 00000000000..2021ad117e8
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -0,0 +1,91 @@
+import InputValidator from '~/validators/input_validator';
+
+import _ from 'underscore';
+import fetchGroupPathAvailability from './fetch_group_path_availability';
+import flash from '~/flash';
+import { __ } from '~/locale';
+
+const debounceTimeoutDuration = 1000;
+const invalidInputClass = 'gl-field-error-outline';
+const successInputClass = 'gl-field-success-outline';
+const successMessageSelector = '.validation-success';
+const pendingMessageSelector = '.validation-pending';
+const unavailableMessageSelector = '.validation-error';
+const suggestionsMessageSelector = '.gl-path-suggestions';
+
+export default class GroupPathValidator extends InputValidator {
+ constructor(opts = {}) {
+ super();
+
+ const container = opts.container || '';
+ const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`);
+
+ this.debounceValidateInput = _.debounce(inputDomElement => {
+ GroupPathValidator.validateGroupPathInput(inputDomElement);
+ }, debounceTimeoutDuration);
+
+ validateElements.forEach(element =>
+ element.addEventListener('input', this.eventHandler.bind(this)),
+ );
+ }
+
+ eventHandler(event) {
+ const inputDomElement = event.target;
+
+ GroupPathValidator.resetInputState(inputDomElement);
+ this.debounceValidateInput(inputDomElement);
+ }
+
+ static validateGroupPathInput(inputDomElement) {
+ const groupPath = inputDomElement.value;
+
+ if (inputDomElement.checkValidity() && groupPath.length > 0) {
+ GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
+
+ fetchGroupPathAvailability(groupPath)
+ .then(({ data }) => data)
+ .then(data => {
+ GroupPathValidator.setInputState(inputDomElement, !data.exists);
+ GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector, false);
+ GroupPathValidator.setMessageVisibility(
+ inputDomElement,
+ data.exists ? unavailableMessageSelector : successMessageSelector,
+ );
+
+ if (data.exists) {
+ GroupPathValidator.showSuggestions(inputDomElement, data.suggests);
+ }
+ })
+ .catch(() => flash(__('An error occurred while validating group path')));
+ }
+ }
+
+ static showSuggestions(inputDomElement, suggestions) {
+ const messageElement = inputDomElement.parentElement.parentElement.querySelector(
+ suggestionsMessageSelector,
+ );
+ const textSuggestions = suggestions && suggestions.length > 0 ? suggestions.join(', ') : 'none';
+ messageElement.textContent = textSuggestions;
+ }
+
+ static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) {
+ const messageElement = inputDomElement.parentElement.parentElement.querySelector(
+ messageSelector,
+ );
+ messageElement.classList.toggle('hide', !isVisible);
+ }
+
+ static setInputState(inputDomElement, success = true) {
+ inputDomElement.classList.toggle(successInputClass, success);
+ inputDomElement.classList.toggle(invalidInputClass, !success);
+ }
+
+ static resetInputState(inputDomElement) {
+ GroupPathValidator.setMessageVisibility(inputDomElement, successMessageSelector, false);
+ GroupPathValidator.setMessageVisibility(inputDomElement, unavailableMessageSelector, false);
+
+ if (inputDomElement.checkValidity()) {
+ inputDomElement.classList.remove(successInputClass, invalidInputClass);
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 57b53eb9e5d..0710fefe70c 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,8 +1,14 @@
+import $ from 'jquery';
import BindInOut from '~/behaviors/bind_in_out';
import Group from '~/group';
import initAvatarPicker from '~/avatar_picker';
+import GroupPathValidator from './group_path_validator';
document.addEventListener('DOMContentLoaded', () => {
+ const parentId = $('#group_parent_id');
+ if (!parentId.val()) {
+ new GroupPathValidator(); // eslint-disable-line no-new
+ }
BindInOut.initAll();
new Group(); // eslint-disable-line no-new
initAvatarPicker();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 84e5bb3c46e..aee67899ca2 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -3,6 +3,7 @@ import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_sta
import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob';
import GpgBadges from '~/gpg_badges';
+import '~/sourcegraph/load';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/clusters/new/index.js b/app/assets/javascripts/pages/projects/clusters/new/index.js
deleted file mode 100644
index 14d5ab21555..00000000000
--- a/app/assets/javascripts/pages/projects/clusters/new/index.js
+++ /dev/null
@@ -1,13 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- if (gon.features.createEksClusters) {
- import(/* webpackChunkName: 'eks_cluster' */ '~/create_cluster/eks_cluster')
- .then(({ default: initCreateEKSCluster }) => {
- const el = document.querySelector('.js-create-eks-cluster-form-container');
-
- if (el) {
- initCreateEKSCluster(el);
- }
- })
- .catch(() => {});
- }
-});
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index f091c01fc98..397f9faf6fe 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,5 +1,5 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-import initGkeNamespace from '~/projects/gke_cluster_namespace';
+import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 5aa4734244e..0eb6f231839 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -9,6 +9,7 @@ import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
+import '~/sourcegraph/load';
document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar');
diff --git a/app/assets/javascripts/pages/projects/error_tracking/details/index.js b/app/assets/javascripts/pages/projects/error_tracking/details/index.js
new file mode 100644
index 00000000000..25d1c744e1b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/error_tracking/details/index.js
@@ -0,0 +1,5 @@
+import ErrorTrackingDetails from '~/error_tracking/details';
+
+document.addEventListener('DOMContentLoaded', () => {
+ ErrorTrackingDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/error_tracking/index.js b/app/assets/javascripts/pages/projects/error_tracking/index.js
deleted file mode 100644
index 5a8fe137e9a..00000000000
--- a/app/assets/javascripts/pages/projects/error_tracking/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import ErrorTracking from '~/error_tracking';
-
-document.addEventListener('DOMContentLoaded', () => {
- ErrorTracking();
-});
diff --git a/app/assets/javascripts/pages/projects/error_tracking/index/index.js b/app/assets/javascripts/pages/projects/error_tracking/index/index.js
new file mode 100644
index 00000000000..ead81cd5d2d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/error_tracking/index/index.js
@@ -0,0 +1,5 @@
+import ErrorTrackingList from '~/error_tracking/list';
+
+document.addEventListener('DOMContentLoaded', () => {
+ ErrorTrackingList();
+});
diff --git a/app/assets/javascripts/pages/projects/graphs/show/index.js b/app/assets/javascripts/pages/projects/graphs/show/index.js
index f79c386b59e..09d9c78c446 100644
--- a/app/assets/javascripts/pages/projects/graphs/show/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/show/index.js
@@ -1,25 +1,3 @@
-import $ from 'jquery';
-import flash from '~/flash';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
-import ContributorsStatGraph from './stat_graph_contributors';
+import initContributorsGraphs from '~/contributors';
-document.addEventListener('DOMContentLoaded', () => {
- const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
-
- axios
- .get(url)
- .then(({ data }) => {
- const graph = new ContributorsStatGraph();
- graph.init(data);
-
- $('#brush_change').change(() => {
- graph.change_date_header();
- graph.redraw_authors();
- });
-
- $('.stat-graph').fadeIn();
- $('.loading-graph').hide();
- })
- .catch(() => flash(__('Error fetching contributors data.')));
-});
+document.addEventListener('DOMContentLoaded', initContributorsGraphs);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
deleted file mode 100644
index 5b873e6b909..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign */
-
-import $ from 'jquery';
-import _ from 'underscore';
-import { n__, s__, createDateTimeFormat, sprintf } from '~/locale';
-import {
- ContributorsGraph,
- ContributorsAuthorGraph,
- ContributorsMasterGraph,
-} from './stat_graph_contributors_graph';
-import ContributorsStatGraphUtil from './stat_graph_contributors_util';
-
-export default (function() {
- function ContributorsStatGraph() {
- this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
- }
-
- ContributorsStatGraph.prototype.init = function(log) {
- var author_commits, total_commits;
- this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
- this.set_current_field('commits');
- total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
- this.add_master_graph(total_commits);
- this.add_authors_graph(author_commits);
- return this.change_date_header();
- };
-
- ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
- this.master_graph = new ContributorsMasterGraph(total_data);
- return this.master_graph.draw();
- };
-
- ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
- var limited_author_data;
- this.authors = [];
- limited_author_data = author_data.slice(0, 100);
- return _.each(
- limited_author_data,
- (function(_this) {
- return function(d) {
- var author_graph, author_header;
- author_header = _this.create_author_header(d);
- $('.contributors-list').append(author_header);
-
- author_graph = new ContributorsAuthorGraph(d.dates);
- _this.authors[d.author_name] = author_graph;
- return author_graph.draw();
- };
- })(this),
- );
- };
-
- ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
- var commits;
- commits = $('<span/>', {
- class: 'graph-author-commits-count',
- });
- commits.text(n__('%d commit', '%d commits', author.commits));
- return $('<span/>').append(commits);
- };
-
- ContributorsStatGraph.prototype.create_author_header = function(author) {
- var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
- list_item = $('<li/>', {
- class: 'person',
- style: 'display: block;',
- });
- author_name = $(`<h4>${author.author_name}</h4>`);
- author_email = $(`<p class="graph-author-email">${author.author_email}</p>`);
- author_commit_info_span = $('<span/>', {
- class: 'commits',
- });
- author_commit_info = this.format_author_commit_info(author);
- author_commit_info_span.html(author_commit_info);
- list_item.append(author_name);
- list_item.append(author_email);
- list_item.append(author_commit_info_span);
- return list_item;
- };
-
- ContributorsStatGraph.prototype.redraw_master = function() {
- var total_data;
- total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
- this.master_graph.set_data(total_data);
- return this.master_graph.redraw();
- };
-
- ContributorsStatGraph.prototype.redraw_authors = function() {
- $('ol').html('');
-
- const { x_domain } = ContributorsGraph.prototype;
- const author_commits = ContributorsStatGraphUtil.get_author_data(
- this.parsed_log,
- this.field,
- x_domain,
- );
-
- return _.each(
- author_commits,
- (function(_this) {
- return function(d) {
- _this.redraw_author_commit_info(d);
- if (_this.authors[d.author_name] != null) {
- $(_this.authors[d.author_name].list_item).appendTo('ol');
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
- }
- return '';
- };
- })(this),
- );
- };
-
- ContributorsStatGraph.prototype.set_current_field = function(field) {
- return (this.field = field);
- };
-
- ContributorsStatGraph.prototype.change_date_header = function() {
- const { x_domain } = ContributorsGraph.prototype;
- const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), {
- startDate: this.dateFormat.format(new Date(x_domain[0])),
- endDate: this.dateFormat.format(new Date(x_domain[1])),
- });
- return $('#date_header').text(formattedDateRange);
- };
-
- ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item, $author;
- $author = this.authors[author.author_name];
- if ($author != null) {
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find('span').html(author_commit_info);
- }
- return '';
- };
-
- return ContributorsStatGraph;
-})();
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
deleted file mode 100644
index 86794800f87..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_graph.js
+++ /dev/null
@@ -1,379 +0,0 @@
-/* eslint-disable func-names, no-restricted-syntax, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, no-return-assign, no-else-return, no-shadow */
-
-import $ from 'jquery';
-import _ from 'underscore';
-import { extent, max } from 'd3-array';
-import { select, event as d3Event } from 'd3-selection';
-import { scaleTime, scaleLinear } from 'd3-scale';
-import { axisLeft, axisBottom } from 'd3-axis';
-import { area } from 'd3-shape';
-import { brushX } from 'd3-brush';
-import { timeParse } from 'd3-time-format';
-import { dateTickFormat } from '~/lib/utils/tick_formats';
-
-const d3 = {
- extent,
- max,
- select,
- scaleTime,
- scaleLinear,
- axisLeft,
- axisBottom,
- area,
- brushX,
- timeParse,
-};
-
-const hasProp = {}.hasOwnProperty;
-const extend = function(child, parent) {
- for (const key in parent) {
- if (hasProp.call(parent, key)) child[key] = parent[key];
- }
- function ctor() {
- this.constructor = child;
- }
- ctor.prototype = parent.prototype;
- child.prototype = new ctor();
- child.__super__ = parent.prototype;
- return child;
-};
-
-export const ContributorsGraph = (function() {
- function ContributorsGraph() {}
-
- ContributorsGraph.prototype.MARGIN = {
- top: 20,
- right: 10,
- bottom: 30,
- left: 40,
- };
-
- ContributorsGraph.prototype.x_domain = null;
-
- ContributorsGraph.prototype.y_domain = null;
-
- ContributorsGraph.prototype.dates = [];
-
- ContributorsGraph.prototype.determine_width = function(baseWidth, $parentElement) {
- const parentPaddingWidth =
- parseFloat($parentElement.css('padding-left')) +
- parseFloat($parentElement.css('padding-right'));
- const marginWidth = this.MARGIN.left + this.MARGIN.right;
- return baseWidth - parentPaddingWidth - marginWidth;
- };
-
- ContributorsGraph.set_x_domain = function(data) {
- return (ContributorsGraph.prototype.x_domain = data);
- };
-
- ContributorsGraph.set_y_domain = function(data) {
- return (ContributorsGraph.prototype.y_domain = [
- 0,
- d3.max(data, d => (d.commits = d.commits || d.additions || d.deletions)),
- ]);
- };
-
- ContributorsGraph.init_x_domain = function(data) {
- return (ContributorsGraph.prototype.x_domain = d3.extent(data, d => d.date));
- };
-
- ContributorsGraph.init_y_domain = function(data) {
- return (ContributorsGraph.prototype.y_domain = [
- 0,
- d3.max(data, d => (d.commits = d.commits || d.additions || d.deletions)),
- ]);
- };
-
- ContributorsGraph.init_domain = function(data) {
- ContributorsGraph.init_x_domain(data);
- return ContributorsGraph.init_y_domain(data);
- };
-
- ContributorsGraph.set_dates = function(data) {
- return (ContributorsGraph.prototype.dates = data);
- };
-
- ContributorsGraph.prototype.set_x_domain = function() {
- return this.x.domain(this.x_domain);
- };
-
- ContributorsGraph.prototype.set_y_domain = function() {
- return this.y.domain(this.y_domain);
- };
-
- ContributorsGraph.prototype.set_domain = function() {
- this.set_x_domain();
- return this.set_y_domain();
- };
-
- ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3
- .scaleTime()
- .range([0, width])
- .clamp(true);
- return (this.y = d3
- .scaleLinear()
- .range([height, 0])
- .nice());
- };
-
- ContributorsGraph.prototype.draw_x_axis = function() {
- return this.svg
- .append('g')
- .attr('class', 'x axis')
- .attr('transform', `translate(0, ${this.height})`)
- .call(this.x_axis);
- };
-
- ContributorsGraph.prototype.draw_y_axis = function() {
- return this.svg
- .append('g')
- .attr('class', 'y axis')
- .call(this.y_axis);
- };
-
- ContributorsGraph.prototype.set_data = function(data) {
- return (this.data = data);
- };
-
- return ContributorsGraph;
-})();
-
-export const ContributorsMasterGraph = (function(superClass) {
- extend(ContributorsMasterGraph, superClass);
-
- function ContributorsMasterGraph(data1) {
- const $parentElement = $('#contributors-master');
-
- this.data = data1;
- this.update_content = this.update_content.bind(this);
- this.width = this.determine_width($('.js-graphs-show').width(), $parentElement);
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.brush = null;
- this.x_max_domain = null;
- }
-
- ContributorsMasterGraph.prototype.process_dates = function(data) {
- const dates = this.get_dates(data);
- this.parse_dates(data);
- return ContributorsGraph.set_dates(dates);
- };
-
- ContributorsMasterGraph.prototype.get_dates = function(data) {
- return _.pluck(data, 'date');
- };
-
- ContributorsMasterGraph.prototype.parse_dates = function(data) {
- const parseDate = d3.timeParse('%Y-%m-%d');
- return data.forEach(d => (d.date = parseDate(d.date)));
- };
-
- ContributorsMasterGraph.prototype.create_scale = function() {
- return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3
- .axisBottom()
- .scale(this.x)
- .tickFormat(dateTickFormat);
- return (this.y_axis = d3
- .axisLeft()
- .scale(this.y)
- .ticks(5));
- };
-
- ContributorsMasterGraph.prototype.create_svg = function() {
- this.svg = d3
- .select('#contributors-master')
- .append('svg')
- .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
- .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr('class', 'tint-box')
- .append('g')
- .attr('transform', `translate(${this.MARGIN.left},${this.MARGIN.top})`);
- return this.svg;
- };
-
- ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return (this.area = d3
- .area()
- .x(d => x(d.date))
- .y0(this.height)
- .y1(d => {
- d.commits = d.commits || d.additions || d.deletions;
- return y(d.commits);
- }));
- };
-
- ContributorsMasterGraph.prototype.create_brush = function() {
- return (this.brush = d3
- .brushX(this.x)
- .extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]])
- .on('end', this.update_content));
- };
-
- ContributorsMasterGraph.prototype.draw_path = function(data) {
- return this.svg
- .append('path')
- .datum(data)
- .attr('class', 'area')
- .attr('d', this.area);
- };
-
- ContributorsMasterGraph.prototype.add_brush = function() {
- return this.svg
- .append('g')
- .attr('class', 'selection')
- .call(this.brush)
- .selectAll('rect')
- .attr('height', this.height);
- };
-
- ContributorsMasterGraph.prototype.update_content = function() {
- // d3Event.selection replaces the function brush.empty() calls
- if (d3Event.selection != null) {
- ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
- } else {
- ContributorsGraph.set_x_domain(this.x_max_domain);
- }
- return $('#brush_change').trigger('change');
- };
-
- ContributorsMasterGraph.prototype.draw = function() {
- this.process_dates(this.data);
- this.create_scale();
- this.create_axes();
- ContributorsGraph.init_domain(this.data);
- this.x_max_domain = this.x_domain;
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.create_brush();
- this.draw_path(this.data);
- this.draw_x_axis();
- this.draw_y_axis();
- return this.add_brush();
- };
-
- ContributorsMasterGraph.prototype.redraw = function() {
- this.process_dates(this.data);
- ContributorsGraph.set_y_domain(this.data);
- this.set_y_domain();
- this.svg.select('path').datum(this.data);
- this.svg.select('path').attr('d', this.area);
- return this.svg.select('.y.axis').call(this.y_axis);
- };
-
- return ContributorsMasterGraph;
-})(ContributorsGraph);
-
-export const ContributorsAuthorGraph = (function(superClass) {
- extend(ContributorsAuthorGraph, superClass);
-
- function ContributorsAuthorGraph(data1) {
- const $parentElements = $('.person');
-
- this.data = data1;
- // Don't split graph size in half for mobile devices.
- if ($(window).width() < 790) {
- this.width = this.determine_width($('.js-graphs-show').width(), $parentElements);
- } else {
- this.width = this.determine_width($('.js-graphs-show').width() / 2, $parentElements);
- }
- this.height = 200;
- this.x = null;
- this.y = null;
- this.x_axis = null;
- this.y_axis = null;
- this.area = null;
- this.svg = null;
- this.list_item = null;
- }
-
- ContributorsAuthorGraph.prototype.create_scale = function() {
- return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
- };
-
- ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3
- .axisBottom()
- .scale(this.x)
- .ticks(8)
- .tickFormat(dateTickFormat);
- return (this.y_axis = d3
- .axisLeft()
- .scale(this.y)
- .ticks(5));
- };
-
- ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return (this.area = d3
- .area()
- .x(d => {
- const parseDate = d3.timeParse('%Y-%m-%d');
- return x(parseDate(d));
- })
- .y0(this.height)
- .y1(
- (function(_this) {
- return function(d) {
- if (_this.data[d] != null) {
- return y(_this.data[d]);
- } else {
- return y(0);
- }
- };
- })(this),
- ));
- };
-
- ContributorsAuthorGraph.prototype.create_svg = function() {
- const persons = document.querySelectorAll('.person');
- this.list_item = persons[persons.length - 1];
- this.svg = d3
- .select(this.list_item)
- .append('svg')
- .attr('width', this.width + this.MARGIN.left + this.MARGIN.right)
- .attr('height', this.height + this.MARGIN.top + this.MARGIN.bottom)
- .attr('class', 'spark')
- .append('g')
- .attr('transform', `translate(${this.MARGIN.left},${this.MARGIN.top})`);
- return this.svg;
- };
-
- ContributorsAuthorGraph.prototype.draw_path = function(data) {
- return this.svg
- .append('path')
- .datum(data)
- .attr('class', 'area-contributor')
- .attr('d', this.area);
- };
-
- ContributorsAuthorGraph.prototype.draw = function() {
- this.create_scale();
- this.create_axes();
- this.set_domain();
- this.create_area(this.x, this.y);
- this.create_svg();
- this.draw_path(this.dates);
- this.draw_x_axis();
- return this.draw_y_axis();
- };
-
- ContributorsAuthorGraph.prototype.redraw = function() {
- this.set_domain();
- this.svg.select('path').datum(this.dates);
- this.svg.select('path').attr('d', this.area);
- this.svg.select('.x.axis').call(this.x_axis);
- return this.svg.select('.y.axis').call(this.y_axis);
- };
-
- return ContributorsAuthorGraph;
-})(ContributorsGraph);
diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
deleted file mode 100644
index a89a13fe37a..00000000000
--- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign, consistent-return, no-cond-assign, no-else-return */
-import _ from 'underscore';
-
-export default {
- parse_log(log) {
- var by_author, by_email, data, entry, i, len, total, normalized_email;
- total = {};
- by_author = {};
- by_email = {};
- for (i = 0, len = log.length; i < len; i += 1) {
- entry = log[i];
- if (total[entry.date] == null) {
- this.add_date(entry.date, total);
- }
- normalized_email = entry.author_email.toLowerCase();
- data = by_author[entry.author_name] || by_email[normalized_email];
- if (data == null) {
- data = this.add_author(entry, by_author, by_email);
- }
- if (!data[entry.date]) {
- this.add_date(entry.date, data);
- }
- this.store_data(entry, total[entry.date], data[entry.date]);
- }
- total = _.toArray(total);
- by_author = _.toArray(by_author);
- return {
- total,
- by_author,
- };
- },
- add_date(date, collection) {
- collection[date] = {};
- return (collection[date].date = date);
- },
- add_author(author, by_author, by_email) {
- var data, normalized_email;
- data = {};
- data.author_name = author.author_name;
- data.author_email = author.author_email;
- normalized_email = author.author_email.toLowerCase();
- by_author[author.author_name] = data;
- by_email[normalized_email] = data;
- return data;
- },
- store_data(entry, total, by_author) {
- this.store_commits(total, by_author);
- this.store_additions(entry, total, by_author);
- return this.store_deletions(entry, total, by_author);
- },
- store_commits(total, by_author) {
- this.add(total, 'commits', 1);
- return this.add(by_author, 'commits', 1);
- },
- add(collection, field, value) {
- if (collection[field] == null) {
- collection[field] = 0;
- }
- return (collection[field] += value);
- },
- store_additions(entry, total, by_author) {
- if (entry.additions == null) {
- entry.additions = 0;
- }
- this.add(total, 'additions', entry.additions);
- return this.add(by_author, 'additions', entry.additions);
- },
- store_deletions(entry, total, by_author) {
- if (entry.deletions == null) {
- entry.deletions = 0;
- }
- this.add(total, 'deletions', entry.deletions);
- return this.add(by_author, 'deletions', entry.deletions);
- },
- get_total_data(parsed_log, field) {
- var log, total_data;
- log = parsed_log.total;
- total_data = this.pick_field(log, field);
- return _.sortBy(total_data, d => d.date);
- },
- pick_field(log, field) {
- var total_data;
- total_data = [];
- _.each(log, d => total_data.push(_.pick(d, [field, 'date'])));
- return total_data;
- },
- get_author_data(parsed_log, field, date_range) {
- var author_data, log;
- if (date_range == null) {
- date_range = null;
- }
- log = parsed_log.by_author;
- author_data = [];
- _.each(
- log,
- (function(_this) {
- return function(log_entry) {
- var parsed_log_entry;
- parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
- if (!_.isEmpty(parsed_log_entry.dates)) {
- return author_data.push(parsed_log_entry);
- }
- };
- })(this),
- );
- return _.sortBy(author_data, d => d[field]).reverse();
- },
- parse_log_entry(log_entry, field, date_range) {
- var parsed_entry;
- parsed_entry = {};
-
- parsed_entry.author_name = log_entry.author_name;
- parsed_entry.author_email = log_entry.author_email;
- parsed_entry.dates = {};
-
- parsed_entry.commits = 0;
- parsed_entry.additions = 0;
- parsed_entry.deletions = 0;
-
- _.each(
- _.omit(log_entry, 'author_name', 'author_email'),
- (function(_this) {
- return function(value) {
- if (_this.in_range(value.date, date_range)) {
- parsed_entry.dates[value.date] = value[field];
- parsed_entry.commits += value.commits;
- parsed_entry.additions += value.additions;
- return (parsed_entry.deletions += value.deletions);
- }
- };
- })(this),
- );
- return parsed_entry;
- },
- in_range(date, date_range) {
- var ref;
- if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
- return true;
- } else {
- return false;
- }
- },
-};
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 196798a9076..190d0806c28 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,24 +1,9 @@
-import initGkeDropdowns from '~/create_cluster/gke_cluster';
-import initGkeNamespace from '~/projects/gke_cluster_namespace';
-import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
+import initCreateCluster from '~/create_cluster/init_create_cluster';
document.addEventListener('DOMContentLoaded', () => {
- const { page } = document.body.dataset;
- const newClusterViews = [
- 'projects:clusters:new',
- 'projects:clusters:create_gcp',
- 'projects:clusters:create_user',
- ];
-
- if (newClusterViews.indexOf(page) > -1) {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
-
- initGkeDropdowns();
- initGkeNamespace();
- }
+ initCreateCluster(document, gon);
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index fa1de1f13cb..16034313af2 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -5,6 +5,7 @@ import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
+import initSourcegraph from '~/sourcegraph';
import initWidget from '../../../vue_merge_request_widget';
export default function() {
@@ -19,4 +20,5 @@ export default function() {
handleLocationHash();
howToMerge();
initWidget();
+ initSourcegraph();
}
diff --git a/app/assets/javascripts/pages/projects/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index 43417fa9702..5f2014f1631 100644
--- a/app/assets/javascripts/pages/projects/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -1,22 +1,19 @@
-/* eslint-disable func-names, no-var */
-
import $ from 'jquery';
import BranchGraph from '../../../network/branch_graph';
-export default (function() {
- function Network(opts) {
- var vph;
- $('#filter_ref').click(function() {
- return $(this)
- .closest('form')
- .submit();
- });
- this.branch_graph = new BranchGraph($('.network-graph'), opts);
- vph = $(window).height() - 250;
- $('.network-graph').css({
- height: `${vph}px`,
- });
+const vph = $(window).height() - 250;
+
+export default class Network {
+ constructor(opts) {
+ this.opts = opts;
+ this.filter_ref = $('#filter_ref');
+ this.network_graph = $('.network-graph');
+ this.filter_ref.click(() => this.submit());
+ this.branch_graph = new BranchGraph(this.network_graph, this.opts);
+ this.network_graph.css({ height: `${vph}px` });
}
- return Network;
-})();
+ submit() {
+ return this.filter_ref.closest('form').submit();
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/pages_domains/form.js b/app/assets/javascripts/pages/projects/pages_domains/form.js
index cef8e92610c..ae5368179b1 100644
--- a/app/assets/javascripts/pages/projects/pages_domains/form.js
+++ b/app/assets/javascripts/pages/projects/pages_domains/form.js
@@ -1,17 +1,23 @@
import setupToggleButtons from '~/toggle_buttons';
+function updateVisibility(selector, isVisible) {
+ Array.from(document.querySelectorAll(selector)).forEach(el => {
+ if (isVisible) {
+ el.classList.remove('d-none');
+ } else {
+ el.classList.add('d-none');
+ }
+ });
+}
+
export default () => {
const toggleContainer = document.querySelector('.js-auto-ssl-toggle-container');
if (toggleContainer) {
const onToggleButtonClicked = isAutoSslEnabled => {
- Array.from(document.querySelectorAll('.js-shown-unless-auto-ssl')).forEach(el => {
- if (isAutoSslEnabled) {
- el.classList.add('d-none');
- } else {
- el.classList.remove('d-none');
- }
- });
+ updateVisibility('.js-shown-unless-auto-ssl', !isAutoSslEnabled);
+
+ updateVisibility('.js-shown-if-auto-ssl', isAutoSslEnabled);
Array.from(document.querySelectorAll('.js-enabled-unless-auto-ssl')).forEach(el => {
if (isAutoSslEnabled) {
diff --git a/app/assets/javascripts/pages/projects/pipelines/test_report/index.js b/app/assets/javascripts/pages/projects/pipelines/test_report/index.js
new file mode 100644
index 00000000000..7e69983c2ed
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/test_report/index.js
@@ -0,0 +1,2 @@
+// /test_report is an alias for show
+import '../show/index';
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 435e8705803..01acfca158f 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -40,11 +40,6 @@ export default class Project {
$label.text(activeText);
});
- $('#modal-geo-info').data({
- cloneUrlSecondary: $this.attr('href'),
- cloneUrlPrimary: $this.data('primaryUrl') || '',
- });
-
if (mobileCloneField) {
mobileCloneField.dataset.clipboardText = url;
} else {
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 98e19705976..a32c188909c 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,9 +1,11 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
+import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
+ mountGrafanaIntegration();
initSettingsPanels();
});
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 89cac42abae..4802cc2ad25 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,7 +1,6 @@
<script>
-/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
@@ -13,7 +12,7 @@ import {
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
-const PAGE_FEATURE_ACCESS_LEVEL = __('Everyone');
+const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
export default {
components: {
@@ -207,7 +206,10 @@ export default {
<template>
<div>
<div class="project-visibility-setting">
- <project-setting-row :help-path="visibilityHelpPath" label="Project visibility">
+ <project-setting-row
+ :help-path="visibilityHelpPath"
+ :label="s__('ProjectSettings|Project visibility')"
+ >
<div class="project-feature-controls">
<div class="select-wrapper">
<select
@@ -220,17 +222,17 @@ export default {
<option
:value="visibilityOptions.PRIVATE"
:disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
- >{{ __('Private') }}</option
+ >{{ s__('ProjectSettings|Private') }}</option
>
<option
:value="visibilityOptions.INTERNAL"
:disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
- >{{ __('Internal') }}</option
+ >{{ s__('ProjectSettings|Internal') }}</option
>
<option
:value="visibilityOptions.PUBLIC"
:disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
- >{{ __('Public') }}</option
+ >{{ s__('ProjectSettings|Public') }}</option
>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
@@ -243,14 +245,15 @@ export default {
type="hidden"
name="project[request_access_enabled]"
/>
- <input v-model="requestAccessEnabled" type="checkbox" /> Allow users to request access
+ <input v-model="requestAccessEnabled" type="checkbox" />
+ {{ s__('ProjectSettings|Allow users to request access') }}
</label>
</project-setting-row>
</div>
<div :class="{ 'highlight-changes': highlightChangesClass }" class="project-feature-settings">
<project-setting-row
- label="Issues"
- help-text="Lightweight issue tracking system for this project"
+ :label="s__('ProjectSettings|Issues')"
+ :help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')"
>
<project-feature-setting
v-model="issuesAccessLevel"
@@ -258,7 +261,10 @@ export default {
name="project[project_feature_attributes][issues_access_level]"
/>
</project-setting-row>
- <project-setting-row label="Repository" help-text="View and edit files in this project">
+ <project-setting-row
+ :label="s__('ProjectSettings|Repository')"
+ :help-text="s__('ProjectSettings|View and edit files in this project')"
+ >
<project-feature-setting
v-model="repositoryAccessLevel"
:options="featureAccessLevelOptions"
@@ -267,8 +273,8 @@ export default {
</project-setting-row>
<div class="project-feature-setting-group">
<project-setting-row
- label="Merge requests"
- help-text="Submit changes to be merged upstream"
+ :label="s__('ProjectSettings|Merge requests')"
+ :help-text="s__('ProjectSettings|Submit changes to be merged upstream')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
@@ -277,7 +283,10 @@ export default {
name="project[project_feature_attributes][merge_requests_access_level]"
/>
</project-setting-row>
- <project-setting-row label="Pipelines" help-text="Build, test, and deploy your changes">
+ <project-setting-row
+ :label="s__('ProjectSettings|Pipelines')"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
+ >
<project-feature-setting
v-model="buildsAccessLevel"
:options="repoFeatureAccessLevelOptions"
@@ -288,11 +297,17 @@ export default {
<project-setting-row
v-if="registryAvailable"
:help-path="registryHelpPath"
- label="Container registry"
- help-text="Every project can have its own space to store its Docker images"
+ :label="s__('ProjectSettings|Container registry')"
+ :help-text="
+ s__('ProjectSettings|Every project can have its own space to store its Docker images')
+ "
>
<div v-if="showContainerRegistryPublicNote" class="text-muted">
- {{ __('Note: the container registry is always visible when a project is public') }}
+ {{
+ s__(
+ 'ProjectSettings|Note: the container registry is always visible when a project is public',
+ )
+ }}
</div>
<project-feature-toggle
v-model="containerRegistryEnabled"
@@ -303,8 +318,10 @@ export default {
<project-setting-row
v-if="lfsAvailable"
:help-path="lfsHelpPath"
- label="Git Large File Storage"
- help-text="Manages large files such as audio, video, and graphics files"
+ :label="s__('ProjectSettings|Git Large File Storage')"
+ :help-text="
+ s__('ProjectSettings|Manages large files such as audio, video, and graphics files')
+ "
>
<project-feature-toggle
v-model="lfsEnabled"
@@ -315,8 +332,10 @@ export default {
<project-setting-row
v-if="packagesAvailable"
:help-path="packagesHelpPath"
- label="Packages"
- help-text="Every project can have its own space to store its packages"
+ :label="s__('ProjectSettings|Packages')"
+ :help-text="
+ s__('ProjectSettings|Every project can have its own space to store its packages')
+ "
>
<project-feature-toggle
v-model="packagesEnabled"
@@ -325,7 +344,10 @@ export default {
/>
</project-setting-row>
</div>
- <project-setting-row label="Wiki" help-text="Pages for project documentation">
+ <project-setting-row
+ :label="s__('ProjectSettings|Wiki')"
+ :help-text="s__('ProjectSettings|Pages for project documentation')"
+ >
<project-feature-setting
v-model="wikiAccessLevel"
:options="featureAccessLevelOptions"
@@ -333,8 +355,8 @@ export default {
/>
</project-setting-row>
<project-setting-row
- label="Snippets"
- help-text="Share code pastes with others out of Git repository"
+ :label="s__('ProjectSettings|Snippets')"
+ :help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
@@ -345,8 +367,10 @@ export default {
<project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
:help-path="pagesHelpPath"
- label="Pages access control"
- help-text="Access control for the project's static website"
+ :label="s__('ProjectSettings|Pages')"
+ :help-text="
+ s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab')
+ "
>
<project-feature-setting
v-model="pagesAccessLevel"
@@ -358,10 +382,13 @@ export default {
<project-setting-row v-if="canDisableEmails" class="mb-3">
<label class="js-emails-disabled">
<input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />
- <input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }}
+ <input v-model="emailsDisabled" type="checkbox" />
+ {{ s__('ProjectSettings|Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
- __('This setting will override user notification preferences for all project members.')
+ s__(
+ 'ProjectSettings|This setting will override user notification preferences for all project members.',
+ )
}}</span>
</project-setting-row>
</div>
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 6aa41d0825b..370f3c6e7a2 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -48,7 +48,7 @@ document.addEventListener('DOMContentLoaded', () => {
leaveByUrl('project');
if (document.getElementById('js-tree-list')) {
- import('~/repository')
+ import('ee_else_ce/repository')
.then(m => m.default())
.catch(e => {
throw e;
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 7b90a3a4f6e..16d71379e31 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
GpgBadges.fetch();
if (document.getElementById('js-tree-list')) {
- import('~/repository')
+ import('ee_else_ce/repository')
.then(m => m.default())
.catch(e => {
throw e;
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 55bc93a2b13..66ee2d9303f 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -5,6 +5,7 @@ import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
+import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
@@ -20,3 +21,16 @@ document.addEventListener('DOMContentLoaded', () => {
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
});
+
+export default function trackData() {
+ if (gon.tracking_data) {
+ const tab = document.querySelector(".new-session-tabs a[href='#register-pane']");
+ const { category, action, ...data } = gon.tracking_data;
+
+ tab.addEventListener('click', () => {
+ Tracking.event(category, action, data);
+ });
+ }
+}
+
+trackData();
diff --git a/app/assets/javascripts/performance_bar/components/add_request.vue b/app/assets/javascripts/performance_bar/components/add_request.vue
new file mode 100644
index 00000000000..54bca8a1b67
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/add_request.vue
@@ -0,0 +1,48 @@
+import { __ } from '~/locale';
+
+<script>
+export default {
+ data() {
+ return {
+ inputEnabled: false,
+ urlOrRequestId: '',
+ };
+ },
+ methods: {
+ toggleInput() {
+ this.inputEnabled = !this.inputEnabled;
+ },
+ addRequest() {
+ this.$emit('add-request', this.urlOrRequestId);
+ this.clearForm();
+ },
+ clearForm() {
+ this.urlOrRequestId = '';
+ this.toggleInput();
+ },
+ },
+};
+</script>
+<template>
+ <div id="peek-view-add-request" class="view">
+ <form class="form-inline" @submit.prevent>
+ <button
+ class="btn-blank btn-link bold"
+ type="button"
+ :title="__(`Add request manually`)"
+ @click="toggleInput"
+ >
+ +
+ </button>
+ <input
+ v-if="inputEnabled"
+ v-model="urlOrRequestId"
+ type="text"
+ :placeholder="__(`URL or request ID`)"
+ class="form-control form-control-sm d-inline-block ml-1"
+ @keyup.enter="addRequest"
+ @keyup.esc="clearForm"
+ />
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 3b07eba02b7..8ce653bf1fb 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,12 +1,14 @@
<script>
import { glEmojiTag } from '~/emoji';
+import AddRequest from './add_request.vue';
import DetailedMetric from './detailed_metric.vue';
import RequestSelector from './request_selector.vue';
import { s__ } from '~/locale';
export default {
components: {
+ AddRequest,
DetailedMetric,
RequestSelector,
},
@@ -118,6 +120,7 @@ export default {
>
<a :href="currentRequest.details.tracing.tracing_url">{{ s__('PerformanceBar|trace') }}</a>
</div>
+ <add-request v-on="$listeners" />
<request-selector
v-if="currentRequest"
:current-request="currentRequest"
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index 1ae9487f391..735c9d804ee 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
@@ -32,6 +34,15 @@ export default ({ container }) =>
PerformanceBarService.removeInterceptor(this.interceptor);
},
methods: {
+ addRequestManually(urlOrRequestId) {
+ if (urlOrRequestId.startsWith('https://') || urlOrRequestId.startsWith('http://')) {
+ // We don't need to do anything with the response, we just
+ // want to trace the request.
+ axios.get(urlOrRequestId);
+ } else {
+ this.loadRequestDetails(urlOrRequestId, urlOrRequestId);
+ }
+ },
loadRequestDetails(requestId, requestUrl) {
if (!this.store.canTrackRequest(requestUrl)) {
return;
@@ -58,6 +69,9 @@ export default ({ container }) =>
peekUrl: this.peekUrl,
profileUrl: this.profileUrl,
},
+ on: {
+ 'add-request': this.addRequestManually,
+ },
});
},
});
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 3c85bb61ce8..fd59a580961 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -88,7 +88,7 @@ export default {
:title="tooltipText"
:class="cssClass"
:disabled="isDisabled"
- class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
+ class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper d-flex align-items-center justify-content-center"
@click="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 5275de3bc8b..afb8439511f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -265,7 +265,11 @@ export default {
<div class="table-section section-10 commit-link">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Status') }}</div>
<div class="table-mobile-content">
- <ci-badge :status="pipelineStatus" :show-text="!isChildView" />
+ <ci-badge
+ :status="pipelineStatus"
+ :show-text="!isChildView"
+ data-qa-selector="pipeline_commit_status"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
new file mode 100644
index 00000000000..388b300b39d
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -0,0 +1,81 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import TestSuiteTable from './test_suite_table.vue';
+import TestSummary from './test_summary.vue';
+import TestSummaryTable from './test_summary_table.vue';
+import store from '~/pipelines/stores/test_reports';
+
+export default {
+ name: 'TestReports',
+ components: {
+ GlLoadingIcon,
+ TestSuiteTable,
+ TestSummary,
+ TestSummaryTable,
+ },
+ store,
+ computed: {
+ ...mapState(['isLoading', 'selectedSuite', 'testReports']),
+ showSuite() {
+ return this.selectedSuite.total_count > 0;
+ },
+ showTests() {
+ return this.testReports.total_count > 0;
+ },
+ },
+ methods: {
+ ...mapActions(['setSelectedSuite', 'removeSelectedSuite']),
+ summaryBackClick() {
+ this.removeSelectedSuite();
+ },
+ summaryTableRowClick(suite) {
+ this.setSelectedSuite(suite);
+ },
+ beforeEnterTransition() {
+ document.documentElement.style.overflowX = 'hidden';
+ },
+ afterLeaveTransition() {
+ document.documentElement.style.overflowX = '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="isLoading">
+ <gl-loading-icon size="lg" class="prepend-top-default js-loading-spinner" />
+ </div>
+
+ <div
+ v-else-if="!isLoading && showTests"
+ ref="container"
+ class="tests-detail position-relative js-tests-detail"
+ >
+ <transition
+ name="slide"
+ @before-enter="beforeEnterTransition"
+ @after-leave="afterLeaveTransition"
+ >
+ <div v-if="showSuite" key="detail" class="w-100 position-absolute slide-enter-to-element">
+ <test-summary :report="selectedSuite" show-back @on-back-click="summaryBackClick" />
+
+ <test-suite-table />
+ </div>
+
+ <div v-else key="summary" class="w-100 position-absolute slide-enter-from-element">
+ <test-summary :report="testReports" />
+
+ <test-summary-table @row-click="summaryTableRowClick" />
+ </div>
+ </transition>
+ </div>
+
+ <div v-else>
+ <div class="row prepend-top-default">
+ <div class="col-12">
+ <p class="js-no-tests-to-show">{{ s__('TestReports|There are no tests to show.') }}</p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
new file mode 100644
index 00000000000..28b2c706320
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -0,0 +1,108 @@
+<script>
+import { mapGetters } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
+import store from '~/pipelines/stores/test_reports';
+import { __ } from '~/locale';
+
+export default {
+ name: 'TestsSuiteTable',
+ components: {
+ Icon,
+ },
+ store,
+ props: {
+ heading: {
+ type: String,
+ required: false,
+ default: __('Tests'),
+ },
+ },
+ computed: {
+ ...mapGetters(['getSuiteTests']),
+ hasSuites() {
+ return this.getSuiteTests.length > 0;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="row prepend-top-default">
+ <div class="col-12">
+ <h4>{{ heading }}</h4>
+ </div>
+ </div>
+
+ <div v-if="hasSuites" class="test-reports-table js-test-cases-table">
+ <div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray">
+ <div role="rowheader" class="table-section section-20">
+ {{ __('Class') }}
+ </div>
+ <div role="rowheader" class="table-section section-20">
+ {{ __('Name') }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-center">
+ {{ __('Status') }}
+ </div>
+ <div role="rowheader" class="table-section flex-grow-1">
+ {{ __('Trace'), }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-right">
+ {{ __('Duration') }}
+ </div>
+ </div>
+
+ <div
+ v-for="(testCase, index) in getSuiteTests"
+ :key="index"
+ class="gl-responsive-table-row rounded align-items-md-start mt-sm-3 js-case-row"
+ >
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div>
+ <div class="table-mobile-content pr-md-1">{{ testCase.classname }}</div>
+ </div>
+
+ <div class="table-section section-20 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
+ <div class="table-mobile-content">{{ testCase.name }}</div>
+ </div>
+
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
+ <div class="table-mobile-content text-center">
+ <div
+ class="add-border ci-status-icon d-flex align-items-center justify-content-end justify-content-md-center"
+ :class="`ci-status-icon-${testCase.status}`"
+ >
+ <icon :size="24" :name="testCase.icon" />
+ </div>
+ </div>
+ </div>
+
+ <div class="table-section flex-grow-1">
+ <div role="rowheader" class="table-mobile-header">{{ __('Trace'), }}</div>
+ <div class="table-mobile-content">
+ <pre
+ v-if="testCase.system_output"
+ class="build-trace build-trace-rounded text-left"
+ ><code class="bash p-0">{{testCase.system_output}}</code></pre>
+ </div>
+ </div>
+
+ <div class="table-section section-10 section-wrap">
+ <div role="rowheader" class="table-mobile-header">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-right">
+ {{ testCase.formattedTime }}
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div v-else>
+ <p class="js-no-test-cases">{{ s__('TestReports|There are no test cases to display.') }}</p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
new file mode 100644
index 00000000000..dce8b020d6f
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -0,0 +1,116 @@
+<script>
+import { GlButton, GlLink, GlProgressBar } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'TestSummary',
+ components: {
+ GlButton,
+ GlLink,
+ GlProgressBar,
+ Icon,
+ },
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ },
+ showBack: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ heading() {
+ return this.report.name || __('Summary');
+ },
+ successPercentage() {
+ return Math.round((this.report.success_count / this.report.total_count) * 100) || 0;
+ },
+ formattedDuration() {
+ return formatTime(secondsToMilliseconds(this.report.total_time));
+ },
+ progressBarVariant() {
+ if (this.successPercentage < 33) {
+ return 'danger';
+ }
+
+ if (this.successPercentage >= 33 && this.successPercentage < 66) {
+ return 'warning';
+ }
+
+ if (this.successPercentage >= 66 && this.successPercentage < 90) {
+ return 'primary';
+ }
+
+ return 'success';
+ },
+ },
+ methods: {
+ onBackClick() {
+ this.$emit('on-back-click');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="row">
+ <div class="col-12 d-flex prepend-top-8 align-items-center">
+ <gl-button
+ v-if="showBack"
+ size="sm"
+ class="append-right-default js-back-button"
+ @click="onBackClick"
+ >
+ <icon name="angle-left" />
+ </gl-button>
+
+ <h4>{{ heading }}</h4>
+ </div>
+ </div>
+
+ <div class="row mt-2">
+ <div class="col-4 col-md">
+ <span class="js-total-tests">{{
+ sprintf(s__('TestReports|%{count} jobs'), { count: report.total_count })
+ }}</span>
+ </div>
+
+ <div class="col-4 col-md text-center text-md-center">
+ <span class="js-failed-tests">{{
+ sprintf(s__('TestReports|%{count} failures'), { count: report.failed_count })
+ }}</span>
+ </div>
+
+ <div class="col-4 col-md text-right text-md-center">
+ <span class="js-errored-tests">{{
+ sprintf(s__('TestReports|%{count} errors'), { count: report.error_count })
+ }}</span>
+ </div>
+
+ <div class="col-6 mt-3 col-md mt-md-0 text-md-center">
+ <span class="js-success-rate">{{
+ sprintf(s__('TestReports|%{rate}%{sign} success rate'), {
+ rate: successPercentage,
+ sign: '%',
+ })
+ }}</span>
+ </div>
+
+ <div class="col-6 mt-3 col-md mt-md-0 text-right">
+ <span class="js-duration">{{ formattedDuration }}</span>
+ </div>
+ </div>
+
+ <div class="row mt-3">
+ <div class="col-12">
+ <gl-progress-bar :value="successPercentage" :variant="progressBarVariant" height="10px" />
+ </div>
+ </div>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..96177512e35
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import store from '~/pipelines/stores/test_reports';
+
+export default {
+ name: 'TestsSummaryTable',
+ store,
+ props: {
+ heading: {
+ type: String,
+ required: false,
+ default: s__('TestReports|Test suites'),
+ },
+ },
+ computed: {
+ ...mapGetters(['getTestSuites']),
+ hasSuites() {
+ return this.getTestSuites.length > 0;
+ },
+ },
+ methods: {
+ tableRowClick(suite) {
+ this.$emit('row-click', suite);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="row prepend-top-default">
+ <div class="col-12">
+ <h4>{{ heading }}</h4>
+ </div>
+ </div>
+
+ <div v-if="hasSuites" class="test-reports-table js-test-suites-table">
+ <div role="row" class="gl-responsive-table-row table-row-header font-weight-bold">
+ <div role="rowheader" class="table-section section-25 pl-3">
+ {{ __('Suite') }}
+ </div>
+ <div role="rowheader" class="table-section section-25">
+ {{ __('Duration') }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-center">
+ {{ __('Failed') }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-center">
+ {{ __('Errors'), }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-center">
+ {{ __('Skipped'), }}
+ </div>
+ <div role="rowheader" class="table-section section-10 text-center">
+ {{ __('Passed'), }}
+ </div>
+ <div role="rowheader" class="table-section section-10 pr-3 text-right">
+ {{ __('Total') }}
+ </div>
+ </div>
+
+ <div
+ 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"
+ @click="tableRowClick(testSuite)"
+ >
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Suite') }}
+ </div>
+ <div class="table-mobile-content underline cgray pl-3">
+ {{ testSuite.name }}
+ </div>
+ </div>
+
+ <div class="table-section section-25">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Duration') }}
+ </div>
+ <div class="table-mobile-content text-md-left">
+ {{ testSuite.formattedTime }}
+ </div>
+ </div>
+
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Failed') }}
+ </div>
+ <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
+ </div>
+
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Errors') }}
+ </div>
+ <div class="table-mobile-content">{{ testSuite.error_count }}</div>
+ </div>
+
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Skipped') }}
+ </div>
+ <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
+ </div>
+
+ <div class="table-section section-10 text-center">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Passed') }}
+ </div>
+ <div class="table-mobile-content">{{ testSuite.success_count }}</div>
+ </div>
+
+ <div class="table-section section-10 text-right pr-md-3">
+ <div role="rowheader" class="table-mobile-header font-weight-bold">
+ {{ __('Total') }}
+ </div>
+ <div class="table-mobile-content">{{ testSuite.total_count }}</div>
+ </div>
+ </div>
+ </div>
+
+ <div v-else>
+ <p class="js-no-tests-suites">{{ s__('TestReports|There are no test suites to show.') }}</p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index d27829db50c..c9655d18a04 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,3 +1,9 @@
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300;
+
+export const TestStatus = {
+ FAILED: 'failed',
+ SKIPPED: 'skipped',
+ SUCCESS: 'success',
+};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index b6f8716d37d..d8dbc3c2454 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -7,6 +7,8 @@ import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
+import TestReports from './components/test_reports/test_reports.vue';
+import testReportsStore from './stores/test_reports';
Vue.use(Translate);
@@ -17,7 +19,7 @@ export default () => {
mediator.fetchPipeline();
- // eslint-disable-next-line
+ // eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-graph-vue',
components: {
@@ -47,7 +49,7 @@ export default () => {
},
});
- // eslint-disable-next-line
+ // eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-header-vue',
components: {
@@ -81,4 +83,23 @@ export default () => {
});
},
});
+
+ const testReportsEnabled =
+ window.gon && window.gon.features && window.gon.features.junitPipelineView;
+
+ if (testReportsEnabled) {
+ testReportsStore.dispatch('setEndpoint', dataset.testReportEndpoint);
+ testReportsStore.dispatch('fetchReports');
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-pipeline-tests-detail',
+ components: {
+ TestReports,
+ },
+ render(createElement) {
+ return createElement('test-reports');
+ },
+ });
+ }
};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
new file mode 100644
index 00000000000..71d875c1a83
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -0,0 +1,30 @@
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+
+export const setEndpoint = ({ commit }, data) => commit(types.SET_ENDPOINT, data);
+
+export const fetchReports = ({ state, commit, dispatch }) => {
+ dispatch('toggleLoading');
+
+ return axios
+ .get(state.endpoint)
+ .then(response => {
+ const { data } = response;
+ commit(types.SET_REPORTS, data);
+ })
+ .catch(() => {
+ createFlash(s__('TestReports|There was an error fetching the test reports.'));
+ })
+ .finally(() => {
+ dispatch('toggleLoading');
+ });
+};
+
+export const setSelectedSuite = ({ commit }, data) => commit(types.SET_SELECTED_SUITE, data);
+export const removeSelectedSuite = ({ commit }) => commit(types.SET_SELECTED_SUITE, {});
+export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
new file mode 100644
index 00000000000..788c1d32987
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -0,0 +1,23 @@
+import { addIconStatus, formattedTime, sortTestCases } from './utils';
+
+export const getTestSuites = state => {
+ const { test_suites: testSuites = [] } = state.testReports;
+
+ return testSuites.map(suite => ({
+ ...suite,
+ formattedTime: formattedTime(suite.total_time),
+ }));
+};
+
+export const getSuiteTests = state => {
+ const { selectedSuite } = state;
+
+ if (selectedSuite.test_cases) {
+ return selectedSuite.test_cases.sort(sortTestCases).map(addIconStatus);
+ }
+
+ return [];
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js
new file mode 100644
index 00000000000..318dff5bcb2
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state,
+});
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
new file mode 100644
index 00000000000..832e45cf7a1
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -0,0 +1,4 @@
+export const SET_ENDPOINT = 'SET_ENDPOINT';
+export const SET_REPORTS = 'SET_REPORTS';
+export const SET_SELECTED_SUITE = 'SET_SELECTED_SUITE';
+export const TOGGLE_LOADING = 'TOGGLE_LOADING';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
new file mode 100644
index 00000000000..349e6ec0469
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -0,0 +1,19 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ENDPOINT](state, endpoint) {
+ Object.assign(state, { endpoint });
+ },
+
+ [types.SET_REPORTS](state, testReports) {
+ Object.assign(state, { testReports });
+ },
+
+ [types.SET_SELECTED_SUITE](state, selectedSuite) {
+ Object.assign(state, { selectedSuite });
+ },
+
+ [types.TOGGLE_LOADING](state) {
+ Object.assign(state, { isLoading: !state.isLoading });
+ },
+};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
new file mode 100644
index 00000000000..80a0c2a46a0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ endpoint: '',
+ testReports: {},
+ selectedSuite: {},
+ isLoading: false,
+});
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
new file mode 100644
index 00000000000..95466587d6b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/utils.js
@@ -0,0 +1,36 @@
+import { TestStatus } from '~/pipelines/constants';
+import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
+
+function iconForTestStatus(status) {
+ switch (status) {
+ case 'success':
+ return 'status_success_borderless';
+ case 'failed':
+ return 'status_failed_borderless';
+ default:
+ return 'status_skipped_borderless';
+ }
+}
+
+export const formattedTime = timeInSeconds => formatTime(secondsToMilliseconds(timeInSeconds));
+
+export const addIconStatus = testCase => ({
+ ...testCase,
+ icon: iconForTestStatus(testCase.status),
+ formattedTime: formattedTime(testCase.execution_time),
+});
+
+export const sortTestCases = (a, b) => {
+ if (a.status === b.status) {
+ return 0;
+ }
+
+ switch (b.status) {
+ case TestStatus.SUCCESS:
+ return -1;
+ case TestStatus.FAILED:
+ return 1;
+ default:
+ return 0;
+ }
+};
diff --git a/app/assets/javascripts/privacy_policy_update_callout.js b/app/assets/javascripts/privacy_policy_update_callout.js
deleted file mode 100644
index 97f41deb30f..00000000000
--- a/app/assets/javascripts/privacy_policy_update_callout.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import PersistentUserCallout from '~/persistent_user_callout';
-
-function initPrivacyPolicyUpdateCallout() {
- const callout = document.querySelector('.js-privacy-policy-update');
- PersistentUserCallout.factory(callout);
-}
-
-export default initPrivacyPolicyUpdateCallout;
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 44bc2d9f5f8..880e1a88975 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-return-assign, one-var, consistent-return, class-methods-use-this */
+/* eslint-disable no-useless-escape, no-underscore-dangle, func-names, no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
import 'cropper';
@@ -59,8 +59,7 @@ import _ from 'underscore';
}
bindEvents() {
- var _this;
- _this = this;
+ const _this = this;
this.fileInput.on('change', function(e) {
_this.onFileInputChange(e, this);
this.value = null;
@@ -70,8 +69,7 @@ import _ from 'underscore';
this.modalCrop.on('hidden.bs.modal', this.onModalHide);
this.uploadImageBtn.on('click', this.onUploadImageBtnClick);
this.cropActionsBtn.on('click', function() {
- var btn;
- btn = this;
+ const btn = this;
return _this.onActionBtnClick(btn);
});
return (this.croppedImageBlob = null);
@@ -82,8 +80,7 @@ import _ from 'underscore';
}
onModalShow() {
- var _this;
- _this = this;
+ const _this = this;
return this.modalCropImg.cropper({
viewMode: 1,
center: false,
@@ -128,8 +125,7 @@ import _ from 'underscore';
}
onActionBtnClick(btn) {
- var data;
- data = $(btn).data();
+ const data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
return this.modalCropImg.cropper(data.method, data.option);
}
@@ -140,9 +136,8 @@ import _ from 'underscore';
}
readFile(input) {
- var _this, reader;
- _this = this;
- reader = new FileReader();
+ const _this = this;
+ const reader = new FileReader();
reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
@@ -151,9 +146,10 @@ import _ from 'underscore';
}
dataURLtoBlob(dataURL) {
- var array, binary, i, len;
- binary = atob(dataURL.split(',')[1]);
- array = [];
+ let i = 0;
+ let len = 0;
+ const binary = atob(dataURL.split(',')[1]);
+ const array = [];
for (i = 0, len = binary.length; i < len; i += 1) {
array.push(binary.charCodeAt(i));
@@ -164,9 +160,8 @@ import _ from 'underscore';
}
setPreview() {
- var filename;
+ const filename = this.fileInput.val().replace(FILENAMEREGEX, '');
this.previewImage.attr('src', this.dataURL);
- filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename);
}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 2c375b39c1f..031c54d2336 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,16 +1,20 @@
-/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, no-return-assign */
+/* eslint-disable func-names, consistent-return, no-return-assign */
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
+import sanitize from 'sanitize-html';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) {
- var j, lastIndex, len, matchIndex, matchedChars, unmatched;
- lastIndex = 0;
- matchedChars = [];
+ let j = 0;
+ let len = 0;
+ let lastIndex = 0;
+ let matchedChars = [];
+ let matchIndex = matches[j];
+ let unmatched = text.substring(lastIndex, matchIndex);
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
unmatched = text.substring(lastIndex, matchIndex);
@@ -54,10 +58,10 @@ export default class ProjectFindFile {
'keyup',
(function(_this) {
return function(event) {
- var oldValue, ref, target, value;
- target = $(event.target);
- value = target.val();
- oldValue = (ref = target.data('oldValue')) != null ? ref : '';
+ const target = $(event.target);
+ const value = target.val();
+ const ref = target.data('oldValue');
+ const oldValue = ref != null ? ref : '';
if (value !== oldValue) {
target.data('oldValue', value);
_this.findFile();
@@ -73,9 +77,8 @@ export default class ProjectFindFile {
}
findFile() {
- var result, searchText;
- searchText = this.inputElement.val();
- result =
+ const searchText = sanitize(this.inputElement.val());
+ const result =
searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText);
// find file
@@ -100,20 +103,21 @@ export default class ProjectFindFile {
// render result
renderList(filePaths, searchText) {
- var blobItemUrl, filePath, html, i, len, matches, results;
+ let i = 0;
+ let len = 0;
+ let matches = [];
+ const results = [];
this.element.find('.tree-table > tbody').empty();
- results = [];
-
for (i = 0, len = filePaths.length; i < len; i += 1) {
- filePath = filePaths[i];
+ const filePath = filePaths[i];
if (i === 20) {
break;
}
if (searchText) {
matches = fuzzaldrinPlus.match(filePath, searchText);
}
- blobItemUrl = `${this.options.blobUrlTemplate}/${encodeURIComponent(filePath)}`;
- html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
+ const blobItemUrl = `${this.options.blobUrlTemplate}/${encodeURIComponent(filePath)}`;
+ const html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl);
results.push(this.element.find('.tree-table > tbody').append(html));
}
@@ -124,8 +128,7 @@ export default class ProjectFindFile {
// make tbody row html
static makeHtml(filePath, matches, blobItemUrl) {
- var $tr;
- $tr = $(
+ const $tr = $(
"<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>",
);
if (matches) {
@@ -140,9 +143,9 @@ export default class ProjectFindFile {
}
selectRow(type) {
- var next, rows, selectedRow;
- rows = this.element.find('.files-slider tr.tree-item');
- selectedRow = this.element.find('.files-slider tr.tree-item.selected');
+ const rows = this.element.find('.files-slider tr.tree-item');
+ let selectedRow = this.element.find('.files-slider tr.tree-item.selected');
+ let next = selectedRow.prev();
if (rows && rows.length > 0) {
if (selectedRow && selectedRow.length > 0) {
if (type === 'UP') {
@@ -174,7 +177,7 @@ export default class ProjectFindFile {
}
goToBlob() {
- var $link = this.element.find('.tree-item.selected .tree-item-file-name a');
+ const $link = this.element.find('.tree-item.selected .tree-item-file-name a');
if ($link.length) {
$link.get(0).click();
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 0fbb7e5ca42..66ce1ab5659 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, one-var, no-else-return */
+/* eslint-disable func-names, no-else-return */
import $ from 'jquery';
import Api from './api';
@@ -7,9 +7,11 @@ import { s__ } from './locale';
const projectSelect = () => {
$('.ajax-project-select').each(function(i, select) {
- var placeholder;
+ let placeholder;
const simpleFilter = $(select).data('simpleFilter') || false;
+ const isInstantiated = $(select).data('select2');
this.groupId = $(select).data('groupId');
+ this.userId = $(select).data('userId');
this.includeGroups = $(select).data('includeGroups');
this.allProjects = $(select).data('allProjects') || false;
this.orderBy = $(select).data('orderBy') || 'id';
@@ -28,55 +30,62 @@ const projectSelect = () => {
$(select).select2({
placeholder,
minimumInputLength: 0,
- query: (function(_this) {
- return function(query) {
- var finalCallback, projectsCallback;
- finalCallback = function(projects) {
- var data;
- data = {
- results: projects,
- };
- return query.callback(data);
+ query: query => {
+ let projectsCallback;
+ const finalCallback = function(projects) {
+ const data = {
+ results: projects,
};
- if (_this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (_this.groupId) {
- return Api.groupProjects(
- _this.groupId,
- query.term,
- {
- with_issues_enabled: _this.withIssuesEnabled,
- with_merge_requests_enabled: _this.withMergeRequestsEnabled,
- with_shared: _this.withShared,
- include_subgroups: _this.includeProjectsInSubgroups,
- },
- 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 query.callback(data);
};
- })(this),
+ if (this.includeGroups) {
+ projectsCallback = function(projects) {
+ const groupsCallback = function(groups) {
+ const data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (this.groupId) {
+ return Api.groupProjects(
+ this.groupId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ } else if (this.userId) {
+ return Api.userProjects(
+ this.userId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ },
+ 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,
+ );
+ }
+ },
id(project) {
if (simpleFilter) return project.id;
return JSON.stringify({
@@ -96,7 +105,7 @@ const projectSelect = () => {
dropdownCssClass: 'ajax-project-dropdown',
});
- if (simpleFilter) return select;
+ if (isInstantiated || simpleFilter) return select;
return new ProjectSelectComboButton(select);
});
};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 9066844f687..2429da9c061 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -182,6 +182,10 @@ const bindEvents = () => {
text: s__('ProjectTemplates|Netlify/Hexo'),
icon: '.template-option .icon-netlify',
},
+ serverless_framework: {
+ text: s__('ProjectTemplates|Serverless Framework/JS'),
+ icon: '.template-option .icon-serverless_framework',
+ },
};
const selectedTemplate = templates[value];
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index 95f8270b5d0..5a6f9370564 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -8,12 +8,13 @@ import {
GlModalDirective,
GlEmptyState,
} from '@gitlab/ui';
-import createFlash from '../../flash';
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import Icon from '../../vue_shared/components/icon.vue';
+import createFlash from '~/flash';
+import Tracking from '~/tracking';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import TableRegistry from './table_registry.vue';
-import { errorMessages, errorMessagesTypes } from '../constants';
-import { __ } from '../../locale';
+import { DELETE_REPO_ERROR_MESSAGE } from '../constants';
+import { __ } from '~/locale';
export default {
name: 'CollapsibeContainerRegisty',
@@ -30,6 +31,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
+ mixins: [Tracking.mixin({})],
props: {
repo: {
type: Object,
@@ -40,6 +42,10 @@ export default {
return {
isOpen: false,
modalId: `confirm-repo-deletion-modal-${this.repo.id}`,
+ tracking: {
+ category: document.body.dataset.page,
+ label: 'registry_repository_delete',
+ },
};
},
computed: {
@@ -61,15 +67,13 @@ export default {
}
},
handleDeleteRepository() {
+ this.track('confirm_delete', {});
return this.deleteItem(this.repo)
.then(() => {
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
})
- .catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
- },
- showError(message) {
- createFlash(errorMessages[message]);
+ .catch(() => createFlash(DELETE_REPO_ERROR_MESSAGE));
},
},
};
@@ -97,10 +101,9 @@ export default {
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- data-track-event="click_button"
- data-track-label="registry_repository_delete"
class="js-remove-repo btn-inverted"
variant="danger"
+ @click="track('click_button', {})"
>
<icon name="remove" />
</gl-button>
@@ -124,7 +127,13 @@ export default {
class="mx-auto my-0"
/>
</div>
- <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
+ <gl-modal
+ ref="deleteModal"
+ :modal-id="modalId"
+ ok-variant="danger"
+ @ok="handleDeleteRepository"
+ @cancel="track('cancel_delete', {})"
+ >
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
<p
v-html="
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index 8470fbc2b59..caa5fd4ff4e 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -1,20 +1,15 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import {
- GlButton,
- GlFormCheckbox,
- GlTooltipDirective,
- GlModal,
- GlModalDirective,
-} from '@gitlab/ui';
-import { n__, s__, sprintf } from '../../locale';
-import createFlash from '../../flash';
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
-import Icon from '../../vue_shared/components/icon.vue';
-import timeagoMixin from '../../vue_shared/mixins/timeago';
-import { errorMessages, errorMessagesTypes } from '../constants';
-import { numberToHumanSize } from '../../lib/utils/number_utils';
+import { GlButton, GlFormCheckbox, GlTooltipDirective, GlModal } from '@gitlab/ui';
+import Tracking from '~/tracking';
+import { n__, s__, sprintf } from '~/locale';
+import createFlash from '~/flash';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { FETCH_REGISTRY_ERROR_MESSAGE, DELETE_REGISTRY_ERROR_MESSAGE } from '../constants';
export default {
components: {
@@ -27,7 +22,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- GlModal: GlModalDirective,
},
mixins: [timeagoMixin],
props: {
@@ -65,12 +59,21 @@ export default {
this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length,
);
},
- },
- mounted() {
- this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents);
+ isMultiDelete() {
+ return this.itemsToBeDeleted.length > 1;
+ },
+ tracking() {
+ return {
+ property: this.repo.name,
+ label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
+ };
+ },
},
methods: {
...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']),
+ track(action) {
+ Tracking.event(document.body.dataset.page, action, this.tracking);
+ },
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = sprintf(
@@ -92,17 +95,11 @@ export default {
formatSize(size) {
return numberToHumanSize(size);
},
- removeModalEvents() {
- this.$refs.deleteModal.$refs.modal.$off('ok');
- },
deleteSingleItem(index) {
this.setModalDescription(index);
this.itemsToBeDeleted = [index];
-
- this.$refs.deleteModal.$refs.modal.$once('ok', () => {
- this.removeModalEvents();
- this.handleSingleDelete(this.repo.list[index]);
- });
+ this.track('click_button');
+ this.$refs.deleteModal.show();
},
deleteMultipleItems() {
this.itemsToBeDeleted = [...this.selectedItems];
@@ -111,17 +108,14 @@ export default {
} else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
-
- this.$refs.deleteModal.$refs.modal.$once('ok', () => {
- this.removeModalEvents();
- this.handleMultipleDelete();
- });
+ this.track('click_button');
+ this.$refs.deleteModal.show();
},
handleSingleDelete(itemToDelete) {
this.itemsToBeDeleted = [];
this.deleteItem(itemToDelete)
.then(() => this.fetchList({ repo: this.repo }))
- .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ .catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
@@ -134,19 +128,16 @@ export default {
items: itemsToBeDeleted.map(x => this.repo.list[x].tag),
})
.then(() => this.fetchList({ repo: this.repo }))
- .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
+ .catch(() => createFlash(DELETE_REGISTRY_ERROR_MESSAGE));
} else {
- this.showError(errorMessagesTypes.DELETE_REGISTRY);
+ createFlash(DELETE_REGISTRY_ERROR_MESSAGE);
}
},
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
- this.showError(errorMessagesTypes.FETCH_REGISTRY),
+ createFlash(FETCH_REGISTRY_ERROR_MESSAGE),
);
},
- showError(message) {
- createFlash(errorMessages[message]);
- },
onSelectAllChange() {
if (this.selectAllChecked) {
this.deselectAll();
@@ -179,6 +170,15 @@ export default {
canDeleteRow(item) {
return item && item.canDelete && !this.isDeleteDisabled;
},
+ onDeletionConfirmed() {
+ this.track('confirm_delete');
+ if (this.isMultiDelete) {
+ this.handleMultipleDelete();
+ } else {
+ const index = this.itemsToBeDeleted[0];
+ this.handleSingleDelete(this.repo.list[index]);
+ }
+ },
},
};
</script>
@@ -202,12 +202,10 @@ export default {
<th>
<gl-button
v-if="canDeleteRepo"
+ ref="bulkDeleteButton"
v-gl-tooltip
- v-gl-modal="modalId"
:disabled="!selectedItems || selectedItems.length === 0"
- class="js-delete-registry float-right"
- data-track-event="click_button"
- data-track-label="bulk_registry_tag_delete"
+ class="float-right"
variant="danger"
:title="s__('ContainerRegistry|Remove selected tags')"
:aria-label="s__('ContainerRegistry|Remove selected tags')"
@@ -259,11 +257,8 @@ export default {
<td class="content action-buttons">
<gl-button
v-if="canDeleteRow(item)"
- v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
- data-track-event="click_button"
- data-track-label="registry_tag_delete"
variant="danger"
class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon"
@click="deleteSingleItem(index)"
@@ -282,7 +277,13 @@ export default {
class="js-registry-pagination"
/>
- <gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger">
+ <gl-modal
+ ref="deleteModal"
+ :modal-id="modalId"
+ ok-variant="danger"
+ @ok="onDeletionConfirmed"
+ @cancel="track('cancel_delete')"
+ >
<template v-slot:modal-title>{{ modalAction }}</template>
<template v-slot:modal-ok>{{ modalAction }}</template>
<p v-html="modalDescription"></p>
diff --git a/app/assets/javascripts/registry/constants.js b/app/assets/javascripts/registry/constants.js
index 712b0fade3d..db798fb88ac 100644
--- a/app/assets/javascripts/registry/constants.js
+++ b/app/assets/javascripts/registry/constants.js
@@ -1,15 +1,8 @@
import { __ } from '../locale';
-export const errorMessagesTypes = {
- FETCH_REGISTRY: 'FETCH_REGISTRY',
- FETCH_REPOS: 'FETCH_REPOS',
- DELETE_REPO: 'DELETE_REPO',
- DELETE_REGISTRY: 'DELETE_REGISTRY',
-};
-
-export const errorMessages = {
- [errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
- [errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
- [errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
- [errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
-};
+export const FETCH_REGISTRY_ERROR_MESSAGE = __(
+ 'Something went wrong while fetching the registry list.',
+);
+export const FETCH_REPOS_ERROR_MESSAGE = __('Something went wrong while fetching the projects.');
+export const DELETE_REPO_ERROR_MESSAGE = __('Something went wrong on our end.');
+export const DELETE_REGISTRY_ERROR_MESSAGE = __('Something went wrong on our end.');
diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js
index 2121f518a7a..6afba618486 100644
--- a/app/assets/javascripts/registry/stores/actions.js
+++ b/app/assets/javascripts/registry/stores/actions.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as types from './mutation_types';
-import { errorMessages, errorMessagesTypes } from '../constants';
+import { FETCH_REPOS_ERROR_MESSAGE, FETCH_REGISTRY_ERROR_MESSAGE } from '../constants';
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
@@ -14,7 +14,7 @@ export const fetchRepos = ({ commit, state }) => {
})
.catch(() => {
commit(types.TOGGLE_MAIN_LOADING);
- createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
+ createFlash(FETCH_REPOS_ERROR_MESSAGE);
});
};
@@ -30,7 +30,7 @@ export const fetchList = ({ commit }, { repo, page }) => {
})
.catch(() => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
- createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
+ createFlash(FETCH_REGISTRY_ERROR_MESSAGE);
});
};
diff --git a/app/assets/javascripts/registry/stores/mutations.js b/app/assets/javascripts/registry/stores/mutations.js
index ea5925247d1..419de848883 100644
--- a/app/assets/javascripts/registry/stores/mutations.js
+++ b/app/assets/javascripts/registry/stores/mutations.js
@@ -1,33 +1,31 @@
import * as types from './mutation_types';
-import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export default {
[types.SET_MAIN_ENDPOINT](state, endpoint) {
- Object.assign(state, { endpoint });
+ state.endpoint = endpoint;
},
[types.SET_IS_DELETE_DISABLED](state, isDeleteDisabled) {
- Object.assign(state, { isDeleteDisabled });
+ state.isDeleteDisabled = isDeleteDisabled;
},
[types.SET_REPOS_LIST](state, list) {
- Object.assign(state, {
- repos: list.map(el => ({
- canDelete: Boolean(el.destroy_path),
- destroyPath: el.destroy_path,
- id: el.id,
- isLoading: false,
- list: [],
- location: el.location,
- name: el.path,
- tagsPath: el.tags_path,
- projectId: el.project_id,
- })),
- });
+ state.repos = list.map(el => ({
+ canDelete: Boolean(el.destroy_path),
+ destroyPath: el.destroy_path,
+ id: el.id,
+ isLoading: false,
+ list: [],
+ location: el.location,
+ name: el.path,
+ tagsPath: el.tags_path,
+ projectId: el.project_id,
+ }));
},
[types.TOGGLE_MAIN_LOADING](state) {
- Object.assign(state, { isLoading: !state.isLoading });
+ state.isLoading = !state.isLoading;
},
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
diff --git a/app/assets/javascripts/releases/detail/components/app.vue b/app/assets/javascripts/releases/detail/components/app.vue
index 54a441de886..073cfcd7694 100644
--- a/app/assets/javascripts/releases/detail/components/app.vue
+++ b/app/assets/javascripts/releases/detail/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
+import _ from 'underscore';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
@@ -23,6 +24,7 @@ export default {
'markdownDocsPath',
'markdownPreviewPath',
'releasesPagePath',
+ 'updateReleaseApiDocsPath',
]),
showForm() {
return !this.isFetchingRelease && !this.fetchError;
@@ -42,6 +44,20 @@ export default {
tagName() {
return this.$store.state.release.tagName;
},
+ tagNameHintText() {
+ return sprintf(
+ __(
+ 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
+ ),
+ {
+ linkStart: `<a href="${_.escape(
+ this.updateReleaseApiDocsPath,
+ )}" target="_blank" rel="noopener noreferrer">`,
+ linkEnd: '</a>',
+ },
+ false,
+ );
+ },
releaseTitle: {
get() {
return this.$store.state.release.name;
@@ -77,22 +93,22 @@ export default {
<div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="updateRelease()">
- <div class="row">
- <gl-form-group class="col-md-6 col-lg-5 col-xl-4">
- <label for="git-ref">{{ __('Tag name') }}</label>
- <gl-form-input
- id="git-ref"
- v-model="tagName"
- type="text"
- class="form-control"
- aria-describedby="tag-name-help"
- disabled
- />
- <div id="tag-name-help" class="form-text text-muted">
- {{ __('Choose an existing tag, or create a new one') }}
+ <gl-form-group>
+ <div class="row">
+ <div class="col-md-6 col-lg-5 col-xl-4">
+ <label for="git-ref">{{ __('Tag name') }}</label>
+ <gl-form-input
+ id="git-ref"
+ v-model="tagName"
+ type="text"
+ class="form-control"
+ aria-describedby="tag-name-help"
+ disabled
+ />
</div>
- </gl-form-group>
- </div>
+ </div>
+ <div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div>
+ </gl-form-group>
<gl-form-group>
<label for="release-title">{{ __('Release title') }}</label>
<gl-form-input
diff --git a/app/assets/javascripts/releases/detail/index.js b/app/assets/javascripts/releases/detail/index.js
index 3da971e6d90..0dab90a1ede 100644
--- a/app/assets/javascripts/releases/detail/index.js
+++ b/app/assets/javascripts/releases/detail/index.js
@@ -5,7 +5,7 @@ import createStore from './store';
export default () => {
const el = document.getElementById('js-edit-release-page');
- const store = createStore(el.dataset);
+ const store = createStore();
store.dispatch('setInitialState', el.dataset);
return new Vue({
diff --git a/app/assets/javascripts/releases/detail/store/state.js b/app/assets/javascripts/releases/detail/store/state.js
index ff98e2bed78..7e3d975f1ae 100644
--- a/app/assets/javascripts/releases/detail/store/state.js
+++ b/app/assets/javascripts/releases/detail/store/state.js
@@ -4,6 +4,7 @@ export default () => ({
releasesPagePath: null,
markdownDocsPath: null,
markdownPreviewPath: null,
+ updateReleaseApiDocsPath: null,
release: null,
diff --git a/app/assets/javascripts/releases/list/components/release_block.vue b/app/assets/javascripts/releases/list/components/release_block.vue
index 8d4b32e9dc0..2b6aa6aeff9 100644
--- a/app/assets/javascripts/releases/list/components/release_block.vue
+++ b/app/assets/javascripts/releases/list/components/release_block.vue
@@ -10,6 +10,7 @@ import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
import { scrollToElement } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ReleaseBlockFooter from './release_block_footer.vue';
export default {
name: 'ReleaseBlock',
@@ -19,6 +20,7 @@ export default {
GlButton,
Icon,
UserAvatarLink,
+ ReleaseBlockFooter,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -76,9 +78,12 @@ export default {
},
shouldShowEditButton() {
return Boolean(
- this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit,
+ this.glFeatures.releaseEditPage && this.release._links && this.release._links.edit_url,
);
},
+ shouldShowFooter() {
+ return this.glFeatures.releaseIssueSummary;
+ },
},
mounted() {
const hash = getLocationHash();
@@ -108,7 +113,7 @@ export default {
v-gl-tooltip
class="btn btn-default js-edit-button ml-2"
:title="__('Edit this release')"
- :href="release._links.edit"
+ :href="release._links.edit_url"
>
<icon name="pencil" />
</gl-link>
@@ -164,7 +169,7 @@ export default {
by
<user-avatar-link
class="prepend-left-4"
- :link-href="author.path"
+ :link-href="author.web_url"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
@@ -216,5 +221,16 @@ export default {
<div v-html="release.description_html"></div>
</div>
</div>
+
+ <release-block-footer
+ v-if="shouldShowFooter"
+ class="card-footer"
+ :commit="release.commit"
+ :commit-path="release.commit_path"
+ :tag-name="release.tag_name"
+ :tag-path="release.tag_path"
+ :author="release.author"
+ :released-at="release.released_at"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/releases/list/components/release_block_footer.vue b/app/assets/javascripts/releases/list/components/release_block_footer.vue
new file mode 100644
index 00000000000..5659f0e530b
--- /dev/null
+++ b/app/assets/javascripts/releases/list/components/release_block_footer.vue
@@ -0,0 +1,112 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { __, sprintf } from '~/locale';
+
+export default {
+ name: 'ReleaseBlockFooter',
+ components: {
+ Icon,
+ GlLink,
+ UserAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ commit: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ commitPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tagName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tagPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ author: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ releasedAt: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ releasedAtTimeAgo() {
+ return this.timeFormated(this.releasedAt);
+ },
+ userImageAltDescription() {
+ return this.author && this.author.username
+ ? sprintf(__("%{username}'s avatar"), { username: this.author.username })
+ : null;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="commit" class="float-left mr-3 d-flex align-items-center js-commit-info">
+ <icon ref="commitIcon" name="commit" class="mr-1" />
+ <div v-gl-tooltip.bottom :title="commit.title">
+ <gl-link v-if="commitPath" :href="commitPath">
+ {{ commit.short_id }}
+ </gl-link>
+ <span v-else>{{ commit.short_id }}</span>
+ </div>
+ </div>
+
+ <div v-if="tagName" class="float-left mr-3 d-flex align-items-center js-tag-info">
+ <icon name="tag" class="mr-1" />
+ <div v-gl-tooltip.bottom :title="__('Tag')">
+ <gl-link v-if="tagPath" :href="tagPath">
+ {{ tagName }}
+ </gl-link>
+ <span v-else>{{ tagName }}</span>
+ </div>
+ </div>
+
+ <div
+ v-if="releasedAt || author"
+ class="float-left d-flex align-items-center js-author-date-info"
+ >
+ <span class="text-secondary">{{ __('Created') }}&nbsp;</span>
+ <template v-if="releasedAt">
+ <span
+ v-gl-tooltip.bottom
+ :title="tooltipTitle(releasedAt)"
+ class="text-secondary flex-shrink-0"
+ >
+ {{ releasedAtTimeAgo }}&nbsp;
+ </span>
+ </template>
+
+ <div v-if="author" class="d-flex">
+ <span class="text-secondary">{{ __('by') }}&nbsp;</span>
+ <user-avatar-link
+ :link-href="author.web_url"
+ :img-src="author.avatar_url"
+ :img-alt="userImageAltDescription"
+ :tooltip-text="author.username"
+ tooltip-placement="bottom"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 386653b9444..62a9338b864 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -50,6 +50,6 @@ export default {
}"
class="report-block-list-icon"
>
- <icon :name="iconName" :size="statusIconSize" />
+ <icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
</div>
</template>
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index f3f7d2648a8..3c8a9e6ebef 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -46,6 +46,7 @@ export default {
<li
:class="{ 'is-dismissed': issue.isDismissed }"
class="report-block-list-issue align-items-center"
+ data-qa-selector="report_item_row"
>
<issue-status-icon
v-if="showReportSectionStatusIcon"
diff --git a/app/assets/javascripts/repository/components/directory_download_links.vue b/app/assets/javascripts/repository/components/directory_download_links.vue
new file mode 100644
index 00000000000..dffadade082
--- /dev/null
+++ b/app/assets/javascripts/repository/components/directory_download_links.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ currentPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ links: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ normalizedLinks() {
+ return this.links.map(link => ({
+ text: link.text,
+ path: `${link.path}?path=${this.currentPath}`,
+ }));
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="border-top pt-1 mt-1">
+ <h5 class="m-0 dropdown-bold-header">{{ __('Download this directory') }}</h5>
+ <div class="dropdown-menu-content">
+ <div class="btn-group ml-0 w-100">
+ <gl-link
+ v-for="(link, index) in normalizedLinks"
+ :key="index"
+ :href="link.path"
+ :class="{ 'btn-primary': index === 0 }"
+ class="btn btn-xs"
+ >
+ {{ link.text }}
+ </gl-link>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 19a2db2db25..70678b0db37 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import defaultAvatarUrl from 'images/no_avatar.png';
import { sprintf, s__ } from '~/locale';
import Icon from '../../vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -38,7 +39,14 @@ export default {
path: this.currentPath.replace(/^\//, ''),
};
},
- update: data => data.project.repository.tree.lastCommit,
+ update: data => {
+ const pipelines = data.project.repository.tree.lastCommit.pipelines.edges;
+
+ return {
+ ...data.project.repository.tree.lastCommit,
+ pipeline: pipelines.length && pipelines[0].node,
+ };
+ },
context: {
isSingleRequest: true,
},
@@ -61,7 +69,7 @@ export default {
computed: {
statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), {
- commitText: this.commit.latestPipeline.detailedStatus.text,
+ commitText: this.commit.pipeline.detailedStatus.text,
});
},
isLoading() {
@@ -76,12 +84,13 @@ export default {
this.showDescription = !this.showDescription;
},
},
+ defaultAvatarUrl,
};
</script>
<template>
<div class="info-well d-none d-sm-flex project-last-commit commit p-3">
- <gl-loading-icon v-if="isLoading" size="md" class="mx-auto" />
+ <gl-loading-icon v-if="isLoading" size="md" class="m-auto" />
<template v-else>
<user-avatar-link
v-if="commit.author"
@@ -90,6 +99,9 @@ export default {
:img-size="40"
class="avatar-cell"
/>
+ <span v-else class="avatar-cell user-avatar-link">
+ <img :src="$options.defaultAvatarUrl" width="40" height="40" class="avatar s40" />
+ </span>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link :href="commit.webUrl" class="commit-row-message item-title">
@@ -102,7 +114,7 @@ export default {
class="text-expander"
@click="toggleShowDescription"
>
- <icon name="ellipsis_h" />
+ <icon name="ellipsis_h" :size="10" />
</gl-button>
<div class="committer">
<gl-link
@@ -112,12 +124,15 @@ export default {
>
{{ commit.author.name }}
</gl-link>
+ <template v-else>
+ {{ commit.authorName }}
+ </template>
{{ s__('LastCommit|authored') }}
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
</div>
<pre
v-if="commit.description"
- v-show="showDescription"
+ :class="{ 'd-block': showDescription }"
class="commit-row-description append-bottom-8"
>
{{ commit.description }}
@@ -125,19 +140,20 @@ export default {
</div>
<div class="commit-actions flex-row">
<div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div>
- <gl-link
- v-if="commit.latestPipeline"
- v-gl-tooltip
- :href="commit.latestPipeline.detailedStatus.detailsPath"
- :title="statusTitle"
- class="js-commit-pipeline"
- >
- <ci-icon
- :status="commit.latestPipeline.detailedStatus"
- :size="24"
- :aria-label="statusTitle"
- />
- </gl-link>
+ <div v-if="commit.pipeline" class="ci-status-link">
+ <gl-link
+ v-gl-tooltip.left
+ :href="commit.pipeline.detailedStatus.detailsPath"
+ :title="statusTitle"
+ class="js-commit-pipeline"
+ >
+ <ci-icon
+ :status="commit.pipeline.detailedStatus"
+ :size="24"
+ :aria-label="statusTitle"
+ />
+ </gl-link>
+ </div>
<div class="commit-sha-group d-flex">
<div class="label label-monospace monospace">
{{ showCommitId }}
@@ -153,3 +169,9 @@ export default {
</template>
</div>
</template>
+
+<style scoped>
+.commit {
+ min-height: 4.75rem;
+}
+</style>
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
new file mode 100644
index 00000000000..7f974838359
--- /dev/null
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlLink, GlLoadingIcon } from '@gitlab/ui';
+import getReadmeQuery from '../../queries/getReadme.query.graphql';
+
+export default {
+ apollo: {
+ readme: {
+ query: getReadmeQuery,
+ variables() {
+ return {
+ url: this.blob.webUrl,
+ };
+ },
+ loadingKey: 'loading',
+ },
+ },
+ components: {
+ GlLink,
+ GlLoadingIcon,
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ readme: null,
+ loading: 0,
+ };
+ },
+};
+</script>
+
+<template>
+ <article class="file-holder limited-width-container readme-holder">
+ <div class="file-title">
+ <i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
+ <gl-link :href="blob.webUrl">
+ <strong>{{ blob.name }}</strong>
+ </gl-link>
+ </div>
+ <div class="blob-viewer">
+ <gl-loading-icon v-if="loading > 0" size="md" class="my-4 mx-auto" />
+ <div v-else-if="readme" v-html="readme.html"></div>
+ </div>
+ </article>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 610c7e8d99e..8f2e9264bca 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,19 +1,15 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlSkeletonLoading } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
-import getFiles from '../../queries/getFiles.query.graphql';
import getProjectPath from '../../queries/getProjectPath.query.graphql';
import TableHeader from './header.vue';
import TableRow from './row.vue';
import ParentRow from './parent_row.vue';
-const PAGE_SIZE = 100;
-
export default {
components: {
- GlLoadingIcon,
+ GlSkeletonLoading,
TableHeader,
TableRow,
ParentRow,
@@ -29,86 +25,39 @@ export default {
type: String,
required: true,
},
+ entries: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
projectPath: '',
- nextPageCursor: '',
- entries: {
- trees: [],
- submodules: [],
- blobs: [],
- },
- isLoadingFiles: false,
};
},
computed: {
tableCaption() {
+ if (this.isLoading) {
+ return sprintf(
+ __(
+ 'Loading files, directories, and submodules in the path %{path} for commit reference %{ref}',
+ ),
+ { path: this.path, ref: this.ref },
+ );
+ }
+
return sprintf(
__('Files, directories, and submodules in the path %{path} for commit reference %{ref}'),
{ path: this.path, ref: this.ref },
);
},
showParentRow() {
- return !this.isLoadingFiles && ['', '/'].indexOf(this.path) === -1;
- },
- },
- watch: {
- $route: function routeChange() {
- this.entries.trees = [];
- this.entries.submodules = [];
- this.entries.blobs = [];
- this.nextPageCursor = '';
- this.fetchFiles();
- },
- },
- mounted() {
- // We need to wait for `ref` and `projectPath` to be set
- this.$nextTick(() => this.fetchFiles());
- },
- methods: {
- fetchFiles() {
- this.isLoadingFiles = true;
-
- return this.$apollo
- .query({
- query: getFiles,
- variables: {
- projectPath: this.projectPath,
- ref: this.ref,
- path: this.path || '/',
- nextPageCursor: this.nextPageCursor,
- pageSize: PAGE_SIZE,
- },
- })
- .then(({ data }) => {
- if (!data) return;
-
- const pageInfo = this.hasNextPage(data.project.repository.tree);
-
- this.isLoadingFiles = false;
- this.entries = Object.keys(this.entries).reduce(
- (acc, key) => ({
- ...acc,
- [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
- }),
- {},
- );
-
- if (pageInfo && pageInfo.hasNextPage) {
- this.nextPageCursor = pageInfo.endCursor;
- this.fetchFiles();
- }
- })
- .catch(() => createFlash(__('An error occurred while fetching folder content.')));
- },
- normalizeData(key, data) {
- return this.entries[key].concat(data.map(({ node }) => node));
- },
- hasNextPage(data) {
- return []
- .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
- .find(({ hasNextPage }) => hasNextPage);
+ return !this.isLoading && ['', '/'].indexOf(this.path) === -1;
},
},
};
@@ -117,12 +66,7 @@ export default {
<template>
<div class="tree-content-holder">
<div class="table-holder bordered-box">
- <table class="table tree-table qa-file-tree" aria-live="polite">
- <caption class="sr-only">
- {{
- tableCaption
- }}
- </caption>
+ <table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite">
<table-header v-once />
<tbody>
<parent-row v-show="showParentRow" :commit-ref="ref" :path="path" />
@@ -131,6 +75,7 @@ export default {
v-for="entry in val"
:id="entry.id"
:key="`${entry.flatPath}-${entry.id}`"
+ :sha="entry.sha"
:project-path="projectPath"
:current-path="path"
:name="entry.name"
@@ -141,9 +86,15 @@ export default {
:lfs-oid="entry.lfsOid"
/>
</template>
+ <template v-if="isLoading">
+ <tr v-for="i in 5" :key="i" aria-hidden="true">
+ <td><gl-skeleton-loading :lines="1" class="h-auto" /></td>
+ <td><gl-skeleton-loading :lines="1" class="h-auto" /></td>
+ <td><gl-skeleton-loading :lines="1" class="ml-auto h-auto w-50" /></td>
+ </tr>
+ </template>
</tbody>
</table>
- <gl-loading-icon v-show="isLoadingFiles" class="my-3" size="md" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 171841178a3..cf0457a2abf 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,7 +1,8 @@
<script>
-import { GlBadge, GlLink, GlSkeletonLoading } from '@gitlab/ui';
+import { GlBadge, GlLink, GlSkeletonLoading, GlTooltipDirective } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import { getIconName } from '../../utils/icon';
import getRefMixin from '../../mixins/get_ref';
import getCommit from '../../queries/getCommit.query.graphql';
@@ -12,6 +13,10 @@ export default {
GlLink,
GlSkeletonLoading,
TimeagoTooltip,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
apollo: {
commit: {
@@ -32,6 +37,10 @@ export default {
type: String,
required: true,
},
+ sha: {
+ type: String,
+ required: true,
+ },
projectPath: {
type: String,
required: true,
@@ -93,15 +102,20 @@ export default {
return this.path.replace(new RegExp(`^${this.currentPath}/`), '');
},
shortSha() {
- return this.id.slice(0, 8);
+ return this.sha.slice(0, 8);
+ },
+ hasLockLabel() {
+ return this.commit && this.commit.lockLabel;
},
},
methods: {
- openRow() {
- if (this.isFolder) {
+ openRow(e) {
+ if (e.target.tagName === 'A') return;
+
+ if (this.isFolder && !e.metaKey) {
this.$router.push(this.routerLinkTo);
} else {
- visitUrl(this.url);
+ visitUrl(this.url, e.metaKey);
}
},
},
@@ -120,15 +134,28 @@ export default {
<template v-if="isSubmodule">
@ <gl-link :href="submoduleTreeUrl" class="commit-sha">{{ shortSha }}</gl-link>
</template>
+ <icon
+ v-if="hasLockLabel"
+ v-gl-tooltip
+ :title="commit.lockLabel"
+ name="lock"
+ :size="12"
+ class="ml-2 vertical-align-middle"
+ />
</td>
<td class="d-none d-sm-table-cell tree-commit">
- <gl-link v-if="commit" :href="commit.commitPath" class="str-truncated-100 tree-commit-link">
+ <gl-link
+ v-if="commit"
+ :href="commit.commitPath"
+ :title="commit.message"
+ class="str-truncated-100 tree-commit-link"
+ >
{{ commit.message }}
</gl-link>
<gl-skeleton-loading v-else :lines="1" class="h-auto" />
</td>
<td class="tree-time-ago text-right">
- <timeago-tooltip v-if="commit" :time="commit.committedDate" tooltip-placement="bottom" />
+ <timeago-tooltip v-if="commit" :time="commit.committedDate" />
<gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" />
</td>
</tr>
diff --git a/app/assets/javascripts/repository/components/tree_action_link.vue b/app/assets/javascripts/repository/components/tree_action_link.vue
new file mode 100644
index 00000000000..72764f3ccc9
--- /dev/null
+++ b/app/assets/javascripts/repository/components/tree_action_link.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ text: {
+ type: String,
+ required: true,
+ },
+ cssClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-link :href="path" :class="cssClass" class="btn">{{ text }}</gl-link>
+</template>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
new file mode 100644
index 00000000000..949e653fc8f
--- /dev/null
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -0,0 +1,115 @@
+<script>
+import createFlash from '~/flash';
+import { __ } from '../../locale';
+import FileTable from './table/index.vue';
+import getRefMixin from '../mixins/get_ref';
+import getFiles from '../queries/getFiles.query.graphql';
+import getProjectPath from '../queries/getProjectPath.query.graphql';
+import FilePreview from './preview/index.vue';
+import { readmeFile } from '../utils/readme';
+
+const PAGE_SIZE = 100;
+
+export default {
+ components: {
+ FileTable,
+ FilePreview,
+ },
+ mixins: [getRefMixin],
+ apollo: {
+ projectPath: {
+ query: getProjectPath,
+ },
+ },
+ props: {
+ path: {
+ type: String,
+ required: false,
+ default: '/',
+ },
+ },
+ data() {
+ return {
+ projectPath: '',
+ nextPageCursor: '',
+ entries: {
+ trees: [],
+ submodules: [],
+ blobs: [],
+ },
+ isLoadingFiles: false,
+ };
+ },
+ computed: {
+ readme() {
+ return readmeFile(this.entries.blobs);
+ },
+ },
+
+ watch: {
+ $route: function routeChange() {
+ this.entries.trees = [];
+ this.entries.submodules = [];
+ this.entries.blobs = [];
+ this.nextPageCursor = '';
+ this.fetchFiles();
+ },
+ },
+ mounted() {
+ // We need to wait for `ref` and `projectPath` to be set
+ this.$nextTick(() => this.fetchFiles());
+ },
+ methods: {
+ fetchFiles() {
+ this.isLoadingFiles = true;
+
+ return this.$apollo
+ .query({
+ query: getFiles,
+ variables: {
+ projectPath: this.projectPath,
+ ref: this.ref,
+ path: this.path || '/',
+ nextPageCursor: this.nextPageCursor,
+ pageSize: PAGE_SIZE,
+ },
+ })
+ .then(({ data }) => {
+ if (!data) return;
+
+ const pageInfo = this.hasNextPage(data.project.repository.tree);
+
+ this.isLoadingFiles = false;
+ this.entries = Object.keys(this.entries).reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
+ }),
+ {},
+ );
+
+ if (pageInfo && pageInfo.hasNextPage) {
+ this.nextPageCursor = pageInfo.endCursor;
+ this.fetchFiles();
+ }
+ })
+ .catch(() => createFlash(__('An error occurred while fetching folder content.')));
+ },
+ normalizeData(key, data) {
+ return this.entries[key].concat(data.map(({ node }) => node));
+ },
+ hasNextPage(data) {
+ return []
+ .concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
+ .find(({ hasNextPage }) => hasNextPage);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <file-table :path="path" :entries="entries" :is-loading="isLoadingFiles" />
+ <file-preview v-if="readme" :blob="readme" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 6cb253c8169..6936c08d852 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import axios from '~/lib/utils/axios_utils';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
import { fetchLogsTree } from './log_tree';
@@ -27,6 +28,11 @@ const defaultClient = createDefaultClient(
});
});
},
+ readme(_, { url }) {
+ return axios
+ .get(url, { params: { viewer: 'rich', format: 'json' } })
+ .then(({ data }) => ({ ...data, __typename: 'ReadmeFile' }));
+ },
},
},
{
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index f9727960040..d826f209815 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -3,13 +3,18 @@ import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import LastCommit from './components/last_commit.vue';
+import TreeActionLink from './components/tree_action_link.vue';
+import DirectoryDownloadLinks from './components/directory_download_links.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
import { parseBoolean } from '../lib/utils/common_utils';
+import { webIDEUrl } from '../lib/utils/url_utility';
+import { __ } from '../locale';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
- const { projectPath, projectShortPath, ref, fullName } = el.dataset;
+ const { dataset } = el;
+ const { projectPath, projectShortPath, ref, fullName } = dataset;
const router = createRouter(projectPath, ref);
apolloProvider.clients.defaultClient.cache.writeData({
@@ -22,19 +27,7 @@ export default function setupVueRepositoryList() {
});
router.afterEach(({ params: { pathMatch } }) => {
- const isRoot = pathMatch === undefined || pathMatch === '/';
-
setTitle(pathMatch, ref, fullName);
-
- if (!isRoot) {
- document
- .querySelectorAll('.js-keep-hidden-on-navigation')
- .forEach(elem => elem.classList.add('hidden'));
- }
-
- document
- .querySelectorAll('.js-hide-on-navigation')
- .forEach(elem => elem.classList.toggle('hidden', !isRoot));
});
const breadcrumbEl = document.getElementById('js-repo-breadcrumb');
@@ -88,7 +81,68 @@ export default function setupVueRepositoryList() {
},
});
- return new Vue({
+ const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
+ const { historyLink } = treeHistoryLinkEl.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: treeHistoryLinkEl,
+ router,
+ render(h) {
+ return h(TreeActionLink, {
+ props: {
+ path: historyLink + (this.$route.params.pathMatch || '/'),
+ text: __('History'),
+ },
+ });
+ },
+ });
+
+ const webIdeLinkEl = document.getElementById('js-tree-web-ide-link');
+
+ if (webIdeLinkEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: webIdeLinkEl,
+ router,
+ render(h) {
+ return h(TreeActionLink, {
+ props: {
+ path: webIDEUrl(`/${projectPath}/edit/${ref}/-${this.$route.params.pathMatch || '/'}`),
+ text: __('Web IDE'),
+ cssClass: 'qa-web-ide-button',
+ },
+ });
+ },
+ });
+ }
+
+ const directoryDownloadLinks = document.getElementById('js-directory-downloads');
+
+ if (directoryDownloadLinks) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: directoryDownloadLinks,
+ router,
+ render(h) {
+ const currentPath = this.$route.params.pathMatch || '/';
+
+ if (currentPath !== '/') {
+ return h(DirectoryDownloadLinks, {
+ props: {
+ currentPath: currentPath.replace(/^\//, ''),
+ links: JSON.parse(directoryDownloadLinks.dataset.links),
+ },
+ });
+ }
+
+ return null;
+ },
+ });
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
el,
router,
apolloProvider,
@@ -96,4 +150,6 @@ export default function setupVueRepositoryList() {
return h(App);
},
});
+
+ return { router, data: dataset };
}
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 2c19aca2397..5bf30e625a0 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -1,4 +1,5 @@
import axios from '~/lib/utils/axios_utils';
+import { normalizeData } from 'ee_else_ce/repository/utils/commit';
import getCommits from './queries/getCommits.query.graphql';
import getProjectPath from './queries/getProjectPath.query.graphql';
import getRef from './queries/getRef.query.graphql';
@@ -6,18 +7,6 @@ import getRef from './queries/getRef.query.graphql';
let fetchpromise;
let resolvers = [];
-export function normalizeData(data) {
- return data.map(d => ({
- sha: d.commit.id,
- message: d.commit.message,
- committedDate: d.commit.committed_date,
- commitPath: d.commit_path,
- fileName: d.file_name,
- type: d.type,
- __typename: 'LogTreeCommit',
- }));
-}
-
export function resolveCommit(commits, { resolve, entry }) {
const commit = commits.find(c => c.fileName === entry.name && c.type === entry.type);
@@ -37,9 +26,12 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
const { ref } = client.readQuery({ query: getRef });
fetchpromise = axios
- .get(`${gon.gitlab_url}/${projectPath}/refs/${ref}/logs_tree${path ? `/${path}` : ''}`, {
- params: { format: 'json', offset },
- })
+ .get(
+ `${gon.relative_url_root}/${projectPath}/refs/${ref}/logs_tree/${path.replace(/^\//, '')}`,
+ {
+ params: { format: 'json', offset },
+ },
+ )
.then(({ data, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
const { commits } = client.readQuery({ query: getCommits });
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
index 2d92e9174ca..29786bf4ec8 100644
--- a/app/assets/javascripts/repository/pages/index.vue
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -1,18 +1,25 @@
<script>
-import FileTable from '../components/table/index.vue';
+import TreePage from './tree.vue';
+import { updateElementsVisibility } from '../utils/dom';
export default {
components: {
- FileTable,
+ TreePage,
},
- data() {
- return {
- ref: '',
- };
+ mounted() {
+ this.updateProjectElements(true);
+ },
+ beforeDestroy() {
+ this.updateProjectElements(false);
+ },
+ methods: {
+ updateProjectElements(isShow) {
+ updateElementsVisibility('.js-show-on-project-root', isShow);
+ },
},
};
</script>
<template>
- <file-table path="/" />
+ <tree-page path="/" />
</template>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
index 3b898d1aa91..dd4d437f4dd 100644
--- a/app/assets/javascripts/repository/pages/tree.vue
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -1,9 +1,10 @@
<script>
-import FileTable from '../components/table/index.vue';
+import TreeContent from '../components/tree_content.vue';
+import { updateElementsVisibility } from '../utils/dom';
export default {
components: {
- FileTable,
+ TreeContent,
},
props: {
path: {
@@ -12,9 +13,26 @@ export default {
default: '/',
},
},
+ computed: {
+ isRoot() {
+ return this.path === '/';
+ },
+ },
+ watch: {
+ isRoot: {
+ immediate: true,
+ handler: 'updateElements',
+ },
+ },
+ methods: {
+ updateElements(isRoot) {
+ updateElementsVisibility('.js-show-on-root', isRoot);
+ updateElementsVisibility('.js-hide-on-root', !isRoot);
+ },
+ },
};
</script>
<template>
- <file-table :path="path" />
+ <tree-content :path="path" />
</template>
diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql
new file mode 100644
index 00000000000..9bb13c475c7
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql
@@ -0,0 +1,8 @@
+fragment TreeEntryCommit on LogTreeCommit {
+ sha
+ message
+ committedDate
+ commitPath
+ fileName
+ type
+}
diff --git a/app/assets/javascripts/repository/queries/getCommit.query.graphql b/app/assets/javascripts/repository/queries/getCommit.query.graphql
index e2a2d831e47..e4aeaaff8fe 100644
--- a/app/assets/javascripts/repository/queries/getCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/getCommit.query.graphql
@@ -1,10 +1,7 @@
+#import "ee_else_ce/repository/queries/commit.fragment.graphql"
+
query getCommit($fileName: String!, $type: String!, $path: String!) {
commit(path: $path, fileName: $fileName, type: $type) @client {
- sha
- message
- committedDate
- commitPath
- fileName
- type
+ ...TreeEntryCommit
}
}
diff --git a/app/assets/javascripts/repository/queries/getCommits.query.graphql b/app/assets/javascripts/repository/queries/getCommits.query.graphql
index df9e67cc440..0976b8f32d7 100644
--- a/app/assets/javascripts/repository/queries/getCommits.query.graphql
+++ b/app/assets/javascripts/repository/queries/getCommits.query.graphql
@@ -1,10 +1,7 @@
+#import "ee_else_ce/repository/queries/commit.fragment.graphql"
+
query getCommits {
commits @client {
- sha
- message
- committedDate
- commitPath
- fileName
- type
+ ...TreeEntryCommit
}
}
diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql
index c4814f8e63a..2aaf5066b4a 100644
--- a/app/assets/javascripts/repository/queries/getFiles.query.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql
@@ -2,6 +2,7 @@
fragment TreeEntry on Entry {
id
+ sha
name
flatPath
type
diff --git a/app/assets/javascripts/repository/queries/getReadme.query.graphql b/app/assets/javascripts/repository/queries/getReadme.query.graphql
new file mode 100644
index 00000000000..cf056330133
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getReadme.query.graphql
@@ -0,0 +1,5 @@
+query getReadme($url: String!) {
+ readme(url: $url) @client {
+ html
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
index 71c1bf12749..9be025afe39 100644
--- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
@@ -5,22 +5,27 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
lastCommit {
sha
title
- message
+ description
webUrl
authoredDate
+ authorName
author {
name
avatarUrl
webUrl
}
signatureHtml
- latestPipeline {
- detailedStatus {
- detailsPath
- icon
- tooltip
- text
- group
+ pipelines(ref: $ref, first: 1) {
+ edges {
+ node {
+ detailedStatus {
+ detailsPath
+ icon
+ tooltip
+ text
+ group
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index 9322c81ab97..ebf0a7091ea 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -16,7 +16,7 @@ export default function createRouter(base, baseRef) {
name: 'treePath',
component: TreePage,
props: route => ({
- path: route.params.pathMatch && route.params.pathMatch.replace(/^\//, ''),
+ path: route.params.pathMatch && (route.params.pathMatch.replace(/^\//, '') || '/'),
}),
},
{
diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js
new file mode 100644
index 00000000000..6c204b57b37
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/commit.js
@@ -0,0 +1,13 @@
+// eslint-disable-next-line import/prefer-default-export
+export function normalizeData(data, extra = () => {}) {
+ return data.map(d => ({
+ sha: d.commit.id,
+ message: d.commit.message,
+ committedDate: d.commit.committed_date,
+ commitPath: d.commit_path,
+ fileName: d.file_name,
+ type: d.type,
+ __typename: 'LogTreeCommit',
+ ...extra(d),
+ }));
+}
diff --git a/app/assets/javascripts/repository/utils/dom.js b/app/assets/javascripts/repository/utils/dom.js
new file mode 100644
index 00000000000..963e6fc0bc4
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/dom.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export const updateElementsVisibility = (selector, isVisible) => {
+ document.querySelectorAll(selector).forEach(elem => elem.classList.toggle('hidden', !isVisible));
+};
diff --git a/app/assets/javascripts/repository/utils/readme.js b/app/assets/javascripts/repository/utils/readme.js
new file mode 100644
index 00000000000..e43b2bdc33a
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/readme.js
@@ -0,0 +1,21 @@
+const MARKDOWN_EXTENSIONS = ['mdown', 'mkd', 'mkdn', 'md', 'markdown'];
+const ASCIIDOC_EXTENSIONS = ['adoc', 'ad', 'asciidoc'];
+const OTHER_EXTENSIONS = ['textile', 'rdoc', 'org', 'creole', 'wiki', 'mediawiki', 'rst'];
+const EXTENSIONS = [...MARKDOWN_EXTENSIONS, ...ASCIIDOC_EXTENSIONS, ...OTHER_EXTENSIONS];
+const PLAIN_FILENAMES = ['readme', 'index'];
+const FILE_REGEXP = new RegExp(
+ `^(${PLAIN_FILENAMES.join('|')})(.(${EXTENSIONS.join('|')}))?$`,
+ 'i',
+);
+const PLAIN_FILE_REGEXP = new RegExp(`^(${PLAIN_FILENAMES.join('|')})`, 'i');
+const EXTENSIONS_REGEXP = new RegExp(`.(${EXTENSIONS.join('|')})$`, 'i');
+
+// eslint-disable-next-line import/prefer-default-export
+export const readmeFile = blobs => {
+ const readMeFiles = blobs.filter(f => f.name.search(FILE_REGEXP) !== -1);
+
+ const previewableReadme = readMeFiles.find(f => f.name.search(EXTENSIONS_REGEXP) !== -1);
+ const plainReadme = readMeFiles.find(f => f.name.search(PLAIN_FILE_REGEXP) !== -1);
+
+ return previewableReadme || plainReadme;
+};
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
index 87d54c01200..ff16fbdd420 100644
--- a/app/assets/javascripts/repository/utils/title.js
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -1,10 +1,14 @@
+const DEFAULT_TITLE = '· GitLab';
// eslint-disable-next-line import/prefer-default-export
export const setTitle = (pathMatch, ref, project) => {
- if (!pathMatch) return;
+ if (!pathMatch) {
+ document.title = `${project} ${DEFAULT_TITLE}`;
+ return;
+ }
const path = pathMatch.replace(/^\//, '');
const isEmpty = path === '';
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project}`;
+ document.title = `${isEmpty ? 'Files' : path} · ${ref} · ${project} ${DEFAULT_TITLE}`;
};
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 87454ee056f..fa5649679d7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, consistent-return, one-var, no-else-return, no-param-reassign */
+/* eslint-disable func-names, consistent-return, no-else-return, no-param-reassign */
import $ from 'jquery';
import _ from 'underscore';
@@ -44,12 +44,11 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
- var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
+ const $this = $(this);
+ const isExpanded = $this.find('i').hasClass('fa-angle-double-right');
+ const tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
+ const $allGutterToggleIcons = $('.js-sidebar-toggle i');
e.preventDefault();
- $this = $(this);
- isExpanded = $this.find('i').hasClass('fa-angle-double-right');
- tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
- $allGutterToggleIcons = $('.js-sidebar-toggle i');
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
@@ -77,15 +76,9 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
};
Sidebar.prototype.toggleTodo = function(e) {
- var $this, ajaxType, url;
- $this = $(e.currentTarget);
- ajaxType = $this.data('deletePath') ? 'delete' : 'post';
-
- if ($this.data('deletePath')) {
- url = String($this.data('deletePath'));
- } else {
- url = String($this.data('createPath'));
- }
+ const $this = $(e.currentTarget);
+ const ajaxType = $this.data('deletePath') ? 'delete' : 'post';
+ const url = String($this.data('deletePath') || $this.data('createPath'));
$this.tooltip('hide');
@@ -141,13 +134,12 @@ Sidebar.prototype.todoUpdateDone = function(data) {
};
Sidebar.prototype.sidebarDropdownLoading = function() {
- var $loading, $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this)
+ const $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
- i = $sidebarCollapsedIcon.find('i');
- $loading = $('<i class="fa fa-spinner fa-spin"></i>');
+ const img = $sidebarCollapsedIcon.find('img');
+ const i = $sidebarCollapsedIcon.find('i');
+ const $loading = $('<i class="fa fa-spinner fa-spin"></i>');
if (img.length) {
img.before($loading);
return img.hide();
@@ -158,13 +150,12 @@ Sidebar.prototype.sidebarDropdownLoading = function() {
};
Sidebar.prototype.sidebarDropdownLoaded = function() {
- var $sidebarCollapsedIcon, i, img;
- $sidebarCollapsedIcon = $(this)
+ const $sidebarCollapsedIcon = $(this)
.closest('.block')
.find('.sidebar-collapsed-icon');
- img = $sidebarCollapsedIcon.find('img');
+ const img = $sidebarCollapsedIcon.find('img');
$sidebarCollapsedIcon.find('i.fa-spin').remove();
- i = $sidebarCollapsedIcon.find('i');
+ const i = $sidebarCollapsedIcon.find('i');
if (img.length) {
return img.show();
} else {
@@ -173,19 +164,17 @@ Sidebar.prototype.sidebarDropdownLoaded = function() {
};
Sidebar.prototype.sidebarCollapseClicked = function(e) {
- var $block, sidebar;
if ($(e.currentTarget).hasClass('dont-change-state')) {
return;
}
- sidebar = e.data;
+ const sidebar = e.data;
e.preventDefault();
- $block = $(this).closest('.block');
+ const $block = $(this).closest('.block');
return sidebar.openDropdown($block);
};
Sidebar.prototype.openDropdown = function(blockOrName) {
- var $block;
- $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
+ const $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
if (!this.isOpen()) {
this.setCollapseAfterUpdate($block);
this.toggleSidebar('open');
@@ -204,10 +193,9 @@ Sidebar.prototype.setCollapseAfterUpdate = function($block) {
};
Sidebar.prototype.onSidebarDropdownHidden = function(e) {
- var $block, sidebar;
- sidebar = e.data;
+ const sidebar = e.data;
e.preventDefault();
- $block = $(e.target).closest('.block');
+ const $block = $(e.target).closest('.block');
return sidebar.sidebarDropdownHidden($block);
};
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index f6722ff7bca..8d888a574d8 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-return-assign, one-var, no-var, consistent-return, class-methods-use-this, no-lonely-if, vars-on-top */
+/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
import { escape, throttle } from 'underscore';
@@ -29,14 +29,14 @@ const KEYCODE = {
};
function setSearchOptions() {
- var $projectOptionsDataEl = $('.js-search-project-options');
- var $groupOptionsDataEl = $('.js-search-group-options');
- var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+ const $projectOptionsDataEl = $('.js-search-project-options');
+ const $groupOptionsDataEl = $('.js-search-group-options');
+ const $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
- var projectPath = $projectOptionsDataEl.data('projectPath');
+ const projectPath = $projectOptionsDataEl.data('projectPath');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
@@ -49,7 +49,7 @@ function setSearchOptions() {
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
- var groupPath = $groupOptionsDataEl.data('groupPath');
+ const groupPath = $groupOptionsDataEl.data('groupPath');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
@@ -95,10 +95,9 @@ export class SearchAutocomplete {
this.createAutocomplete();
}
- this.searchInput.addClass('disabled');
- this.saveTextLength();
this.bindEvents();
this.dropdownToggle.dropdown();
+ this.searchInput.addClass('js-autocomplete-disabled');
}
// Finds an element inside wrapper element
@@ -107,7 +106,7 @@ export class SearchAutocomplete {
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
- this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ this.onSearchInputChange = this.onSearchInputChange.bind(this);
this.setScrollFade = this.setScrollFade.bind(this);
}
getElement(selector) {
@@ -118,10 +117,6 @@ export class SearchAutocomplete {
return (this.originalState = this.serializeState());
}
- saveTextLength() {
- return (this.lastTextLength = this.searchInput.val().length);
- }
-
createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
@@ -318,12 +313,16 @@ export class SearchAutocomplete {
}
bindEvents() {
- this.searchInput.on('keydown', this.onSearchInputKeyDown);
+ this.searchInput.on('input', this.onSearchInputChange);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
+
+ this.searchInput.on('click', e => {
+ e.stopPropagation();
+ });
}
enableAutocomplete() {
@@ -338,47 +337,23 @@ export class SearchAutocomplete {
if (!this.dropdown.hasClass('show')) {
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
- return this.searchInput.removeClass('disabled');
+ return this.searchInput.removeClass('js-autocomplete-disabled');
}
}
- // Saves last length of the entered text
- onSearchInputKeyDown() {
- return this.saveTextLength();
+ onSearchInputChange() {
+ this.enableAutocomplete();
}
onSearchInputKeyUp(e) {
switch (e.keyCode) {
- case KEYCODE.BACKSPACE:
- // When removing the last character and no badge is present
- if (this.lastTextLength === 1) {
- this.disableAutocomplete();
- }
- // When removing any character from existin value
- if (this.lastTextLength > 1) {
- this.enableAutocomplete();
- }
- break;
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
this.disableAutocomplete();
break;
- case KEYCODE.UP:
- case KEYCODE.DOWN:
- return;
default:
- // Handle the case when deleting the input value other than backspace
- // e.g. Pressing ctrl + backspace or ctrl + x
- if (this.searchInput.val() === '') {
- this.disableAutocomplete();
- } else {
- // We should display the menu only when input is not empty
- if (e.keyCode !== KEYCODE.ENTER) {
- this.enableAutocomplete();
- }
- }
}
this.wrap.toggleClass('has-value', Boolean(e.target.value));
}
@@ -412,36 +387,33 @@ export class SearchAutocomplete {
}
restoreOriginalState() {
- var i, input, inputs, len;
- inputs = Object.keys(this.originalState);
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
+ const inputs = Object.keys(this.originalState);
+ for (let i = 0, len = inputs.length; i < len; i += 1) {
+ const input = inputs[i];
this.getElement(`#${input}`).val(this.originalState[input]);
}
}
resetSearchState() {
- var i, input, inputs, len, results;
- inputs = Object.keys(this.originalState);
- results = [];
- for (i = 0, len = inputs.length; i < len; i += 1) {
- input = inputs[i];
+ const inputs = Object.keys(this.originalState);
+ const results = [];
+ for (let i = 0, len = inputs.length; i < len; i += 1) {
+ const input = inputs[i];
results.push(this.getElement(`#${input}`).val(''));
}
return results;
}
disableAutocomplete() {
- if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('show')) {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('show').trigger('hidden.bs.dropdown');
+ if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
+ this.searchInput.addClass('js-autocomplete-disabled');
+ this.dropdown.dropdown('toggle');
this.restoreMenu();
}
}
restoreMenu() {
- var html;
- html = `<ul><li class="dropdown-menu-empty-item"><a>${__('Loading...')}</a></li></ul>`;
+ const html = `<ul><li class="dropdown-menu-empty-item"><a>${__('Loading...')}</a></li></ul>`;
return this.dropdownContent.html(html);
}
diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/sentry/index.js
index 4dd0175e528..06e4e0aa507 100644
--- a/app/assets/javascripts/raven/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,8 +1,8 @@
-import RavenConfig from './raven_config';
+import SentryConfig from './sentry_config';
const index = function index() {
- RavenConfig.init({
- sentryDsn: gon.sentry_dsn,
+ SentryConfig.init({
+ dsn: gon.sentry_dsn,
currentUserId: gon.current_user_id,
whitelistUrls:
process.env.NODE_ENV === 'production'
@@ -15,7 +15,7 @@ const index = function index() {
},
});
- return RavenConfig;
+ return SentryConfig;
};
index();
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 7259e0df104..bc3b2f16a6a 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,4 +1,4 @@
-import Raven from 'raven-js';
+import * as Sentry from '@sentry/browser';
import $ from 'jquery';
import { __ } from '~/locale';
@@ -26,7 +26,7 @@ const IGNORE_ERRORS = [
'conduitPage',
];
-const IGNORE_URLS = [
+const BLACKLIST_URLS = [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
@@ -43,62 +43,62 @@ const IGNORE_URLS = [
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
];
-const SAMPLE_RATE = 95;
+const SAMPLE_RATE = 0.95;
-const RavenConfig = {
+const SentryConfig = {
IGNORE_ERRORS,
- IGNORE_URLS,
+ BLACKLIST_URLS,
SAMPLE_RATE,
init(options = {}) {
this.options = options;
this.configure();
- this.bindRavenErrors();
+ this.bindSentryErrors();
if (this.options.currentUserId) this.setUser();
},
configure() {
- Raven.config(this.options.sentryDsn, {
- release: this.options.release,
- tags: this.options.tags,
- whitelistUrls: this.options.whitelistUrls,
- environment: this.options.environment,
- ignoreErrors: this.IGNORE_ERRORS,
- ignoreUrls: this.IGNORE_URLS,
- shouldSendCallback: this.shouldSendSample.bind(this),
- }).install();
+ const { dsn, release, tags, whitelistUrls, environment } = this.options;
+ Sentry.init({
+ dsn,
+ release,
+ tags,
+ whitelistUrls,
+ environment,
+ ignoreErrors: this.IGNORE_ERRORS, // TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
+ blacklistUrls: this.BLACKLIST_URLS,
+ sampleRate: SAMPLE_RATE,
+ });
},
setUser() {
- Raven.setUserContext({
+ Sentry.setUser({
id: this.options.currentUserId,
});
},
- bindRavenErrors() {
- $(document).on('ajaxError.raven', this.handleRavenErrors);
+ bindSentryErrors() {
+ $(document).on('ajaxError.sentry', this.handleSentryErrors);
},
- handleRavenErrors(event, req, config, err) {
+ handleSentryErrors(event, req, config, err) {
const error = err || req.statusText;
- const responseText = req.responseText || __('Unknown response text');
+ const { responseText = __('Unknown response text') } = req;
+ const { type, url, data } = config;
+ const { status } = req;
- Raven.captureMessage(error, {
+ Sentry.captureMessage(error, {
extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
+ type,
+ url,
+ data,
+ status,
response: responseText,
error,
event,
},
});
},
-
- shouldSendSample() {
- return Math.random() * 100 <= this.SAMPLE_RATE;
- },
};
-export default RavenConfig;
+export default SentryConfig;
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 95a2c8cce6e..91fe5fc50a9 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -33,6 +33,8 @@ export default {
<div class="block subscriptions">
<subscriptions
:loading="store.isFetching.subscriptions"
+ :project-emails-disabled="store.projectEmailsDisabled"
+ :subscribe-disabled-description="store.subscribeDisabledDescription"
:subscribed="store.subscribed"
@toggleSubscription="onToggleSubscription"
/>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index ea5edb3ce3f..0e489b28593 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -26,6 +26,16 @@ export default {
required: false,
default: false,
},
+ projectEmailsDisabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ subscribeDisabledDescription: {
+ type: String,
+ required: false,
+ default: '',
+ },
subscribed: {
type: Boolean,
required: false,
@@ -42,11 +52,23 @@ export default {
return this.subscribed === null;
},
notificationIcon() {
+ if (this.projectEmailsDisabled) {
+ return ICON_OFF;
+ }
return this.subscribed ? ICON_ON : ICON_OFF;
},
notificationTooltip() {
+ if (this.projectEmailsDisabled) {
+ return this.subscribeDisabledDescription;
+ }
return this.subscribed ? LABEL_ON : LABEL_OFF;
},
+ notificationText() {
+ if (this.projectEmailsDisabled) {
+ return this.subscribeDisabledDescription;
+ }
+ return __('Notifications');
+ },
},
methods: {
/**
@@ -81,6 +103,7 @@ export default {
<template>
<div>
<span
+ ref="tooltip"
v-tooltip
class="sidebar-collapsed-icon"
:title="notificationTooltip"
@@ -96,8 +119,9 @@ export default {
class="sidebar-item-icon is-active"
/>
</span>
- <span class="issuable-header-text hide-collapsed float-left"> {{ __('Notifications') }} </span>
+ <span class="issuable-header-text hide-collapsed float-left"> {{ notificationText }} </span>
<toggle-button
+ v-if="!projectEmailsDisabled"
ref="toggleButton"
:is-loading="showLoadingState"
:value="subscribed"
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 63c4a2a3f84..66f7f9e3c66 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -28,6 +28,8 @@ export default class SidebarStore {
this.moveToProjectId = 0;
this.isLockDialogOpen = false;
this.participants = [];
+ this.projectEmailsDisabled = false;
+ this.subscribeDisabledDescription = '';
this.subscribed = null;
SidebarStore.singleton = this;
@@ -53,6 +55,8 @@ export default class SidebarStore {
}
setSubscriptionsData(data) {
+ this.projectEmailsDisabled = data.project_emails_disabled || false;
+ this.subscribeDisabledDescription = data.subscribe_disabled_description;
this.isFetching.subscriptions = false;
this.subscribed = data.subscribed || false;
}
diff --git a/app/assets/javascripts/sourcegraph/index.js b/app/assets/javascripts/sourcegraph/index.js
new file mode 100644
index 00000000000..796e90bf08e
--- /dev/null
+++ b/app/assets/javascripts/sourcegraph/index.js
@@ -0,0 +1,28 @@
+function loadScript(path) {
+ const script = document.createElement('script');
+ script.type = 'application/javascript';
+ script.src = path;
+ script.defer = true;
+ document.head.appendChild(script);
+}
+
+/**
+ * Loads the Sourcegraph integration for support for Sourcegraph extensions and
+ * code intelligence.
+ */
+export default function initSourcegraph() {
+ const { url } = gon.sourcegraph || {};
+
+ if (!url) {
+ return;
+ }
+
+ const assetsUrl = new URL('/assets/webpack/sourcegraph/', window.location.href);
+ const scriptPath = new URL('scripts/integration.bundle.js', assetsUrl).href;
+
+ window.SOURCEGRAPH_ASSETS_URL = assetsUrl.href;
+ window.SOURCEGRAPH_URL = url;
+ window.SOURCEGRAPH_INTEGRATION = 'gitlab-integration';
+
+ loadScript(scriptPath);
+}
diff --git a/app/assets/javascripts/sourcegraph/load.js b/app/assets/javascripts/sourcegraph/load.js
new file mode 100644
index 00000000000..f9491505d42
--- /dev/null
+++ b/app/assets/javascripts/sourcegraph/load.js
@@ -0,0 +1,6 @@
+import initSourcegraph from './index';
+
+/**
+ * Load sourcegraph in it's own listener so that it's isolated from failures.
+ */
+document.addEventListener('DOMContentLoaded', initSourcegraph);
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 69b3d20914a..a530c4a99e2 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, consistent-return, no-var, one-var, no-else-return, class-methods-use-this */
+/* eslint-disable func-names, consistent-return, one-var, no-else-return, class-methods-use-this */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
@@ -9,9 +9,8 @@ export default class TreeView {
// Code browser tree slider
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
$('.tree-content-holder .tree-item').on('click', function(e) {
- var $clickedEl, path;
- $clickedEl = $(e.target);
- path = $('.tree-item-file-name a', this).attr('href');
+ const $clickedEl = $(e.target);
+ const path = $('.tree-item-file-name a', this).attr('href');
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
if (e.metaKey || e.which === 2) {
e.preventDefault();
@@ -26,11 +25,10 @@ export default class TreeView {
}
initKeyNav() {
- var li, liSelected;
- li = $('tr.tree-item');
- liSelected = null;
+ const li = $('tr.tree-item');
+ let liSelected = null;
return $('body').keydown(e => {
- var next, path;
+ let next, path;
if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) {
return false;
}
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index c0b7587be10..7d6a725b30f 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -73,9 +73,14 @@ const handleUserPopoverMouseOver = event => {
location: userData.location,
bio: userData.bio,
organization: userData.organization,
+ status: userData.status,
loaded: true,
});
+ if (userData.status) {
+ return Promise.resolve();
+ }
+
return UsersCache.retrieveStatusById(userId);
})
.then(status => {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index 339e154affc..57be97855e3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -65,9 +65,13 @@ export default {
simplePoll(this.checkRebaseStatus);
})
.catch(error => {
- this.rebasingError = error.merge_error;
this.isMakingRequest = false;
- Flash(__('Something went wrong. Please try again.'));
+
+ if (error.response && error.response.data && error.response.data.merge_error) {
+ this.rebasingError = error.response.data.merge_error;
+ } else {
+ Flash(__('Something went wrong. Please try again.'));
+ }
});
},
checkRebaseStatus(continuePolling, stopPolling) {
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 1e6f4c376c1..66155ddcdd9 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
@@ -18,6 +18,11 @@ export default {
required: false,
default: 0,
},
+ filePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
projectPath: {
type: String,
required: false,
@@ -52,6 +57,7 @@ export default {
<component
:is="viewer"
:path="path"
+ :file-path="filePath"
:file-size="fileSize"
:project-path="projectPath"
:content="content"
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 655f0054887..c50304f057d 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
@@ -16,6 +16,11 @@ export default {
type: String,
required: true,
},
+ filePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
projectPath: {
type: String,
required: true,
@@ -48,6 +53,7 @@ export default {
this.isLoading = true;
const postBody = {
text: this.content,
+ path: this.filePath,
};
const postOptions = {
cancelToken: axiosSource.token,
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index ebb253ff422..b874bedab36 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -23,6 +23,11 @@ export default {
type: String,
required: true,
},
+ newSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
oldPath: {
type: String,
required: true,
@@ -31,6 +36,11 @@ export default {
type: String,
required: true,
},
+ oldSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: false,
@@ -85,6 +95,8 @@ export default {
:diff-mode="diffMode"
:new-path="fullNewPath"
:old-path="fullOldPath"
+ :old-size="oldSize"
+ :new-size="newSize"
:project-path="projectPath"
:a-mode="aMode"
:b-mode="bMode"
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
index a17fc022195..4dbfdb6d79c 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue
@@ -14,6 +14,16 @@ export default {
type: String,
required: true,
},
+ newSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ oldSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
};
</script>
@@ -22,12 +32,14 @@ export default {
<div class="two-up view d-flex">
<image-viewer
:path="oldPath"
+ :file-size="oldSize"
:render-info="true"
inner-css-classes="frame deleted"
class="wrap w-50"
/>
<image-viewer
:path="newPath"
+ :file-size="newSize"
:render-info="true"
:inner-css-classes="['frame', 'added']"
class="wrap w-50"
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
index cab92297ca7..e30871b66fc 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue
@@ -22,6 +22,16 @@ export default {
type: String,
required: true,
},
+ newSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ oldSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 341c9534763..611001df32f 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -218,7 +218,7 @@ export default {
display: inline-block;
flex: 1;
max-width: inherit;
- height: 18px;
+ height: 19px;
line-height: 16px;
text-overflow: ellipsis;
white-space: nowrap;
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 73f4dfef062..80908cbbc9c 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -61,7 +61,7 @@ export default {
</script>
<template>
- <svg :class="[iconSizeClass, iconTestClass]" aria-hidden="true">
+ <svg :class="[iconSizeClass, iconTestClass]" aria-hidden="true" v-on="$listeners">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
index 715cf97f0ac..1524b313f9f 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -1,7 +1,6 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
@@ -16,44 +15,47 @@ export default {
type: Array,
required: true,
},
+ iconSize: {
+ type: Number,
+ required: false,
+ default: 24,
+ },
+ imgCssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ maxVisible: {
+ type: Number,
+ required: false,
+ default: 3,
+ },
},
data() {
return {
- maxVisibleAssignees: 2,
- maxAssigneeAvatars: 3,
maxAssignees: 99,
};
},
computed: {
- countOverLimit() {
- return this.assignees.length - this.maxVisibleAssignees;
- },
assigneesToShow() {
- if (this.assignees.length > this.maxAssigneeAvatars) {
- return this.assignees.slice(0, this.maxVisibleAssignees);
- }
- return this.assignees;
+ const numShownAssignees = this.assignees.length - this.numHiddenAssignees;
+ return this.assignees.slice(0, numShownAssignees);
},
assigneesCounterTooltip() {
- const { countOverLimit, maxAssignees } = this;
- const count = countOverLimit > maxAssignees ? maxAssignees : countOverLimit;
-
- return sprintf(__('%{count} more assignees'), { count });
+ return sprintf(__('%{count} more assignees'), { count: this.numHiddenAssignees });
},
- shouldRenderAssigneesCounter() {
- const assigneesCount = this.assignees.length;
- if (assigneesCount <= this.maxAssigneeAvatars) {
- return false;
+ numHiddenAssignees() {
+ if (this.assignees.length > this.maxVisible) {
+ return this.assignees.length - this.maxVisible + 1;
}
-
- return assigneesCount > this.countOverLimit;
+ return 0;
},
assigneeCounterLabel() {
- if (this.countOverLimit > this.maxAssignees) {
+ if (this.numHiddenAssignees > this.maxAssignees) {
return `${this.maxAssignees}+`;
}
- return `+${this.countOverLimit}`;
+ return `+${this.numHiddenAssignees}`;
},
},
methods: {
@@ -81,8 +83,9 @@ export default {
:key="assignee.id"
:link-href="webUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
+ :img-css-classes="imgCssClasses"
:img-src="avatarUrl(assignee)"
- :img-size="24"
+ :img-size="iconSize"
class="js-no-trigger"
tooltip-placement="bottom"
>
@@ -92,7 +95,7 @@ export default {
</span>
</user-avatar-link>
<span
- v-if="shouldRenderAssigneesCounter"
+ v-if="numHiddenAssignees > 0"
v-gl-tooltip
:title="assigneesCounterTooltip"
class="avatar-counter"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 4d27d1c9179..af4ac024e4f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -124,7 +124,7 @@ export default {
:cursor-offset="4"
:tag-content="lineContent"
icon="doc-code"
- class="qa-suggestion-btn js-suggestion-btn"
+ class="js-suggestion-btn"
@click="handleSuggestDismissed"
/>
<gl-popover
@@ -168,7 +168,7 @@ export default {
:prepend="true"
tag="* [ ] "
:button-title="__('Add a task list')"
- icon="task-done"
+ icon="list-task"
/>
<toolbar-button
:tag="mdTable"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 12de3671477..cc700440a23 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -55,7 +55,7 @@ export default {
<gl-button
v-else-if="canApply"
v-gl-tooltip.viewport="__('This also resolves the discussion')"
- class="btn-inverted qa-apply-btn js-apply-btn"
+ class="btn-inverted js-apply-btn"
:disabled="isApplying"
variant="success"
@click="applySuggestion"
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index d6dfe9eded8..f8e010c4f42 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -17,9 +17,11 @@
* />
*/
import $ from 'jquery';
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
+import { GlSkeletonLoading } from '@gitlab/ui';
import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
import initMRPopovers from '~/mr_popover/';
@@ -32,7 +34,9 @@ export default {
Icon,
noteHeader,
TimelineEntryItem,
+ GlSkeletonLoading,
},
+ mixins: [descriptionVersionHistoryMixin],
props: {
note: {
type: Object,
@@ -75,13 +79,16 @@ export default {
mounted() {
initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
},
+ methods: {
+ ...mapActions(['fetchDescriptionVersion']),
+ },
};
</script>
<template>
<timeline-entry-item
:id="noteAnchorId"
- :class="{ target: isTargetNote }"
+ :class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
class="note system-note note-wrapper"
>
<div class="timeline-icon" v-html="iconHtml"></div>
@@ -89,14 +96,18 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-html="actionTextHtml"></span>
+ <template v-if="canSeeDescriptionVersion" slot="extra-controls">
+ &middot;
+ <button type="button" class="btn-blank btn-link" @click="toggleDescriptionVersion">
+ {{ __('Compare with previous version') }}
+ <icon :name="descriptionVersionToggleIcon" :size="12" class="append-left-5" />
+ </button>
+ </template>
</note-header>
</div>
<div class="note-body">
<div
- :class="{
- 'system-note-commit-list': hasMoreCommits,
- 'hide-shade': expanded,
- }"
+ :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
class="note-text md"
v-html="note.note_html"
></div>
@@ -106,6 +117,12 @@ export default {
<span>{{ __('Toggle commit list') }}</span>
</div>
</div>
+ <div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
+ <pre v-if="isLoadingDescriptionVersion" class="loading-state">
+ <gl-skeleton-loading />
+ </pre>
+ <pre v-else class="wrapper mt-2" v-html="descriptionVersion"></pre>
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 478e44d104c..f984a0a6203 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -1,6 +1,6 @@
<script>
import _ from 'underscore';
-import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlLoadingIcon, GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import ProjectListItem from './project_list_item.vue';
const SEARCH_INPUT_TIMEOUT_MS = 500;
@@ -10,6 +10,7 @@ export default {
components: {
GlLoadingIcon,
GlSearchBoxByType,
+ GlInfiniteScroll,
ProjectListItem,
},
props: {
@@ -41,6 +42,11 @@ export default {
required: false,
default: false,
},
+ totalResults: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -51,6 +57,9 @@ export default {
projectClicked(project) {
this.$emit('projectClicked', project);
},
+ bottomReached() {
+ this.$emit('bottomReached');
+ },
isSelected(project) {
return Boolean(_.find(this.selectedProjects, { id: project.id }));
},
@@ -71,18 +80,25 @@ export default {
@input="onInput"
/>
<div class="d-flex flex-column">
- <gl-loading-icon v-if="showLoadingIndicator" :size="2" class="py-2 px-4" />
- <div v-if="!showLoadingIndicator" class="d-flex flex-column">
- <project-list-item
- v-for="project in projectSearchResults"
- :key="project.id"
- :selected="isSelected(project)"
- :project="project"
- :matcher="searchQuery"
- class="js-project-list-item"
- @click="projectClicked(project)"
- />
- </div>
+ <gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" />
+ <gl-infinite-scroll
+ :max-list-height="402"
+ :fetched-items="projectSearchResults.length"
+ :total-items="totalResults"
+ @bottomReached="bottomReached"
+ >
+ <div v-if="!showLoadingIndicator" slot="items" class="d-flex flex-column">
+ <project-list-item
+ v-for="project in projectSearchResults"
+ :key="project.id"
+ :selected="isSelected(project)"
+ :project="project"
+ :matcher="searchQuery"
+ class="js-project-list-item"
+ @click="projectClicked(project)"
+ />
+ </div>
+ </gl-infinite-scroll>
<div v-if="showNoResultsMessage" class="text-muted ml-2 js-no-results-message">
{{ __('Sorry, no projects matched your search') }}
</div>
diff --git a/app/assets/javascripts/vue_shared/components/slot_switch.vue b/app/assets/javascripts/vue_shared/components/slot_switch.vue
new file mode 100644
index 00000000000..67726f01744
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/slot_switch.vue
@@ -0,0 +1,35 @@
+<script>
+/**
+ * Allows to toggle slots based on an array of slot names.
+ */
+export default {
+ name: 'SlotSwitch',
+
+ props: {
+ activeSlotNames: {
+ type: Array,
+ required: true,
+ },
+
+ tagName: {
+ type: String,
+ required: false,
+ default: 'div',
+ },
+ },
+
+ computed: {
+ allSlotNames() {
+ return Object.keys(this.$slots);
+ },
+ },
+};
+</script>
+
+<template>
+ <component :is="tagName">
+ <template v-for="slotName in allSlotNames">
+ <slot v-if="activeSlotNames.includes(slotName)" :name="slotName"></slot>
+ </template>
+ </component>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
new file mode 100644
index 00000000000..f7dc00a345c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -0,0 +1,76 @@
+<script>
+import _ from 'underscore';
+
+import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
+
+const isValidItem = item =>
+ _.isString(item.eventName) && _.isString(item.title) && _.isString(item.description);
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ },
+
+ props: {
+ actionItems: {
+ type: Array,
+ required: true,
+ validator(value) {
+ return value.length > 1 && value.every(isValidItem);
+ },
+ },
+ menuClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ selectedItem: this.actionItems[0],
+ };
+ },
+
+ computed: {
+ dropdownToggleText() {
+ return this.selectedItem.title;
+ },
+ },
+
+ methods: {
+ triggerEvent() {
+ this.$emit(this.selectedItem.eventName);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ :menu-class="`dropdown-menu-selectable ${menuClass}`"
+ split
+ :text="dropdownToggleText"
+ v-bind="$attrs"
+ @click="triggerEvent"
+ >
+ <template v-for="(item, itemIndex) in actionItems">
+ <gl-dropdown-item
+ :key="item.eventName"
+ :active="selectedItem === item"
+ active-class="is-active"
+ @click="selectedItem = item"
+ >
+ <strong>{{ item.title }}</strong>
+ <div>{{ item.description }}</div>
+ </gl-dropdown-item>
+
+ <gl-dropdown-divider
+ v-if="itemIndex < actionItems.length - 1"
+ :key="`${item.eventName}-divider`"
+ />
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index 8bcad7ac765..43935cf31d5 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
<time
- v-gl-tooltip="{ placement: tooltipPlacement }"
+ v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
:title="tooltipTitle(time)"
v-text="timeFormated(time)"
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 7c7d46ee759..4a72cca5f02 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -51,7 +51,7 @@ export default {
</script>
<template>
- <gl-popover :target="target" boundary="viewport" placement="top" show>
+ <gl-popover :target="target" boundary="viewport" placement="top" offset="0, 1" show>
<div class="user-popover d-flex">
<div class="p-1 flex-shrink-1">
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
@@ -90,7 +90,7 @@ export default {
name="location"
class="category-icon flex-shrink-0"
/>
- <span class="ml-1">{{ user.location }}</span>
+ <span v-if="user.location" class="ml-1">{{ user.location }}</span>
<gl-skeleton-loading
v-if="locationIsLoading"
:lines="1"
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 7a60ab1380f..044d703630e 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, consistent-return, camelcase, class-methods-use-this */
+/* eslint-disable consistent-return, camelcase, class-methods-use-this */
// Zen Mode (full screen) textarea
//
@@ -47,26 +47,16 @@ export default class ZenMode {
e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:leave');
});
- $(document).on(
- 'zen_mode:enter',
- (function(_this) {
- return function(e) {
- return _this.enter(
- $(e.target)
- .closest('.md-area')
- .find('.zen-backdrop'),
- );
- };
- })(this),
- );
- $(document).on(
- 'zen_mode:leave',
- (function(_this) {
- return function() {
- return _this.exit();
- };
- })(this),
- );
+ $(document).on('zen_mode:enter', e => {
+ this.enter(
+ $(e.target)
+ .closest('.md-area')
+ .find('.zen-backdrop'),
+ );
+ });
+ $(document).on('zen_mode:leave', () => {
+ this.exit();
+ });
$(document).on('keydown', e => {
// Esc
if (e.keyCode === 27) {
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index c9b00e5ff27..885e9ac6667 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -282,8 +282,7 @@ pre code {
white-space: pre-wrap;
}
-.alert,
-.flash-notice {
+.alert {
border-radius: 0;
}
@@ -310,12 +309,10 @@ pre code {
.alert-success,
.alert-info,
.alert-warning,
-.alert-danger,
-.flash-notice {
+.alert-danger {
color: $white-light;
h4,
- a:not(.btn),
.alert-link {
color: $white-light;
}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 56a88ca44db..249e9a24b17 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -2,7 +2,7 @@
@import 'framework/variables_overrides';
@import 'framework/mixins';
-@import '@gitlab/ui/scss/gitlab_ui';
+@import '@gitlab/ui/src/scss/gitlab_ui';
@import 'bootstrap_migration';
@import 'framework/layout';
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index 28d7492b99c..cae7b9b5e46 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -99,3 +99,13 @@
color: $gl-text-color-disabled;
}
}
+
+.group-variable-list {
+ color: $gray-700;
+
+ .table-section:not(:first-child) {
+ @include media-breakpoint-down(sm) {
+ border-top: hidden;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 4b89a2f2b04..31ea59df4c5 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -562,4 +562,20 @@ img.emoji {
}
.gl-font-size-small { font-size: $gl-font-size-small; }
+.gl-font-size-large { font-size: $gl-font-size-large; }
+
.gl-line-height-24 { line-height: $gl-line-height-24; }
+
+.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-20 { font-size: $gl-font-size-20; }
+.gl-font-size-28 { font-size: $gl-font-size-28; }
+.gl-font-size-42 { font-size: $gl-font-size-42; }
+
+.border-section {
+ @include gl-py-6;
+ @include gl-m-0;
+
+ border-top: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ce74aa6ed02..d53a4c1286c 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -506,7 +506,8 @@
.dropdown-menu-selectable {
li {
a,
- button {
+ button,
+ .dropdown-item {
padding: 8px 40px;
position: relative;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 487fbf0fcff..4938215b2e7 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -376,6 +376,10 @@ span.idiff {
float: none;
}
+ .file-actions .ide-edit-button {
+ z-index: 2;
+ }
+
@include media-breakpoint-down(xs) {
display: block;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 5984efd1cf8..2d826064569 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -249,7 +249,7 @@
}
.filtered-search-input-dropdown-menu {
- max-height: $dropdown-max-height;
+ max-height: $dropdown-max-height-lg;
max-width: 280px;
overflow: auto;
@@ -357,12 +357,18 @@
}
}
- .filter-dropdown-container > div {
- margin: 0;
+ .filter-dropdown-container {
+ > div {
+ margin: 0;
- > .btn {
- margin: 0 0 10px;
- width: 100%;
+ > .btn {
+ margin: 0 0 10px;
+ width: 100%;
+ }
+ }
+
+ .board-labels-toggle-wrapper {
+ margin-bottom: $gl-input-padding;
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 8fc2fd5f53b..d604d97d270 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -12,17 +12,22 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
position: -webkit-sticky;
top: $flash-container-top;
z-index: 251;
+ }
- .flash-content {
- box-shadow: 0 2px 4px 0 $notification-box-shadow-color;
- }
+ &.flash-container-page {
+ margin-bottom: 0;
+ }
+
+ &:empty {
+ margin: 0;
}
.close-icon-wrapper {
- padding: ($gl-btn-padding + $gl-padding-4) $gl-padding $gl-btn-padding;
+ padding: ($gl-padding + $gl-padding-4) $gl-padding $gl-padding;
position: absolute;
right: 0;
top: 0;
+ bottom: 0;
cursor: pointer;
.close-icon {
@@ -31,13 +36,12 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
}
}
- .flash-notice,
.flash-alert,
+ .flash-notice,
.flash-success,
.flash-warning {
- border-radius: $border-radius-default;
- color: $white-light;
- padding-right: $gl-padding * 2;
+ padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
+ margin-top: 10px;
.container-fluid,
.container-fluid.container-limited {
@@ -45,75 +49,31 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
}
}
- .flash-notice {
- @extend .alert;
- background-color: $blue-500;
- margin: 0;
-
- &.flash-notice-persistent {
- background-color: $blue-100;
- color: $gl-text-color;
+ .flash-alert {
+ background-color: $red-100;
+ color: $red-700;
+ }
- a {
- color: $blue-600;
+ .flash-notice {
+ background-color: $blue-100;
+ color: $blue-700;
+ }
- &:hover {
- color: $blue-800;
- text-decoration: none;
- }
- }
- }
+ .flash-success {
+ background-color: $theme-green-100;
+ color: $green-700;
}
.flash-warning {
- @extend .alert;
background-color: $orange-100;
- color: $orange-900;
+ color: $orange-800;
cursor: default;
- margin: 0;
}
.flash-text,
.flash-action {
display: inline-block;
}
-
- .flash-alert {
- @extend .alert;
- background-color: $red-500;
- margin: 0;
-
- .flash-action {
- margin-left: 5px;
- text-decoration: none;
- font-weight: $gl-font-weight-normal;
- border-bottom: 1px solid;
-
- &:hover {
- border-color: transparent;
- }
- }
- }
-
- .flash-success {
- @extend .alert;
- background-color: $green-500;
- margin: 0;
- }
-
- &.flash-container-page {
- margin-bottom: 0;
-
- .flash-notice,
- .flash-alert,
- .flash-success {
- border-radius: 0;
- }
- }
-
- &:empty {
- margin: 0;
- }
}
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 7205324e86f..8038a367fb9 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -33,7 +33,8 @@ body {
&.limit-container-width {
.flash-container.sticky {
max-width: $limited-layout-width;
- margin: 0 auto;
+ margin-right: auto;
+ margin-left: auto;
}
}
}
diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss
index 81cdf6b59e4..c84010c6f10 100644
--- a/app/assets/stylesheets/framework/memory_graph.scss
+++ b/app/assets/stylesheets/framework/memory_graph.scss
@@ -1,11 +1,7 @@
.memory-graph-container {
svg {
background: $white-light;
- cursor: pointer;
-
- &:hover {
- box-shadow: 0 0 4px $gray-darkest inset;
- }
+ border: 1px solid $gray-200;
}
path {
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 9c924559135..757264add93 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -49,7 +49,7 @@
line-height: $line-height-base;
position: relative;
min-height: $modal-body-height;
- padding: #{2 * $grid-size} #{6 * $grid-size} #{2 * $grid-size} #{2 * $grid-size};
+ padding: #{2 * $grid-size};
text-align: left;
white-space: normal;
@@ -70,9 +70,9 @@
margin: 0;
}
- .btn + .btn:not(.dropdown-toggle-split),
.btn + .btn-group,
- .btn-group + .btn {
+ .btn-group + .btn,
+ .btn-group + .btn-group {
margin-left: $grid-size;
}
@@ -83,13 +83,6 @@
@include media-breakpoint-down(xs) {
flex-direction: column;
- .btn + .btn:not(.dropdown-toggle-split),
- .btn + .btn-group,
- .btn-group + .btn {
- margin-left: 0;
- margin-top: $grid-size;
- }
-
.btn-group .btn + .btn {
margin-left: -1px;
margin-top: 0;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index fd6f80e26cb..1878fac1c60 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -20,6 +20,17 @@
@extend .gl-responsive-table-row-layout;
margin-top: 10px;
border: 1px solid $border-color;
+ color: $gray-700;
+
+ &.gl-responsive-table-row-clickable {
+ &:hover {
+ background-color: $gray-light;
+
+ .underline {
+ text-decoration: underline;
+ }
+ }
+ }
@include media-breakpoint-up(md) {
margin: 0;
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index f57b1d9f351..404f60f17ee 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -4,7 +4,12 @@
}
.snippet-filename {
- padding: 0 2px;
+ color: $gl-text-color-secondary;
+ font-weight: normal;
+ }
+
+ .snippet-info {
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index dfc39d8e03b..0f77c451fac 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -21,6 +21,27 @@ $spacing-scale: (
);
/*
+ * Why another sizing scale???
+ * Great question, friend!
+ * This size scale is a "backport" of the equivalent set of "named" sizes
+ * (e.g. `xl` versus `70`) that came from the following design document as of 2019-10-23:
+ *
+ * https://gitlab-org.gitlab.io/gitlab-design/hosted/design-gitlab-specs/forms-spec-previews/
+ *
+ * (See `input-` items at the bottom)
+ *
+ * The presumption here is that these sizes will be standardized in GitLab UI and thus will be
+ * broadly useful here in the GitLab product when not using the GitLab UI components.
+ */
+$size-scale: (
+ 'xs': #{10 * $grid-size},
+ 's': #{20 * $grid-size},
+ 'm': #{30 * $grid-size},
+ 'l': #{40 * $grid-size},
+ 'xl': #{70 * $grid-size}
+);
+
+/*
* Color schema
*/
$darken-normal-factor: 7%;
@@ -304,6 +325,12 @@ $gl-grayish-blue: #7f8fa4;
$gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
$gl-header-color: #4c4e54;
+$gl-font-size-12: 12px;
+$gl-font-size-14: 14px;
+$gl-font-size-16: 16px;
+$gl-font-size-20: 20px;
+$gl-font-size-28: 28px;
+$gl-font-size-42: 42px;
$type-scale: (
1: 12px,
diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss
index e3bdc0b0199..a082cd25abe 100644
--- a/app/assets/stylesheets/framework/vue_transitions.scss
+++ b/app/assets/stylesheets/framework/vue_transitions.scss
@@ -11,3 +11,27 @@
.fade-leave-to {
opacity: 0;
}
+
+.slide-enter-from-element {
+ &.slide-enter,
+ &.slide-leave-to {
+ transform: translateX(-150%);
+ }
+}
+
+.slide-enter-to-element {
+ &.slide-enter,
+ &.slide-leave-to {
+ transform: translateX(150%);
+ }
+}
+
+.slide-enter-active,
+.slide-leave-active {
+ transition: transform 300ms ease-out;
+}
+
+.slide-enter-to,
+.slide-leave {
+ transform: translateX(0);
+}
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
new file mode 100644
index 00000000000..f7d93870a25
--- /dev/null
+++ b/app/assets/stylesheets/mailer.scss
@@ -0,0 +1,117 @@
+@import 'framework/variables';
+
+// Do not use 3-letter hex codes, bgcolor vs css background-color is problematic in emails
+// See https://stackoverflow.com/questions/28551981/why-are-3-digit-hex-color-code-values-interpreted-differently-in-internet-explor
+//
+// stylelint-disable color-hex-length
+
+$mailer-font: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+$mailer-text-color: #333333;
+$mailer-bg-color: #fafafa;
+$mailer-link-color: #3777b0;
+$mailer-link-muted-color: #333333;
+$mailer-line-cell-bg-color: #6b4fbb;
+$mailer-wrapper-cell-bg-color: #ffffff;
+$mailer-wrapper-cell-border-color: #ededed;
+$mailer-header-footer-text-color: #5c5c5c;
+
+body {
+ margin: 0 !important;
+ background-color: $mailer-bg-color;
+ padding: 0;
+ text-align: center;
+ min-width: 640px;
+ width: 100%;
+ height: 100%;
+ font-family: $mailer-font;
+}
+
+table#body {
+ background-color: $mailer-bg-color;
+ margin: 0;
+ padding: 0;
+ text-align: center;
+ min-width: 640px;
+ width: 100%;
+}
+
+a {
+ color: $mailer-link-color;
+ text-decoration: none;
+
+ &.muted {
+ color: $mailer-link-muted-color;
+ }
+}
+
+.highlight {
+ font-weight: 500;
+}
+
+tr td {
+ font-family: $mailer-font;
+}
+
+tr.line td {
+ font-family: $mailer-font;
+ background-color: $mailer-line-cell-bg-color;
+ height: 4px;
+ font-size: 4px;
+ line-height: 4px;
+}
+
+tr.header td,
+tr.footer td,
+td.footer-message {
+ font-family: $mailer-font;
+ padding: 25px 0;
+ font-size: 13px;
+ line-height: 1.6;
+ color: $mailer-header-footer-text-color;
+}
+
+table.wrapper {
+ width: 640px;
+ margin: 0 auto;
+ border-collapse: separate;
+ border-spacing: 0;
+
+ td.wrapper-cell {
+ font-family: $mailer-font;
+ background-color: $mailer-wrapper-cell-bg-color;
+ text-align: left;
+ padding: 18px 25px;
+ border: 1px solid $mailer-wrapper-cell-border-color;
+ border-radius: 3px;
+ overflow: hidden;
+ }
+}
+
+table.content {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+
+ td.text-content {
+ font-family: $mailer-font;
+ color: $mailer-text-color;
+ font-size: 15px;
+ font-weight: 400;
+ line-height: 1.4;
+ padding: 15px 5px;
+ text-align: center;
+ }
+}
+
+tr.footer td {
+ img {
+ display: block;
+ margin: 0 auto 1em;
+ }
+
+ .mng-notif-link,
+ .help-link {
+ color: $mailer-link-color;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/mailer_client_specific.scss b/app/assets/stylesheets/mailer_client_specific.scss
new file mode 100644
index 00000000000..41bedecf90f
--- /dev/null
+++ b/app/assets/stylesheets/mailer_client_specific.scss
@@ -0,0 +1,65 @@
+/* CLIENT-SPECIFIC STYLES */
+
+// These are client-specific rules, ignore some linting rules
+//
+// stylelint-disable property-no-vendor-prefix, property-no-unknown, length-zero-no-unit
+// scss-lint:disable PropertySpelling, ZeroUnit
+
+body,
+table,
+td,
+a {
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+
+table,
+td {
+ mso-table-lspace: 0pt;
+ mso-table-rspace: 0pt;
+}
+
+img {
+ -ms-interpolation-mode: bicubic;
+}
+
+.hidden {
+ display: none !important;
+ visibility: hidden !important;
+}
+
+/* iOS BLUE LINKS */
+a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ font-size: inherit !important;
+ font-family: inherit !important;
+ font-weight: inherit !important;
+ line-height: inherit !important;
+}
+
+/* ANDROID MARGIN HACK */
+div[style*='margin: 16px 0'] {
+ margin: 0 !important;
+}
+
+@media only screen and (max-width: 639px) {
+ body,
+ #body {
+ min-width: 320px !important;
+ }
+
+ table.wrapper {
+ width: 100% !important;
+ min-width: 320px !important;
+ }
+
+ table.wrapper td.wrapper-cell {
+ border-left: 0 !important;
+ border-right: 0 !important;
+ border-radius: 0 !important;
+ padding-left: 10px !important;
+ padding-right: 10px !important;
+ }
+}
+
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 2a7a53d8bd7..d26979bc174 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -497,7 +497,7 @@
.add-issues-footer-to-list {
padding-left: $gl-vert-padding;
padding-right: $gl-vert-padding;
- line-height: 34px;
+ line-height: $input-height;
}
.issue-card-selected {
@@ -545,3 +545,11 @@
.board-issue-path.js-show-tooltip {
cursor: help;
}
+
+.board-labels-toggle-wrapper {
+ /**
+ * Make the wrapper the same height as a button so it aligns properly when the
+ * filtered-search-box input element increases in size on Linux smaller breakpoints
+ */
+ height: $input-height;
+}
diff --git a/app/assets/stylesheets/pages/error_details.scss b/app/assets/stylesheets/pages/error_details.scss
new file mode 100644
index 00000000000..0515db914e9
--- /dev/null
+++ b/app/assets/stylesheets/pages/error_details.scss
@@ -0,0 +1,18 @@
+.error-details {
+ li {
+ @include gl-line-height-32;
+ }
+}
+
+.stacktrace {
+ .file-title {
+ svg {
+ vertical-align: middle;
+ top: -1px;
+ }
+ }
+
+ .line_content.old::before {
+ content: none !important;
+ }
+}
diff --git a/app/assets/stylesheets/pages/experimental_separate_sign_up.scss b/app/assets/stylesheets/pages/experimental_separate_sign_up.scss
index 8b1ec1ced35..5a80ea79600 100644
--- a/app/assets/stylesheets/pages/experimental_separate_sign_up.scss
+++ b/app/assets/stylesheets/pages/experimental_separate_sign_up.scss
@@ -23,6 +23,7 @@
.signup-heading h2 {
font-weight: $gl-font-weight-bold;
+ padding: 0 $gl-padding;
@include media-breakpoint-down(md) {
font-size: $gl-font-size-large;
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 3febf4cf826..a8de8303a19 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -17,21 +17,6 @@
}
}
-.graphs {
- .graph-author-email {
- float: right;
- color: $gl-gray-500;
- }
-
- .graph-additions {
- color: $green-600;
- }
-
- .graph-deletions {
- color: $red-500;
- }
-}
-
.svg-graph-container {
width: 100%;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 00d84df1650..b399662997c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -30,7 +30,8 @@ $status-box-line-height: 26px;
margin-bottom: $gl-padding-4;
}
- .milestone-progress {
+ .milestone-progress,
+ .milestone-release-links {
a {
color: $blue-600;
}
@@ -238,10 +239,6 @@ $status-box-line-height: 26px;
}
}
-.milestone-range {
- color: $gl-text-color-tertiary;
-}
-
@include media-breakpoint-down(xs) {
.milestone-banner-text,
.milestone-banner-link {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 21a9f143039..1da9f691639 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -310,6 +310,17 @@ $note-form-margin-left: 72px;
.note-body {
overflow: hidden;
+ .description-version {
+ pre {
+ max-height: $dropdown-max-height-lg;
+ white-space: pre-wrap;
+
+ &.loading-state {
+ height: 94px;
+ }
+ }
+ }
+
.system-note-commit-list-toggler {
color: $blue-600;
padding: 10px 0 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 1b2af932733..364fe3da71e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -871,7 +871,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
- top: 1px;
+ top: auto;
vertical-align: initial;
}
}
@@ -1082,3 +1082,13 @@ button.mini-pipeline-graph-dropdown-toggle {
.legend-success {
color: $green-500;
}
+
+.test-reports-table {
+ .build-trace {
+ @include build-trace();
+ }
+}
+
+.progress-bar.bg-primary {
+ background-color: $blue-500 !important;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index b2c1d0b6dc5..d96cc163738 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -252,6 +252,7 @@
.fa-caret-down {
margin-left: 3px;
+ line-height: 0;
&.dropdown-btn-icon {
margin-left: 0;
@@ -273,6 +274,12 @@
height: 24px;
}
+ .git-clone-holder {
+ .btn {
+ height: auto;
+ }
+ }
+
.dropdown-toggle,
.clone-dropdown-btn {
.fa {
diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss
index 0fbf7033aa5..390ebd48685 100644
--- a/app/assets/stylesheets/pages/reports.scss
+++ b/app/assets/stylesheets/pages/reports.scss
@@ -131,7 +131,6 @@
.modal-security-report-dast {
.modal-dialog {
- width: $modal-lg;
max-width: $modal-lg;
}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
deleted file mode 100644
index 31ccdacbc02..00000000000
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ /dev/null
@@ -1,62 +0,0 @@
-.tint-box {
- background: $stat-graph-common-bg;
- position: relative;
- margin-bottom: 10px;
-}
-
-.area {
- fill: $green-500;
- fill-opacity: 0.5;
-}
-
-.axis {
- font-size: 10px;
-}
-
-#contributors-master {
- @include media-breakpoint-up(md) {
- @include make-col-ready();
- @include make-col(12);
- }
-}
-
-#contributors {
- flex: 1;
-
- .contributors-list {
- margin: 0 0 10px;
- list-style: none;
- padding: 0;
- }
-
- .person {
- @include media-breakpoint-up(md) {
- @include make-col-ready();
- @include make-col(6);
- }
-
- margin-top: 10px;
-
- @include media-breakpoint-down(xs) {
- width: 100%;
- }
-
- .spark {
- display: block;
- background: $stat-graph-common-bg;
- width: 100%;
- }
-
- .area-contributor {
- fill: $orange-500;
- }
- }
-}
-
-.selection rect {
- fill-opacity: 0.1;
- stroke-width: 1px;
- stroke-opacity: 0.4;
- shape-rendering: crispedges;
- stroke-dasharray: 3 3;
-}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index ad7d87f0bf6..87e650c7659 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -18,6 +18,11 @@
width: 200px;
}
+ input {
+ color: $gl-gray-400;
+ width: $input-short-width - 60px;
+ }
+
&.disabled {
display: none;
}
@@ -25,7 +30,8 @@
&.production {
background-color: $perf-bar-production;
- select {
+ select,
+ input {
background: $perf-bar-production;
}
}
@@ -33,7 +39,8 @@
&.staging {
background-color: $perf-bar-staging;
- select {
+ select,
+ input {
background: $perf-bar-staging;
}
}
@@ -41,7 +48,8 @@
&.development {
background-color: $perf-bar-development;
- select {
+ select,
+ input {
background: $perf-bar-development;
}
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index d2906ce0780..3b3a2778b23 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -16,8 +16,18 @@
}
}
+@each $index, $size in $size-scale {
+ #{'.mw-#{$index}'} {
+ max-width: $size;
+ }
+}
+
.border-width-1px { border-width: 1px; }
.border-style-dashed { border-style: dashed; }
.border-style-solid { border-style: solid; }
.border-color-blue-300 { border-color: $blue-300; }
.border-color-default { border-color: $border-color; }
+.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
+
+.gl-w-64 { width: px-to-rem($grid-size * 8); }
+.gl-h-64 { height: px-to-rem($grid-size * 8); }
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index d5537023b26..31d825c235b 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -1,12 +1,9 @@
# frozen_string_literal: true
class Admin::AbuseReportsController < Admin::ApplicationController
- # rubocop: disable CodeReuse/ActiveRecord
def index
- @abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
- @abuse_reports.includes(:reporter, :user)
+ @abuse_reports = AbuseReportsFinder.new(params).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
def destroy
abuse_report = AbuseReport.find(params[:id])
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 22e629ccf59..907b295870d 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -44,7 +44,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
def destroy
@application.destroy
- redirect_to admin_applications_url, status: 302, notice: _('Application was successfully destroyed.')
+ redirect_to admin_applications_url, status: :found, notice: _('Application was successfully destroyed.')
end
private
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 85a37fcd43e..5455cefdc8e 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -69,7 +69,7 @@ class Admin::GroupsController < Admin::ApplicationController
Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path,
- status: 302,
+ status: :found,
alert: _('Group %{group_name} was scheduled for deletion.') % { group_name: @group.name }
end
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index f518f7a657f..8f2e34a6294 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -38,9 +38,9 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
RepairLdapBlockedUserService.new(@user).execute
- redirect_to admin_user_identities_path(@user), status: 302, notice: _('User identity was successfully removed.')
+ redirect_to admin_user_identities_path(@user), status: :found, notice: _('User identity was successfully removed.')
else
- redirect_to admin_user_identities_path(@user), status: 302, alert: _('Failed to remove user identity.')
+ redirect_to admin_user_identities_path(@user), status: :found, alert: _('Failed to remove user identity.')
end
end
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 340eecd7632..58ea19d1210 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -17,9 +17,9 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format|
if key.destroy
- format.html { redirect_to keys_admin_user_path(user), status: 302, notice: _('User key was successfully removed.') }
+ format.html { redirect_to keys_admin_user_path(user), status: :found, notice: _('User key was successfully removed.') }
else
- format.html { redirect_to keys_admin_user_path(user), status: 302, alert: _('Failed to remove user key.') }
+ format.html { redirect_to keys_admin_user_path(user), status: :found, alert: _('Failed to remove user key.') }
end
end
end
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index 90c1694fd2e..6cb206c1686 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -43,7 +43,7 @@ class Admin::LabelsController < Admin::ApplicationController
respond_to do |format|
format.html do
- redirect_to admin_labels_path, status: 302, notice: _('Label was removed')
+ redirect_to admin_labels_path, status: :found, notice: _('Label was removed')
end
format.js
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 0e8c69eb7d6..cdedc34e634 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -41,7 +41,7 @@ class Admin::ProjectsController < Admin::ApplicationController
redirect_to admin_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => ex
- redirect_to admin_projects_path, status: 302, alert: ex.message
+ redirect_to admin_projects_path, status: :found, alert: ex.message
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 45cf0d3207e..a41d8a22650 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -13,7 +13,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
if params[:remove_user]
spam_log.remove_user(deleted_by: current_user)
redirect_to admin_spam_logs_path,
- status: 302,
+ status: :found,
notice: _('User %{username} was successfully removed.') % { username: spam_log.user.username }
else
spam_log.destroy
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 4c1ac8f206a..9fbfc59f630 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -169,7 +169,7 @@ class Admin::UsersController < Admin::ApplicationController
user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete))
respond_to do |format|
- format.html { redirect_to admin_users_path, status: 302, notice: _("The user is being deleted.") }
+ format.html { redirect_to admin_users_path, status: :found, notice: _("The user is being deleted.") }
format.json { head :ok }
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 27e88ae569e..25c1d80b117 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base
include EnforcesTwoFactorAuthentication
include WithPerformanceBar
include SessionlessAuthentication
+ include SessionsHelper
include ConfirmEmailWarning
include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern
@@ -29,13 +30,13 @@ class ApplicationController < ActionController::Base
before_action :active_user_check, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
before_action :check_impersonation_availability
- before_action :require_role
+ before_action :required_signup_info
around_action :set_locale
around_action :set_session_storage
after_action :set_page_title_header, if: :json_request?
- after_action :limit_unauthenticated_session_times
+ after_action :limit_session_time, if: -> { !current_user }
protect_from_forgery with: :exception, prepend: true
@@ -57,7 +58,7 @@ class ApplicationController < ActionController::Base
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
- render "errors/encoding", layout: "errors", status: 500
+ render "errors/encoding", layout: "errors", status: :internal_server_error
end
rescue_from ActiveRecord::RecordNotFound do |exception|
@@ -103,24 +104,6 @@ class ApplicationController < ActionController::Base
end
end
- # By default, all sessions are given the same expiration time configured in
- # the session store (e.g. 1 week). However, unauthenticated users can
- # generate a lot of sessions, primarily for CSRF verification. It makes
- # sense to reduce the TTL for unauthenticated to something much lower than
- # the default (e.g. 1 hour) to limit Redis memory. In addition, Rails
- # creates a new session after login, so the short TTL doesn't even need to
- # be extended.
- def limit_unauthenticated_session_times
- return if current_user
-
- # Rack sets this header, but not all tests may have it: https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L251-L259
- return unless request.env['rack.session.options']
-
- # This works because Rack uses these options every time a request is handled:
- # https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L342
- request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay']
- end
-
def render(*args)
super.tap do
# Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse
@@ -214,25 +197,29 @@ class ApplicationController < ActionController::Base
end
def git_not_found!
- render "errors/git_not_found.html", layout: "errors", status: 404
+ render "errors/git_not_found.html", layout: "errors", status: :not_found
end
def render_403
respond_to do |format|
format.any { head :forbidden }
- format.html { render "errors/access_denied", layout: "errors", status: 403 }
+ format.html { render "errors/access_denied", layout: "errors", status: :forbidden }
end
end
def render_404
respond_to do |format|
- format.html { render "errors/not_found", layout: "errors", status: 404 }
+ format.html { render "errors/not_found", layout: "errors", status: :not_found }
# Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file
format.js { render json: '', status: :not_found, content_type: 'application/json' }
format.any { head :not_found }
end
end
+ def respond_201
+ head :created
+ end
+
def respond_422
head :unprocessable_entity
end
@@ -551,10 +538,13 @@ class ApplicationController < ActionController::Base
@current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
end
- # A user requires a role when they are part of the experimental signup flow (executed by the Growth team). Users
- # are redirected to the welcome page when their role is required and the experiment is enabled for the current user.
- def require_role
- return unless current_user && current_user.role_required? && experiment_enabled?(:signup_flow)
+ # A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup
+ # flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the
+ # experiment is enabled for the current user.
+ def required_signup_info
+ return unless current_user
+ return unless current_user.role_required?
+ return unless experiment_enabled?(:signup_flow)
store_location_for :user, request.fullpath
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 9894dd7d180..1298b33471b 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -13,7 +13,7 @@ module Boards
requires_cross_project_access if: -> { board&.group_board? }
- before_action :whitelist_query_limiting, only: [:index, :update, :bulk_move]
+ before_action :whitelist_query_limiting, only: [:bulk_move]
before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update]
@@ -130,8 +130,7 @@ module Boards
end
def whitelist_query_limiting
- # Also see https://gitlab.com/gitlab-org/gitlab-foss/issues/42439
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42428')
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/35174')
end
def validate_id_list
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 16c2365f85d..be68d0d0a1d 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, :email)
+ params.permit(:application, :hostname, :kibana_hostname, :email, :stack)
end
def cluster_application_destroy_params
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 993aba661f3..9a539cf7c24 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -3,18 +3,22 @@
class Clusters::ClustersController < Clusters::BaseController
include RoutableActions
- before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
+ before_action :cluster, only: [:cluster_status, :show, :update, :destroy]
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
- before_action :authorize_create_cluster!, only: [:new]
+ before_action :authorize_create_cluster!, only: [:new, :authorize_aws_role, :revoke_aws_role, :aws_proxy]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:cluster_status]
before_action only: [:new, :create_gcp] do
push_frontend_feature_flag(:create_eks_clusters)
end
+ before_action only: [:show] do
+ push_frontend_feature_flag(:enable_cluster_application_elastic_stack)
+ push_frontend_feature_flag(:enable_cluster_application_crossplane)
+ end
helper_method :token_in_session
@@ -40,10 +44,13 @@ class Clusters::ClustersController < Clusters::BaseController
def new
return unless Feature.enabled?(:create_eks_clusters)
- @gke_selected = params[:provider] == 'gke'
- @eks_selected = params[:provider] == 'eks'
+ if params[:provider] == 'aws'
+ @aws_role = current_user.aws_role || Aws::Role.new
+ @aws_role.ensure_role_external_id!
- return redirect_to @authorize_url if @gke_selected && @authorize_url && !@valid_gcp_token
+ elsif params[:provider] == 'gcp'
+ redirect_to @authorize_url if @authorize_url && !@valid_gcp_token
+ end
end
# Overridding ActionController::Metal#status is NOT a good idea
@@ -86,13 +93,12 @@ class Clusters::ClustersController < Clusters::BaseController
end
def destroy
- if cluster.destroy
- flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to clusterable.index_path, status: :found
- else
- flash[:notice] = _('Kubernetes cluster integration was not removed.')
- render :show
- end
+ response = Clusters::DestroyService
+ .new(current_user, destroy_params)
+ .execute(cluster)
+
+ flash[:notice] = response[:message]
+ redirect_to clusterable.index_path, status: :found
end
def create_gcp
@@ -112,6 +118,19 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
+ def create_aws
+ @aws_cluster = ::Clusters::CreateService
+ .new(current_user, create_aws_cluster_params)
+ .execute
+ .present(current_user: current_user)
+
+ if @aws_cluster.persisted?
+ head :created, location: @aws_cluster.show_path
+ else
+ render status: :unprocessable_entity, json: @aws_cluster.errors
+ end
+ end
+
def create_user
@user_cluster = ::Clusters::CreateService
.new(current_user, create_user_cluster_params)
@@ -129,8 +148,37 @@ class Clusters::ClustersController < Clusters::BaseController
end
end
+ def authorize_aws_role
+ role = current_user.build_aws_role(create_role_params)
+
+ role.save ? respond_201 : respond_422
+ end
+
+ def revoke_aws_role
+ current_user.aws_role&.destroy
+
+ head :no_content
+ end
+
+ def aws_proxy
+ response = Clusters::Aws::ProxyService.new(
+ current_user.aws_role,
+ params: params
+ ).execute
+
+ render json: response.body, status: response.status
+ end
+
private
+ def destroy_params
+ # To be uncomented on https://gitlab.com/gitlab-org/gitlab/merge_requests/16954
+ # This MR got split into other since it was too big.
+ #
+ # params.permit(:cleanup)
+ {}
+ end
+
def update_params
if cluster.provided_by_user?
params.require(:cluster).permit(
@@ -139,6 +187,7 @@ class Clusters::ClustersController < Clusters::BaseController
:environment_scope,
:managed,
:base_domain,
+ :management_project_id,
platform_kubernetes_attributes: [
:api_url,
:token,
@@ -152,6 +201,7 @@ class Clusters::ClustersController < Clusters::BaseController
:environment_scope,
:managed,
:base_domain,
+ :management_project_id,
platform_kubernetes_attributes: [
:namespace
]
@@ -179,6 +229,28 @@ class Clusters::ClustersController < Clusters::BaseController
)
end
+ def create_aws_cluster_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :environment_scope,
+ :managed,
+ provider_aws_attributes: [
+ :key_name,
+ :role_arn,
+ :region,
+ :vpc_id,
+ :instance_type,
+ :num_nodes,
+ :security_group_id,
+ subnet_ids: []
+ ]).merge(
+ provider_type: :aws,
+ platform_type: :kubernetes,
+ clusterable: clusterable.subject
+ )
+ end
+
def create_user_cluster_params
params.require(:cluster).permit(
:enabled,
@@ -198,6 +270,10 @@ class Clusters::ClustersController < Clusters::BaseController
)
end
+ def create_role_params
+ params.require(:cluster).permit(:role_arn, :role_external_id)
+ end
+
def generate_gcp_authorize_url
params = Feature.enabled?(:create_eks_clusters) ? { provider: :gke } : {}
state = generate_session_key_redirect(clusterable.new_path(params).to_s)
diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb
index 5a4b5897a4f..86df0010665 100644
--- a/app/controllers/concerns/confirm_email_warning.rb
+++ b/app/controllers/concerns/confirm_email_warning.rb
@@ -16,7 +16,7 @@ module ConfirmEmailWarning
email = current_user.unconfirmed_email || current_user.email
- flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % {
+ flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % {
email: email,
resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
update_link: view_context.link_to(_('Update it'), profile_path)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index c9a8de0b290..5aa00af8910 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -148,7 +148,7 @@ module IssuableCollections
when 'Issue'
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
- common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
+ common_attributes + [:target_project, :latest_merge_request_diff, source_project: :route, head_pipeline: :project, target_project: :namespace]
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 417bb169f39..61072eec535 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -56,7 +56,7 @@ module LfsRequest
documentation_url: help_url
},
content_type: CONTENT_TYPE,
- status: 403
+ status: :forbidden
)
end
@@ -67,7 +67,7 @@ module LfsRequest
documentation_url: help_url
},
content_type: CONTENT_TYPE,
- status: 404
+ status: :not_found
)
end
diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb
index 62efdacb710..dc392147cb8 100644
--- a/app/controllers/concerns/metrics_dashboard.rb
+++ b/app/controllers/concerns/metrics_dashboard.rb
@@ -3,21 +3,26 @@
# Provides an action which fetches a metrics dashboard according
# to the parameters specified by the controller.
module MetricsDashboard
+ include RenderServiceResults
+ include ChecksCollaboration
+
extend ActiveSupport::Concern
def metrics_dashboard
result = dashboard_finder.find(
project_for_dashboard,
current_user,
- metrics_dashboard_params
+ metrics_dashboard_params.to_h.symbolize_keys
)
- if include_all_dashboards?
- result[:all_dashboards] = dashboard_finder.find_all_paths(project_for_dashboard)
+ if include_all_dashboards? && result
+ result[:all_dashboards] = all_dashboards
end
respond_to do |format|
- if result[:status] == :success
+ if result.nil?
+ format.json { continue_polling_response }
+ elsif result[:status] == :success
format.json { render dashboard_success_response(result) }
else
format.json { render dashboard_error_response(result) }
@@ -27,6 +32,30 @@ module MetricsDashboard
private
+ def all_dashboards
+ dashboards = dashboard_finder.find_all_paths(project_for_dashboard)
+ dashboards.map do |dashboard|
+ amend_dashboard(dashboard)
+ end
+ end
+
+ def amend_dashboard(dashboard)
+ project_dashboard = project_for_dashboard && !dashboard[:system_dashboard]
+
+ dashboard[:can_edit] = project_dashboard ? can_edit?(dashboard) : false
+ dashboard[:project_blob_path] = project_dashboard ? dashboard_project_blob_path(dashboard) : nil
+
+ dashboard
+ end
+
+ def dashboard_project_blob_path(dashboard)
+ project_blob_path(project_for_dashboard, File.join(project_for_dashboard.default_branch, dashboard.fetch(:path, "")))
+ end
+
+ def can_edit?(dashboard)
+ can_collaborate_with_project?(project_for_dashboard, ref: project_for_dashboard.default_branch)
+ end
+
# Override in class to provide arguments to the finder.
def metrics_dashboard_params
{}
@@ -56,7 +85,7 @@ module MetricsDashboard
def dashboard_error_response(result)
{
- status: result[:http_status],
+ status: result[:http_status] || :bad_request,
json: result.slice(:all_dashboards, :message, :status)
}
end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 672d31ec779..dbc575a1487 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -53,12 +53,10 @@ module MilestoneActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def milestone_redirect_path
- if @project
- project_milestone_path(@project, @milestone)
- elsif @group
- group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
+ if @milestone.global_milestone?
+ url_for(action: :show, title: @milestone.title)
else
- dashboard_milestone_path(@milestone.safe_title, title: @milestone.title)
+ url_for(action: :show)
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 2a9729b6ffd..c7c9f2e9b70 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -5,19 +5,10 @@ module PreviewMarkdown
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def preview_markdown
- result = PreviewMarkdownService.new(@project, current_user, params).execute
-
- markdown_params =
- case controller_name
- when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
- when 'snippets' then { skip_project_check: true }
- when 'groups' then { group: group }
- when 'projects' then projects_filter_params
- else {}
- end
+ result = PreviewMarkdownService.new(@project, current_user, markdown_service_params).execute
render json: {
- body: view_context.markdown(result[:text], markdown_params),
+ body: view_context.markdown(result[:text], markdown_context_params),
references: {
users: result[:users],
suggestions: SuggestionSerializer.new.represent_diff(result[:suggestions]),
@@ -26,11 +17,28 @@ module PreviewMarkdown
}
end
+ private
+
def projects_filter_params
{
issuable_state_filter_enabled: true,
suggestions_filter_enabled: params[:preview_suggestions].present?
}
end
+
+ def markdown_service_params
+ params
+ end
+
+ def markdown_context_params
+ case controller_name
+ when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
+ when 'snippets' then { skip_project_check: true }
+ when 'groups' then { group: group }
+ when 'projects' then projects_filter_params
+ else {}
+ end.merge(requested_path: params[:path])
+ end
+
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/redirects_for_missing_path_on_tree.rb b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
new file mode 100644
index 00000000000..085afbf3975
--- /dev/null
+++ b/app/controllers/concerns/redirects_for_missing_path_on_tree.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module RedirectsForMissingPathOnTree
+ def redirect_to_tree_root_for_missing_path(project, ref, path)
+ redirect_to project_tree_path(project, ref), notice: missing_path_on_ref(path, ref)
+ end
+
+ private
+
+ def missing_path_on_ref(path, ref)
+ _('"%{path}" did not exist on "%{ref}"') % { path: truncate_path(path), ref: ref }
+ end
+
+ def truncate_path(path)
+ path.reverse.truncate(60, separator: "/").reverse
+ end
+end
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
index ed9b898a2a3..826fae834fa 100644
--- a/app/controllers/concerns/renders_commits.rb
+++ b/app/controllers/concerns/renders_commits.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
module RendersCommits
- def limited_commits(commits)
- if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
+ def limited_commits(commits, commits_count)
+ if commits_count > MergeRequestDiff::COMMITS_SAFE_SIZE
[
commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE),
- commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE
+ commits_count - MergeRequestDiff::COMMITS_SAFE_SIZE
]
else
[commits, 0]
@@ -14,9 +14,10 @@ module RendersCommits
# This is used as a helper method in a controller.
# rubocop: disable Gitlab/ModuleWithInstanceVariables
- def set_commits_for_rendering(commits)
- @total_commit_count = commits.size
- limited, @hidden_commit_count = limited_commits(commits)
+ def set_commits_for_rendering(commits, commits_count: nil)
+ @total_commit_count = commits_count || commits.size
+ limited, @hidden_commit_count = limited_commits(commits, @total_commit_count)
+ commits.each(&:lazy_author) # preload authors
prepare_commits_for_rendering(limited)
end
# rubocop: enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 45f9888a040..1b2e6461dee 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -47,7 +47,7 @@ module RoutableActions
canonical_path = routable.full_path
if canonical_path != requested_full_path
- if canonical_path.casecmp(requested_full_path) != 0
+ if !request.xhr? && request.format.html? && canonical_path.casecmp(requested_full_path) != 0
flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
end
diff --git a/app/controllers/concerns/sourcegraph_gon.rb b/app/controllers/concerns/sourcegraph_gon.rb
new file mode 100644
index 00000000000..ab4abd734fb
--- /dev/null
+++ b/app/controllers/concerns/sourcegraph_gon.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module SourcegraphGon
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :push_sourcegraph_gon, unless: :json_request?
+ end
+
+ private
+
+ def push_sourcegraph_gon
+ return unless sourcegraph_enabled?
+
+ gon.push({
+ sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url }
+ })
+ end
+
+ def sourcegraph_enabled?
+ Gitlab::CurrentSettings.sourcegraph_enabled && sourcegraph_enabled_for_project? && current_user&.sourcegraph_enabled
+ end
+
+ def sourcegraph_enabled_for_project?
+ return false unless project && Gitlab::Sourcegraph.feature_enabled?(project)
+ return project.public? if Gitlab::CurrentSettings.sourcegraph_public_only
+
+ true
+ end
+end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 80c0a0d88a8..ebee8e9094e 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -22,7 +22,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format|
format.html do
redirect_to dashboard_todos_path,
- status: 302,
+ status: :found,
notice: _('To-do item successfully marked as done.')
end
format.js { head :ok }
@@ -34,7 +34,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, status: 302, notice: _('Everything on your to-do list is marked as done.') }
+ format.html { redirect_to dashboard_todos_path, status: :found, notice: _('Everything on your to-do list is marked as done.') }
format.js { head :ok }
format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 3c86f3108ab..8c9bf17f017 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -6,7 +6,7 @@ class Groups::BoardsController < Groups::ApplicationController
before_action :assign_endpoint_vars
before_action do
- push_frontend_feature_flag(:multi_select_board)
+ push_frontend_feature_flag(:multi_select_board, default_enabled: true)
end
private
diff --git a/app/controllers/groups/group_links_controller.rb b/app/controllers/groups/group_links_controller.rb
new file mode 100644
index 00000000000..7965311c5f1
--- /dev/null
+++ b/app/controllers/groups/group_links_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class Groups::GroupLinksController < Groups::ApplicationController
+ before_action :check_feature_flag!
+ before_action :authorize_admin_group!
+
+ def create
+ shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present?
+
+ if shared_with_group
+ result = Groups::GroupLinks::CreateService
+ .new(shared_with_group, current_user, group_link_create_params)
+ .execute(group)
+
+ return render_404 if result[:http_status] == 404
+
+ flash[:alert] = result[:message] if result[:status] == :error
+ else
+ flash[:alert] = _('Please select a group.')
+ end
+
+ redirect_to group_group_members_path(group)
+ end
+
+ private
+
+ def group_link_create_params
+ params.permit(:shared_group_access, :expires_at)
+ end
+
+ def check_feature_flag!
+ render_404 unless Feature.enabled?(:share_group_with_group)
+ end
+end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 26768c628ca..1034ca6cd7b 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -63,7 +63,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format|
format.html do
- redirect_to group_labels_path(@group), status: 302, notice: "#{@label.name} deleted permanently"
+ redirect_to group_labels_path(@group), status: :found, notice: "#{@label.name} deleted permanently"
end
format.js
end
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index e09a9e6eb21..cfddd8a3ba9 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -16,7 +16,7 @@ module Groups
render json: ContainerRepositoriesSerializer
.new(current_user: current_user)
- .represent(@images)
+ .represent_read_only(@images)
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 35e364abba3..755d97b091c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -6,6 +6,7 @@ class GroupsController < Groups::ApplicationController
include ParamsBackwardCompatibility
include PreviewMarkdown
include RecordUserLastActivity
+ extend ::Gitlab::Utils::Override
respond_to :html
@@ -24,6 +25,10 @@ class GroupsController < Groups::ApplicationController
before_action :user_actions, only: [:show]
+ before_action do
+ push_frontend_feature_flag(:vue_issuables_list, @group)
+ end
+
skip_cross_project_access_check :index, :new, :create, :edit, :update,
:destroy, :projects
# When loading show as an atom feed, we render events that could leak cross
@@ -111,7 +116,7 @@ class GroupsController < Groups::ApplicationController
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
- redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
+ redirect_to root_path, status: :found, alert: "Group '#{@group.name}' was scheduled for deletion."
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -233,6 +238,11 @@ class GroupsController < Groups::ApplicationController
@group.self_and_descendants.public_or_visible_to_user(current_user)
end
end
+
+ override :markdown_service_params
+ def markdown_service_params
+ params.merge(group: group)
+ end
end
GroupsController.prepend_if_ee('EE::GroupsController')
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index efd5f0fc607..c6a02250896 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -5,6 +5,11 @@ class HealthController < ActionController::Base
include RequiresWhitelistedMonitoringClient
CHECKS = [
+ Gitlab::HealthChecks::MasterCheck
+ ].freeze
+
+ ALL_CHECKS = [
+ *CHECKS,
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::Redis::RedisCheck,
Gitlab::HealthChecks::Redis::CacheCheck,
@@ -14,8 +19,9 @@ class HealthController < ActionController::Base
].freeze
def readiness
- # readiness check is a collection with all above application-level checks
- render_checks(*CHECKS)
+ # readiness check is a collection of application-level checks
+ # and optionally all service checks
+ render_checks(params[:all] ? ALL_CHECKS : CHECKS)
end
def liveness
@@ -25,7 +31,7 @@ class HealthController < ActionController::Base
private
- def render_checks(*checks)
+ def render_checks(checks = [])
result = Gitlab::HealthChecks::Probes::Collection
.new(*checks)
.execute
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index a58235790ad..97895d6461c 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -36,7 +36,7 @@ class HelpController < ApplicationController
render 'show.html.haml'
else
# Force template to Haml
- render 'errors/not_found.html.haml', layout: 'errors', status: 404
+ render 'errors/not_found.html.haml', layout: 'errors', status: :not_found
end
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index 4d8875937eb..71a88bf3395 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -4,7 +4,7 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
extend ::Gitlab::Utils::Override
def self.define_providers!
- return unless Gitlab::Auth::LDAP::Config.enabled?
+ return unless Gitlab::Auth::LDAP::Config.sign_in_enabled?
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
alias_method server['provider_name'], :ldap
@@ -14,6 +14,8 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
+ return unless Gitlab::Auth::LDAP::Config.sign_in_enabled?
+
sign_in_user_flow(Gitlab::Auth::LDAP::User)
end
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index c97fec0a6ee..e5d4a4bb073 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -16,12 +16,7 @@ class NotificationSettingsController < ApplicationController
@notification_setting = current_user.notification_settings.find(params[:id])
@saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source))
- if params[:hide_label].present?
- btn_class = params[:project_id].present? ? 'btn-xs' : ''
- render_response("shared/notifications/_new_button", btn_class)
- else
- render_response
- end
+ render_response
end
private
@@ -42,7 +37,16 @@ class NotificationSettingsController < ApplicationController
can?(current_user, ability_name, resource)
end
- def render_response(response_template = "shared/notifications/_button", btn_class = nil)
+ def render_response
+ btn_class = nil
+
+ if params[:hide_label].present?
+ btn_class = 'btn-xs' if params[:project_id].present?
+ response_template = 'shared/notifications/_new_button'
+ else
+ response_template = 'shared/notifications/_button'
+ end
+
render json: {
html: view_to_html_string(response_template, notification_setting: @notification_setting, btn_class: btn_class),
saved: @saved
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 12dc2d1af1c..8dd51ce1d64 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -57,7 +57,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
end
rescue_from ActiveRecord::RecordNotFound do |exception|
- render "errors/not_found", layout: "errors", status: 404
+ render "errors/not_found", layout: "errors", status: :not_found
end
def create_application_params
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index a59ade559b3..9cfa57c53a5 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -13,7 +13,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
end
redirect_to applications_profile_url,
- status: 302,
+ status: :found,
notice: I18n.t(:notice, scope: [:doorkeeper, :flash, :authorized_applications, :destroy])
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index b992972dfb8..eca58748cc5 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -47,7 +47,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def omniauth_error
@provider = params[:provider]
@error = params[:error]
- render 'errors/omniauth_error', layout: "oauth_error", status: 422
+ render 'errors/omniauth_error', layout: "oauth_error", status: :unprocessable_entity
end
def cas3
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 42d4d785174..214640a5295 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -47,7 +47,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:preferred_language,
:time_display_relative,
:time_format_in_24h,
- :show_whitespace_in_diffs
+ :show_whitespace_in_diffs,
+ :sourcegraph_enabled
]
end
end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
index 866c4dee6e2..84ce4a56e64 100644
--- a/app/controllers/profiles/u2f_registrations_controller.rb
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -4,6 +4,6 @@ class Profiles::U2fRegistrationsController < Profiles::ApplicationController
def destroy
u2f_registration = current_user.u2f_registrations.find(params[:id])
u2f_registration.destroy
- redirect_to profile_two_factor_auth_path, status: 302, notice: _("Successfully deleted U2F device.")
+ redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted U2F device.")
end
end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 9076bdb9f04..92655d593dd 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -3,6 +3,7 @@
# Controller for viewing a file's blame
class Projects::BlameController < Projects::ApplicationController
include ExtractsPath
+ include RedirectsForMissingPathOnTree
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -11,7 +12,9 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
- return render_404 unless @blob
+ unless @blob
+ return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
+ end
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7c3d43fb49a..7c97f771a70 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -7,6 +7,9 @@ class Projects::BlobController < Projects::ApplicationController
include RendersBlob
include NotesHelper
include ActionView::Helpers::SanitizeHelper
+ include RedirectsForMissingPathOnTree
+ include SourcegraphGon
+
prepend_before_action :authenticate_user!, only: [:edit]
around_action :allow_gitaly_ref_name_caching, only: [:show]
@@ -119,7 +122,7 @@ class Projects::BlobController < Projects::ApplicationController
end
end
- return render_404
+ return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 3b335fa4af4..db05da0bb7f 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -8,7 +8,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars
before_action do
- push_frontend_feature_flag(:multi_select_board)
+ push_frontend_feature_flag(:multi_select_board, default_enabled: true)
end
private
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 939a09d4fd2..afb670b687b 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -8,6 +8,7 @@ class Projects::CommitController < Projects::ApplicationController
include CreatesCommit
include DiffForPath
include DiffHelper
+ include SourcegraphGon
# Authorize
before_action :require_non_empty_project
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index c053ca19a94..4562296cea0 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -13,8 +13,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
- push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint, default_enabled: true)
- push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
end
@@ -133,7 +131,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
if environment
redirect_to environment_metrics_path(environment)
else
- render :empty
+ render :empty_metrics
end
end
@@ -199,8 +197,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def metrics_dashboard_params
params
- .permit(:embedded, :group, :title, :y_label)
- .to_h.symbolize_keys
+ .permit(:embedded, :group, :title, :y_label, :dashboard_path, :environment)
.merge(dashboard_path: params[:dashboard], environment: environment)
end
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
index 88d0755f41f..9dea6b663ea 100644
--- a/app/controllers/projects/error_tracking_controller.rb
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -15,6 +15,23 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end
end
+ def details
+ respond_to do |format|
+ format.html
+ format.json do
+ render_issue_detail_json
+ end
+ end
+ end
+
+ def stack_trace
+ respond_to do |format|
+ format.json do
+ render_issue_stack_trace_json
+ end
+ end
+ end
+
def list_projects
respond_to do |format|
format.json do
@@ -29,10 +46,7 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
service = ErrorTracking::ListIssuesService.new(project, current_user)
result = service.execute
- unless result[:status] == :success
- return render json: { message: result[:message] },
- status: result[:http_status] || :bad_request
- end
+ return if handle_errors(result)
render json: {
errors: serialize_errors(result[:issues]),
@@ -40,6 +54,28 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
}
end
+ def render_issue_detail_json
+ service = ErrorTracking::IssueDetailsService.new(project, current_user, issue_details_params)
+ result = service.execute
+
+ return if handle_errors(result)
+
+ render json: {
+ error: serialize_detailed_error(result[:issue])
+ }
+ end
+
+ def render_issue_stack_trace_json
+ service = ErrorTracking::IssueLatestEventService.new(project, current_user, issue_details_params)
+ result = service.execute
+
+ return if handle_errors(result)
+
+ render json: {
+ error: serialize_error_event(result[:latest_event])
+ }
+ end
+
def render_project_list_json
service = ErrorTracking::ListProjectsService.new(
project,
@@ -62,10 +98,21 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
end
end
+ def handle_errors(result)
+ unless result[:status] == :success
+ render json: { message: result[:message] },
+ status: result[:http_status] || :bad_request
+ end
+ end
+
def list_projects_params
params.require(:error_tracking_setting).permit([:api_host, :token])
end
+ def issue_details_params
+ params.permit(:issue_id)
+ end
+
def set_polling_interval
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
end
@@ -76,6 +123,18 @@ class Projects::ErrorTrackingController < Projects::ApplicationController
.represent(errors)
end
+ def serialize_detailed_error(error)
+ ErrorTracking::DetailedErrorSerializer
+ .new(project: project, user: current_user)
+ .represent(error)
+ end
+
+ def serialize_error_event(event)
+ ErrorTracking::ErrorEventSerializer
+ .new(project: project, user: current_user)
+ .represent(event)
+ end
+
def serialize_projects(projects)
ErrorTracking::ProjectSerializer
.new(project: project, user: current_user)
diff --git a/app/controllers/projects/grafana_api_controller.rb b/app/controllers/projects/grafana_api_controller.rb
index 4bdf4c12cac..380a18818ab 100644
--- a/app/controllers/projects/grafana_api_controller.rb
+++ b/app/controllers/projects/grafana_api_controller.rb
@@ -2,6 +2,7 @@
class Projects::GrafanaApiController < Projects::ApplicationController
include RenderServiceResults
+ include MetricsDashboard
def proxy
result = ::Grafana::ProxyService.new(
@@ -19,6 +20,10 @@ class Projects::GrafanaApiController < Projects::ApplicationController
private
+ def metrics_dashboard_params
+ params.permit(:embedded, :grafana_url)
+ end
+
def query_params
params.permit(:query, :start, :end, :step)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 96cb400950b..009765702ab 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
+ push_frontend_feature_flag(:release_search_filter, project)
end
respond_to :html
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 386a1f00bd2..b7aeab8f5ff 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -76,7 +76,7 @@ class Projects::LabelsController < Projects::ApplicationController
@labels = find_labels
redirect_to project_labels_path(@project),
- status: 302,
+ status: :found,
notice: 'Label was removed'
end
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index a1983bc5462..1273c55b83a 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -109,7 +109,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
message: lfs_read_only_message
},
content_type: LfsRequest::CONTENT_TYPE,
- status: 403
+ status: :forbidden
)
end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 808265634da..78dc196b08e 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -109,7 +109,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@target_project = @merge_request.target_project
@source_project = @merge_request.source_project
- @commits = set_commits_for_rendering(@merge_request.commits)
+
+ @commits =
+ set_commits_for_rendering(
+ @merge_request.recent_commits.with_latest_pipeline(@merge_request.source_branch),
+ commits_count: @merge_request.commits_count
+ )
+
@commit = @merge_request.diff_head_commit
# FIXME: We have to assign a presenter to another instance variable
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 4a37dfe5c19..42f9c0522a3 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -31,6 +31,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
options = {
merge_request: @merge_request,
+ diff_view: diff_view,
pagination_data: diffs.pagination_data
}
@@ -60,7 +61,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
render: ->(partial, locals) { view_to_html_string(partial, locals) }
}
- render json: DiffsSerializer.new(request).represent(@diffs, additional_attributes)
+ options = additional_attributes.merge(diff_view: diff_view)
+
+ render json: DiffsSerializer.new(request).represent(@diffs, options)
end
def define_diff_vars
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ff199e05e99..766ec1e33f3 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -9,11 +9,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include ToggleAwardEmoji
include IssuableCollections
include RecordUserLastActivity
+ include SourcegraphGon
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_test_reports!, only: [:test_reports]
+ before_action :authorize_read_actual_head_pipeline!, only: [:test_reports, :exposed_artifacts]
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]
@@ -23,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
+ push_frontend_feature_flag(:release_search_filter, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
@@ -89,7 +91,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Get commits from repository
# or from cache if already merged
@commits =
- set_commits_for_rendering(@merge_request.commits.with_latest_pipeline)
+ set_commits_for_rendering(
+ @merge_request.recent_commits.with_latest_pipeline(@merge_request.source_branch),
+ commits_count: @merge_request.commits_count
+ )
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
@@ -115,6 +120,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
reports_response(@merge_request.compare_test_reports)
end
+ def exposed_artifacts
+ if @merge_request.has_exposed_artifacts?
+ reports_response(@merge_request.find_exposed_artifacts)
+ else
+ head :no_content
+ end
+ end
+
def edit
define_edit_vars
end
@@ -218,6 +231,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.rebase_async(current_user.id)
head :ok
+ rescue MergeRequest::RebaseLockTimeout => e
+ render json: { merge_error: e.message }, status: :conflict
end
def discussions
@@ -241,7 +256,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def merge_params_attributes
- [:should_remove_source_branch, :commit_message, :squash_commit_message, :squash, :auto_merge_strategy]
+ MergeRequest::KNOWN_MERGE_PARAMS
end
def auto_merge_requested?
@@ -281,7 +296,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
- @merge_request.update(merge_error: nil, squash: merge_params.fetch(:squash, false))
+ @merge_request.update(merge_error: nil, squash: params.fetch(:squash, false))
if auto_merge_requested?
if merge_request.auto_merge_enabled?
@@ -353,12 +368,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
when :error
render json: { status_reason: report_comparison[:status_reason] }, status: :bad_request
else
- render json: { status_reason: 'Unknown error' }, status: :internal_server_error
+ raise "Failed to build comparison response as comparison yielded unknown status '#{report_comparison[:status]}'"
end
end
- def authorize_test_reports!
- # MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports.
+ def authorize_read_actual_head_pipeline!
return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline)
end
end
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 73e629ab7c3..722fc30b3ff 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -21,7 +21,7 @@ class Projects::PagesController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to project_pages_path(@project),
- status: 302,
+ status: :found,
notice: 'Pages were removed'
end
end
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index c287e440db0..b693642981e 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -8,6 +8,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :domain, except: [:new, :create]
def show
+ redirect_to edit_project_pages_domain_path(@project, @domain)
end
def new
@@ -23,7 +24,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
flash[:alert] = 'Failed to verify domain ownership'
end
- redirect_to project_pages_domain_path(@project, @domain)
+ redirect_to edit_project_pages_domain_path(@project, @domain)
end
def edit
@@ -33,7 +34,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
@domain = @project.pages_domains.create(create_params)
if @domain.valid?
- redirect_to project_pages_domain_path(@project, @domain)
+ redirect_to edit_project_pages_domain_path(@project, @domain)
else
render 'new'
end
@@ -42,7 +43,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
def update
if @domain.update(update_params)
redirect_to project_pages_path(@project),
- status: 302,
+ status: :found,
notice: 'Domain was updated'
else
render 'edit'
@@ -55,13 +56,21 @@ class Projects::PagesDomainsController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to project_pages_path(@project),
- status: 302,
+ status: :found,
notice: 'Domain was removed'
end
format.js
end
end
+ def clean_certificate
+ unless @domain.update(user_provided_certificate: nil, user_provided_key: nil)
+ flash[:alert] = @domain.errors.full_messages.join(', ')
+ end
+
+ redirect_to edit_project_pages_domain_path(@project, @domain)
+ end
+
private
def create_params
@@ -69,7 +78,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def update_params
- params.require(:pages_domain).permit(:user_provided_key, :user_provided_certificate, :auto_ssl_enabled)
+ params.fetch(:pages_domain, {}).permit(:user_provided_key, :user_provided_certificate, :auto_ssl_enabled)
end
def domain
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 106ef1b72c1..4d35353d5f5 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -12,6 +12,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
push_frontend_feature_flag(:hide_dismissed_vulnerabilities)
+ push_frontend_feature_flag(:junit_pipeline_view)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show]
@@ -156,14 +157,21 @@ class Projects::PipelinesController < Projects::ApplicationController
def test_report
return unless Feature.enabled?(:junit_pipeline_view, project)
- if pipeline_test_report == :error
- render json: { status: :error_parsing_report }
- return
- end
+ respond_to do |format|
+ format.html do
+ render 'show'
+ end
- render json: TestReportSerializer
- .new(current_user: @current_user)
- .represent(pipeline_test_report)
+ format.json do
+ if pipeline_test_report == :error
+ render json: { status: :error_parsing_report }
+ else
+ render json: TestReportSerializer
+ .new(current_user: @current_user)
+ .represent(pipeline_test_report)
+ end
+ end
+ end
end
private
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 717df9f09e0..72c82aec31d 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -2,12 +2,48 @@
class Projects::ReleasesController < Projects::ApplicationController
# Authorize
- before_action :require_non_empty_project
+ before_action :require_non_empty_project, except: [:index]
+ before_action :release, only: %i[edit update]
before_action :authorize_read_release!
before_action do
- push_frontend_feature_flag(:release_edit_page, project)
+ push_frontend_feature_flag(:release_edit_page, project, default_enabled: true)
+ push_frontend_feature_flag(:release_issue_summary, project)
end
+ before_action :authorize_update_release!, only: %i[edit update]
def index
+ respond_to do |format|
+ format.html do
+ require_non_empty_project
+ end
+ format.json { render json: releases }
+ end
+ end
+
+ protected
+
+ def releases
+ ReleasesFinder.new(@project, current_user).execute
+ end
+
+ def edit
+ respond_to do |format|
+ format.html { render 'edit' }
+ end
+ end
+
+ private
+
+ def authorize_update_release!
+ access_denied! unless Feature.enabled?(:release_edit_page, project, default_enabled: true)
+ access_denied! unless can?(current_user, :update_release, release)
+ end
+
+ def release
+ @release ||= project.releases.find_by_tag!(sanitized_tag_name)
+ end
+
+ def sanitized_tag_name
+ CGI.unescape(params[:tag])
end
end
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 5bf3618b389..1571cb8cd34 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -70,7 +70,7 @@ module Projects
project: [:slug, :name, :organization_slug, :organization_name]
],
- grafana_integration_attributes: [:token, :grafana_url]
+ grafana_integration_attributes: [:token, :grafana_url, :enabled]
}
end
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 7d9387b1d94..c89bfd110c4 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -84,7 +84,7 @@ class Projects::TagsController < Projects::ApplicationController
format.html do
redirect_to project_tags_path(@project),
- alert: @error, status: 303
+ alert: @error, status: :see_other
end
format.js do
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 7509cc29a76..eec89afe354 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -5,6 +5,7 @@ class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
include CreatesCommit
include ActionView::Helpers::SanitizeHelper
+ include RedirectsForMissingPathOnTree
around_action :allow_gitaly_ref_name_caching, only: [:show]
@@ -19,12 +20,9 @@ class Projects::TreeController < Projects::ApplicationController
if tree.entries.empty?
if @repository.blob_at(@commit.id, @path)
- return redirect_to(
- project_blob_path(@project,
- File.join(@ref, @path))
- )
+ return redirect_to project_blob_path(@project, File.join(@ref, @path))
elsif @path.present?
- return render_404
+ return redirect_to_tree_root_for_missing_path(@project, @ref, @path)
end
end
diff --git a/app/controllers/projects/usage_ping_controller.rb b/app/controllers/projects/usage_ping_controller.rb
new file mode 100644
index 00000000000..ebdf28bd59c
--- /dev/null
+++ b/app/controllers/projects/usage_ping_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Projects::UsagePingController < Projects::ApplicationController
+ before_action :authenticate_user!
+
+ def web_ide_clientside_preview
+ return render_404 unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
+
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_previews_count
+
+ head(200)
+ end
+end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index b187fdb2723..fb06299676c 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -110,7 +110,7 @@ class Projects::WikisController < Projects::ApplicationController
WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
- status: 302,
+ status: :found,
notice: _("Page was successfully deleted")
rescue Gitlab::Git::Wiki::OperationError => e
@error = e
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index abd19df9a3d..e5dea031bb5 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -154,7 +154,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to dashboard_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => ex
- redirect_to edit_project_path(@project), status: 302, alert: ex.message
+ redirect_to edit_project_path(@project), status: :found, alert: ex.message
end
def new_issuable_address
@@ -371,6 +371,7 @@ class ProjectsController < Projects::ApplicationController
:path,
:printing_merge_request_link_enabled,
:public_builds,
+ :remove_source_branch_after_merge,
:request_access_enabled,
:runners_token,
:tag_list,
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 4a746fc915d..5fc7f5c84f0 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 :require_role, only: [:welcome, :update_role]
+ skip_before_action :required_signup_info, only: [:welcome, :update_registration]
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
before_action :ensure_terms_accepted,
@@ -16,6 +16,7 @@ class RegistrationsController < Devise::RegistrationsController
def new
if experiment_enabled?(:signup_flow)
+ track_experiment_event(:signup_flow, 'start') # We want this event to be tracked when the user is _in_ the experimental group
@resource = build_resource
else
redirect_to new_user_session_path(anchor: 'register-pane')
@@ -23,6 +24,8 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
+ track_experiment_event(:signup_flow, 'end') unless experiment_enabled?(:signup_flow) # We want this event to be tracked when the user is _in_ the control group
+
accept_pending_invitations
super do |new_user|
@@ -42,29 +45,30 @@ class RegistrationsController < Devise::RegistrationsController
if destroy_confirmation_valid?
current_user.delete_async(deleted_by: current_user)
session.try(:destroy)
- redirect_to new_user_session_path, status: 303, notice: s_('Profiles|Account scheduled for removal.')
+ redirect_to new_user_session_path, status: :see_other, notice: s_('Profiles|Account scheduled for removal.')
else
- redirect_to profile_account_path, status: 303, alert: destroy_confirmation_failure_message
+ redirect_to profile_account_path, status: :see_other, alert: destroy_confirmation_failure_message
end
end
def welcome
return redirect_to new_user_registration_path unless current_user
- return redirect_to stored_location_or_dashboard_or_almost_there_path(current_user) if current_user.role.present?
+ return redirect_to stored_location_or_dashboard_or_almost_there_path(current_user) if current_user.role.present? && !current_user.setup_for_company.nil?
- current_user.name = nil
+ current_user.name = nil if current_user.name == current_user.username
render layout: 'devise_experimental_separate_sign_up_flow'
end
- def update_role
- user_params = params.require(:user).permit(:name, :role)
- result = ::Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
+ def update_registration
+ user_params = params.require(:user).permit(:name, :role, :setup_for_company)
+ result = ::Users::SignupService.new(current_user, user_params).execute
if result[:status] == :success
+ track_experiment_event(:signup_flow, 'end') # We want this event to be tracked when the user is _in_ the experimental group
set_flash_message! :notice, :signed_up
redirect_to stored_location_or_dashboard_or_almost_there_path(current_user)
else
- redirect_to users_sign_up_welcome_path, alert: result[:message]
+ render :welcome, layout: 'devise_experimental_separate_sign_up_flow'
end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1c506065b56..0007d5826ba 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -24,6 +24,7 @@ class SessionsController < Devise::SessionsController
before_action :store_unauthenticated_sessions, only: [:new]
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha
+ before_action :frontend_tracking_data, only: [:new]
after_action :log_failed_login, if: :action_new_and_failed_login?
@@ -269,7 +270,13 @@ class SessionsController < Devise::SessionsController
end
def ldap_servers
- @ldap_servers ||= Gitlab::Auth::LDAP::Config.available_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?
@@ -293,6 +300,11 @@ class SessionsController < Devise::SessionsController
"standard"
end
end
+
+ def frontend_tracking_data
+ # We want tracking data pushed to the frontend when the user is _in_ the control group
+ frontend_experimentation_tracking_data(:signup_flow, 'start') unless experiment_enabled?(:signup_flow)
+ end
end
SessionsController.prepend_if_ee('EE::SessionsController')
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index c3c227b08c5..06374736dcf 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -16,7 +16,7 @@ class UsersController < ApplicationController
skip_before_action :authenticate_user!
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
- before_action :user, except: [:exists]
+ before_action :user, except: [:exists, :suggests]
before_action :authorize_read_user_profile!,
only: [:calendar, :calendar_activities, :groups, :projects, :contributed_projects, :starred_projects, :snippets]
@@ -114,6 +114,14 @@ class UsersController < ApplicationController
render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
end
+ def suggests
+ namespace_path = params[:username]
+ exists = !!Namespace.find_by_path_or_name(namespace_path)
+ suggestions = exists ? [Namespace.clean_path(namespace_path)] : []
+
+ render json: { exists: exists, suggests: suggestions }
+ end
+
private
def user
diff --git a/app/finders/abuse_reports_finder.rb b/app/finders/abuse_reports_finder.rb
new file mode 100644
index 00000000000..04043f36426
--- /dev/null
+++ b/app/finders/abuse_reports_finder.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AbuseReportsFinder
+ attr_reader :params
+
+ def initialize(params = {})
+ @params = params
+ end
+
+ def execute
+ reports = AbuseReport.all
+ reports = reports.by_user(params[:user_id]) if params[:user_id].present?
+
+ reports.with_order_id_desc
+ .with_users
+ .page(params[:page])
+ end
+end
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index e2b9b0b44c1..53dbf65c43a 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -12,7 +12,7 @@ class Admin::ProjectsFinder
def execute
items = Project.without_deleted.with_statistics.with_route
items = by_namespace_id(items)
- items = by_visibilty_level(items)
+ items = by_visibility_level(items)
items = by_with_push(items)
items = by_abandoned(items)
items = by_last_repository_check_failed(items)
@@ -31,7 +31,7 @@ class Admin::ProjectsFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def by_visibilty_level(items)
+ def by_visibility_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 291a24c1405..8001c70a9b2 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
-class BranchesFinder
+class BranchesFinder < GitRefsFinder
def initialize(repository, params = {})
- @repository = repository
- @params = params
+ super(repository, params)
end
def execute
@@ -15,56 +14,10 @@ class BranchesFinder
private
- attr_reader :repository, :params
-
def names
@params[:names].presence
end
- def search
- @params[:search].presence
- end
-
- def sort
- @params[:sort].presence || 'name'
- end
-
- def by_search(branches)
- return branches unless search
-
- case search
- when ->(v) { v.starts_with?('^') }
- filter_branches_with_prefix(branches, search.slice(1..-1).upcase)
- when ->(v) { v.ends_with?('$') }
- filter_branches_with_suffix(branches, search.chop.upcase)
- else
- matches = filter_branches_by_name(branches, search.upcase)
- set_exact_match_as_first_result(matches, search)
- end
- end
-
- def filter_branches_with_prefix(branches, prefix)
- branches.select { |branch| branch.name.upcase.starts_with?(prefix) }
- end
-
- def filter_branches_with_suffix(branches, suffix)
- branches.select { |branch| branch.name.upcase.ends_with?(suffix) }
- end
-
- def filter_branches_by_name(branches, term)
- branches.select { |branch| branch.name.upcase.include?(term) }
- end
-
- def set_exact_match_as_first_result(matches, term)
- exact_match_index = find_exact_match_index(matches, term)
- matches.insert(0, matches.delete_at(exact_match_index)) if exact_match_index
- matches
- end
-
- def find_exact_match_index(matches, term)
- matches.index { |branch| branch.name.casecmp(term) == 0 }
- end
-
def by_names(branches)
return branches unless names
diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb
index eb91d7f825b..34921df840b 100644
--- a/app/finders/container_repositories_finder.rb
+++ b/app/finders/container_repositories_finder.rb
@@ -1,34 +1,38 @@
# frozen_string_literal: true
class ContainerRepositoriesFinder
- # id: group or project id
- # container_type: :group or :project
- def initialize(id:, container_type:)
- @id = id
- @type = container_type.to_sym
+ VALID_SUBJECTS = [Group, Project].freeze
+
+ def initialize(user:, subject:)
+ @user = user
+ @subject = subject
end
def execute
- if project_type?
- project.container_repositories
- else
- group.container_repositories
- end
+ 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)
end
private
- attr_reader :id, :type
+ def valid_subject_type?
+ VALID_SUBJECTS.include?(@subject.class)
+ end
+
+ def project_repositories
+ return unless @subject.container_registry_enabled
- def project_type?
- type == :project
+ @subject.container_repositories
end
- def project
- Project.find(id)
+ def group_repositories
+ ContainerRepository.for_group_and_its_subgroups(@subject)
end
- def group
- Group.find(id)
+ def authorized?
+ Ability.allowed?(@user, :read_container_image, @subject)
end
end
diff --git a/app/finders/git_refs_finder.rb b/app/finders/git_refs_finder.rb
new file mode 100644
index 00000000000..2289b34e562
--- /dev/null
+++ b/app/finders/git_refs_finder.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class GitRefsFinder
+ def initialize(repository, params = {})
+ @repository = repository
+ @params = params
+ end
+
+ protected
+
+ attr_reader :repository, :params
+
+ def search
+ @params[:search].presence
+ end
+
+ def sort
+ @params[:sort].presence || 'name'
+ end
+
+ def by_search(refs)
+ return refs unless search
+
+ case search
+ when ->(v) { v.starts_with?('^') }
+ filter_refs_with_prefix(refs, search.slice(1..-1))
+ when ->(v) { v.ends_with?('$') }
+ filter_refs_with_suffix(refs, search.chop)
+ else
+ matches = filter_refs_by_name(refs, search)
+ set_exact_match_as_first_result(matches, search)
+ end
+ end
+
+ def filter_refs_with_prefix(refs, prefix)
+ refs.select { |ref| ref.name.upcase.starts_with?(prefix.upcase) }
+ end
+
+ def filter_refs_with_suffix(refs, suffix)
+ refs.select { |ref| ref.name.upcase.ends_with?(suffix.upcase) }
+ end
+
+ def filter_refs_by_name(refs, term)
+ refs.select { |ref| ref.name.upcase.include?(term.upcase) }
+ end
+
+ def set_exact_match_as_first_result(matches, term)
+ exact_match_index = find_exact_match_index(matches, term)
+ matches.insert(0, matches.delete_at(exact_match_index)) if exact_match_index
+ matches
+ end
+
+ def find_exact_match_index(matches, term)
+ matches.index { |ref| ref.name.casecmp(term) == 0 }
+ end
+end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 4e489a9c930..1f6829a97d6 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -80,7 +80,7 @@ class GroupDescendantsFinder
if current_user
authorized_groups = GroupsFinder.new(current_user,
all_available: false)
- .execute.as('authorized')
+ .execute.arel.as('authorized')
authorized_to_user = groups_table.project(1).from(authorized_groups)
.where(authorized_groups[:id].eq(groups_table[:id]))
.exists
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 477093ddadf..dfddd32d7df 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -385,6 +385,9 @@ class IssuableFinder
end
def count_key(value)
+ # value may be an array if the finder used in `count_by_state` added an
+ # additional `group by`. Anyway we are sure that state will be always the
+ # last item because it's added as the last one to the query.
value = Array(value).last
klass.available_states.key(value)
end
@@ -483,6 +486,7 @@ class IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_search(items)
return items unless search
+ return items if items.is_a?(ActiveRecord::NullRelation)
if use_cte_for_search?
cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index df06e68c941..42a15234e57 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -110,7 +110,10 @@ class ProjectsFinder < UnionFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_ids(items)
- project_ids_relation ? items.where(id: project_ids_relation) : items
+ items = items.where(id: project_ids_relation) if project_ids_relation
+ items = items.where('id > ?', params[:id_after]) if params[:id_after]
+ items = items.where('id < ?', params[:id_before]) if params[:id_before]
+ items
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/prometheus_metrics_finder.rb b/app/finders/prometheus_metrics_finder.rb
new file mode 100644
index 00000000000..84a071abbd5
--- /dev/null
+++ b/app/finders/prometheus_metrics_finder.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+class PrometheusMetricsFinder
+ ACCEPTED_PARAMS = [
+ :project,
+ :group,
+ :title,
+ :y_label,
+ :identifier,
+ :id,
+ :common,
+ :ordered
+ ].freeze
+
+ # Cautiously preferring a memoized class method over a constant
+ # so that the DB connection is accessed after the class is loaded.
+ def self.indexes
+ @indexes ||= PrometheusMetric
+ .connection
+ .indexes(:prometheus_metrics)
+ .map { |index| index.columns.map(&:to_sym) }
+ end
+
+ def initialize(params = {})
+ @params = params.slice(*ACCEPTED_PARAMS)
+ end
+
+ # @return [PrometheusMetric, PrometheusMetric::ActiveRecord_Relation]
+ def execute
+ validate_params!
+
+ metrics = by_project(::PrometheusMetric.all)
+ metrics = by_group(metrics)
+ metrics = by_title(metrics)
+ metrics = by_y_label(metrics)
+ metrics = by_common(metrics)
+ metrics = by_ordered(metrics)
+ metrics = by_identifier(metrics)
+ metrics = by_id(metrics)
+
+ metrics
+ end
+
+ private
+
+ attr_reader :params
+
+ def by_project(metrics)
+ return metrics unless params[:project]
+
+ metrics.for_project(params[:project])
+ end
+
+ def by_group(metrics)
+ return metrics unless params[:group]
+
+ metrics.for_group(params[:group])
+ end
+
+ def by_title(metrics)
+ return metrics unless params[:title]
+
+ metrics.for_title(params[:title])
+ end
+
+ def by_y_label(metrics)
+ return metrics unless params[:y_label]
+
+ metrics.for_y_label(params[:y_label])
+ end
+
+ def by_common(metrics)
+ return metrics unless params[:common]
+
+ metrics.common
+ end
+
+ def by_ordered(metrics)
+ return metrics unless params[:ordered]
+
+ metrics.ordered
+ end
+
+ def by_identifier(metrics)
+ return metrics unless params[:identifier]
+
+ metrics.for_identifier(params[:identifier])
+ end
+
+ def by_id(metrics)
+ return metrics unless params[:id]
+
+ metrics.id_in(params[:id])
+ end
+
+ def validate_params!
+ validate_params_present!
+ validate_id_params!
+ validate_indexes!
+ end
+
+ # Ensure all provided params are supported
+ def validate_params_present!
+ raise ArgumentError, "Please provide one or more of: #{ACCEPTED_PARAMS}" if params.blank?
+ end
+
+ # Protect against the caller "finding" the wrong metric
+ def validate_id_params!
+ raise ArgumentError, 'Only one of :identifier, :id is permitted' if params[:identifier] && params[:id]
+ raise ArgumentError, ':identifier must be scoped to a :project or :common' if params[:identifier] && !(params[:project] || params[:common])
+ end
+
+ # Protect against unaccounted-for, complex/slow queries.
+ # This is not a hard and fast rule, but is meant to encourage
+ # mindful inclusion of new queries.
+ def validate_indexes!
+ indexable_params = params.except(:ordered, :id, :project).keys
+ indexable_params << :project_id if params[:project]
+ indexable_params.sort!
+
+ raise ArgumentError, "An index should exist for params: #{indexable_params}" unless appropriate_index?(indexable_params)
+ end
+
+ def appropriate_index?(indexable_params)
+ return true if indexable_params.blank?
+
+ self.class.indexes.any? { |index| (index - indexable_params).empty? }
+ end
+end
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
index 59e84198fde..72bf968c8ec 100644
--- a/app/finders/releases_finder.rb
+++ b/app/finders/releases_finder.rb
@@ -6,9 +6,11 @@ class ReleasesFinder
@current_user = current_user
end
- def execute
+ def execute(preload: true)
return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
- @project.releases.sorted
+ releases = @project.releases
+ releases = releases.preloaded if preload
+ releases.sorted
end
end
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index 2ffd46245e9..fd58f478b45 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -1,31 +1,13 @@
# frozen_string_literal: true
-class TagsFinder
+class TagsFinder < GitRefsFinder
def initialize(repository, params)
- @repository = repository
- @params = params
+ super(repository, params)
end
def execute
- tags = @repository.tags_sorted_by(sort)
- filter_by_name(tags)
- end
-
- private
-
- def sort
- @params[:sort].presence
- end
-
- def search
- @params[:search].presence
- end
-
- def filter_by_name(tags)
- if search
- tags.select { |tag| tag.name.include?(search) }
- else
- tags
- end
+ tags = repository.tags_sorted_by(sort)
+ tags = by_search(tags)
+ tags
end
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 2b46e51290f..e56009be33d 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -23,10 +23,16 @@ class TodosFinder
NONE = '0'
- TODO_TYPES = Set.new(%w(Issue MergeRequest Epic)).freeze
+ TODO_TYPES = Set.new(%w(Issue MergeRequest)).freeze
attr_accessor :current_user, :params
+ class << self
+ def todo_types
+ TODO_TYPES
+ end
+ end
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params
@@ -111,12 +117,6 @@ class TodosFinder
params[:group_id].present?
end
- def project
- strong_memoize(:project) do
- Project.find_without_deleted(params[:project_id]) if project?
- end
- end
-
def group
strong_memoize(:group) do
Group.find(params[:group_id])
@@ -124,7 +124,7 @@ class TodosFinder
end
def type?
- type.present? && TODO_TYPES.include?(type)
+ type.present? && self.class.todo_types.include?(type)
end
def type
@@ -175,7 +175,7 @@ class TodosFinder
def by_project(items)
if project?
- items.for_project(project)
+ items.for_undeleted_projects.for_project(params[:project_id])
else
items
end
@@ -188,11 +188,9 @@ class TodosFinder
end
def by_state(items)
- if params[:state].to_s == 'done'
- items.done
- else
- items.pending
- end
+ return items.pending if params[:state].blank?
+
+ items.with_states(params[:state])
end
def by_type(items)
@@ -203,3 +201,5 @@ class TodosFinder
end
end
end
+
+TodosFinder.prepend_if_ee('EE::TodosFinder')
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 1899278ff3c..a5ddf316572 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -46,7 +46,7 @@ class GitlabSchema < GraphQL::Schema
super(query_str, **kwargs)
end
- def id_from_object(object)
+ def id_from_object(object, _type = nil, _ctx = nil)
unless object.respond_to?(:to_global_id)
# This is an error in our schema and needs to be solved. So raise a
# more meaningful error message
@@ -57,7 +57,7 @@ class GitlabSchema < GraphQL::Schema
object.to_global_id
end
- def object_from_id(global_id)
+ def object_from_id(global_id, _ctx = nil)
gid = GlobalID.parse(global_id)
unless gid
diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb
new file mode 100644
index 00000000000..8f0025f0a58
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_assignees.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetAssignees < Base
+ graphql_name 'MergeRequestSetAssignees'
+
+ argument :assignee_usernames,
+ [GraphQL::STRING_TYPE],
+ required: true,
+ description: <<~DESC
+ The usernames to assign to the merge request. Replaces existing assignees by default.
+ DESC
+
+ argument :operation_mode,
+ Types::MutationOperationModeEnum,
+ required: false,
+ description: <<~DESC
+ The operation to perform. Defaults to REPLACE.
+ DESC
+
+ def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098')
+
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ assignee_ids = []
+ assignee_ids += merge_request.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode)
+ user_ids = UsersFinder.new(current_user, username: assignee_usernames).execute.map(&:id)
+
+ if operation_mode == Types::MutationOperationModeEnum.enum[:remove]
+ assignee_ids -= user_ids
+ else
+ assignee_ids |= user_ids
+ end
+
+ ::MergeRequests::UpdateService.new(project, current_user, assignee_ids: assignee_ids)
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_labels.rb b/app/graphql/mutations/merge_requests/set_labels.rb
new file mode 100644
index 00000000000..71f7a353bc9
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_labels.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetLabels < Base
+ graphql_name 'MergeRequestSetLabels'
+
+ argument :label_ids,
+ [GraphQL::ID_TYPE],
+ required: true,
+ description: <<~DESC
+ The Label IDs to set. Replaces existing labels by default.
+ DESC
+
+ argument :operation_mode,
+ Types::MutationOperationModeEnum,
+ required: false,
+ description: <<~DESC
+ Changes the operation mode. Defaults to REPLACE.
+ DESC
+
+ def resolve(project_path:, iid:, label_ids:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ label_ids = label_ids
+ .select(&method(:label_descendant?))
+ .map { |gid| GlobalID.parse(gid).model_id } # MergeRequests::UpdateService expects integers
+
+ attribute_name = case operation_mode
+ when Types::MutationOperationModeEnum.enum[:append]
+ :add_label_ids
+ when Types::MutationOperationModeEnum.enum[:remove]
+ :remove_label_ids
+ else
+ :label_ids
+ end
+
+ ::MergeRequests::UpdateService.new(project, current_user, attribute_name => label_ids)
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+
+ def label_descendant?(gid)
+ GlobalID.parse(gid)&.model_class&.ancestors&.include?(Label)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_locked.rb b/app/graphql/mutations/merge_requests/set_locked.rb
new file mode 100644
index 00000000000..09aaa0b39aa
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_locked.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetLocked < Base
+ graphql_name 'MergeRequestSetLocked'
+
+ argument :locked,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: <<~DESC
+ Whether or not to lock the merge request.
+ DESC
+
+ def resolve(project_path:, iid:, locked:)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ ::MergeRequests::UpdateService.new(project, current_user, discussion_locked: locked)
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_milestone.rb b/app/graphql/mutations/merge_requests/set_milestone.rb
new file mode 100644
index 00000000000..707d6677952
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_milestone.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetMilestone < Base
+ graphql_name 'MergeRequestSetMilestone'
+
+ argument :milestone_id,
+ GraphQL::ID_TYPE,
+ required: false,
+ loads: Types::MilestoneType,
+ description: <<~DESC
+ The milestone to assign to the merge request.
+ DESC
+
+ def resolve(project_path:, iid:, milestone: nil)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ ::MergeRequests::UpdateService.new(project, current_user, milestone: milestone)
+ .execute(merge_request)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/merge_requests/set_subscription.rb b/app/graphql/mutations/merge_requests/set_subscription.rb
new file mode 100644
index 00000000000..86750152775
--- /dev/null
+++ b/app/graphql/mutations/merge_requests/set_subscription.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Mutations
+ module MergeRequests
+ class SetSubscription < Base
+ graphql_name 'MergeRequestSetSubscription'
+
+ argument :subscribed_state,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: 'The desired state of the subscription'
+
+ def resolve(project_path:, iid:, subscribed_state:)
+ merge_request = authorized_find!(project_path: project_path, iid: iid)
+ project = merge_request.project
+
+ merge_request.set_subscription(current_user, subscribed_state, project)
+
+ {
+ merge_request: merge_request,
+ errors: merge_request.errors.full_messages
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/todos/base.rb b/app/graphql/mutations/todos/base.rb
new file mode 100644
index 00000000000..b6c7b320be1
--- /dev/null
+++ b/app/graphql/mutations/todos/base.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Todos
+ class Base < ::Mutations::BaseMutation
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id)
+ end
+
+ def to_global_id(id)
+ ::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/todos/mark_done.rb b/app/graphql/mutations/todos/mark_done.rb
new file mode 100644
index 00000000000..5483708b5c6
--- /dev/null
+++ b/app/graphql/mutations/todos/mark_done.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Todos
+ class MarkDone < ::Mutations::Todos::Base
+ graphql_name 'TodoMarkDone'
+
+ authorize :update_todo
+
+ argument :id,
+ GraphQL::ID_TYPE,
+ required: true,
+ description: 'The global id of the todo to mark as done'
+
+ field :todo, Types::TodoType,
+ null: false,
+ description: 'The requested todo'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def resolve(id:)
+ todo = authorized_find!(id: id)
+ mark_done(Todo.where(id: todo.id)) unless todo.done?
+
+ {
+ todo: todo.reset,
+ errors: errors_on_object(todo)
+ }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def mark_done(todo)
+ TodoService.new.mark_todos_as_done(todo, current_user)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 5b7eb57841c..85d6b377934 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -10,6 +10,14 @@ module Resolvers
end
end
+ def self.last
+ @last ||= Class.new(self) do
+ def resolve(**args)
+ super.last
+ end
+ end
+ end
+
def self.resolver_complexity(args, child_complexity:)
complexity = 1
complexity += 1 if args[:sort]
diff --git a/app/graphql/resolvers/commit_pipelines_resolver.rb b/app/graphql/resolvers/commit_pipelines_resolver.rb
new file mode 100644
index 00000000000..92a83523593
--- /dev/null
+++ b/app/graphql/resolvers/commit_pipelines_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class CommitPipelinesResolver < BaseResolver
+ include ::ResolvesPipelines
+
+ alias_method :commit, :object
+
+ def resolve(**args)
+ resolve_pipelines(commit.project, args.merge!({ sha: commit.sha }))
+ end
+ end
+end
diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb
index cf43fea45e6..94f6c47e876 100644
--- a/app/graphql/types/base_enum.rb
+++ b/app/graphql/types/base_enum.rb
@@ -2,5 +2,18 @@
module Types
class BaseEnum < GraphQL::Schema::Enum
+ class << self
+ def value(*args, **kwargs, &block)
+ enum[args[0].downcase] = kwargs[:value] || args[0]
+
+ super(*args, **kwargs, &block)
+ end
+
+ # Returns an indifferent access hash with the key being the downcased name of the attribute
+ # and the value being the Ruby value (either the explicit `value` passed or the same as the value attr).
+ def enum
+ @enum_values ||= {}.with_indifferent_access
+ end
+ end
end
end
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index fe71791f413..87f84ec576f 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -8,25 +8,39 @@ module Types
present_using CommitPresenter
- field :id, type: GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :sha, type: GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :title, type: GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :description, type: GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :message, type: GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :authored_date, type: Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
- field :web_url, type: GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :signature_html, type: GraphQL::STRING_TYPE,
- null: true, calls_gitaly: true, description: 'Rendered html for the commit signature'
+ field :id, type: GraphQL::ID_TYPE, null: false,
+ description: 'ID (global ID) of the commit'
+ field :sha, type: GraphQL::STRING_TYPE, null: false,
+ description: 'SHA1 ID of the commit'
+ field :title, type: GraphQL::STRING_TYPE, null: true,
+ description: 'Title of the commit message'
+ field :description, type: GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the commit message'
+ field :message, type: GraphQL::STRING_TYPE, null: true,
+ description: 'Raw commit message'
+ field :authored_date, type: Types::TimeType, null: true,
+ description: 'Timestamp of when the commit was authored'
+ field :web_url, type: GraphQL::STRING_TYPE, null: false,
+ description: 'Web URL of the commit'
+ field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ description: 'Rendered HTML of the commit signature'
+ field :author_name, type: GraphQL::STRING_TYPE, null: true,
+ description: 'Commit authors name'
# models/commit lazy loads the author by email
- field :author, type: Types::UserType, null: true # rubocop:disable Graphql/Descriptions
+ field :author, type: Types::UserType, null: true,
+ description: 'Author of the commit'
+
+ field :pipelines, Types::Ci::PipelineType.connection_type,
+ null: true,
+ description: 'Pipelines of the commit ordered latest first',
+ resolver: Resolvers::CommitPipelinesResolver
field :latest_pipeline,
type: Types::Ci::PipelineType,
null: true,
- description: "Latest pipeline for this commit",
- resolve: -> (obj, ctx, args) do
- Gitlab::Graphql::Loaders::PipelineForShaLoader.new(obj.project, obj.sha).find_last
- end
+ description: "Latest pipeline of the commit",
+ deprecation_reason: 'use pipelines',
+ resolver: Resolvers::CommitPipelinesResolver.last
end
end
diff --git a/app/graphql/types/extended_issue_type.rb b/app/graphql/types/extended_issue_type.rb
deleted file mode 100644
index e007c1109a3..00000000000
--- a/app/graphql/types/extended_issue_type.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- class ExtendedIssueType < IssueType
- graphql_name 'ExtendedIssue'
-
- authorize :read_issue
- expose_permissions Types::PermissionTypes::Issue
- present_using IssuePresenter
-
- field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
- description: 'Boolean flag for whether the currently logged in user is subscribed to this issue'
- end
-end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 1e52c0cb147..386ae6ed4a3 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -8,14 +8,17 @@ module Types
expose_permissions Types::PermissionTypes::Group
- field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :web_url, GraphQL::STRING_TYPE, null: false,
+ description: 'Web URL of the group'
- field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (group, args, ctx) do # rubocop:disable Graphql/Descriptions
- group.avatar_url(only_path: false)
- end
+ field :avatar_url, GraphQL::STRING_TYPE, null: true,
+ description: 'Avatar URL of the group',
+ resolve: -> (group, args, ctx) do
+ group.avatar_url(only_path: false)
+ end
- field :parent, GroupType, # rubocop:disable Graphql/Descriptions
- null: true,
+ field :parent, GroupType, null: true,
+ description: 'Parent group',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find }
end
end
diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb
index ad919b55481..48ff5819286 100644
--- a/app/graphql/types/issue_sort_enum.rb
+++ b/app/graphql/types/issue_sort_enum.rb
@@ -5,6 +5,10 @@ module Types
class IssueSortEnum < IssuableSortEnum
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'
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 4965601fe65..4cbb849da3a 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -12,53 +12,79 @@ module Types
present_using IssuePresenter
- field :iid, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :title, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :iid, GraphQL::ID_TYPE, null: false,
+ description: "Internal ID of the issue"
+ field :title, GraphQL::STRING_TYPE, null: false,
+ description: 'Title of the issue'
markdown_field :title_html, null: true
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the issue'
markdown_field :description_html, null: true
- field :state, IssueStateEnum, null: false # rubocop:disable Graphql/Descriptions
-
- field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference do # rubocop:disable Graphql/Descriptions
- argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false # rubocop:disable Graphql/Descriptions
+ field :state, IssueStateEnum, null: false,
+ description: 'State of the issue'
+
+ field :reference, GraphQL::STRING_TYPE, null: false,
+ description: 'Internal reference of the issue. Returned in shortened format by default',
+ method: :to_reference do
+ argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false,
+ description: 'Boolean option specifying whether the reference should be returned in full'
end
- field :author, Types::UserType, # rubocop:disable Graphql/Descriptions
- null: false,
+ field :author, Types::UserType, null: false,
+ description: 'User that created the issue',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
# Remove complexity when BatchLoader is used
- field :assignees, Types::UserType.connection_type, null: true, complexity: 5 # rubocop:disable Graphql/Descriptions
+ field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
+ description: 'Assignees of the issue'
# Remove complexity when BatchLoader is used
- field :labels, Types::LabelType.connection_type, null: true, complexity: 5 # rubocop:disable Graphql/Descriptions
- field :milestone, Types::MilestoneType, # rubocop:disable Graphql/Descriptions
- null: true,
+ field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
+ description: 'Labels of the issue'
+ field :milestone, Types::MilestoneType, null: true,
+ description: 'Milestone of the issue',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
- field :due_date, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
- field :confidential, GraphQL::BOOLEAN_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :discussion_locked, GraphQL::BOOLEAN_TYPE, # rubocop:disable Graphql/Descriptions
- null: false,
+ field :due_date, Types::TimeType, null: true,
+ description: 'Due date of the issue'
+ field :confidential, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates the issue is confidential'
+ field :discussion_locked, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates discussion is locked on the issue',
resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked }
- field :upvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :downvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :user_notes_count, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path # rubocop:disable Graphql/Descriptions
- field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :relative_position, GraphQL::INT_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'List of participants for the issue'
- field :time_estimate, GraphQL::INT_TYPE, null: false, description: 'The time estimate on the issue'
- field :total_time_spent, GraphQL::INT_TYPE, null: false, description: 'Total time reported as spent on the issue'
-
- field :closed_at, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
-
- field :created_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
- field :updated_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
-
- field :task_completion_status, Types::TaskCompletionStatus, null: false # rubocop:disable Graphql/Descriptions
+ field :upvotes, GraphQL::INT_TYPE, null: false,
+ description: 'Number of upvotes the issue has received'
+ field :downvotes, GraphQL::INT_TYPE, null: false,
+ description: 'Number of downvotes the issue has received'
+ field :user_notes_count, GraphQL::INT_TYPE, null: false,
+ description: 'Number of user notes of the issue'
+ field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path,
+ description: 'Web path of the issue'
+ field :web_url, GraphQL::STRING_TYPE, null: false,
+ description: 'Web URL of the issue'
+ field :relative_position, GraphQL::INT_TYPE, null: true,
+ description: 'Relative position of the issue (used for positioning in epic tree and issue boards)'
+
+ field :participants, Types::UserType.connection_type, null: true, complexity: 5,
+ description: 'List of participants in the issue'
+ field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
+ description: 'Boolean flag for whether the currently logged in user is subscribed to this issue'
+ field :time_estimate, GraphQL::INT_TYPE, null: false,
+ description: 'Time estimate of the issue'
+ field :total_time_spent, GraphQL::INT_TYPE, null: false,
+ description: 'Total time reported as spent on the issue'
+
+ field :closed_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the issue was closed'
+
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the issue was created'
+ field :updated_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the issue was last updated'
+
+ field :task_completion_status, Types::TaskCompletionStatus, null: false,
+ description: 'Task completion status of the issue'
end
end
diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb
index 384a27df563..d0bcf2068b7 100644
--- a/app/graphql/types/label_type.rb
+++ b/app/graphql/types/label_type.rb
@@ -6,10 +6,16 @@ module Types
authorize :read_label
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'Label ID'
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the label (markdown rendered as HTML for caching)'
markdown_field :description_html, null: true
- field :title, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :color, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :text_color, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :title, GraphQL::STRING_TYPE, null: false,
+ description: 'Content of the label'
+ field :color, GraphQL::STRING_TYPE, null: false,
+ description: 'Background color of the label'
+ field :text_color, GraphQL::STRING_TYPE, null: false,
+ description: 'Text color of the label'
end
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 71a65dc6713..278a95fe3ca 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -12,70 +12,116 @@ module Types
present_using MergeRequestPresenter
- field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :iid, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :title, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the merge request'
+ field :iid, GraphQL::STRING_TYPE, null: false,
+ description: 'Internal ID of the merge request'
+ field :title, GraphQL::STRING_TYPE, null: false,
+ description: 'Title of the merge request'
markdown_field :title_html, null: true
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the merge request (markdown rendered as HTML for caching)'
markdown_field :description_html, null: true
- field :state, MergeRequestStateEnum, null: false # rubocop:disable Graphql/Descriptions
- field :created_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
- field :updated_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
- field :source_project, Types::ProjectType, null: true # rubocop:disable Graphql/Descriptions
- field :target_project, Types::ProjectType, null: false # rubocop:disable Graphql/Descriptions
- field :diff_refs, Types::DiffRefsType, null: true # rubocop:disable Graphql/Descriptions
- # Alias for target_project
- field :project, Types::ProjectType, null: false # rubocop:disable Graphql/Descriptions
- field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id # rubocop:disable Graphql/Descriptions
- field :source_project_id, GraphQL::INT_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :target_project_id, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :source_branch, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :target_branch, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false # rubocop:disable Graphql/Descriptions
- field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :diff_head_sha, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :merge_commit_sha, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :user_notes_count, GraphQL::INT_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true # rubocop:disable Graphql/Descriptions
- field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true # rubocop:disable Graphql/Descriptions
- field :merge_status, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :merge_error, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false # rubocop:disable Graphql/Descriptions
- field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true # rubocop:disable Graphql/Descriptions
- # rubocop:disable Graphql/Descriptions
- field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage"
- # rubocop:enable Graphql/Descriptions
- field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false # rubocop:disable Graphql/Descriptions
- field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false # rubocop:disable Graphql/Descriptions
- field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :web_url, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :upvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :downvotes, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :state, MergeRequestStateEnum, null: false,
+ description: 'State of the merge request'
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the merge request was created'
+ field :updated_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the merge request was last updated'
+ field :source_project, Types::ProjectType, null: true,
+ description: 'Source project of the merge request'
+ field :target_project, Types::ProjectType, null: false,
+ description: 'Target project of the merge request'
+ field :diff_refs, Types::DiffRefsType, null: true,
+ description: 'References of the base SHA, the head SHA, and the start SHA for this merge request'
+ field :project, Types::ProjectType, null: false,
+ description: 'Alias for target_project'
+ field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id,
+ description: 'ID of the merge request project'
+ field :source_project_id, GraphQL::INT_TYPE, null: true,
+ description: 'ID of the merge request source project'
+ field :target_project_id, GraphQL::INT_TYPE, null: false,
+ description: 'ID of the merge request target project'
+ field :source_branch, GraphQL::STRING_TYPE, null: false,
+ description: 'Source branch of the merge request'
+ field :target_branch, GraphQL::STRING_TYPE, null: false,
+ description: 'Target branch of the merge request'
+ field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
+ description: 'Indicates if the merge request is a work in progress (WIP)'
+ field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)'
+ field :diff_head_sha, GraphQL::STRING_TYPE, null: true,
+ description: 'Diff head SHA of the merge request'
+ field :merge_commit_sha, GraphQL::STRING_TYPE, null: true,
+ description: 'SHA of the merge request commit (set once merged)'
+ field :user_notes_count, GraphQL::INT_TYPE, null: true,
+ description: 'User notes count of the merge request'
+ field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true,
+ description: 'Indicates if the source branch of the merge request will be deleted after merge'
+ field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true,
+ description: 'Indicates if the project settings will lead to source branch deletion after merge'
+ field :merge_status, GraphQL::STRING_TYPE, null: true,
+ description: 'Status of the merge request'
+ field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true,
+ description: 'Commit SHA of the merge request if merge is in progress'
+ field :merge_error, GraphQL::STRING_TYPE, null: true,
+ description: 'Error message due to a merge error'
+ field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if members of the target project can push to the fork'
+ field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false,
+ description: 'Indicates if the merge request will be rebased'
+ field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true,
+ description: 'Rebase commit SHA of the merge request'
+ field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false, calls_gitaly: true,
+ description: 'Indicates if there is a rebase currently in progress for the merge request'
+ field :merge_commit_message, GraphQL::STRING_TYPE, method: :default_merge_commit_message, null: true, deprecation_reason: "Renamed to defaultMergeCommitMessage",
+ description: 'Deprecated - renamed to defaultMergeCommitMessage'
+ field :default_merge_commit_message, GraphQL::STRING_TYPE, null: true,
+ description: 'Default merge commit message of the merge request'
+ field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false,
+ description: 'Indicates if a merge is currently occurring'
+ field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false,
+ description: 'Indicates if the source branch of the merge request exists'
+ field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged'
+ field :web_url, GraphQL::STRING_TYPE, null: true,
+ description: 'Web URL of the merge request'
+ field :upvotes, GraphQL::INT_TYPE, null: false,
+ description: 'Number of upvotes for the merge request'
+ field :downvotes, GraphQL::INT_TYPE, null: false,
+ description: 'Number of downvotes for the merge request'
- field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline # rubocop:disable Graphql/Descriptions
- field :pipelines, Types::Ci::PipelineType.connection_type, # rubocop:disable Graphql/Descriptions
+ field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline,
+ description: 'The pipeline running on the branch HEAD of the merge request'
+ field :pipelines, Types::Ci::PipelineType.connection_type,
+ description: 'Pipelines for the merge request',
resolver: Resolvers::MergeRequestPipelinesResolver
- field :milestone, Types::MilestoneType, description: 'The milestone this merge request is linked to',
- null: true,
+ field :milestone, Types::MilestoneType, null: true,
+ description: 'The milestone of the merge request',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
- field :assignees, Types::UserType.connection_type, null: true, complexity: 5, description: 'The list of assignees for the merge request'
- field :participants, Types::UserType.connection_type, null: true, complexity: 5, description: 'The list of participants on the merge request'
+ field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
+ description: 'Assignees of the merge request'
+ field :participants, Types::UserType.connection_type, null: true, complexity: 5,
+ description: 'Participants in the merge request'
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
- description: 'Boolean flag for whether the currently logged in user is subscribed to this MR'
- field :labels, Types::LabelType.connection_type, null: true, complexity: 5, description: 'The list of labels on the merge request'
- field :discussion_locked, GraphQL::BOOLEAN_TYPE, description: 'Boolean flag determining if comments on the merge request are locked to members only',
+ description: 'Indicates if the currently logged in user is subscribed to this merge request'
+ field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
+ description: 'Labels of the merge request'
+ field :discussion_locked, GraphQL::BOOLEAN_TYPE,
+ description: 'Indicates if comments on the merge request are locked to members only',
null: false,
resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked }
- field :time_estimate, GraphQL::INT_TYPE, null: false, description: 'The time estimate for the merge request'
- field :total_time_spent, GraphQL::INT_TYPE, null: false, description: 'Total time reported as spent on the merge request'
- field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference, description: 'Internal merge request reference. Returned in shortened format by default' do
- argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false, description: 'Boolean option specifying whether the reference should be returned in full'
+ field :time_estimate, GraphQL::INT_TYPE, null: false,
+ description: 'Time estimate of the merge request'
+ field :total_time_spent, GraphQL::INT_TYPE, null: false,
+ description: 'Total time reported as spent on the merge request'
+ field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference,
+ description: 'Internal reference of the merge request. Returned in shortened format by default' do
+ argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false,
+ description: 'Boolean option specifying whether the reference should be returned in full'
end
- field :task_completion_status, Types::TaskCompletionStatus, null: false # rubocop:disable Graphql/Descriptions
+ field :task_completion_status, Types::TaskCompletionStatus, null: false,
+ description: Types::TaskCompletionStatus.description
end
end
diff --git a/app/graphql/types/metadata_type.rb b/app/graphql/types/metadata_type.rb
index bfcb929f5ac..1998b036a53 100644
--- a/app/graphql/types/metadata_type.rb
+++ b/app/graphql/types/metadata_type.rb
@@ -6,7 +6,9 @@ module Types
authorize :read_instance_metadata
- field :version, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :revision, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :version, GraphQL::STRING_TYPE, null: false,
+ description: 'Version'
+ field :revision, GraphQL::STRING_TYPE, null: false,
+ description: 'Revision'
end
end
diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb
index 78d0a8220ec..9c3afb28674 100644
--- a/app/graphql/types/milestone_type.rb
+++ b/app/graphql/types/milestone_type.rb
@@ -6,14 +6,23 @@ module Types
authorize :read_milestone
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :title, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :state, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the milestone'
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the milestone'
+ field :title, GraphQL::STRING_TYPE, null: false,
+ description: 'Title of the milestone'
+ field :state, GraphQL::STRING_TYPE, null: false,
+ description: 'State of the milestone'
- field :due_date, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
- field :start_date, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
+ field :due_date, Types::TimeType, null: true,
+ description: 'Timestamp of the milestone due date'
+ field :start_date, Types::TimeType, null: true,
+ description: 'Timestamp of the milestone start date'
- field :created_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
- field :updated_at, Types::TimeType, null: false # rubocop:disable Graphql/Descriptions
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of milestone creation'
+ field :updated_at, Types::TimeType, null: false,
+ description: 'Timestamp of last milestone update'
end
end
diff --git a/app/graphql/types/mutation_operation_mode_enum.rb b/app/graphql/types/mutation_operation_mode_enum.rb
new file mode 100644
index 00000000000..90a29d2b0e5
--- /dev/null
+++ b/app/graphql/types/mutation_operation_mode_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ class MutationOperationModeEnum < BaseEnum
+ graphql_name 'MutationOperationMode'
+ description 'Different toggles for changing mutator behavior.'
+
+ # Suggested param name for the enum: `operation_mode`
+
+ value 'REPLACE', 'Performs a replace operation'
+ value 'APPEND', 'Performs an append operation'
+ value 'REMOVE', 'Performs a removal operation'
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 17f922a5e54..b3c7c162bb3 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -9,12 +9,18 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
+ mount_mutation Mutations::MergeRequests::SetLabels
+ mount_mutation Mutations::MergeRequests::SetLocked
+ mount_mutation Mutations::MergeRequests::SetMilestone
+ mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
+ mount_mutation Mutations::MergeRequests::SetAssignees
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
mount_mutation Mutations::Notes::Update
mount_mutation Mutations::Notes::Destroy
+ mount_mutation Mutations::Todos::MarkDone
end
end
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
index cc1d06b19e1..1714284a5cf 100644
--- a/app/graphql/types/namespace_type.rb
+++ b/app/graphql/types/namespace_type.rb
@@ -6,27 +6,35 @@ module Types
authorize :read_namespace
- field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the namespace'
- field :name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :path, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :full_name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :full_path, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :name, GraphQL::STRING_TYPE, null: false,
+ description: 'Name of the namespace'
+ field :path, GraphQL::STRING_TYPE, null: false,
+ description: 'Path of the namespace'
+ field :full_name, GraphQL::STRING_TYPE, null: false,
+ description: 'Full name of the namespace'
+ field :full_path, GraphQL::ID_TYPE, null: false,
+ description: 'Full path of the namespace'
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description of the namespace'
markdown_field :description_html, null: true
- field :visibility, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? # rubocop:disable Graphql/Descriptions
- field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :visibility, GraphQL::STRING_TYPE, null: true,
+ description: 'Visibility of the namespace'
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?,
+ description: 'Indicates if Large File Storage (LFS) is enabled for namespace'
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if users can request access to namespace'
field :root_storage_statistics, Types::RootStorageStatisticsType,
null: true,
- description: 'The aggregated storage statistics. Only available for root namespaces',
+ description: 'Aggregated storage statistics of the namespace. Only available for root namespaces',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(obj.id).find }
- field :projects, # rubocop:disable Graphql/Descriptions
- Types::ProjectType.connection_type,
- null: false,
+ field :projects, Types::ProjectType.connection_type, null: false,
+ description: 'Projects within this namespace',
resolver: ::Resolvers::NamespaceProjectsResolver
end
end
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index 5045471a75b..c46410df6c0 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -6,13 +6,20 @@ module Types
authorize :read_statistics
- field :commit_count, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :commit_count, GraphQL::INT_TYPE, null: false,
+ description: 'Commit count of the project'
- field :storage_size, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :repository_size, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :lfs_objects_size, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :build_artifacts_size, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :packages_size, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :wiki_size, GraphQL::INT_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :storage_size, GraphQL::INT_TYPE, null: false,
+ description: 'Storage size of the project'
+ field :repository_size, GraphQL::INT_TYPE, null: false,
+ description: 'Repository size of the project'
+ field :lfs_objects_size, GraphQL::INT_TYPE, null: false,
+ description: 'Large File Storage (LFS) object size of the project'
+ field :build_artifacts_size, GraphQL::INT_TYPE, null: false,
+ description: 'Build artifacts size of the project'
+ field :packages_size, GraphQL::INT_TYPE, null: false,
+ description: 'Packages size of the project'
+ field :wiki_size, GraphQL::INT_TYPE, null: true,
+ description: 'Wiki size of the project'
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 5663f833b7a..73255021119 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -8,97 +8,142 @@ module Types
expose_permissions Types::PermissionTypes::Project
- field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the project'
- field :full_path, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :path, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :full_path, GraphQL::ID_TYPE, null: false,
+ description: 'Full path of the project'
+ field :path, GraphQL::STRING_TYPE, null: false,
+ description: 'Path of the project'
- field :name_with_namespace, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :name_with_namespace, GraphQL::STRING_TYPE, null: false,
+ description: 'Full name of the project with its namespace'
+ field :name, GraphQL::STRING_TYPE, null: false,
+ description: 'Name of the project (without namespace)'
- field :description, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Short description of the project'
markdown_field :description_html, null: true
- field :tag_list, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :http_url_to_repo, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :web_url, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :star_count, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true # 4 times # rubocop:disable Graphql/Descriptions
-
- field :created_at, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
- field :last_activity_at, Types::TimeType, null: true # rubocop:disable Graphql/Descriptions
-
- field :archived, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :visibility, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true, resolve: -> (project, args, ctx) do # rubocop:disable Graphql/Descriptions
- project.avatar_url(only_path: false)
- end
+ field :tag_list, GraphQL::STRING_TYPE, null: true,
+ description: 'List of project tags'
+
+ field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true,
+ description: 'URL to connect to the project via SSH'
+ field :http_url_to_repo, GraphQL::STRING_TYPE, null: true,
+ description: 'URL to connect to the project via HTTPS'
+ field :web_url, GraphQL::STRING_TYPE, null: true,
+ description: 'Web URL of the project'
+
+ field :star_count, GraphQL::INT_TYPE, null: false,
+ description: 'Number of times the project has been starred'
+ field :forks_count, GraphQL::INT_TYPE, null: false, calls_gitaly: true, # 4 times
+ description: 'Number of times the project has been forked'
+
+ field :created_at, Types::TimeType, null: true,
+ description: 'Timestamp of the project creation'
+ field :last_activity_at, Types::TimeType, null: true,
+ description: 'Timestamp of the project last activity'
+
+ field :archived, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Archived status of the project'
+
+ field :visibility, GraphQL::STRING_TYPE, null: true,
+ description: 'Visibility of the project'
+
+ field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if the project stores Docker container images in a container registry'
+ field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if shared runners are enabled on the project'
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if the project has Large File Storage (LFS) enabled'
+ field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.'
+
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ description: 'URL to avatar image file of the project',
+ resolve: -> (project, args, ctx) do
+ project.avatar_url(only_path: false)
+ end
%i[issues merge_requests wiki snippets].each do |feature|
- field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do # rubocop:disable Graphql/Descriptions
- project.feature_available?(feature, ctx[:current_user])
- end
+ field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true,
+ description: "(deprecated) Does this project have #{feature} enabled?. Use `#{feature}_access_level` instead",
+ resolve: -> (project, args, ctx) do
+ project.feature_available?(feature, ctx[:current_user])
+ end
end
- field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do # rubocop:disable Graphql/Descriptions
- project.feature_available?(:builds, ctx[:current_user])
- end
-
- field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true # rubocop:disable Graphql/Descriptions
-
- field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do # rubocop:disable Graphql/Descriptions
- project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
- end
-
- field :import_status, GraphQL::STRING_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
- field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true # rubocop:disable Graphql/Descriptions
-
- field :namespace, Types::NamespaceType, null: true # rubocop:disable Graphql/Descriptions
- field :group, Types::GroupType, null: true # rubocop:disable Graphql/Descriptions
-
- field :statistics, Types::ProjectStatisticsType, # rubocop:disable Graphql/Descriptions
+ field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: '(deprecated) Enable jobs for this project. Use `builds_access_level` instead',
+ resolve: -> (project, args, ctx) do
+ project.feature_available?(:builds, ctx[:current_user])
+ end
+
+ field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true,
+ description: 'Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts'
+
+ field :open_issues_count, GraphQL::INT_TYPE, null: true,
+ description: 'Number of open issues for the project',
+ resolve: -> (project, args, ctx) do
+ project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
+ end
+
+ field :import_status, GraphQL::STRING_TYPE, null: true,
+ description: 'Status of project import background job of the project'
+
+ field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if merge requests of the project can only be merged with successful jobs'
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if users can request member access to the project'
+ field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if merge requests of the project can only be merged when all the discussions are resolved'
+ field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true,
+ description: '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'
+ field :remove_source_branch_after_merge, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project'
+
+ field :namespace, Types::NamespaceType, null: true,
+ description: 'Namespace of the project'
+ field :group, Types::GroupType, null: true,
+ description: 'Group of the project'
+
+ field :statistics, Types::ProjectStatisticsType,
null: true,
+ description: 'Statistics of the project',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
- field :repository, Types::RepositoryType, null: true # rubocop:disable Graphql/Descriptions
+ field :repository, Types::RepositoryType, null: true,
+ description: 'Git repository of the project'
- field :merge_requests, # rubocop:disable Graphql/Descriptions
+ field :merge_requests,
Types::MergeRequestType.connection_type,
null: true,
+ description: 'Merge requests of the project',
resolver: Resolvers::MergeRequestsResolver
- field :merge_request, # rubocop:disable Graphql/Descriptions
+ field :merge_request,
Types::MergeRequestType,
null: true,
+ description: 'A single merge request of the project',
resolver: Resolvers::MergeRequestsResolver.single
- field :issues, # rubocop:disable Graphql/Descriptions
+ field :issues,
Types::IssueType.connection_type,
null: true,
+ description: 'Issues of the project',
resolver: Resolvers::IssuesResolver
- field :issue, # rubocop:disable Graphql/Descriptions
- Types::ExtendedIssueType,
+ field :issue,
+ Types::IssueType,
null: true,
+ description: 'A single issue of the project',
resolver: Resolvers::IssuesResolver.single
- field :pipelines, # rubocop:disable Graphql/Descriptions
+ field :pipelines,
Types::Ci::PipelineType.connection_type,
null: true,
+ description: 'Build pipelines of the project',
resolver: Resolvers::ProjectPipelinesResolver
end
end
diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb
index 9ecd336b41d..f0c25e13a26 100644
--- a/app/graphql/types/repository_type.rb
+++ b/app/graphql/types/repository_type.rb
@@ -6,9 +6,13 @@ module Types
authorize :download_code
- field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true # rubocop:disable Graphql/Descriptions
- field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true # rubocop:disable Graphql/Descriptions
- field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists? # rubocop:disable Graphql/Descriptions
- field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true # rubocop:disable Graphql/Descriptions
+ field :root_ref, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
+ description: 'Default branch of the repository'
+ field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty?, calls_gitaly: true,
+ description: 'Indicates repository has no visible content'
+ field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists?,
+ description: 'Indicates a corresponding Git repository exists on disk'
+ field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true,
+ description: 'Tree of the repository'
end
end
diff --git a/app/graphql/types/task_completion_status.rb b/app/graphql/types/task_completion_status.rb
index 0aa8fc60a7c..73a8b4f3020 100644
--- a/app/graphql/types/task_completion_status.rb
+++ b/app/graphql/types/task_completion_status.rb
@@ -8,8 +8,10 @@ module Types
graphql_name 'TaskCompletionStatus'
description 'Completion status of tasks'
- field :count, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :completed_count, GraphQL::INT_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :count, GraphQL::INT_TYPE, null: false,
+ description: 'Number of total tasks'
+ field :completed_count, GraphQL::INT_TYPE, null: false,
+ description: 'Number of completed tasks'
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb
index 9a7391dcd99..8358a86b35c 100644
--- a/app/graphql/types/todo_target_enum.rb
+++ b/app/graphql/types/todo_target_enum.rb
@@ -2,8 +2,10 @@
module Types
class TodoTargetEnum < BaseEnum
- value 'Issue'
- value 'MergeRequest'
- value 'Epic'
+ value 'COMMIT', value: 'Commit', description: 'A Commit'
+ value 'ISSUE', value: 'Issue', description: 'An Issue'
+ value 'MERGEREQUEST', value: 'MergeRequest', description: 'A MergeRequest'
end
end
+
+Types::TodoTargetEnum.prepend_if_ee('::EE::Types::TodoTargetEnum')
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index d36daaf7dec..5ce5093c55e 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -40,7 +40,8 @@ module Types
field :body, GraphQL::STRING_TYPE,
description: 'Body of the todo',
- null: false
+ null: false,
+ calls_gitaly: true # TODO This is only true when `target_type` is `Commit`. See https://gitlab.com/gitlab-org/gitlab/issues/34757#note_234752665
field :state, Types::TodoStateEnum,
description: 'State of the todo',
diff --git a/app/graphql/types/tree/entry_type.rb b/app/graphql/types/tree/entry_type.rb
index 10c2ad8815e..87a3eced896 100644
--- a/app/graphql/types/tree/entry_type.rb
+++ b/app/graphql/types/tree/entry_type.rb
@@ -5,6 +5,7 @@ module Types
include Types::BaseInterface
field :id, GraphQL::ID_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :sha, GraphQL::STRING_TYPE, null: false, description: "Last commit sha for entry", method: :id
field :name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :type, Tree::TypeEnum, null: false # rubocop:disable Graphql/Descriptions
field :path, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 1ba37927b40..b45c7893e75 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -8,12 +8,16 @@ module Types
present_using UserPresenter
- field :name, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :username, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :avatar_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
- field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :name, GraphQL::STRING_TYPE, null: false,
+ description: 'Human-readable name of the user'
+ 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: false,
+ description: "URL of the user's avatar"
+ field :web_url, GraphQL::STRING_TYPE, null: false,
+ description: 'Web URL of the user'
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
- description: 'Todos of this user'
+ description: 'Todos of the user'
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ecaeb7060c8..3ae804ff231 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -324,6 +324,15 @@ module ApplicationHelper
}
end
+ def asset_to_string(name)
+ app = Rails.application
+ if Rails.configuration.assets.compile
+ app.assets.find_asset(name).to_s
+ else
+ controller.view_context.render(file: Rails.root.join('public/assets', app.assets_manifest.assets[name]).to_s)
+ end
+ end
+
private
def appearance
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index df17b82412f..a011209375e 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -3,11 +3,11 @@
module ApplicationSettingsHelper
extend self
- delegate :allow_signup?,
- :gravatar_enabled?,
- :password_authentication_enabled_for_web?,
- :akismet_enabled?,
- to: :'Gitlab::CurrentSettings.current_application_settings'
+ delegate :allow_signup?,
+ :gravatar_enabled?,
+ :password_authentication_enabled_for_web?,
+ :akismet_enabled?,
+ to: :'Gitlab::CurrentSettings.current_application_settings'
def user_oauth_applications?
Gitlab::CurrentSettings.user_oauth_applications
@@ -176,6 +176,7 @@ module ApplicationSettingsHelper
:container_registry_token_expire_delay,
:default_artifacts_expire_in,
:default_branch_protection,
+ :default_ci_config_path,
:default_group_visibility,
:default_project_creation,
:default_project_visibility,
@@ -193,6 +194,10 @@ module ApplicationSettingsHelper
:dsa_key_restriction,
:ecdsa_key_restriction,
:ed25519_key_restriction,
+ :eks_integration_enabled,
+ :eks_account_id,
+ :eks_access_key_id,
+ :eks_secret_access_key,
:email_author_in_body,
:enabled_git_access_protocol,
:enforce_terms,
@@ -254,6 +259,9 @@ module ApplicationSettingsHelper
:shared_runners_text,
:sign_in_text,
:signup_enabled,
+ :sourcegraph_enabled,
+ :sourcegraph_url,
+ :sourcegraph_public_only,
:terminal_max_session_time,
:terms,
:throttle_authenticated_api_enabled,
@@ -289,7 +297,8 @@ module ApplicationSettingsHelper
:snowplow_collector_hostname,
:snowplow_cookie_domain,
:snowplow_enabled,
- :snowplow_site_id,
+ :snowplow_app_id,
+ :snowplow_iglu_registry_url,
:push_event_hooks_limit,
:push_event_activities_limit,
:custom_http_clone_url_root
@@ -312,6 +321,10 @@ module ApplicationSettingsHelper
Rails.env.test?
end
+ def integration_expanded?(substring)
+ @application_setting.errors.any? { |k| k.to_s.start_with?(substring) }
+ end
+
def instance_clusters_enabled?
can?(current_user, :read_cluster, Clusters::Instance.new)
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 9e6fcf6a267..a9c4cfe7dcc 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -8,6 +8,10 @@ module AuthHelper
Gitlab::Auth::LDAP::Config.enabled?
end
+ def ldap_sign_in_enabled?
+ Gitlab::Auth::LDAP::Config.sign_in_enabled?
+ end
+
def omniauth_enabled?
Gitlab::Auth.omniauth_enabled?
end
@@ -56,6 +60,16 @@ module AuthHelper
auth_providers.select { |provider| form_based_provider?(provider) }
end
+ def any_form_based_providers_enabled?
+ form_based_providers.any? { |provider| form_enabled_for_sign_in?(provider) }
+ end
+
+ def form_enabled_for_sign_in?(provider)
+ return true unless provider.to_s.match?(LDAP_PROVIDER)
+
+ ldap_sign_in_enabled?
+ end
+
def crowd_enabled?
auth_providers.include? :crowd
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 5c24b0e1704..912f0b61978 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -47,7 +47,7 @@ module BlobHelper
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
return unless blob = readable_blob(options, path, project, ref)
- common_classes = "btn js-edit-blob #{options[:extra_class]}"
+ common_classes = "btn btn-primary js-edit-blob #{options[:extra_class]}"
edit_button_tag(blob,
common_classes,
@@ -62,7 +62,7 @@ module BlobHelper
return unless blob = readable_blob(options, path, project, ref)
edit_button_tag(blob,
- 'btn btn-default',
+ 'btn btn-inverted btn-primary ide-edit-button',
_('Web IDE'),
ide_edit_path(project, ref, path, options),
project,
@@ -108,7 +108,7 @@ module BlobHelper
path,
label: _("Delete"),
action: "delete",
- btn_class: "remove",
+ btn_class: "default",
modal_type: "remove"
)
end
@@ -141,11 +141,7 @@ module BlobHelper
if @build && @entry
raw_project_job_artifacts_url(@project, @build, path: @entry.path, **kwargs)
elsif @snippet
- if @snippet.project_id
- raw_project_snippet_url(@project, @snippet, **kwargs)
- else
- raw_snippet_url(@snippet, **kwargs)
- end
+ reliable_raw_snippet_url(@snippet)
elsif @blob
project_raw_url(@project, @id, **kwargs)
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index a5fe6bb8f07..2def3488184 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -4,12 +4,12 @@ module BuildsHelper
def build_summary(build, skip: false)
if build.has_trace?
if skip
- link_to _("View job trace"), pipeline_job_url(build.pipeline, build)
+ link_to _("View job log"), pipeline_job_url(build.pipeline, build)
else
build.trace.html(last_lines: 10).html_safe
end
else
- _("No job trace")
+ _("No job log")
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 7ca509873cc..0037c49f134 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -6,6 +6,28 @@ module ClustersHelper
false
end
+ def create_new_cluster_label(provider: nil)
+ case provider
+ when 'aws'
+ s_('ClusterIntegration|Create new Cluster on EKS')
+ when 'gcp'
+ s_('ClusterIntegration|Create new Cluster on GKE')
+ else
+ s_('ClusterIntegration|Create new Cluster')
+ end
+ end
+
+ def new_cluster_partial(provider: nil)
+ case provider
+ when 'aws'
+ 'clusters/clusters/aws/new'
+ when 'gcp'
+ 'clusters/clusters/gcp/new'
+ else
+ 'clusters/clusters/cloud_providers/cloud_provider_selector'
+ end
+ end
+
def render_gcp_signup_offer
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?
@@ -18,7 +40,7 @@ module ClustersHelper
def has_rbac_enabled?(cluster)
return cluster.platform_kubernetes_rbac? if cluster.platform_kubernetes
- !cluster.provider.legacy_abac?
+ cluster.provider.has_rbac_enabled?
end
end
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 518cb7c9714..679622897aa 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -27,16 +27,25 @@ module DashboardHelper
false
end
- def feature_entry(title, href: nil, enabled: true)
+ def feature_entry(title, href: nil, enabled: true, doc_href: nil)
enabled_text = enabled ? 'on' : 'off'
label = "#{title}: status #{enabled_text}"
link_or_title = href && enabled ? tag.a(title, href: href) : title
tag.p(aria: { label: label }) do
concat(link_or_title)
+
concat(tag.span(class: ['light', 'float-right']) do
- concat(boolean_to_icon(enabled))
+ boolean_to_icon(enabled)
end)
+
+ if doc_href.present?
+ link_to_doc = link_to(sprite_icon('question', size: 16), doc_href,
+ class: 'prepend-left-5', title: _('Documentation'),
+ target: '_blank', rel: 'noopener noreferrer')
+
+ concat(link_to_doc)
+ end
end
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index c642a64ad61..f57d0fa19d4 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -34,6 +34,7 @@ module EnvironmentsHelper
"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
}
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 4f31cc67ccc..404ea7b00d4 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -66,7 +66,7 @@ module GitlabRoutingHelper
end
def preview_markdown_path(parent, *args)
- return group_preview_markdown_path(parent) if parent.is_a?(Group)
+ return group_preview_markdown_path(parent, *args) if parent.is_a?(Group)
if @snippet.is_a?(PersonalSnippet)
preview_markdown_snippets_path
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index df9d1933271..3c72f41a4c9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -281,10 +281,7 @@ module IssuablesHelper
}
data[:hasClosingMergeRequest] = issuable.merge_requests_count(current_user) != 0 if issuable.is_a?(Issue)
-
- zoom_links = Gitlab::ZoomLinkExtractor.new(issuable.description).links
-
- data[:zoomMeetingUrl] = zoom_links.last if zoom_links.any?
+ data[:zoomMeetingUrl] = ZoomMeeting.canonical_meeting_url(issuable) if issuable.is_a?(Issue)
if parent.is_a?(Group)
data[:groupPath] = parent.path
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index e2524938e10..e1e756c2f4c 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -51,12 +51,15 @@ module MarkupHelper
text = fragment.children[0].text
fragment.children[0].replace(link_to(text, url, html_options))
else
- # Traverse the fragment's first generation of children looking for pure
- # text, wrapping anything found in the requested link
+ # Traverse the fragment's first generation of children looking for
+ # either pure text or emojis, wrapping anything found in the
+ # requested link
fragment.children.each do |node|
- next unless node.text?
-
- node.replace(link_to(node.text, url, html_options))
+ if node.text?
+ node.replace(link_to(node.text, url, html_options))
+ elsif node.name == 'gl-emoji'
+ node.replace(link_to(node.to_html.html_safe, url, html_options))
+ end
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index e769734f27b..b12b39073ef 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -4,6 +4,18 @@ module MilestonesHelper
include EntityDateHelper
include Gitlab::Utils::StrongMemoize
+ def milestone_status_string(milestone)
+ if milestone.closed?
+ _('Closed')
+ elsif milestone.expired?
+ _('Past due')
+ elsif milestone.upcoming?
+ _('Upcoming')
+ else
+ _('Open')
+ end
+ end
+
def milestones_filter_path(opts = {})
if @project
project_milestones_path(@project, opts)
@@ -170,6 +182,23 @@ module MilestonesHelper
content.join('<br />').html_safe
end
+ def milestone_releases_tooltip_text(milestone)
+ count = milestone.releases.count
+
+ return _("Releases") if count.zero?
+
+ n_("%{releases} release", "%{releases} releases", count) % { releases: count }
+ end
+
+ def recent_releases_with_counts(milestone)
+ total_count = milestone.releases.size
+ return [[], 0, 0] if total_count == 0
+
+ recent_releases = milestone.releases.recent.to_a
+ more_count = total_count - recent_releases.size
+ [recent_releases, total_count, more_count]
+ end
+
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone.due_date, milestone.start_date)})"
@@ -196,33 +225,19 @@ module MilestonesHelper
end
end
- def milestone_merge_request_tab_path(milestone)
- if @project
- merge_requests_project_milestone_path(@project, milestone, format: :json)
- elsif @group
- merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
- else
- merge_requests_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
- end
- end
-
- def milestone_participants_tab_path(milestone)
- if @project
- participants_project_milestone_path(@project, milestone, format: :json)
- elsif @group
- participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
+ def milestone_tab_path(milestone, tab)
+ if milestone.global_milestone?
+ url_for(action: tab, title: milestone.title, format: :json)
else
- participants_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
+ url_for(action: tab, format: :json)
end
end
- def milestone_labels_tab_path(milestone)
- if @project
- labels_project_milestone_path(@project, milestone, format: :json)
- elsif @group
- labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
+ def update_milestone_path(milestone, params = {})
+ if milestone.project_milestone?
+ project_milestone_path(milestone.project, milestone, milestone: params)
else
- labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json)
+ group_milestone_route(milestone, params)
end
end
@@ -247,6 +262,14 @@ module MilestonesHelper
milestone_path(milestone.milestone, params)
end
+ def edit_milestone_path(milestone)
+ if milestone.group_milestone?
+ edit_group_milestone_path(milestone.group, milestone)
+ elsif milestone.project_milestone?
+ edit_project_milestone_path(milestone.project, milestone)
+ end
+ end
+
def can_admin_project_milestones?
strong_memoize(:can_admin_project_milestones) do
can?(current_user, :admin_milestone, @project)
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
index fd1222a1dfb..2f5f612ed4c 100644
--- a/app/helpers/projects/error_tracking_helper.rb
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -13,4 +13,13 @@ module Projects::ErrorTrackingHelper
'illustration-path' => image_path('illustrations/cluster_popover.svg')
}
end
+
+ def error_details_data(project, issue)
+ opts = [project, issue, { format: :json }]
+
+ {
+ 'issue-details-path' => details_namespace_project_error_tracking_index_path(*opts),
+ 'issue-stack-trace-path' => stack_trace_namespace_project_error_tracking_index_path(*opts)
+ }
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 16360c7139a..47214ac4ee2 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -362,6 +362,10 @@ module ProjectsHelper
@project.grafana_integration&.token
end
+ def grafana_integration_enabled?
+ @project.grafana_integration&.enabled?
+ end
+
private
def get_project_nav_tabs(project, current_user)
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 68a19152d8f..c4fe40a0875 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -26,7 +26,8 @@ module ReleasesHelper
tag_name: @release.tag,
markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
- releases_page_path: project_releases_path(@project, anchor: @release.tag)
+ 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')
}
end
end
diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb
index cf7eee7fff3..7834e86adab 100644
--- a/app/helpers/repository_languages_helper.rb
+++ b/app/helpers/repository_languages_helper.rb
@@ -4,7 +4,7 @@ module RepositoryLanguagesHelper
def repository_languages_bar(languages)
return if languages.none?
- content_tag :div, class: 'progress repository-languages-bar' do
+ content_tag :div, class: 'progress repository-languages-bar js-show-on-project-root' do
safe_join(languages.map { |lang| language_progress(lang) })
end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 9a19758b4e8..777fe82e4c0 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module SearchHelper
+ SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
+
def search_autocomplete_opts(term)
return unless current_user
@@ -96,8 +98,9 @@ module SearchHelper
result
end
- def search_blob_title(project, filename)
- filename
+ # Overriden in EE
+ def search_blob_title(project, path)
+ path
end
def search_service
@@ -199,7 +202,7 @@ module SearchHelper
search_params = params
.merge(search)
.merge({ scope: scope })
- .permit(:search, :scope, :project_id, :group_id, :repository_ref, :snippets)
+ .permit(SEARCH_PERMITTED_PARAMS)
if @scope == scope
li_class = 'active'
@@ -235,6 +238,7 @@ module SearchHelper
opts[:data]['project-id'] = @project.id
opts[:data]['labels-endpoint'] = project_labels_path(@project)
opts[:data]['milestones-endpoint'] = project_milestones_path(@project)
+ opts[:data]['releases-endpoint'] = project_releases_path(@project)
elsif @group.present?
opts[:data]['group-id'] = @group.id
opts[:data]['labels-endpoint'] = group_labels_path(@group)
@@ -272,7 +276,7 @@ module SearchHelper
sanitize(html, tags: %w(a p ol ul li pre code))
end
- def search_tabs?(tab)
+ def show_user_search_tab?
return false if Feature.disabled?(:users_search, default_enabled: true)
if @project
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index ea7c7af72d3..19a27ba3499 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -32,7 +32,7 @@ module ServicesHelper
end
def service_save_button(service)
- button_tag(class: 'btn btn-success', type: 'submit', disabled: service.deprecated?) do
+ button_tag(class: 'btn btn-success', type: 'submit', disabled: service.deprecated?, 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
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index af98a611b8b..ef737b25bc7 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -4,4 +4,20 @@ module SessionsHelper
def unconfirmed_email?
flash[:alert] == t(:unconfirmed, scope: [:devise, :failure])
end
+
+ # By default, all sessions are given the same expiration time configured in
+ # the session store (e.g. 1 week). However, unauthenticated users can
+ # generate a lot of sessions, primarily for CSRF verification. It makes
+ # sense to reduce the TTL for unauthenticated to something much lower than
+ # the default (e.g. 1 hour) to limit Redis memory. In addition, Rails
+ # creates a new session after login, so the short TTL doesn't even need to
+ # be extended.
+ def limit_session_time
+ # Rack sets this header, but not all tests may have it: https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L251-L259
+ return unless request.env['rack.session.options']
+
+ # This works because Rack uses these options every time a request is handled:
+ # https://github.com/rack/rack/blob/fdcd03a3c5a1c51d1f96fc97f9dfa1a9deac0c77/lib/rack/session/abstract/id.rb#L342
+ request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay']
+ end
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 6ccc1fb2ed1..10e31fb8888 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -11,22 +11,40 @@ module SnippetsHelper
end
end
- def reliable_snippet_path(snippet, opts = nil)
+ def reliable_snippet_path(snippet, opts = {})
+ reliable_snippet_url(snippet, opts.merge(only_path: true))
+ end
+
+ def reliable_raw_snippet_path(snippet, opts = {})
+ reliable_raw_snippet_url(snippet, opts.merge(only_path: true))
+ end
+
+ def reliable_snippet_url(snippet, opts = {})
if snippet.project_id?
- project_snippet_path(snippet.project, snippet, opts)
+ project_snippet_url(snippet.project, snippet, nil, opts)
else
- snippet_path(snippet, opts)
+ snippet_url(snippet, nil, opts)
end
end
- def download_snippet_path(snippet)
- if snippet.project_id
- raw_project_snippet_path(@project, snippet, inline: false)
+ def reliable_raw_snippet_url(snippet, opts = {})
+ if snippet.project_id?
+ raw_project_snippet_url(snippet.project, snippet, nil, opts)
else
- raw_snippet_path(snippet, inline: false)
+ raw_snippet_url(snippet, nil, opts)
end
end
+ def download_raw_snippet_button(snippet)
+ link_to(icon('download'),
+ reliable_raw_snippet_path(snippet, inline: false),
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ class: "btn btn-sm has-tooltip",
+ title: 'Download',
+ data: { container: 'body' })
+ end
+
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
@@ -114,30 +132,45 @@ module SnippetsHelper
{ snippet_object: snippet, snippet_chunks: snippet_chunks }
end
- def snippet_embed
- "<script src=\"#{url_for(only_path: false, overwrite_params: nil)}.js\"></script>"
+ def snippet_embed_tag(snippet)
+ content_tag(:script, nil, src: reliable_snippet_url(snippet, format: :js, only_path: false))
+ end
+
+ def snippet_badge(snippet)
+ return unless attrs = snippet_badge_attributes(snippet)
+
+ css_class, text = attrs
+ tag.span(class: ['badge', 'badge-gray']) do
+ concat(tag.i(class: ['fa', css_class]))
+ concat(' ')
+ concat(text)
+ end
+ end
+
+ def snippet_badge_attributes(snippet)
+ if snippet.private?
+ ['fa-lock', _('private')]
+ end
end
- def embedded_snippet_raw_button
+ def embedded_raw_snippet_button
blob = @snippet.blob
return if blob.empty? || blob.binary? || blob.stored_externally?
- snippet_raw_url = if @snippet.is_a?(PersonalSnippet)
- raw_snippet_url(@snippet)
- else
- raw_project_snippet_url(@snippet.project, @snippet)
- end
-
- link_to external_snippet_icon('doc-code'), snippet_raw_url, class: 'btn', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw'
+ link_to(external_snippet_icon('doc-code'),
+ reliable_raw_snippet_url(@snippet),
+ class: 'btn',
+ target: '_blank',
+ rel: 'noopener noreferrer',
+ title: 'Open raw')
end
def embedded_snippet_download_button
- download_url = if @snippet.is_a?(PersonalSnippet)
- raw_snippet_url(@snippet, inline: false)
- else
- raw_project_snippet_url(@snippet.project, @snippet, inline: false)
- end
-
- link_to external_snippet_icon('download'), download_url, class: 'btn', target: '_blank', title: 'Download', rel: 'noopener noreferrer'
+ link_to(external_snippet_icon('download'),
+ reliable_raw_snippet_url(@snippet, inline: false),
+ class: 'btn',
+ target: '_blank',
+ title: 'Download',
+ rel: 'noopener noreferrer')
end
end
diff --git a/app/helpers/sourcegraph_helper.rb b/app/helpers/sourcegraph_helper.rb
new file mode 100644
index 00000000000..cc5a5c77e9a
--- /dev/null
+++ b/app/helpers/sourcegraph_helper.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module SourcegraphHelper
+ def sourcegraph_url_message
+ link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: Gitlab::CurrentSettings.sourcegraph_url }
+ link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
+
+ message =
+ if Gitlab::CurrentSettings.sourcegraph_url_is_com?
+ s_('SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}.').html_safe
+ else
+ s_('SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}.').html_safe
+ end
+
+ message % { link_start: link_start, link_end: link_end }
+ end
+
+ def sourcegraph_experimental_message
+ if Gitlab::Sourcegraph.feature_conditional?
+ s_("SourcegraphPreferences|This feature is experimental and currently limited to certain projects.")
+ elsif Gitlab::CurrentSettings.sourcegraph_public_only
+ s_("SourcegraphPreferences|This feature is experimental and limited to public projects.")
+ else
+ s_("SourcegraphPreferences|This feature is experimental.")
+ end
+ end
+end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 53739cb63e3..58edb327be0 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -108,16 +108,6 @@ module TabHelper
current_controller?(c) && current_action?(a)
end
- def project_tab_class
- if controller.controller_path.start_with?('projects')
- return 'active'
- end
-
- if %w(services hooks deploy_keys protected_branches).include? controller.controller_name
- "active"
- end
- end
-
def branches_tab_class
if current_controller?(:protected_branches) ||
current_controller?(:branches) ||
@@ -125,14 +115,6 @@ module TabHelper
'active'
end
end
-
- def profile_tab_class
- if controller.controller_path.start_with?('profiles')
- return 'active'
- end
-
- 'active' if current_controller?('oauth/applications')
- end
end
TabHelper.prepend_if_ee('EE::TabHelper')
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index afa057421e0..fc25b78da93 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -186,6 +186,24 @@ module TreeHelper
attrs
end
+
+ def vue_file_list_data(project, ref)
+ {
+ project_path: project.full_path,
+ project_short_path: project.path,
+ ref: ref,
+ full_name: project.name_with_namespace
+ }
+ end
+
+ def directory_download_links(project, ref, archive_prefix)
+ Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt|
+ {
+ text: fmt,
+ path: project_archive_path(project, id: tree_join(ref, archive_prefix), format: fmt)
+ }
+ end
+ end
end
TreeHelper.prepend_if_ee('::EE::TreeHelper')
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 4ff25d021fb..ef0cb8b4bcb 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -95,6 +95,14 @@ module UsersHelper
tabs
end
+ def trials_link_url
+ 'https://about.gitlab.com/free-trial/'
+ end
+
+ def trials_allowed?(user)
+ false
+ end
+
def get_current_user_menu_items
items = []
@@ -105,6 +113,7 @@ module UsersHelper
items << :help
items << :profile if can?(current_user, :read_user, current_user)
items << :settings if can?(current_user, :update_user, current_user)
+ items << :start_trial if trials_allowed?(current_user)
items
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 2bd803c0177..a36de5dc548 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -201,9 +201,9 @@ module VisibilityLevelHelper
def visibility_level_errors_for_group(group, level_name)
group_name = link_to group.name, group_path(group)
- change_visiblity = link_to 'change the visibility', edit_group_path(group)
+ change_visibility = link_to 'change the visibility', edit_group_path(group)
{ reason: "the visibility of #{group_name} is #{group.visibility}",
- instruction: " To make this group #{level_name}, you must first #{change_visiblity} of the parent group." }
+ instruction: " To make this group #{level_name}, you must first #{change_visibility} of the parent group." }
end
end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index ea8032324aa..06d2219d6a9 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -15,16 +15,18 @@ module Emails
user = User.find(recipient_id)
- mail(to: user.notification_email_for(notification_group),
- subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+ member_email_with_layout(
+ to: user.notification_email_for(notification_group),
+ subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_access_granted_email(member_source_type, member_id)
@member_source_type = member_source_type
@member_id = member_id
- mail(to: member.user.notification_email_for(notification_group),
- subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
+ member_email_with_layout(
+ to: member.user.notification_email_for(notification_group),
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was granted"))
end
def member_access_denied_email(member_source_type, source_id, user_id)
@@ -33,8 +35,9 @@ module Emails
user = User.find(user_id)
- mail(to: user.notification_email_for(notification_group),
- subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
+ member_email_with_layout(
+ to: user.notification_email_for(notification_group),
+ subject: subject("Access to the #{member_source.human_name} #{member_source.model_name.singular} was denied"))
end
def member_invited_email(member_source_type, member_id, token)
@@ -42,8 +45,9 @@ module Emails
@member_id = member_id
@token = token
- mail(to: member.invite_email,
- subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
+ member_email_with_layout(
+ to: member.invite_email,
+ subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_invite_accepted_email(member_source_type, member_id)
@@ -51,8 +55,9 @@ module Emails
@member_id = member_id
return unless member.created_by
- mail(to: member.created_by.notification_email_for(notification_group),
- subject: subject('Invitation accepted'))
+ member_email_with_layout(
+ to: member.created_by.notification_email_for(notification_group),
+ subject: subject('Invitation accepted'))
end
def member_invite_declined_email(member_source_type, source_id, invite_email, created_by_id)
@@ -64,8 +69,9 @@ module Emails
user = User.find(created_by_id)
- mail(to: user.notification_email_for(notification_group),
- subject: subject('Invitation declined'))
+ member_email_with_layout(
+ to: user.notification_email_for(notification_group),
+ subject: subject('Invitation declined'))
end
def member
@@ -85,5 +91,12 @@ module Emails
def member_source_class
@member_source_type.classify.constantize
end
+
+ def member_email_with_layout(to:, subject:)
+ mail(to: to, subject: subject) do |format|
+ format.html { render layout: 'mailer' }
+ format.text { render layout: 'mailer' }
+ end
+ end
end
end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 34e12a5fa6d..95bb52d8f97 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -18,12 +18,11 @@ module Emails
@merge_request = pipeline.all_merge_requests.first
add_headers
- # We use bcc here because we don't want to generate this emails for a
+ # We use bcc here because we don't want to generate these emails for a
# thousand times. This could be potentially expensive in a loop, and
# recipients would contain all project watchers so it could be a lot.
mail(bcc: recipients,
- subject: pipeline_subject(status),
- skip_premailer: true) do |format|
+ subject: pipeline_subject(status)) do |format|
format.html { render layout: 'mailer' }
format.text { render layout: 'mailer' }
end
diff --git a/app/mailers/emails/releases.rb b/app/mailers/emails/releases.rb
index 137858d31e8..c9c77ab9333 100644
--- a/app/mailers/emails/releases.rb
+++ b/app/mailers/emails/releases.rb
@@ -21,7 +21,13 @@ module Emails
private
def release_email_subject
- release_info = [@release.name, @release.tag].select(&:presence).join(' - ')
+ release_info =
+ if @release.name == @release.tag
+ @release.tag
+ else
+ [@release.name, @release.tag].select(&:presence).join(' - ')
+ end
+
"New release: #{release_info}"
end
end
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 3d42423ba46..381a4f54d9e 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -77,7 +77,7 @@ class NotifyPreview < ActionMailer::Preview
end
def import_issues_csv_email
- Notify.import_issues_csv_email(user, project, { success: 3, errors: [5, 6, 7], valid_file: true })
+ Notify.import_issues_csv_email(user.id, project.id, { success: 3, errors: [5, 6, 7], valid_file: true })
end
def closed_merge_request_email
@@ -109,11 +109,11 @@ class NotifyPreview < ActionMailer::Preview
end
def member_access_requested_email
- Notify.member_access_requested_email('group', user.id, user.id).message
+ Notify.member_access_requested_email(member.source_type, member.id, user.id).message
end
def member_invite_accepted_email
- Notify.member_invite_accepted_email('project', user.id).message
+ Notify.member_invite_accepted_email(member.source_type, member.id).message
end
def member_invite_declined_email
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index a3a1748142f..7cfebf0473f 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -2,6 +2,7 @@
class AbuseReport < ApplicationRecord
include CacheMarkdownField
+ include Sortable
cache_markdown_field :message, pipeline: :single_line
@@ -13,6 +14,9 @@ class AbuseReport < ApplicationRecord
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
+ scope :by_user, -> (user) { where(user_id: user) }
+ scope :with_users, -> { includes(:reporter, :user) }
+
# For CacheMarkdownField
alias_method :author, :reporter
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index 23f0db0829b..b2c16444a2a 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -10,6 +10,25 @@ module Analytics
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
+
+ delegate :group, to: :project
+
+ validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? }
+
+ def self.relative_positioning_query_base(stage)
+ where(project_id: stage.project_id)
+ end
+
+ def self.relative_positioning_parent_column
+ :project_id
+ end
+
+ private
+
+ # Project should belong to a group when the stage has Label based events since only GroupLabels are allowed.
+ def validate_project_group_for_label_events
+ errors.add(:project, s_('CycleAnalyticsStage|should be under a group')) unless project.group
+ end
end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index a07933d4975..4028d711fd1 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -6,6 +6,12 @@ class ApplicationSetting < ApplicationRecord
include TokenAuthenticatable
include ChronicDurationAttribute
+ # Only remove this >= %12.6 and >= 2019-12-01
+ self.ignored_columns += %i[
+ pendo_enabled
+ pendo_url
+ ]
+
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
@@ -18,12 +24,6 @@ class ApplicationSetting < ApplicationRecord
# fix a lot of tests using allow_any_instance_of
include ApplicationSettingImplementation
- attr_encrypted :asset_proxy_secret_key,
- mode: :per_attribute_iv,
- insecure_mode: true,
- key: Settings.attr_encrypted_db_key_base_truncated,
- algorithm: 'aes-256-cbc'
-
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -99,11 +99,20 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :plantuml_enabled
+ validates :sourcegraph_url,
+ presence: true,
+ if: :sourcegraph_enabled
+
validates :snowplow_collector_hostname,
presence: true,
hostname: true,
if: :snowplow_enabled
+ validates :snowplow_iglu_registry_url,
+ addressable_url: true,
+ allow_blank: true,
+ if: :snowplow_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -270,12 +279,40 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :lets_encrypt_terms_of_service_accepted?
+ validates :eks_integration_enabled,
+ inclusion: { in: [true, false] }
+
+ validates :eks_account_id,
+ format: { with: Gitlab::Regex.aws_account_id_regex,
+ message: Gitlab::Regex.aws_account_id_message },
+ if: :eks_integration_enabled?
+
+ validates :eks_access_key_id,
+ length: { in: 16..128 },
+ if: :eks_integration_enabled?
+
+ validates :eks_secret_access_key,
+ presence: true,
+ if: :eks_integration_enabled?
+
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
if: -> (setting) { setting.external_auth_client_cert.present? }
+ validates :default_ci_config_path,
+ format: { without: %r{(\.{2}|\A/)},
+ message: N_('cannot include leading slash or directory traversal.') },
+ length: { maximum: 255 },
+ allow_blank: true
+
+ attr_encrypted :asset_proxy_secret_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-cbc',
+ insecure_mode: true
+
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -294,6 +331,12 @@ class ApplicationSetting < ApplicationRecord
algorithm: 'aes-256-gcm',
encode: true
+ attr_encrypted :eks_secret_access_key,
+ mode: :per_attribute_iv,
+ key: Settings.attr_encrypted_db_key_base_truncated,
+ algorithm: 'aes-256-gcm',
+ encode: true
+
before_validation :ensure_uuid!
before_save :ensure_runners_registration_token
@@ -304,6 +347,10 @@ class ApplicationSetting < ApplicationRecord
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
+ def sourcegraph_url_is_com?
+ !!(sourcegraph_url =~ /\Ahttps:\/\/(www\.)?sourcegraph\.com/)
+ end
+
def self.create_from_defaults
transaction(requires_new: true) do
super
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 0c0ffb67c9a..7bb89f0d1e2 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -42,6 +42,7 @@ module ApplicationSettingImplementation
container_registry_token_expire_delay: 5,
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
+ default_ci_config_path: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
@@ -54,6 +55,10 @@ module ApplicationSettingImplementation
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
+ eks_integration_enabled: false,
+ eks_account_id: nil,
+ eks_access_key_id: nil,
+ eks_secret_access_key: nil,
first_day_of_week: 0,
gitaly_timeout_default: 55,
gitaly_timeout_fast: 10,
@@ -97,6 +102,9 @@ module ApplicationSettingImplementation
shared_runners_text: nil,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
+ sourcegraph_enabled: false,
+ sourcegraph_url: nil,
+ sourcegraph_public_only: true,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -128,8 +136,10 @@ module ApplicationSettingImplementation
snowplow_collector_hostname: nil,
snowplow_cookie_domain: nil,
snowplow_enabled: false,
- snowplow_site_id: nil,
- custom_http_clone_url_root: nil
+ snowplow_app_id: nil,
+ snowplow_iglu_registry_url: nil,
+ custom_http_clone_url_root: nil,
+ productivity_analytics_start_date: Time.now
}
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 24fcb97db6e..5a33a8f89df 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -6,11 +6,14 @@ class AwardEmoji < ApplicationRecord
include Participable
include GhostUser
+ include Importable
belongs_to :awardable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user
- validates :awardable, :user, presence: true
+ validates :user, presence: true
+ validates :awardable, presence: true, unless: :importing?
+
validates :name, presence: true, inclusion: { in: Gitlab::Emoji.emojis_names }
validates :name, uniqueness: { scope: [:user, :awardable_type, :awardable_id] }, unless: :ghost_user?
diff --git a/app/models/aws/role.rb b/app/models/aws/role.rb
index 836107435ad..54132be749d 100644
--- a/app/models/aws/role.rb
+++ b/app/models/aws/role.rb
@@ -13,5 +13,11 @@ module Aws
with: Gitlab::Regex.aws_arn_regex,
message: Gitlab::Regex.aws_arn_regex_message
}
+
+ before_validation :ensure_role_external_id!, on: :create
+
+ def ensure_role_external_id!
+ self.role_external_id ||= SecureRandom.hex(20)
+ end
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index c48ab28ce73..59a2c09bd28 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -35,10 +35,13 @@ module Ci
refspecs: -> (build) { build.merge_request_ref? }
}.freeze
+ DEFAULT_RETRIES = {
+ scheduler_failure: 2
+ }.freeze
+
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
- has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
@@ -52,7 +55,6 @@ module Ci
accepts_nested_attributes_for :runner_session
accepts_nested_attributes_for :job_variables
- accepts_nested_attributes_for :needs
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
@@ -118,6 +120,11 @@ module Ci
scope :eager_load_job_artifacts, -> { includes(:job_artifacts) }
+ scope :with_exposed_artifacts, -> do
+ joins(:metadata).merge(Ci::BuildMetadata.with_exposed_artifacts)
+ .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 :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
@@ -367,18 +374,25 @@ module Ci
pipeline.builds.retried.where(name: self.name).count
end
- def retries_max
- normalized_retry.fetch(:max, 0)
+ def retry_failure?
+ max_allowed_retries = nil
+ max_allowed_retries ||= options_retry_max if retry_on_reason_or_always?
+ max_allowed_retries ||= DEFAULT_RETRIES.fetch(failure_reason.to_sym, 0)
+
+ max_allowed_retries > 0 && retries_count < max_allowed_retries
end
- def retry_when
- normalized_retry.fetch(:when, ['always'])
+ def options_retry_max
+ options_retry[:max]
end
- def retry_failure?
- return false if retries_max.zero? || retries_count >= retries_max
+ def options_retry_when
+ options_retry.fetch(:when, ['always'])
+ end
- retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
+ def retry_on_reason_or_always?
+ options_retry_when.include?(failure_reason.to_s) ||
+ options_retry_when.include?('always')
end
def latest?
@@ -595,6 +609,14 @@ module Ci
update_column(:trace, nil)
end
+ def artifacts_expose_as
+ options.dig(:artifacts, :expose_as)
+ end
+
+ def artifacts_paths
+ options.dig(:artifacts, :paths)
+ end
+
def needs_touch?
Time.now - updated_at > 15.minutes.to_i
end
@@ -818,6 +840,13 @@ module Ci
:creating
end
+ # Consider this object to have a structural integrity problems
+ def doom!
+ update_columns(
+ status: :failed,
+ failure_reason: :data_integrity_failure)
+ end
+
private
def successful_deployment_status
@@ -862,19 +891,13 @@ module Ci
# format, but builds created before GitLab 11.5 and saved in database still
# have the old integer only format. This method returns the retry option
# normalized as a hash in 11.5+ format.
- def normalized_retry
- strong_memoize(:normalized_retry) do
+ def options_retry
+ strong_memoize(:options_retry) do
value = options&.dig(:retry)
value = value.is_a?(Integer) ? { max: value } : value.to_h
value.with_indifferent_access
end
end
-
- def build_attributes_from_config
- return {} unless pipeline.config_processor
-
- pipeline.config_processor.build_attributes(name)
- end
end
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 3097e40dd3b..0df5ebfe843 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -27,6 +27,7 @@ module Ci
scope :scoped_build, -> { where('ci_builds_metadata.build_id = ci_builds.id') }
scope :with_interruptible, -> { where(interruptible: true) }
+ scope :with_exposed_artifacts, -> { where(has_exposed_artifacts: true) }
enum timeout_source: {
unknown_timeout_source: 1,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3bf19399cec..f730b949ee9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -405,7 +405,7 @@ module Ci
.where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg)
- .pluck('sg.stage', status_sql, "(#{warnings_sql})")
+ .pluck('sg.stage', Arel.sql(status_sql), Arel.sql("(#{warnings_sql})"))
stages_with_statuses.map do |stage|
Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
@@ -551,23 +551,6 @@ module Ci
end
end
- def stage_seeds
- return [] unless config_processor
-
- strong_memoize(:stage_seeds) do
- seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes|
- seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages)
- previous_stages + [seed]
- end
-
- seeds.select(&:included?)
- end
- end
-
- def seeds_size
- stage_seeds.sum(&:size)
- end
-
def has_kubernetes_active?
project.deployment_platform&.active?
end
@@ -587,56 +570,14 @@ module Ci
end
end
- def set_config_source
- if ci_yaml_from_repo
- self.config_source = :repository_source
- elsif implied_ci_yaml_file
- self.config_source = :auto_devops_source
- end
- end
-
- ##
- # TODO, setting yaml_errors should be moved to the pipeline creation chain.
- #
- def config_processor
- return unless ci_yaml_file
- return @config_processor if defined?(@config_processor)
-
- @config_processor ||= begin
- ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user })
- rescue Gitlab::Ci::YamlProcessor::ValidationError => e
- self.yaml_errors = e.message
- nil
- rescue
- self.yaml_errors = 'Undefined error'
- nil
- end
- end
-
- def ci_yaml_file_path
+ # TODO: this logic is duplicate with Pipeline::Chain::Config::Content
+ # we should persist this is `ci_pipelines.config_path`
+ def config_path
return unless repository_source? || unknown_source?
project.ci_config_path.presence || '.gitlab-ci.yml'
end
- def ci_yaml_file
- return @ci_yaml_file if defined?(@ci_yaml_file)
-
- @ci_yaml_file =
- if auto_devops_source?
- implied_ci_yaml_file
- else
- ci_yaml_from_repo
- end
-
- if @ci_yaml_file
- @ci_yaml_file
- else
- self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
- nil
- end
- end
-
def has_yaml_errors?
yaml_errors.present?
end
@@ -705,7 +646,7 @@ module Ci
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
- variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
+ variables.append(key: 'CI_CONFIG_PATH', value: config_path)
variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
@@ -783,6 +724,10 @@ module Ci
end
end
+ def has_exposed_artifacts?
+ complete? && builds.latest.with_exposed_artifacts.exists?
+ end
+
def branch_updated?
strong_memoize(:branch_updated) do
push_details.branch_updated?
@@ -896,24 +841,6 @@ module Ci
private
- def ci_yaml_from_repo
- return unless project
- return unless sha
- return unless ci_yaml_file_path
-
- project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
- rescue GRPC::NotFound, GRPC::Internal
- nil
- end
-
- def implied_ci_yaml_file
- return unless project
-
- if project.auto_devops_enabled?
- Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
- end
- end
-
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 18cbf827a67..7ba04d1a2de 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -65,7 +65,7 @@ module Clusters
end
def retry_command(command)
- "for i in $(seq 1 30); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
+ "for i in $(seq 1 90); do #{command} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
end
def post_delete_script
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
new file mode 100644
index 00000000000..36246b26066
--- /dev/null
+++ b/app/models/clusters/applications/crossplane.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class Crossplane < ApplicationRecord
+ VERSION = '0.4.1'
+
+ self.table_name = 'clusters_applications_crossplane'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ default_value_for :stack do |crossplane|
+ ''
+ end
+
+ validates :stack, presence: true
+
+ def chart
+ 'crossplane/crossplane'
+ end
+
+ def repository
+ 'https://charts.crossplane.io/alpha'
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: 'crossplane',
+ repository: repository,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files
+ )
+ end
+
+ def values
+ crossplane_values.to_yaml
+ end
+
+ private
+
+ def crossplane_values
+ {
+ "clusterStacks" => {
+ self.stack => {
+ "deploy" => true,
+ "version" => "alpha"
+ }
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
new file mode 100644
index 00000000000..8589f8c00cb
--- /dev/null
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ElasticStack < ApplicationRecord
+ VERSION = '1.8.0'
+
+ ELASTICSEARCH_PORT = 9200
+
+ self.table_name = 'clusters_applications_elastic_stacks'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+ include ::Gitlab::Utils::StrongMemoize
+
+ default_value_for :version, VERSION
+
+ def set_initial_status
+ return unless not_installable?
+ return unless cluster&.application_ingress_available?
+
+ ingress = cluster.application_ingress
+ self.status = status_states[:installable] if ingress.external_ip_or_hostname?
+ end
+
+ def chart
+ 'stable/elastic-stack'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: 'elastic-stack',
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files
+ )
+ end
+
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: 'elastic-stack',
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files,
+ postdelete: post_delete_script
+ )
+ end
+
+ def elasticsearch_client
+ 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)
+
+ Elasticsearch::Client.new(url: proxy_url) do |faraday|
+ # ensures headers containing auth data are appended to original client options
+ faraday.headers.merge!(kube_client.headers)
+ # ensure TLS certs are properly verified
+ faraday.ssl[:verify] = kube_client.ssl_options[:verify_ssl]
+ faraday.ssl[:cert_store] = kube_client.ssl_options[:cert_store]
+ end
+
+ rescue Kubeclient::HttpError => error
+ # If users have mistakenly set parameters or removed the depended clusters,
+ # `proxy_url` could raise an exception because gitlab can not communicate with the cluster.
+ # We check for a nil client in downstream use and behaviour is equivalent to an empty state
+ log_exception(error, :failed_to_create_elasticsearch_client)
+ end
+ end
+
+ private
+
+ def specification
+ {
+ "kibana" => {
+ "ingress" => {
+ "hosts" => [kibana_hostname],
+ "tls" => [{
+ "hosts" => [kibana_hostname],
+ "secretName" => "kibana-cert"
+ }]
+ }
+ }
+ }
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+
+ def post_delete_script
+ [
+ Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack")
+ ].compact
+ end
+
+ def kube_client
+ cluster&.kubeclient&.core_client
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 885e4ff7197..d140649af3c 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -21,6 +21,7 @@ module Clusters
}
FETCH_IP_ADDRESS_DELAY = 30.seconds
+ MODSEC_SIDECAR_INITIAL_DELAY_SECONDS = 10
state_machine :status do
after_transition any => [:installed] do |application|
@@ -40,7 +41,7 @@ module Clusters
end
def allowed_to_uninstall?
- external_ip_or_hostname? && application_jupyter_nil_or_installable?
+ external_ip_or_hostname? && application_jupyter_nil_or_installable? && application_elastic_stack_nil_or_installable?
end
def install_command
@@ -78,12 +79,74 @@ module Clusters
"controller" => {
"config" => {
"enable-modsecurity" => "true",
- "enable-owasp-modsecurity-crs" => "true"
- }
+ "enable-owasp-modsecurity-crs" => "true",
+ "modsecurity.conf" => modsecurity_config_content
+ },
+ "extraContainers" => [
+ {
+ "name" => "modsecurity-log",
+ "image" => "busybox",
+ "args" => [
+ "/bin/sh",
+ "-c",
+ "tail -f /var/log/modsec/audit.log"
+ ],
+ "volumeMounts" => [
+ {
+ "name" => "modsecurity-log-volume",
+ "mountPath" => "/var/log/modsec",
+ "readOnly" => true
+ }
+ ],
+ "startupProbe" => {
+ "exec" => {
+ "command" => ["ls", "/var/log/modsec"]
+ },
+ "initialDelaySeconds" => MODSEC_SIDECAR_INITIAL_DELAY_SECONDS
+ }
+ }
+ ],
+ "extraVolumeMounts" => [
+ {
+ "name" => "modsecurity-template-volume",
+ "mountPath" => "/etc/nginx/modsecurity/modsecurity.conf",
+ "subPath" => "modsecurity.conf"
+ },
+ {
+ "name" => "modsecurity-log-volume",
+ "mountPath" => "/var/log/modsec"
+ }
+ ],
+ "extraVolumes" => [
+ {
+ "name" => "modsecurity-template-volume",
+ "configMap" => {
+ "name" => "ingress-nginx-ingress-controller",
+ "items" => [
+ {
+ "key" => "modsecurity.conf",
+ "path" => "modsecurity.conf"
+ }
+ ]
+ }
+ },
+ {
+ "name" => "modsecurity-log-volume",
+ "emptyDir" => {}
+ }
+ ]
}
}
end
+ def modsecurity_config_content
+ File.read(modsecurity_config_file_path)
+ end
+
+ def modsecurity_config_file_path
+ Rails.root.join('vendor', 'ingress', 'modsecurity.conf')
+ end
+
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
@@ -91,6 +154,10 @@ module Clusters
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
+
+ def application_elastic_stack_nil_or_installable?
+ cluster.application_elastic_stack.nil? || cluster.application_elastic_stack&.installable?
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 954046c143b..37ba8a7c97e 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.9.0'
+ VERSION = '0.10.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index d6f5d7c3f93..f522f3f2fdb 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -6,20 +6,21 @@ module Clusters
include Gitlab::Utils::StrongMemoize
include FromUnion
include ReactiveCaching
+ include AfterCommitQueue
self.table_name = 'clusters'
- PROJECT_ONLY_APPLICATIONS = {
- }.freeze
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::CertManager.application_name => Applications::CertManager,
+ Applications::Crossplane.application_name => Applications::Crossplane,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative
- }.merge(PROJECT_ONLY_APPLICATIONS).freeze
+ Applications::Knative.application_name => Applications::Knative,
+ Applications::ElasticStack.application_name => Applications::ElasticStack
+ }.freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
@@ -47,14 +48,17 @@ module Clusters
has_one_cluster_application :helm
has_one_cluster_application :ingress
has_one_cluster_application :cert_manager
+ has_one_cluster_application :crossplane
has_one_cluster_application :prometheus
has_one_cluster_application :runner
has_one_cluster_application :jupyter
has_one_cluster_application :knative
+ has_one_cluster_application :elastic_stack
has_many :kubernetes_namespaces
accepts_nested_attributes_for :provider_gcp, update_only: true
+ accepts_nested_attributes_for :provider_aws, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
@@ -72,6 +76,7 @@ module Clusters
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
+ delegate :knative_pre_installed?, to: :provider, allow_nil: true
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
@@ -115,6 +120,8 @@ module Clusters
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
+ scope :for_project_namespace, -> (namespace_id) { joins(:projects).where(projects: { namespace_id: namespace_id }) }
+
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
return [] if clusterable.is_a?(Instance)
@@ -124,7 +131,55 @@ module Clusters
hierarchy_groups.flat_map(&:clusters) + Instance.new.clusters
end
+ state_machine :cleanup_status, initial: :cleanup_not_started do
+ state :cleanup_not_started, value: 1
+ state :cleanup_uninstalling_applications, value: 2
+ state :cleanup_removing_project_namespaces, value: 3
+ state :cleanup_removing_service_account, value: 4
+ state :cleanup_errored, value: 5
+
+ event :start_cleanup do |cluster|
+ transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications
+ end
+
+ event :continue_cleanup do
+ transition(
+ cleanup_uninstalling_applications: :cleanup_removing_project_namespaces,
+ cleanup_removing_project_namespaces: :cleanup_removing_service_account)
+ end
+
+ event :make_cleanup_errored do
+ transition any => :cleanup_errored
+ end
+
+ before_transition any => [:cleanup_errored] do |cluster, transition|
+ status_reason = transition.args.first
+ cluster.cleanup_status_reason = status_reason if status_reason
+ end
+
+ after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::AppWorker.perform_async(cluster.id)
+ end
+ end
+
+ after_transition cleanup_uninstalling_applications: :cleanup_removing_project_namespaces do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::ProjectNamespaceWorker.perform_async(cluster.id)
+ end
+ end
+
+ after_transition cleanup_removing_project_namespaces: :cleanup_removing_service_account do |cluster|
+ cluster.run_after_commit do
+ Clusters::Cleanup::ServiceAccountWorker.perform_async(cluster.id)
+ end
+ end
+ end
+
def status_name
+ return cleanup_status_name if cleanup_errored?
+ return :cleanup_ongoing unless cleanup_not_started?
+
provider&.status_name || connection_status.presence || :created
end
@@ -207,10 +262,6 @@ module Clusters
end
end
- def knative_pre_installed?
- provider&.knative_pre_installed?
- end
-
private
def unique_management_project_environment_scope
diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb
index a906eb2888b..c9c18d8c96a 100644
--- a/app/models/clusters/clusters_hierarchy.rb
+++ b/app/models/clusters/clusters_hierarchy.rb
@@ -20,7 +20,7 @@ module Clusters
.with
.recursive(cte.to_arel)
.from(cte_alias)
- .order(DEPTH_COLUMN => :asc)
+ .order(depth_order_clause)
end
private
@@ -40,7 +40,7 @@ module Clusters
end
if clusterable.is_a?(::Project) && include_management_project
- cte << management_clusters_query
+ cte << same_namespace_management_clusters_query
end
cte << base_query
@@ -49,13 +49,42 @@ module Clusters
cte
end
+ # Returns project-level clusters where the project is the management project
+ # for the cluster. The management project has to be in the same namespace /
+ # group as the cluster's project.
+ #
+ # Support for management project in sub-groups is planned in
+ # https://gitlab.com/gitlab-org/gitlab/issues/34650
+ #
+ # NB: group_parent_id is un-used but we still need to match the same number of
+ # columns as other queries in the CTE.
+ def same_namespace_management_clusters_query
+ clusterable.management_clusters
+ .project_type
+ .select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
+ .for_project_namespace(clusterable.namespace_id)
+ end
+
# Management clusters should be first in the hierarchy so we use 0 for the
# depth column.
#
- # group_parent_id is un-used but we still need to match the same number of
- # columns as other queries in the CTE.
- def management_clusters_query
- clusterable.management_clusters.select([clusters_star, 'NULL AS group_parent_id', "0 AS #{DEPTH_COLUMN}"])
+ # Only applicable if the clusterable is a project (most especially when
+ # requesting project.deployment_platform).
+ def depth_order_clause
+ return { DEPTH_COLUMN => :asc } unless clusterable.is_a?(::Project) && include_management_project
+
+ order = <<~SQL
+ (CASE clusters.management_project_id
+ WHEN :project_id THEN 0
+ ELSE #{DEPTH_COLUMN}
+ END) ASC
+ SQL
+
+ values = {
+ project_id: clusterable.id
+ }
+
+ model.sanitize_sql_array([Arel.sql(order), values])
end
def group_clusters_base_query
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index 979cf0645f5..21b98534808 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -60,6 +60,24 @@ module Clusters
# Override if your application needs any action after
# being uninstalled by Helm
end
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_exception(error, event)
+ logger.error({
+ exception: error.class.name,
+ status_code: error.error_code,
+ cluster_id: cluster&.id,
+ application_id: id,
+ class_name: self.class.name,
+ event: event,
+ message: error.message
+ })
+
+ Gitlab::Sentry.track_acceptable_exception(error, extra: { cluster_id: cluster&.id, application_id: id })
+ end
end
end
end
diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb
index f21dbdf7f26..8c9d9ab9ab1 100644
--- a/app/models/clusters/instance.rb
+++ b/app/models/clusters/instance.rb
@@ -9,5 +9,9 @@ module Clusters
def feature_available?(feature)
::Feature.enabled?(feature, default_enabled: true)
end
+
+ def flipper_id
+ self.class.to_s
+ end
end
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index ae4156896bc..78eb75ddcc0 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -3,12 +3,12 @@
module Clusters
module Providers
class Aws < ApplicationRecord
+ include Gitlab::Utils::StrongMemoize
include Clusters::Concerns::ProviderStatus
self.table_name = 'cluster_providers_aws'
belongs_to :cluster, inverse_of: :provider_aws, class_name: 'Clusters::Cluster'
- belongs_to :created_by_user, class_name: 'User'
default_value_for :region, 'us-east-1'
default_value_for :num_nodes, 3
@@ -42,6 +42,30 @@ module Clusters
session_token: nil
)
end
+
+ def api_client
+ strong_memoize(:api_client) do
+ ::Aws::CloudFormation::Client.new(credentials: credentials, region: region)
+ end
+ end
+
+ def credentials
+ strong_memoize(:credentials) do
+ ::Aws::Credentials.new(access_key_id, secret_access_key, session_token)
+ end
+ end
+
+ def has_rbac_enabled?
+ true
+ end
+
+ def knative_pre_installed?
+ false
+ end
+
+ def created_by_user
+ cluster.user
+ end
end
end
end
diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb
index f871674676f..2ca7d0249dc 100644
--- a/app/models/clusters/providers/gcp.rb
+++ b/app/models/clusters/providers/gcp.rb
@@ -54,6 +54,10 @@ module Clusters
assign_attributes(operation_id: operation_id)
end
+ def has_rbac_enabled?
+ !legacy_abac
+ end
+
def knative_pre_installed?
cloud_run?
end
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
index a540e291990..2ca6d15e642 100644
--- a/app/models/commit_status_enums.rb
+++ b/app/models/commit_status_enums.rb
@@ -15,7 +15,9 @@ module CommitStatusEnums
stale_schedule: 7,
job_execution_timeout: 8,
archived_failure: 9,
- unmet_prerequisites: 10
+ unmet_prerequisites: 10,
+ scheduler_failure: 11,
+ data_integrity_failure: 12
}
end
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 54e9a13d1ea..0e07806dd6f 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -4,19 +4,28 @@ module Analytics
module CycleAnalytics
module Stage
extend ActiveSupport::Concern
+ include RelativePositioning
+ include Gitlab::Utils::StrongMemoize
included do
+ belongs_to :start_event_label, class_name: 'GroupLabel', optional: true
+ belongs_to :end_event_label, class_name: 'GroupLabel', optional: true
+
validates :name, presence: true
validates :name, exclusion: { in: Gitlab::Analytics::CycleAnalytics::DefaultStages.names }, if: :custom?
validates :start_event_identifier, presence: true
validates :end_event_identifier, presence: true
+ validates :start_event_label, presence: true, if: :start_event_label_based?
+ validates :end_event_label, presence: true, if: :end_event_label_based?
validate :validate_stage_event_pairs
+ validate :validate_labels
enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier
enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier
alias_attribute :custom_stage?, :custom
scope :default_stages, -> { where(custom: false) }
+ scope :ordered, -> { order(:relative_position, :id) }
end
def parent=(_)
@@ -28,19 +37,41 @@ module Analytics
end
def start_event
- Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
+ strong_memoize(:start_event) do
+ Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event)
+ end
end
def end_event
- Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
+ strong_memoize(:end_event) do
+ Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event)
+ end
+ end
+
+ def start_event_label_based?
+ start_event_identifier && start_event.label_based?
+ end
+
+ def end_event_label_based?
+ end_event_identifier && end_event.label_based?
+ end
+
+ def start_event_identifier=(identifier)
+ clear_memoization(:start_event)
+ super
+ end
+
+ def end_event_identifier=(identifier)
+ clear_memoization(:end_event)
+ super
end
def params_for_start_event
- {}
+ start_event_label.present? ? { label: start_event_label } : {}
end
def params_for_end_event
- {}
+ end_event_label.present? ? { label: end_event_label } : {}
end
def default_stage?
@@ -58,19 +89,44 @@ module Analytics
end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
end
+ def find_with_same_parent!(id)
+ parent.cycle_analytics_stages.find(id)
+ end
+
private
def validate_stage_event_pairs
return if start_event_identifier.nil? || end_event_identifier.nil?
unless pairing_rules.fetch(start_event.class, []).include?(end_event.class)
- errors.add(:end_event, :not_allowed_for_the_given_start_event)
+ errors.add(:end_event, s_('CycleAnalytics|not allowed for the given start event'))
end
end
def pairing_rules
Gitlab::Analytics::CycleAnalytics::StageEvents.pairing_rules
end
+
+ def validate_labels
+ validate_label_within_group(:start_event_label, start_event_label_id) if start_event_label_id_changed?
+ validate_label_within_group(:end_event_label, end_event_label_id) if end_event_label_id_changed?
+ end
+
+ def validate_label_within_group(association_name, label_id)
+ return unless label_id
+ return unless group
+
+ unless label_available_for_group?(label_id)
+ errors.add(association_name, s_('CycleAnalyticsStage|is not available for the selected group'))
+ end
+ end
+
+ def label_available_for_group?(label_id)
+ LabelsFinder.new(nil, { group_id: group.id, include_ancestor_groups: true, only_group_labels: true })
+ .execute(skip_authorization: true)
+ .by_ids(label_id)
+ .exists?
+ end
end
end
end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index a0ca8a34c6d..17d431bacf2 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -16,6 +16,7 @@ module Ci
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
+ delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
before_create :ensure_metadata
end
@@ -45,6 +46,9 @@ module Ci
def options=(value)
write_metadata_attribute(:options, :config_options, value)
+
+ # Store presence of exposed artifacts in build metadata to make it easier to query
+ ensure_metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
end
def yaml_variables=(value)
diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb
index 268fa8ec692..ed0087f34d4 100644
--- a/app/models/concerns/ci/processable.rb
+++ b/app/models/concerns/ci/processable.rb
@@ -8,6 +8,14 @@ module Ci
#
#
module Processable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
+
+ accepts_nested_attributes_for :needs
+ end
+
def schedulable?
raise NotImplementedError
end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index fe8e9609820..3b893a56bd6 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -12,7 +12,7 @@ module DeploymentPlatform
private
def cluster_management_project_enabled?
- Feature.enabled?(:cluster_management_project, default_enabled: true)
+ Feature.enabled?(:cluster_management_project, self, default_enabled: true)
end
def find_deployment_platform(environment)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 852576dbbc2..01cd1e0224b 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -118,8 +118,8 @@ module Issuable
# rubocop:enable GitlabSecurity/SqlInjection
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('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
- scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
+ scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
+ scope :order_milestone_due_asc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC')) }
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 :any_label, -> { joins(:label_links).group(:id) }
@@ -137,6 +137,26 @@ module Issuable
strip_attributes :title
+ # The state_machine gem will reset the value of state_id unless it
+ # is a raw attribute passed in here:
+ # https://gitlab.com/gitlab-org/gitlab/issues/35746#note_241148787
+ #
+ # This assumes another initialize isn't defined. Otherwise this
+ # method may need to be prepended.
+ def initialize(attributes = nil)
+ if attributes.is_a?(Hash)
+ attr = attributes.symbolize_keys
+
+ if attr.key?(:state) && !attr.key?(:state_id)
+ value = attr.delete(:state)
+ state_id = self.class.available_states[value]
+ attributes[:state_id] = state_id if state_id
+ end
+ end
+
+ super(attributes)
+ end
+
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 42b370990ac..b1a7d7ec819 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -101,6 +101,10 @@ module Milestoneish
false
end
+ def global_milestone?
+ false
+ end
+
def total_issue_time_spent
@total_issue_time_spent ||= issues.joins(:timelogs).sum(:time_spent)
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 3065e0ba6c5..19f2daa1b01 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -108,10 +108,6 @@ module Noteable
discussions_resolvable? && resolvable_discussions.none?(&:to_be_resolved?)
end
- def discussions_to_be_resolved?
- discussions_resolvable? && !discussions_resolved?
- end
-
def discussions_to_be_resolved
@discussions_to_be_resolved ||= resolvable_discussions.select(&:to_be_resolved?)
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ebacc459cb5..d9a7f0a96dc 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -39,8 +39,8 @@ module ProtectedRef
end
end
- def developers_can?(action, ref)
- access_levels_for_ref(ref, action: action).any? do |access_level|
+ def developers_can?(action, ref, protected_refs: nil)
+ access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER
end
end
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 78544405c49..9c2b0372d54 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -55,20 +55,22 @@ module Storage
def move_repositories
# Move the namespace directory in all storages used by member projects
- repository_storages.each do |repository_storage|
+ repository_storages(legacy_only: true).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
+ Gitlab::GitalyClient::NamespaceService.allow do
+ gitlab_shell.add_namespace(repository_storage, full_path_before_last_save)
- # Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
+ # Ensure new directory exists before moving it (if there's a parent)
+ gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
- unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
+ unless gitlab_shell.mv_namespace(repository_storage, full_path_before_last_save, full_path)
- Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}" # rubocop:disable Gitlab/RailsLogger
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ end
end
end
end
@@ -77,12 +79,14 @@ module Storage
@old_repository_storage_paths ||= repository_storages
end
- def repository_storages
+ def repository_storages(legacy_only: false)
# We need to get the storage paths for all the projects, even the ones that are
# pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT.
Project.unscoped do
- all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage)
+ namespace_projects = all_projects
+ namespace_projects = namespace_projects.without_storage_feature(:repository) if legacy_only
+ namespace_projects.pluck(Arel.sql('distinct(repository_storage)'))
end
end
@@ -93,13 +97,15 @@ module Storage
# We will remove it later async
new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
- Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
+ Gitlab::GitalyClient::NamespaceService.allow do
+ if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
+ Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
- # Remove namespace directory async with delay so
- # GitLab has time to remove all projects first
- run_after_commit do
- GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
+ # Remove namespace directory async with delay so
+ # GitLab has time to remove all projects first
+ run_after_commit do
+ GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
+ end
end
end
end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 92a5c1112af..33e9e0e38fb 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -59,6 +59,14 @@ module Subscribable
.update(subscribed: false)
end
+ def set_subscription(user, desired_state, project = nil)
+ if desired_state
+ subscribe(user, project)
+ else
+ unsubscribe(user, project)
+ end
+ end
+
private
def unsubscribe_from_other_levels(user, project)
diff --git a/app/models/concerns/worker_attributes.rb b/app/models/concerns/worker_attributes.rb
index af40e9e3b19..506215ca9ed 100644
--- a/app/models/concerns/worker_attributes.rb
+++ b/app/models/concerns/worker_attributes.rb
@@ -3,6 +3,10 @@
module WorkerAttributes
extend ActiveSupport::Concern
+ # Resource boundaries that workers can declare through the
+ # `worker_resource_boundary` attribute
+ VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze
+
class_methods do
def feature_category(value)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
@@ -24,6 +28,48 @@ module WorkerAttributes
get_worker_attribute(:feature_category) == :not_owned
end
+ # This should be set for jobs that need to be run immediately, or, if
+ # they are delayed, risk creating inconsistencies in the application
+ # that could being perceived by the user as incorrect behavior
+ # (ie, a bug)
+ # See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
+ # for details
+ def latency_sensitive_worker!
+ worker_attributes[:latency_sensitive] = true
+ end
+
+ # Returns a truthy value if the worker is latency sensitive.
+ # See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
+ # for details
+ def latency_sensitive_worker?
+ worker_attributes[:latency_sensitive]
+ end
+
+ # Set this attribute on a job when it will call to services outside of the
+ # application, such as 3rd party applications, other k8s clusters etc See
+ # doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
+ # details
+ def worker_has_external_dependencies!
+ worker_attributes[:external_dependencies] = true
+ end
+
+ # Returns a truthy value if the worker has external dependencies.
+ # See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
+ # for details
+ def worker_has_external_dependencies?
+ worker_attributes[:external_dependencies]
+ end
+
+ def worker_resource_boundary(boundary)
+ raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
+
+ worker_attributes[:resource_boundary] = boundary
+ end
+
+ def get_worker_resource_boundary
+ worker_attributes[:resource_boundary] || :unknown
+ end
+
protected
# Returns a worker attribute declared on this class or its parent class.
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 27bb76835c7..152aa7b3218 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -11,7 +11,10 @@ class ContainerRepository < ApplicationRecord
delegate :client, to: :registry
scope :ordered, -> { order(:name) }
- scope :with_api_entity_associations, -> { preload(:project) }
+ scope :with_api_entity_associations, -> { preload(project: [:route, { namespace: :route }]) }
+ scope :for_group_and_its_subgroups, ->(group) do
+ where(project_id: Project.for_group_and_its_subgroups(group).with_container_registry.select(:id))
+ end
# rubocop: disable CodeReuse/ServiceClass
def registry
diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb
index ec52f1ed370..cf6094682f3 100644
--- a/app/models/dashboard_group_milestone.rb
+++ b/app/models/dashboard_group_milestone.rb
@@ -18,4 +18,8 @@ class DashboardGroupMilestone < GlobalMilestone
milestones = milestones.search_title(params[:search_title]) if params[:search_title].present?
Milestone.filter_by_state(milestones, params[:state]).map { |m| new(m) }
end
+
+ def dashboard_milestone?
+ true
+ end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 7ccd5e98360..4a38912db9b 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -10,6 +10,10 @@ class Deployment < ApplicationRecord
belongs_to :cluster, class_name: 'Clusters::Cluster', optional: true
belongs_to :user
belongs_to :deployable, polymorphic: true, optional: true # rubocop:disable Cop/PolymorphicAssociations
+ has_many :deployment_merge_requests
+
+ has_many :merge_requests,
+ through: :deployment_merge_requests
has_internal_id :iid, scope: :project, init: ->(s) do
Deployment.where(project: s.project).maximum(:iid) if s&.project
@@ -75,6 +79,11 @@ class Deployment < ApplicationRecord
find(ids)
end
+ def self.distinct_on_environment
+ order('environment_id, deployments.id DESC')
+ .select('DISTINCT ON (environment_id) deployments.*')
+ end
+
def self.find_successful_deployment!(iid)
success.find_by!(iid: iid)
end
@@ -144,6 +153,18 @@ class Deployment < ApplicationRecord
project.deployments.joins(:environment)
.where(environments: { name: self.environment.name }, ref: self.ref)
.where.not(id: self.id)
+ .order(id: :desc)
+ .take
+ end
+
+ def previous_environment_deployment
+ project
+ .deployments
+ .success
+ .joins(:environment)
+ .where(environments: { name: environment.name })
+ .where.not(id: self.id)
+ .order(id: :desc)
.take
end
@@ -176,6 +197,18 @@ class Deployment < ApplicationRecord
deployable&.user || user
end
+ def link_merge_requests(relation)
+ select = relation.select(['merge_requests.id', id]).to_sql
+
+ # We don't use `Gitlab::Database.bulk_insert` here so that we don't need to
+ # first pluck lots of IDs into memory.
+ DeploymentMergeRequest.connection.execute(<<~SQL)
+ INSERT INTO #{DeploymentMergeRequest.table_name}
+ (merge_request_id, deployment_id)
+ #{select}
+ SQL
+ end
+
private
def ref_path
diff --git a/app/models/deployment_merge_request.rb b/app/models/deployment_merge_request.rb
new file mode 100644
index 00000000000..ff4d9f66202
--- /dev/null
+++ b/app/models/deployment_merge_request.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class DeploymentMergeRequest < ApplicationRecord
+ belongs_to :deployment, optional: false
+ belongs_to :merge_request, optional: false
+end
diff --git a/app/models/description_version.rb b/app/models/description_version.rb
index abab7f94212..05362a2f90b 100644
--- a/app/models/description_version.rb
+++ b/app/models/description_version.rb
@@ -10,6 +10,10 @@ class DescriptionVersion < ApplicationRecord
%i(issue merge_request).freeze
end
+ def issuable
+ issue || merge_request
+ end
+
private
def exactly_one_issuable
diff --git a/app/models/environment.rb b/app/models/environment.rb
index af0c219d9a0..327b1e594d7 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -4,12 +4,20 @@ class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include ReactiveCaching
+ self.reactive_cache_refresh_interval = 1.minute
+ self.reactive_cache_lifetime = 55.seconds
+
belongs_to :project, required: true
has_many :deployments, -> { visible }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :successful_deployments, -> { success }, class_name: 'Deployment'
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment'
+ has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
+ has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
+ has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
+ has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
+ has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -60,6 +68,10 @@ class Environment < ApplicationRecord
scope :for_project, -> (project) { where(project_id: project) }
scope :with_deployment, -> (sha) { where('EXISTS (?)', Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)) }
+ scope :unfoldered, -> { where(environment_type: nil) }
+ scope :with_rank, -> do
+ select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
+ end
state_machine :state, initial: :available do
event :start do
@@ -188,6 +200,10 @@ class Environment < ApplicationRecord
prometheus_adapter.query(:environment, self) if has_metrics?
end
+ def prometheus_status
+ deployment_platform&.cluster&.application_prometheus&.status_name
+ end
+
def additional_metrics(*args)
return unless has_metrics?
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 0b4fef5eac1..2aa058a243f 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -7,6 +7,7 @@ module ErrorTracking
SENTRY_API_ERROR_TYPE_MISSING_KEYS = 'missing_keys_in_sentry_response'
SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE = 'non_20x_response_from_sentry'
+ SENTRY_API_ERROR_INVALID_SIZE = 'invalid_size_of_sentry_response'
API_URL_PATH_REGEXP = %r{
\A
@@ -87,15 +88,37 @@ module ErrorTracking
{ projects: sentry_client.list_projects }
end
+ def issue_details(opts = {})
+ with_reactive_cache('issue_details', opts.stringify_keys) do |result|
+ result
+ end
+ end
+
+ def issue_latest_event(opts = {})
+ with_reactive_cache('issue_latest_event', opts.stringify_keys) do |result|
+ result
+ end
+ end
+
def calculate_reactive_cache(request, opts)
case request
when 'list_issues'
{ issues: sentry_client.list_issues(**opts.symbolize_keys) }
+ when 'issue_details'
+ {
+ issue: sentry_client.issue_details(**opts.symbolize_keys)
+ }
+ when 'issue_latest_event'
+ {
+ latest_event: sentry_client.issue_latest_event(**opts.symbolize_keys)
+ }
end
rescue Sentry::Client::Error => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE }
rescue Sentry::Client::MissingKeysError => e
{ error: e.message, error_type: SENTRY_API_ERROR_TYPE_MISSING_KEYS }
+ rescue Sentry::Client::ResponseInvalidSizeError => e
+ { error: e.message, error_type: SENTRY_API_ERROR_INVALID_SIZE }
end
# http://HOST/api/0/projects/ORG/PROJECT
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index 7d766e1f25c..65fd5c1b35a 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -11,7 +11,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, to: :milestone
+ :milestoneish_id, :resource_parent, :releases, to: :milestone
def to_hash
{
@@ -100,4 +100,8 @@ class GlobalMilestone
def labels
@labels ||= GlobalLabel.build_collection(milestone.labels).sort_by!(&:title)
end
+
+ def global_milestone?
+ true
+ end
end
diff --git a/app/models/grafana_integration.rb b/app/models/grafana_integration.rb
index 51cc398394d..ed4c279965a 100644
--- a/app/models/grafana_integration.rb
+++ b/app/models/grafana_integration.rb
@@ -14,7 +14,13 @@ class GrafanaIntegration < ApplicationRecord
validates :token, :project, presence: true
+ validates :enabled, inclusion: { in: [true, false] }
+
+ scope :enabled, -> { where(enabled: true) }
+
def client
+ return unless enabled?
+
@client ||= ::Grafana::Client.new(api_url: grafana_url.chomp('/'), token: token)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 042201ffa14..8289d4f099c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -30,6 +30,10 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
+ 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
+ has_many :shared_with_groups, through: :shared_with_group_links, source: :shared_with_group
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
@@ -51,6 +55,8 @@ class Group < Namespace
has_many :todos
+ has_one :import_export_upload
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
@@ -120,7 +126,7 @@ class Group < Namespace
def visible_to_user_arel(user)
groups_table = self.arel_table
- authorized_groups = user.authorized_groups.as('authorized')
+ authorized_groups = user.authorized_groups.arel.as('authorized')
groups_table.project(1)
.from(authorized_groups)
@@ -259,8 +265,8 @@ class Group < Namespace
members_with_parents.maintainers.exists?(user_id: user)
end
- def has_container_repositories?
- container_repositories.exists?
+ def has_container_repository_including_subgroups?
+ ::ContainerRepository.for_group_and_its_subgroups(self).exists?
end
# @deprecated
@@ -376,11 +382,12 @@ class Group < Namespace
return GroupMember::OWNER if user.admin?
- members_with_parents
- .where(user_id: user)
- .reorder(access_level: :desc)
- .first&.
- access_level || GroupMember::NO_ACCESS
+ max_member_access = members_with_parents.where(user_id: user)
+ .reorder(access_level: :desc)
+ .first
+ &.access_level
+
+ max_member_access || max_member_access_for_user_from_shared_groups(user) || GroupMember::NO_ACCESS
end
def mattermost_team_params
@@ -444,6 +451,14 @@ class Group < Namespace
false
end
+ def export_file_exists?
+ export_file&.file
+ end
+
+ def export_file
+ import_export_upload&.export_file
+ end
+
private
def update_two_factor_requirement
@@ -474,6 +489,26 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
+ def max_member_access_for_user_from_shared_groups(user)
+ return unless Feature.enabled?(:share_group_with_group)
+
+ group_group_link_table = GroupGroupLink.arel_table
+ group_member_table = GroupMember.arel_table
+
+ group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
+ cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
+
+ link = GroupGroupLink
+ .with(cte.to_arel)
+ .from([group_member_table, cte.alias_to(group_group_link_table)])
+ .where(group_member_table[:user_id].eq(user.id))
+ .where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
+ .reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
+ .first
+
+ link&.group_access
+ end
+
def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids))
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
new file mode 100644
index 00000000000..4b279b7af5b
--- /dev/null
+++ b/app/models/group_group_link.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class GroupGroupLink < ApplicationRecord
+ include Expirable
+
+ belongs_to :shared_group, class_name: 'Group', foreign_key: :shared_group_id
+ belongs_to :shared_with_group, class_name: 'Group', foreign_key: :shared_with_group_id
+
+ validates :shared_group, presence: true
+ validates :shared_group_id, uniqueness: { scope: [:shared_with_group_id],
+ message: _('The group has already been shared with this group') }
+ validates :shared_with_group, presence: true
+ validates :group_access, inclusion: { in: Gitlab::Access.values },
+ presence: true
+
+ def self.access_options
+ Gitlab::Access.options
+ end
+
+ def self.default_access
+ Gitlab::Access::DEVELOPER
+ end
+end
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
index 60f5491849a..7d73fd281f1 100644
--- a/app/models/import_export_upload.rb
+++ b/app/models/import_export_upload.rb
@@ -5,6 +5,7 @@ class ImportExportUpload < ApplicationRecord
include ObjectStorage::BackgroundMove
belongs_to :project
+ belongs_to :group
# These hold the project Import/Export archives (.tar.gz files)
mount_uploader :import_file, ImportExportUploader
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b9b481ac29b..948cadc34e5 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,6 +40,7 @@ class Issue < ApplicationRecord
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
+ has_many :zoom_meetings
validates :project, presence: true
@@ -54,9 +55,9 @@ class Issue < ApplicationRecord
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 :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
- scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
- scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
+ 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')) }
+ scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :preload_associations, -> { preload(:labels, project: :namespace) }
@@ -65,6 +66,8 @@ class Issue < ApplicationRecord
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
+ scope :counts_by_state, -> { reorder(nil).group(:state).count }
+
after_commit :expire_etag_cache
after_save :ensure_metrics, unless: :imported?
@@ -137,8 +140,8 @@ class Issue < ApplicationRecord
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
- when 'due_date', 'due_date_asc' then order_due_date_asc
- when 'due_date_desc' then order_due_date_desc
+ when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
+ when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
else
super
@@ -206,7 +209,16 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- "#{iid}-#{title.parameterize}"
+ branch_name = "#{iid}-#{title.parameterize}"
+
+ if branch_name.length > 100
+ truncated_string = branch_name[0, 100]
+ # Delete everything dangling after the last hyphen so as not to risk
+ # existence of unintended words in the branch name due to mid-word split.
+ branch_name = truncated_string[0, truncated_string.rindex("-")]
+ end
+
+ branch_name
end
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 535c3cf2ba1..48c971194c6 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -18,6 +18,11 @@ class LfsObject < ApplicationRecord
after_save :update_file_store, if: :saved_change_to_file?
+ def self.not_linked_to_project(project)
+ where('NOT EXISTS (?)',
+ project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
+ end
+
def update_file_store
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 32741046f39..7e1898e7142 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,9 @@ class MergeRequest < ApplicationRecord
include ReactiveCaching
include FromUnion
include DeprecatedAssignee
+ include ShaAttribute
+
+ sha_attribute :squash_commit_sha
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -65,6 +68,7 @@ class MergeRequest < ApplicationRecord
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
has_many :suggestions, through: :notes
+ has_many :unresolved_notes, -> { unresolved }, as: :noteable, class_name: 'Note'
has_many :merge_request_assignees
has_many :assignees, class_name: "User", through: :merge_request_assignees
@@ -202,11 +206,14 @@ class MergeRequest < ApplicationRecord
scope :by_commit_sha, ->(sha) do
where('EXISTS (?)', MergeRequestDiff.select(1).where('merge_requests.latest_merge_request_diff_id = merge_request_diffs.id').by_commit_sha(sha)).reorder(nil)
end
+ scope :by_merge_commit_sha, -> (sha) do
+ where(merge_commit_sha: sha)
+ end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
scope :with_api_entity_associations, -> {
- preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
- latest_merge_request_diff: [:merge_request_diff_commits],
+ preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
+ :timelogs, :latest_merge_request_diff,
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
@@ -217,17 +224,27 @@ class MergeRequest < ApplicationRecord
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
scope :preload_source_project, -> { preload(:source_project) }
- scope :with_open_merge_when_pipeline_succeeds, -> do
- with_state(:opened).where(merge_when_pipeline_succeeds: true)
+ scope :with_auto_merge_enabled, -> do
+ with_state(:opened).where(auto_merge_enabled: true)
end
after_save :keep_around_commit
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
+
+ # Currently, `merge_when_pipeline_succeeds` column is used as a flag
+ # to check if _any_ auto merge strategy is activated on the merge request.
+ # Today, we have multiple strategies and MWPS is one of them.
+ # we'd eventually rename the column for avoiding confusions, but in the mean time
+ # please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`.
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
alias_method :issuing_parent, :target_project
+ RebaseLockTimeout = Class.new(StandardError)
+
+ REBASE_LOCK_MESSAGE = _("Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later.")
+
def self.reference_prefix
'!'
end
@@ -357,11 +374,12 @@ class MergeRequest < ApplicationRecord
"#{project.to_reference(from, full: full)}#{reference}"
end
- def commits
- return merge_request_diff.commits if persisted?
+ def commits(limit: nil)
+ return merge_request_diff.commits(limit: limit) if persisted?
commits_arr = if compare_commits
- compare_commits.reverse
+ reversed_commits = compare_commits.reverse
+ limit ? reversed_commits.take(limit) : reversed_commits
else
[]
end
@@ -369,6 +387,10 @@ class MergeRequest < ApplicationRecord
CommitCollection.new(source_project, commits_arr, source_branch)
end
+ def recent_commits
+ commits(limit: MergeRequestDiff::COMMITS_SAFE_SIZE)
+ end
+
def commits_count
if persisted?
merge_request_diff.commits_count
@@ -379,14 +401,17 @@ class MergeRequest < ApplicationRecord
end
end
- def commit_shas
- if persisted?
- merge_request_diff.commit_shas
- elsif compare_commits
- compare_commits.to_a.reverse.map(&:sha)
- else
- Array(diff_head_sha)
- end
+ def commit_shas(limit: nil)
+ return merge_request_diff.commit_shas(limit: limit) if persisted?
+
+ shas =
+ if compare_commits
+ compare_commits.to_a.reverse.map(&:sha)
+ else
+ Array(diff_head_sha)
+ end
+
+ limit ? shas.take(limit) : shas
end
# Returns true if there are commits that match at least one commit SHA.
@@ -417,9 +442,7 @@ class MergeRequest < ApplicationRecord
# Set off a rebase asynchronously, atomically updating the `rebase_jid` of
# the MR so that the status of the operation can be tracked.
def rebase_async(user_id)
- transaction do
- lock!
-
+ with_rebase_lock do
raise ActiveRecord::StaleObjectError if !open? || rebase_in_progress?
# Although there is a race between setting rebase_jid here and clearing it
@@ -782,6 +805,8 @@ class MergeRequest < ApplicationRecord
end
def check_mergeability
+ return if Feature.enabled?(:merge_requests_conditional_mergeability_check, default_enabled: true) && !recheck_merge_status?
+
MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -896,7 +921,7 @@ class MergeRequest < ApplicationRecord
def commit_notes
# Fetch comments only from last 100 commits
- commit_ids = commit_shas.take(100)
+ commit_ids = commit_shas(limit: 100)
Note
.user
@@ -907,7 +932,7 @@ class MergeRequest < ApplicationRecord
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
- !discussions_to_be_resolved?
+ unresolved_notes.none?(&:to_be_resolved?)
end
def for_fork?
@@ -1087,7 +1112,7 @@ class MergeRequest < ApplicationRecord
return true unless project.only_allow_merge_if_pipeline_succeeds?
return false unless actual_head_pipeline
- actual_head_pipeline.success? || actual_head_pipeline.skipped?
+ actual_head_pipeline.success?
end
def environments_for(current_user)
@@ -1263,6 +1288,27 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::CompareTestReportsService)
end
+ def has_exposed_artifacts?
+ return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+ actual_head_pipeline&.has_exposed_artifacts?
+ 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.
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+ def find_exposed_artifacts
+ unless has_exposed_artifacts?
+ return { status: :error, status_reason: 'This merge request does not have exposed artifacts' }
+ end
+
+ compare_reports(Ci::GenerateExposedArtifactsReportService)
+ end
+
+ # TODO: consider renaming this as with exposed artifacts we generate reports,
+ # not always compare
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
def compare_reports(service_class, current_user = nil)
with_reactive_cache(service_class.name, current_user&.id) do |data|
unless service_class.new(project, current_user)
@@ -1277,6 +1323,8 @@ class MergeRequest < ApplicationRecord
def calculate_reactive_cache(identifier, current_user_id = nil, *args)
service_class = identifier.constantize
+ # TODO: the type check should change to something that includes exposed artifacts service
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
raise NameError, service_class unless service_class < Ci::CompareReportsBaseService
current_user = User.find_by(id: current_user_id)
@@ -1453,6 +1501,30 @@ class MergeRequest < ApplicationRecord
private
+ def with_rebase_lock
+ if Feature.enabled?(:merge_request_rebase_nowait_lock, default_enabled: true)
+ with_retried_nowait_lock { yield }
+ else
+ with_lock(true) { yield }
+ end
+ end
+
+ # If the merge request is idle in transaction or has a SELECT FOR
+ # UPDATE, we don't want to block indefinitely or this could cause a
+ # queue of SELECT FOR UPDATE calls. Instead, try to get the lock for
+ # 5 s before raising an error to the user.
+ def with_retried_nowait_lock
+ # Try at most 0.25 + (1.5 * .25) + (1.5^2 * .25) ... (1.5^5 * .25) = 5.2 s to get the lock
+ Retriable.retriable(on: ActiveRecord::LockWaitTimeout, tries: 6, base_interval: 0.25) do
+ with_lock('FOR UPDATE NOWAIT') do
+ yield
+ end
+ end
+ rescue ActiveRecord::LockWaitTimeout => e
+ Gitlab::Sentry.track_acceptable_exception(e)
+ raise RebaseLockTimeout, REBASE_LOCK_MESSAGE
+ end
+
def source_project_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless source_project
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 735ad046f22..70ce4df5678 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -213,12 +213,14 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def commits
- @commits ||= load_commits
+ def commits(limit: nil)
+ strong_memoize(:"commits_#{limit || 'all'}") do
+ load_commits(limit: limit)
+ end
end
def last_commit_sha
- commit_shas.first
+ commit_shas(limit: 1).first
end
def first_commit
@@ -247,8 +249,8 @@ class MergeRequestDiff < ApplicationRecord
project.commit_by(oid: head_commit_sha)
end
- def commit_shas
- merge_request_diff_commits.map(&:sha)
+ def commit_shas(limit: nil)
+ merge_request_diff_commits.limit(limit).pluck(:sha)
end
def commits_by_shas(shas)
@@ -529,8 +531,9 @@ class MergeRequestDiff < ApplicationRecord
end
end
- def load_commits
- commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
+ def load_commits(limit: nil)
+ commits = merge_request_diff_commits.limit(limit)
+ .map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index a9f4cdec901..d0be54eed02 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -60,6 +60,7 @@ class Milestone < ApplicationRecord
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
@@ -330,6 +331,6 @@ class Milestone < ApplicationRecord
end
def issues_finder_params
- { project_id: project_id }
+ { project_id: project_id, group_id: group_id }.compact
end
end
diff --git a/app/models/notification_reason.rb b/app/models/notification_reason.rb
index 6856d397413..a7967239417 100644
--- a/app/models/notification_reason.rb
+++ b/app/models/notification_reason.rb
@@ -6,12 +6,14 @@ class NotificationReason
OWN_ACTIVITY = 'own_activity'
ASSIGNED = 'assigned'
MENTIONED = 'mentioned'
+ SUBSCRIBED = 'subscribed'
# Priority list for selecting which reason to return in the notification
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
- MENTIONED
+ MENTIONED,
+ SUBSCRIBED
].freeze
# returns the priority of a reason as an integer
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 7903a2182dd..3869d86b667 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -24,6 +24,8 @@ class PagesDomain < ApplicationRecord
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
+ default_value_for(:auto_ssl_enabled, allow_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
+
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
diff --git a/app/models/project.rb b/app/models/project.rb
index 74da042d5a5..f4aa336fbcd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -76,6 +76,10 @@ class Project < ApplicationRecord
delegate :no_import?, to: :import_state, allow_nil: true
+ # TODO: remove once GitLab 12.5 is released
+ # https://gitlab.com/gitlab-org/gitlab/issues/34638
+ self.ignored_columns += %i[merge_requests_require_code_owner_approval]
+
default_value_for :archived, false
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
@@ -87,6 +91,8 @@ class Project < ApplicationRecord
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
+ default_value_for :remove_source_branch_after_merge, true
+ default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
@@ -281,6 +287,7 @@ class Project < ApplicationRecord
has_many :variables, class_name: 'Ci::Variable'
has_many :triggers, class_name: 'Ci::Trigger'
has_many :environments
+ has_many :environments_for_dashboard, -> { from(with_rank.unfoldered.available, :environments).where('rank <= 3') }, class_name: 'Environment'
has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :project_deploy_tokens
@@ -390,6 +397,7 @@ class Project < ApplicationRecord
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
+ scope :with_container_registry, -> { where(container_registry_enabled: true) }
scope :inside_path, ->(path) do
# We need routes alias rs for JOIN so it does not conflict with
# includes(:route) which we use in ProjectsFinder.
@@ -456,13 +464,6 @@ class Project < ApplicationRecord
# Used by Projects::CleanupService to hold a map of rewritten object IDs
mount_uploader :bfg_object_map, AttachmentUploader
- # Returns a project, if it is not about to be removed.
- #
- # id - The ID of the project to retrieve.
- def self.find_without_deleted(id)
- without_deleted.find_by_id(id)
- end
-
def self.eager_load_namespace_and_owner
includes(namespace: :owner)
end
@@ -656,6 +657,11 @@ class Project < ApplicationRecord
end
end
+ def preload_protected_branches
+ preloader = ActiveRecord::Associations::Preloader.new
+ preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels])
+ end
+
# returns all ancestor-groups upto but excluding the given namespace
# when no namespace is given, all ancestors upto the top are returned
def ancestors_upto(top = nil, hierarchy_order: nil)
@@ -1906,7 +1912,7 @@ class Project < ApplicationRecord
end
def default_environment
- production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
+ production_first = Arel.sql("(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC")
environments
.with_state(:available)
@@ -1961,27 +1967,6 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables
end
- def append_or_update_attribute(name, value)
- if Project.reflect_on_association(name).try(:macro) == :has_many
- # if this is 1-to-N relation, update the parent object
- value.each do |item|
- item.update!(
- Project.reflect_on_association(name).foreign_key => id)
- end
-
- # force to drop relation cache
- public_send(name).reset # rubocop:disable GitlabSecurity/PublicSend
-
- # succeeded
- true
- else
- # if this is another relation or attribute, update just object
- update_attribute(name, value)
- end
- rescue ActiveRecord::RecordInvalid => e
- raise e, "Failed to set #{name}: #{e.message}"
- end
-
# Tries to set repository as read_only, checking for existing Git transfers in progress beforehand
#
# @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index a495d34c07c..d089a004d3d 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class ProjectCiCdSetting < ApplicationRecord
+ # TODO: remove once GitLab 12.7 is released
+ # https://gitlab.com/gitlab-org/gitlab/issues/36651
+ self.ignored_columns += %i[merge_trains_enabled]
belongs_to :project, inverse_of: :ci_cd_settings
# The version of the schema that first introduced this model/table.
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index a3793d9937b..46fe894cfc3 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -75,11 +75,11 @@ module ChatMessage
def activity
{
- title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}") %
+ title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
{
pipeline_link: pipeline_link,
ref_type: ref_type,
- branch_link: branch_link,
+ ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status
},
@@ -123,7 +123,7 @@ module ChatMessage
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
- value: Slack::Notifier::LinkFormatter.format(ref_name_link),
+ value: Slack::Notifier::LinkFormatter.format(ref_link),
short: true
},
{
@@ -141,12 +141,12 @@ module ChatMessage
end
def message
- s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
+ s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
{
project_link: project_link,
pipeline_link: pipeline_link,
ref_type: ref_type,
- branch_link: branch_link,
+ ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status,
duration: pretty_duration(duration)
@@ -193,12 +193,16 @@ module ChatMessage
end
end
- def branch_url
- "#{project_url}/commits/#{ref}"
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
end
- def branch_link
- "[#{ref}](#{branch_url})"
+ def ref_link
+ "[#{ref}](#{ref_url})"
end
def project_url
@@ -266,14 +270,6 @@ module ChatMessage
"[#{commit.title}](#{commit_url})"
end
- def commits_page_url
- "#{project_url}/commits/#{ref}"
- end
-
- def ref_name_link
- "[#{ref}](#{commits_page_url})"
- end
-
def author_url
return unless user && committer
diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb
index 8163fca33a2..07622f570c2 100644
--- a/app/models/project_services/chat_message/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -82,16 +82,20 @@ module ChatMessage
Gitlab::Git.blank_ref?(after)
end
- def branch_url
- "#{project_url}/commits/#{ref}"
+ def ref_url
+ if ref_type == 'tag'
+ "#{project_url}/-/tags/#{ref}"
+ else
+ "#{project_url}/commits/#{ref}"
+ end
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
- def branch_link
- "[#{ref}](#{branch_url})"
+ def ref_link
+ "[#{ref}](#{ref_url})"
end
def project_link
@@ -104,11 +108,11 @@ module ChatMessage
def compose_action_details
if new_branch?
- ['pushed new', branch_link, "to #{project_link}"]
+ ['pushed new', ref_link, "to #{project_link}"]
elsif removed_branch?
['removed', ref, "from #{project_link}"]
else
- ['pushed to', branch_link, "of #{project_link} (#{compare_link})"]
+ ['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
index cffb493d569..cf406a784ce 100644
--- a/app/models/project_services/data_fields.rb
+++ b/app/models/project_services/data_fields.rb
@@ -50,7 +50,7 @@ module DataFields
end
def data_fields_present?
- data_fields.persisted?
+ data_fields.present?
rescue NotImplementedError
false
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 6eff2ea2e3a..a0273fe0e5a 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -7,8 +7,15 @@ class PrometheusService < MonitoringService
prop_accessor :api_url
boolean_accessor :manual_configuration
+ # We need to allow the self-monitoring project to connect to the internal
+ # Prometheus instance.
+ # Since the internal Prometheus instance is usually a localhost URL, we need
+ # to allow localhost URLs when the following conditions are true:
+ # 1. project is the self-monitoring project.
+ # 2. api_url is the internal Prometheus URL.
with_options presence: true, if: :manual_configuration? do
- validates :api_url, public_url: true
+ validates :api_url, public_url: true, unless: proc { |object| object.allow_local_api_url? }
+ validates :api_url, url: true, if: proc { |object| object.allow_local_api_url? }
end
before_save :synchronize_service_state
@@ -82,12 +89,28 @@ class PrometheusService < MonitoringService
project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
end
+ def allow_local_api_url?
+ self_monitoring_project? && internal_prometheus_url?
+ end
+
private
+ def self_monitoring_project?
+ project && project.id == current_settings.instance_administration_project_id
+ end
+
+ def internal_prometheus_url?
+ api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri
+ end
+
def should_return_client?
api_url.present? && manual_configuration? && active? && valid?
end
+ def current_settings
+ Gitlab::CurrentSettings.current_application_settings
+ end
+
def synchronize_service_state
self.active = prometheus_available? || manual_configuration?
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index b3585c4cf4c..e732c1bd86f 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -2,13 +2,6 @@
class ProjectSnippet < Snippet
belongs_to :project
- belongs_to :author, class_name: "User"
validates :project, presence: true
-
- # Scopes
- scope :fresh, -> { order("created_at DESC") }
-
- participant :author
- participant :notes_with_associations
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index bb222ac7629..f02ccd9e55e 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -160,12 +160,6 @@ class ProjectWiki
update_project_activity
end
- def page_formatted_data(page)
- page_title, page_dir = page_title_and_dir(page.title)
-
- wiki.page_formatted_data(title: page_title, dir: page_dir, version: page.version)
- end
-
def page_title_and_dir(title)
return unless title
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index 08f4df7ea01..d0dc31476ff 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -14,7 +14,13 @@ class PrometheusMetric < ApplicationRecord
validates :project, presence: true, unless: :common?
validates :project, absence: true, if: :common?
+ scope :for_project, -> (project) { where(project: project) }
+ scope :for_group, -> (group) { where(group: group) }
+ scope :for_title, -> (title) { where(title: title) }
+ scope :for_y_label, -> (y_label) { where(y_label: y_label) }
+ scope :for_identifier, -> (identifier) { where(identifier: identifier) }
scope :common, -> { where(common: true) }
+ scope :ordered, -> { reorder(created_at: :asc) }
def priority
group_details(group).fetch(:priority)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 1857a59e01c..735e2bdea81 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -38,7 +38,7 @@ class ProtectedBranch < ApplicationRecord
end
def self.protected_refs(project)
- project.protected_branches.select(:name)
+ project.protected_branches
end
def self.branch_requires_code_owner_approval?(project, branch_name)
diff --git a/app/models/release.rb b/app/models/release.rb
index 5a7bfe2d495..401e8359f47 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Release < ApplicationRecord
+ include Presentable
include CacheMarkdownField
include Gitlab::Utils::StrongMemoize
@@ -26,13 +27,21 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) }
+ scope :preloaded, -> { includes(project: :namespace) }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
+ scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
delegate :repository, to: :project
after_commit :create_evidence!, on: :create
after_commit :notify_new_release, on: :create
+ MAX_NUMBER_TO_DISPLAY = 3
+
+ def to_param
+ CGI.escape(tag)
+ end
+
def commit
strong_memoize(:commit) do
repository.commit(actual_sha)
@@ -60,6 +69,10 @@ class Release < ApplicationRecord
released_at.present? && released_at > Time.zone.now
end
+ def name
+ self.read_attribute(:name) || tag
+ end
+
private
def actual_sha
diff --git a/app/models/releases/source.rb b/app/models/releases/source.rb
index 4d3d54457af..2f00d25d768 100644
--- a/app/models/releases/source.rb
+++ b/app/models/releases/source.rb
@@ -6,11 +6,9 @@ module Releases
attr_accessor :project, :tag_name, :format
- FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
-
class << self
def all(project, tag_name)
- Releases::Source::FORMATS.map do |format|
+ Gitlab::Workhorse::ARCHIVE_FORMATS.map do |format|
Releases::Source.new(project: project,
tag_name: tag_name,
format: format)
diff --git a/app/models/service.rb b/app/models/service.rb
index 305cf7b78a2..6d5b974dd31 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -40,6 +40,7 @@ class Service < ApplicationRecord
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
+ scope :by_type, -> (type) { where(type: type) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 1927b54510e..f217c942e8e 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -55,7 +55,8 @@ class Todo < ApplicationRecord
scope :done, -> { with_state(:done) }
scope :for_action, -> (action) { where(action: action) }
scope :for_author, -> (author) { where(author: author) }
- scope :for_project, -> (project) { where(project: project) }
+ scope :for_project, -> (projects) { where(project: projects) }
+ scope :for_undeleted_projects, -> { joins(:project).merge(Project.without_deleted) }
scope :for_group, -> (group) { where(group: group) }
scope :for_type, -> (type) { where(target_type: type) }
scope :for_target, -> (id) { where(target_id: id) }
@@ -160,6 +161,10 @@ class Todo < ApplicationRecord
action == ASSIGNED
end
+ def done?
+ state == 'done'
+ end
+
def action_name
ACTION_NAMES[action]
end
diff --git a/app/models/user.rb b/app/models/user.rb
index eec8ad6edbb..d0e758b0055 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,9 +56,6 @@ class User < ApplicationRecord
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
- # Removed in GitLab 12.3. Keep until after 2019-09-22.
- self.ignored_columns += %i[support_bot]
-
MINIMUM_INACTIVE_DAYS = 180
# Override Devise::Models::Trackable#update_tracked_fields!
@@ -243,6 +240,8 @@ class User < ApplicationRecord
delegate :time_display_relative, :time_display_relative=, to: :user_preference
delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
+ delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
+ delegate :setup_for_company, :setup_for_company=, to: :user_preference
accepts_nested_attributes_for :user_preference, update_only: true
@@ -1423,14 +1422,13 @@ class User < ApplicationRecord
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
- # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
+ # <https://github.com/plataformatec/devise/blob/v4.7.1/lib/devise/models/lockable.rb#L104>
#
# rubocop: disable CodeReuse/ServiceClass
def increment_failed_attempts!
return if ::Gitlab::Database.read_only?
- self.failed_attempts ||= 0
- self.failed_attempts += 1
+ increment_failed_attempts
if attempts_exceeded?
lock_access! unless access_locked?
@@ -1458,7 +1456,7 @@ class User < ApplicationRecord
# Does the user have access to all private groups & projects?
# Overridden in EE to also check auditor?
def full_private_access?
- admin?
+ can?(:read_all_resources)
end
def update_two_factor_requirement
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 68241d2bd95..f9c562364cb 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -112,11 +112,6 @@ class WikiPage
wiki.page_title_and_dir(slug)&.last.to_s
end
- # The processed/formatted content of this page.
- def formatted_content
- @attributes[:formatted_content] ||= @wiki.page_formatted_data(@page)
- end
-
# The markup format for the page.
def format
@attributes[:format] || :markdown
diff --git a/app/models/zoom_meeting.rb b/app/models/zoom_meeting.rb
new file mode 100644
index 00000000000..a7ecd1e6a2c
--- /dev/null
+++ b/app/models/zoom_meeting.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ZoomMeeting < ApplicationRecord
+ belongs_to :project, optional: false
+ belongs_to :issue, optional: false
+
+ validates :url, presence: true, length: { maximum: 255 }, zoom_url: true
+ validates :issue, same_project_association: true
+
+ enum issue_status: {
+ added: 1,
+ removed: 2
+ }
+
+ scope :added_to_issue, -> { where(issue_status: :added) }
+ scope :removed_from_issue, -> { where(issue_status: :removed) }
+ scope :canonical, -> (issue) { where(issue: issue).added_to_issue }
+
+ def self.canonical_meeting(issue)
+ canonical(issue)&.take
+ end
+
+ def self.canonical_meeting_url(issue)
+ canonical_meeting(issue)&.url
+ end
+end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 18c23cbd13a..8f5c6957a20 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -21,10 +21,6 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? }
- desc "User has access to all private groups & projects"
- with_options scope: :user, score: 0
- condition(:full_private_access) { @user&.full_private_access? }
-
with_options scope: :user, score: 0
condition(:external_user) { @user.nil? || @user.external? }
@@ -40,10 +36,12 @@ class BasePolicy < DeclarativePolicy::Base
::Gitlab::ExternalAuthorization.perform_check?
end
- rule { external_authorization_enabled & ~full_private_access }.policy do
+ rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
prevent :read_cross_project
end
+ rule { admin }.enable :read_all_resources
+
rule { default }.enable :read_cross_project
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 13e5b4ae41a..1cd400e4dfa 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -44,6 +44,7 @@ class GroupPolicy < BasePolicy
rule { public_group }.policy do
enable :read_group
+ enable :read_package
end
rule { logged_in_viewable }.enable :read_group
@@ -70,7 +71,10 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
- rule { developer }.enable :admin_milestone
+ rule { developer }.policy do
+ enable :admin_milestone
+ enable :read_package
+ end
rule { reporter }.policy do
enable :read_container_image
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 40dd49b4afd..91a8f3a7133 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -10,7 +10,7 @@ class PersonalSnippetPolicy < BasePolicy
enable :create_note
end
- rule { is_author }.policy do
+ rule { is_author | admin }.policy do
enable :read_personal_snippet
enable :update_personal_snippet
enable :destroy_personal_snippet
@@ -30,5 +30,5 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
- rule { full_private_access }.enable :read_personal_snippet
+ rule { can?(:read_all_resources) }.enable :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index ea2be37d7e6..ff70c6e6aeb 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -117,6 +117,10 @@ class ProjectPolicy < BasePolicy
!@subject.builds_enabled?
end
+ condition(:user_confirmed?) do
+ @user && @user.confirmed?
+ end
+
features = %w[
merge_requests
issues
@@ -249,10 +253,7 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
- enable :create_pipeline
- enable :update_pipeline
enable :read_pipeline_schedule
- enable :create_pipeline_schedule
enable :create_merge_request_from
enable :create_wiki
enable :push_code
@@ -267,6 +268,12 @@ class ProjectPolicy < BasePolicy
enable :update_release
end
+ rule { can?(:developer_access) & user_confirmed? }.policy do
+ enable :create_pipeline
+ enable :update_pipeline
+ enable :create_pipeline_schedule
+ end
+
rule { can?(:maintainer_access) }.policy do
enable :admin_board
enable :push_to_delete_protected_branch
@@ -418,7 +425,7 @@ class ProjectPolicy < BasePolicy
# These rules are included to allow maintainers of projects to push to certain
# to run pipelines for the branches they have access to.
- rule { can?(:public_access) & has_merge_requests_allowing_pushes }.policy do
+ rule { can?(:public_access) & has_merge_requests_allowing_pushes & user_confirmed? }.policy do
enable :create_build
enable :create_pipeline
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 2a3e4ca174b..d9d09eb04cd 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -28,7 +28,7 @@ class ProjectSnippetPolicy < BasePolicy
all?(private_snippet | (internal_snippet & external_user),
~project.guest,
~is_author,
- ~full_private_access)
+ ~can?(:read_all_resources))
end.prevent :read_project_snippet
rule { internal_snippet & ~is_author & ~admin }.policy do
diff --git a/app/policies/todo_policy.rb b/app/policies/todo_policy.rb
index f8644217f04..d01a046c343 100644
--- a/app/policies/todo_policy.rb
+++ b/app/policies/todo_policy.rb
@@ -7,4 +7,5 @@ class TodoPolicy < BasePolicy
end
rule { own_todo }.enable :read_todo
+ rule { own_todo }.enable :update_todo
end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 34dffbf40fd..2306f55f1f4 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -29,6 +29,18 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
new_polymorphic_path([clusterable, :cluster], options)
end
+ def aws_api_proxy_path(resource)
+ polymorphic_path([clusterable, :clusters], action: :aws_proxy, resource: resource)
+ end
+
+ def authorize_aws_role_path
+ polymorphic_path([clusterable, :clusters], action: :authorize_aws_role)
+ end
+
+ def revoke_aws_role_path
+ polymorphic_path([clusterable, :clusters], action: :revoke_aws_role)
+ end
+
def create_user_clusters_path
polymorphic_path([clusterable, :clusters], action: :create_user)
end
@@ -37,6 +49,10 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
polymorphic_path([clusterable, :clusters], action: :create_gcp)
end
+ def create_aws_clusters_path
+ polymorphic_path([clusterable, :clusters], action: :create_aws)
+ end
+
def cluster_status_cluster_path(cluster, params = {})
raise NotImplementedError
end
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index f1182ec26f4..66ae840a619 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -11,7 +11,9 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
stale_schedule: 'Delayed job could not be executed by some reason, please try again',
job_execution_timeout: 'The script exceeded the maximum execution time set for the job',
archived_failure: 'The job is archived and cannot be run',
- unmet_prerequisites: 'The job failed to complete prerequisite tasks'
+ unmet_prerequisites: 'The job failed to complete prerequisite tasks',
+ scheduler_failure: 'The scheduler failed to assign job to the runner, please try again or contact system administrator',
+ data_integrity_failure: 'There has been a structural integrity problem detected, please contact system administrator'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
@@ -33,6 +35,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
end
def unrecoverable?
- script_failure? || missing_dependency_failure? || archived_failure?
+ script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
end
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 908cd17678d..c6572e8ce71 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -52,6 +52,26 @@ class InstanceClusterablePresenter < ClusterablePresenter
create_gcp_admin_clusters_path
end
+ override :create_aws_clusters_path
+ def create_aws_clusters_path
+ create_aws_admin_clusters_path
+ end
+
+ override :authorize_aws_role_path
+ def authorize_aws_role_path
+ authorize_aws_role_admin_clusters_path
+ end
+
+ override :revoke_aws_role_path
+ def revoke_aws_role_path
+ revoke_aws_role_admin_clusters_path
+ end
+
+ override :aws_api_proxy_path
+ def aws_api_proxy_path(resource)
+ aws_proxy_admin_clusters_path(resource: resource)
+ end
+
override :empty_state_help_text
def empty_state_help_text
s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 6d370f6241c..81018398d5d 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -21,7 +21,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_anchors(show_auto_devops_callout:)
[
- license_anchor_data,
commits_anchor_data,
branches_anchor_data,
tags_anchor_data,
@@ -32,6 +31,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def statistics_buttons(show_auto_devops_callout:)
[
readme_anchor_data,
+ license_anchor_data,
changelog_anchor_data,
contribution_guide_anchor_data,
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
@@ -41,15 +41,14 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def empty_repo_statistics_anchors
- [
- license_anchor_data
- ].compact.select { |item| item.is_link }
+ []
end
def empty_repo_statistics_buttons
[
new_file_anchor_data,
readme_anchor_data,
+ license_anchor_data,
changelog_anchor_data,
contribution_guide_anchor_data,
gitlab_ci_anchor_data
@@ -227,17 +226,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
icon = statistic_icon('scale')
if repository.license_blob.present?
- AnchorData.new(true,
- icon + content_tag(:strong, license_short_name, class: 'project-stat-value'),
- license_path)
+ AnchorData.new(false,
+ icon + content_tag(:span, license_short_name, class: 'project-stat-value'),
+ license_path,
+ 'default')
else
if current_user && can_current_user_push_to_default_branch?
- AnchorData.new(true,
- content_tag(:span, icon + _('Add license'), class: 'add-license-link d-flex'),
+ AnchorData.new(false,
+ content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'),
add_license_path)
else
- AnchorData.new(true,
- icon + content_tag(:strong, _('No license. All rights reserved'), class: 'project-stat-value'),
+ AnchorData.new(false,
+ icon + content_tag(:span, _('No license. All rights reserved'), class: 'project-stat-value'),
nil)
end
end
diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb
new file mode 100644
index 00000000000..42463d6dbda
--- /dev/null
+++ b/app/presenters/release_presenter.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class ReleasePresenter < Gitlab::View::Presenter::Delegated
+ include ActionView::Helpers::UrlHelper
+
+ presents :release
+
+ delegate :project, :tag, to: :release
+
+ def commit_path
+ return unless release.commit && can_download_code?
+
+ project_commit_path(project, release.commit.id)
+ end
+
+ def tag_path
+ return unless can_download_code?
+
+ project_tag_path(project, release.tag)
+ end
+
+ def merge_requests_url
+ return unless release_mr_issue_urls_available?
+
+ project_merge_requests_url(project, params_for_issues_and_mrs)
+ end
+
+ def issues_url
+ return unless release_mr_issue_urls_available?
+
+ project_issues_url(project, params_for_issues_and_mrs)
+ end
+
+ def edit_url
+ return unless release_edit_page_available?
+
+ edit_project_release_url(project, release)
+ end
+
+ private
+
+ def can_download_code?
+ can?(current_user, :download_code, project)
+ end
+
+ def params_for_issues_and_mrs
+ { scope: 'all', state: 'opened', release_tag: release.tag }
+ end
+
+ def release_mr_issue_urls_available?
+ ::Feature.enabled?(:release_mr_issue_urls, project)
+ end
+
+ def release_edit_page_available?
+ ::Feature.enabled?(:release_edit_page, project, default_enabled: true)
+ end
+end
diff --git a/app/presenters/todo_presenter.rb b/app/presenters/todo_presenter.rb
index b57fc712c5a..291be7848e2 100644
--- a/app/presenters/todo_presenter.rb
+++ b/app/presenters/todo_presenter.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
class TodoPresenter < Gitlab::View::Presenter::Delegated
- include GlobalID::Identification
-
presents :todo
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 2a916b13f52..218bdd21e37 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -8,7 +8,9 @@ class ClusterApplicationEntity < Grape::Entity
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :external_hostname, if: -> (e, _) { e.respond_to?(:external_hostname) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
+ expose :kibana_hostname, if: -> (e, _) { e.respond_to?(:kibana_hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
+ expose :stack, if: -> (e, _) { e.respond_to?(:stack) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
expose :can_uninstall?, as: :can_uninstall
end
diff --git a/app/serializers/container_repositories_serializer.rb b/app/serializers/container_repositories_serializer.rb
index e1ce3c7b3ae..bc35a67ff24 100644
--- a/app/serializers/container_repositories_serializer.rb
+++ b/app/serializers/container_repositories_serializer.rb
@@ -2,4 +2,8 @@
class ContainerRepositoriesSerializer < BaseSerializer
entity ContainerRepositoryEntity
+
+ def represent_read_only(resource)
+ represent(resource, except: [:destroy_path])
+ end
end
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index ee68b4b98e0..302fe3d7c67 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -89,6 +89,14 @@ class DiffFileBaseEntity < Grape::Entity
expose :viewer, using: DiffViewerEntity
+ expose :old_size do |diff_file|
+ diff_file.old_blob&.raw_size
+ end
+
+ expose :new_size do |diff_file|
+ diff_file.new_blob&.raw_size
+ end
+
private
def memoized_submodule_links(diff_file, options)
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
index 2a5121a2266..af7d1172f17 100644
--- a/app/serializers/diff_file_entity.rb
+++ b/app/serializers/diff_file_entity.rb
@@ -53,7 +53,7 @@ class DiffFileEntity < DiffFileBaseEntity
end
# Used for inline diffs
- expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
+ expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, options) { inline_diff_view?(options) && diff_file.text? } do |diff_file|
diff_file.diff_lines_for_serializer
end
@@ -62,5 +62,21 @@ class DiffFileEntity < DiffFileBaseEntity
end
# Used for parallel diffs
- expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? }
+ expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, options) { parallel_diff_view?(options) && diff_file.text? }
+
+ private
+
+ def parallel_diff_view?(options)
+ return true unless Feature.enabled?(:single_mr_diff_view)
+
+ # If we're not rendering inline, we must be rendering parallel
+ !inline_diff_view?(options)
+ end
+
+ def inline_diff_view?(options)
+ return true unless Feature.enabled?(:single_mr_diff_view)
+
+ # If nothing is present, inline will be the default.
+ options.fetch(:diff_view, :inline).to_sym == :inline
+ end
end
diff --git a/app/serializers/error_tracking/detailed_error_entity.rb b/app/serializers/error_tracking/detailed_error_entity.rb
new file mode 100644
index 00000000000..8f08f84aa41
--- /dev/null
+++ b/app/serializers/error_tracking/detailed_error_entity.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class DetailedErrorEntity < Grape::Entity
+ expose :count,
+ :culprit,
+ :external_base_url,
+ :external_url,
+ :first_release_last_commit,
+ :first_release_short_version,
+ :first_seen,
+ :frequency,
+ :id,
+ :last_release_last_commit,
+ :last_release_short_version,
+ :last_seen,
+ :message,
+ :project_id,
+ :project_name,
+ :project_slug,
+ :short_id,
+ :status,
+ :title,
+ :type,
+ :user_count
+ end
+end
diff --git a/app/serializers/error_tracking/detailed_error_serializer.rb b/app/serializers/error_tracking/detailed_error_serializer.rb
new file mode 100644
index 00000000000..201da16a1ae
--- /dev/null
+++ b/app/serializers/error_tracking/detailed_error_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class DetailedErrorSerializer < BaseSerializer
+ entity DetailedErrorEntity
+ end
+end
diff --git a/app/serializers/error_tracking/error_event_entity.rb b/app/serializers/error_tracking/error_event_entity.rb
new file mode 100644
index 00000000000..6cf0e6e3ae2
--- /dev/null
+++ b/app/serializers/error_tracking/error_event_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorEventEntity < Grape::Entity
+ expose :issue_id, :date_received, :stack_trace_entries
+ end
+end
diff --git a/app/serializers/error_tracking/error_event_serializer.rb b/app/serializers/error_tracking/error_event_serializer.rb
new file mode 100644
index 00000000000..bc4eae16368
--- /dev/null
+++ b/app/serializers/error_tracking/error_event_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class ErrorEventSerializer < BaseSerializer
+ entity ErrorEventEntity
+ end
+end
diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index fb35b7522c5..0e1fcc58d7a 100644
--- a/app/serializers/issuable_sidebar_extras_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -3,11 +3,20 @@
class IssuableSidebarExtrasEntity < Grape::Entity
include RequestAwareEntity
include TimeTrackableEntity
+ include NotificationsHelper
expose :participants, using: ::API::Entities::UserBasic do |issuable|
issuable.participants(request.current_user)
end
+ expose :project_emails_disabled do |issuable|
+ issuable.project.emails_disabled?
+ end
+
+ expose :subscribe_disabled_description do |issuable|
+ notification_description(:owner_disabled)
+ end
+
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index b8f799a7456..13897279815 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -2,7 +2,6 @@
class IssueBoardEntity < Grape::Entity
include RequestAwareEntity
- include TimeTrackableEntity
expose :id
expose :iid
diff --git a/app/serializers/job_artifact_report_entity.rb b/app/serializers/job_artifact_report_entity.rb
index 4280351a6b0..bdab8f64785 100644
--- a/app/serializers/job_artifact_report_entity.rb
+++ b/app/serializers/job_artifact_report_entity.rb
@@ -8,6 +8,6 @@ class JobArtifactReportEntity < Grape::Entity
expose :size
expose :download_path do |artifact|
- download_project_job_artifacts_path(artifact.job.project, artifact.job, file_type: artifact.file_format)
+ download_project_job_artifacts_path(artifact.job.project, artifact.job, file_type: artifact.file_type)
end
end
diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb
index 7e3053e5881..5c79b165ee9 100644
--- a/app/serializers/merge_request_diff_entity.rb
+++ b/app/serializers/merge_request_diff_entity.rb
@@ -21,6 +21,8 @@ class MergeRequestDiffEntity < Grape::Entity
expose :latest?, as: :latest
expose :short_commit_sha do |merge_request_diff|
+ next unless merge_request_diff.head_commit_sha
+
short_sha(merge_request_diff.head_commit_sha)
end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 854349e8507..2a61187a856 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -65,6 +65,12 @@ class MergeRequestPollWidgetEntity < IssuableEntity
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)
+ end
+ end
+
expose :create_issue_to_resolve_discussions_path do |merge_request|
presenter(merge_request).create_issue_to_resolve_discussions_path
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 1d3b59eb1b7..c49dec2a93c 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -79,3 +79,5 @@ class NoteEntity < API::Entities::Note
request.current_user
end
end
+
+NoteEntity.prepend_if_ee('EE::NoteEntity')
diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb
index a1e0bf02d11..10360e575bb 100644
--- a/app/serializers/projects/serverless/service_entity.rb
+++ b/app/serializers/projects/serverless/service_entity.rb
@@ -44,28 +44,52 @@ module Projects
end
expose :url do |service|
- service.dig('status', 'url') || "http://#{service.dig('status', 'domain')}"
+ knative_06_07_url(service) || knative_05_url(service)
end
expose :description do |service|
+ knative_07_description(service) || knative_05_06_description(service)
+ end
+
+ expose :image do |service|
service.dig(
'spec',
'runLatest',
'configuration',
- 'revisionTemplate',
+ 'build',
+ 'template',
+ 'name')
+ end
+
+ private
+
+ def knative_07_description(service)
+ service.dig(
+ 'spec',
+ 'template',
'metadata',
'annotations',
- 'Description')
+ 'Description'
+ )
end
- expose :image do |service|
+ def knative_05_url(service)
+ "http://#{service.dig('status', 'domain')}"
+ end
+
+ def knative_06_07_url(service)
+ service.dig('status', 'url')
+ end
+
+ def knative_05_06_description(service)
service.dig(
'spec',
'runLatest',
'configuration',
- 'build',
- 'template',
- 'name')
+ 'revisionTemplate',
+ 'metadata',
+ 'annotations',
+ 'Description')
end
end
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index c39edd5c114..bc0b968f516 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -50,16 +50,24 @@ class BaseService
private
- def error(message, http_status = nil)
+ # 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
diff --git a/app/services/ci/compare_reports_base_service.rb b/app/services/ci/compare_reports_base_service.rb
index 5b76e1824e4..83ba70e8437 100644
--- a/app/services/ci/compare_reports_base_service.rb
+++ b/app/services/ci/compare_reports_base_service.rb
@@ -1,6 +1,11 @@
# frozen_string_literal: true
module Ci
+ # TODO: when using this class with exposed artifacts we see that there are
+ # 2 responsibilities:
+ # 1. reactive caching interface (same in all cases)
+ # 2. data generator (report comparison in most of the case but not always)
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
class CompareReportsBaseService < ::BaseService
def execute(base_pipeline, head_pipeline)
comparer = comparer_class.new(get_report(base_pipeline), get_report(head_pipeline))
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index eb4176035d3..5778a48bce6 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -7,11 +7,14 @@ module Ci
CreateError = Class.new(StandardError)
SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
- Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
- Gitlab::Ci::Pipeline::Chain::Validate::Config,
+ Gitlab::Ci::Pipeline::Chain::Config::Content,
+ Gitlab::Ci::Pipeline::Chain::Config::Process,
+ Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs,
Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
+ Gitlab::Ci::Pipeline::Chain::Seed,
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create,
diff --git a/app/services/ci/find_exposed_artifacts_service.rb b/app/services/ci/find_exposed_artifacts_service.rb
new file mode 100644
index 00000000000..5c75af294bf
--- /dev/null
+++ b/app/services/ci/find_exposed_artifacts_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Ci
+ # This class loops through all builds with exposed artifacts and returns
+ # basic information about exposed artifacts for given jobs for the frontend
+ # to display them as custom links in the merge request.
+ #
+ # This service must be used with care.
+ # Looking for exposed artifacts is very slow and should be done asynchronously.
+ class FindExposedArtifactsService < ::BaseService
+ include Gitlab::Routing
+
+ MAX_EXPOSED_ARTIFACTS = 10
+
+ def for_pipeline(pipeline, limit: MAX_EXPOSED_ARTIFACTS)
+ results = []
+
+ pipeline.builds.latest.with_exposed_artifacts.find_each do |job|
+ if job_exposed_artifacts = for_job(job)
+ results << job_exposed_artifacts
+ end
+
+ break if results.size >= limit
+ end
+
+ results
+ end
+
+ def for_job(job)
+ return unless job.has_exposed_artifacts?
+
+ metadata_entries = first_2_metadata_entries_for_artifacts_paths(job)
+ return if metadata_entries.empty?
+
+ {
+ text: job.artifacts_expose_as,
+ url: path_for_entries(metadata_entries, job),
+ job_path: project_job_path(project, job),
+ job_name: job.name
+ }
+ end
+
+ private
+
+ # we don't need to fetch all artifacts entries for a job because
+ # it could contain many. We only need to know whether it has 1 or more
+ # artifacts, so fetching the first 2 would be sufficient.
+ def first_2_metadata_entries_for_artifacts_paths(job)
+ job.artifacts_paths
+ .lazy
+ .map { |path| job.artifacts_metadata_entry(path, recursive: true) }
+ .select { |entry| entry.exists? }
+ .first(2)
+ end
+
+ def path_for_entries(entries, job)
+ return if entries.empty?
+
+ if single_artifact?(entries)
+ file_project_job_artifacts_path(project, job, entries.first.path)
+ else
+ browse_project_job_artifacts_path(project, job)
+ end
+ end
+
+ def single_artifact?(entries)
+ entries.size == 1 && entries.first.file?
+ end
+ end
+end
diff --git a/app/services/ci/generate_exposed_artifacts_report_service.rb b/app/services/ci/generate_exposed_artifacts_report_service.rb
new file mode 100644
index 00000000000..b9bf580bcbc
--- /dev/null
+++ b/app/services/ci/generate_exposed_artifacts_report_service.rb
@@ -0,0 +1,30 @@
+# 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 GenerateExposedArtifactsReportService < CompareReportsBaseService
+ def execute(base_pipeline, head_pipeline)
+ data = FindExposedArtifactsService.new(project, current_user).for_pipeline(head_pipeline)
+ {
+ status: :parsed,
+ key: key(base_pipeline, head_pipeline),
+ data: data
+ }
+ rescue => e
+ Gitlab::Sentry.track_acceptable_exception(e, extra: { project_id: project.id })
+ {
+ status: :error,
+ key: key(base_pipeline, head_pipeline),
+ status_reason: _('An error occurred while fetching exposed artifacts.')
+ }
+ 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/register_job_service.rb b/app/services/ci/register_job_service.rb
index d8f32ff88ce..30e2a66e04a 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -42,26 +42,16 @@ module Ci
end
builds.each do |build|
- next unless runner.can_pick?(build)
-
- begin
- # In case when 2 runners try to assign the same build, second runner will be declined
- # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
- if assign_runner!(build, params)
- register_success(build)
-
- return Result.new(build, true)
- end
- rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
- # We are looping to find another build that is not conflicting
- # It also indicates that this build can be picked and passed to runner.
- # If we don't do it, basically a bunch of runners would be competing for a build
- # and thus we will generate a lot of 409. This will increase
- # the number of generated requests, also will reduce significantly
- # how many builds can be picked by runner in a unit of time.
- # In case we hit the concurrency-access lock,
- # we still have to return 409 in the end,
- # to make sure that this is properly handled by runner.
+ result = process_build(build, params)
+ next unless result
+
+ if result.valid?
+ register_success(result.build)
+
+ return result
+ else
+ # The usage of valid: is described in
+ # handling of ActiveRecord::StaleObjectError
valid = false
end
end
@@ -73,6 +63,35 @@ module Ci
private
+ def process_build(build, params)
+ return unless runner.can_pick?(build)
+
+ # In case when 2 runners try to assign the same build, second runner will be declined
+ # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
+ if assign_runner!(build, params)
+ Result.new(build, true)
+ end
+ rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
+ # We are looping to find another build that is not conflicting
+ # It also indicates that this build can be picked and passed to runner.
+ # If we don't do it, basically a bunch of runners would be competing for a build
+ # and thus we will generate a lot of 409. This will increase
+ # the number of generated requests, also will reduce significantly
+ # how many builds can be picked by runner in a unit of time.
+ # In case we hit the concurrency-access lock,
+ # we still have to return 409 in the end,
+ # 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)
+
+ # skip, and move to next one
+ nil
+ end
+
def assign_runner!(build, params)
build.runner_id = runner.id
build.runner_session_attributes = params[:session] if params[:session].present?
@@ -96,6 +115,28 @@ module Ci
true
end
+ def scheduler_failure!(build)
+ Gitlab::OptimisticLocking.retry_lock(build, 3) do |subject|
+ subject.drop!(:scheduler_failure)
+ end
+ rescue => ex
+ build.doom!
+
+ # This requires extra exception, otherwise we would loose information
+ # why we cannot perform `scheduler_failure`
+ track_exception_for_build(ex, build)
+ end
+
+ def track_exception_for_build(ex, build)
+ Gitlab::Sentry.track_acceptable_exception(ex, extra: {
+ build_id: build.id,
+ build_name: build.name,
+ build_stage: build.stage,
+ pipeline_id: build.pipeline_id,
+ project_id: build.project_id
+ })
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def builds_for_shared_runner
new_builds.
@@ -108,7 +149,7 @@ module Ci
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id")
- .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
+ .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_builds.id ASC')
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
index 67fb3ac8355..c9f7917938f 100644
--- a/app/services/clusters/applications/base_service.rb
+++ b/app/services/clusters/applications/base_service.rb
@@ -19,10 +19,18 @@ module Clusters
application.hostname = params[:hostname]
end
+ if application.has_attribute?(:kibana_hostname)
+ application.kibana_hostname = params[:kibana_hostname]
+ end
+
if application.has_attribute?(:email)
application.email = params[:email]
end
+ if application.has_attribute?(:stack)
+ application.stack = params[:stack]
+ end
+
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
end
@@ -60,19 +68,13 @@ module Clusters
end
def invalid_application?
- unknown_application? || (!cluster.project_type? && project_only_application?)
+ unknown_application? || (application_name == Applications::ElasticStack.application_name && !Feature.enabled?(:enable_cluster_application_elastic_stack)) || (application_name == Applications::Crossplane.application_name && !Feature.enabled?(:enable_cluster_application_crossplane))
end
def unknown_application?
Clusters::Cluster::APPLICATIONS.keys.exclude?(application_name)
end
- # These applications will need extra configuration to enable them to work
- # with groups of projects
- def project_only_application?
- Clusters::Cluster::PROJECT_ONLY_APPLICATIONS.include?(application_name)
- end
-
def application_name
params[:application]
end
diff --git a/app/services/clusters/aws/fetch_credentials_service.rb b/app/services/clusters/aws/fetch_credentials_service.rb
new file mode 100644
index 00000000000..2724d4b657b
--- /dev/null
+++ b/app/services/clusters/aws/fetch_credentials_service.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Aws
+ class FetchCredentialsService
+ attr_reader :provision_role
+
+ MissingRoleError = Class.new(StandardError)
+
+ def initialize(provision_role, region:, provider: nil)
+ @provision_role = provision_role
+ @region = region
+ @provider = provider
+ end
+
+ def execute
+ raise MissingRoleError.new('AWS provisioning role not configured') unless provision_role.present?
+
+ ::Aws::AssumeRoleCredentials.new(
+ client: client,
+ role_arn: provision_role.role_arn,
+ role_session_name: session_name,
+ external_id: provision_role.role_external_id
+ ).credentials
+ end
+
+ private
+
+ attr_reader :provider, :region
+
+ def client
+ ::Aws::STS::Client.new(credentials: gitlab_credentials, region: region)
+ end
+
+ def gitlab_credentials
+ ::Aws::Credentials.new(access_key_id, secret_access_key)
+ end
+
+ def access_key_id
+ Gitlab::CurrentSettings.eks_access_key_id
+ end
+
+ def secret_access_key
+ Gitlab::CurrentSettings.eks_secret_access_key
+ end
+
+ def session_name
+ if provider.present?
+ "gitlab-eks-cluster-#{provider.cluster_id}-user-#{provision_role.user_id}"
+ else
+ "gitlab-eks-autofill-user-#{provision_role.user_id}"
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/aws/finalize_creation_service.rb b/app/services/clusters/aws/finalize_creation_service.rb
new file mode 100644
index 00000000000..54f07e1d44c
--- /dev/null
+++ b/app/services/clusters/aws/finalize_creation_service.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Aws
+ class FinalizeCreationService
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :provider
+
+ delegate :cluster, to: :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider
+ create_gitlab_service_account!
+ configure_platform_kubernetes
+ configure_node_authentication!
+
+ cluster.save!
+ rescue ::Aws::CloudFormation::Errors::ServiceError => e
+ log_service_error(e.class.name, provider.id, e.message)
+ provider.make_errored!(s_('ClusterIntegration|Failed to fetch CloudFormation stack: %{message}') % { message: e.message })
+ rescue Kubeclient::HttpError => e
+ log_service_error(e.class.name, provider.id, e.message)
+ provider.make_errored!(s_('ClusterIntegration|Failed to run Kubeclient: %{message}') % { message: e.message })
+ rescue ActiveRecord::RecordInvalid => e
+ log_service_error(e.class.name, provider.id, e.message)
+ provider.make_errored!(s_('ClusterIntegration|Failed to configure EKS provider: %{message}') % { message: e.message })
+ end
+
+ private
+
+ def create_gitlab_service_account!
+ Clusters::Kubernetes::CreateOrUpdateServiceAccountService.gitlab_creator(
+ kube_client,
+ rbac: true
+ ).execute
+ end
+
+ def configure_provider
+ provider.status_event = :make_created
+ end
+
+ def configure_platform_kubernetes
+ cluster.build_platform_kubernetes(
+ api_url: cluster_endpoint,
+ ca_cert: cluster_certificate,
+ token: request_kubernetes_token)
+ end
+
+ def request_kubernetes_token
+ Clusters::Kubernetes::FetchKubernetesTokenService.new(
+ kube_client,
+ Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE
+ ).execute
+ end
+
+ def kube_client
+ @kube_client ||= build_kube_client!(
+ cluster_endpoint,
+ cluster_certificate
+ )
+ end
+
+ def build_kube_client!(api_url, ca_pem)
+ raise "Incomplete settings" unless api_url
+
+ Gitlab::Kubernetes::KubeClient.new(
+ api_url,
+ auth_options: kubeclient_auth_options,
+ ssl_options: kubeclient_ssl_options(ca_pem),
+ http_proxy_uri: ENV['http_proxy']
+ )
+ end
+
+ def kubeclient_auth_options
+ { bearer_token: Kubeclient::AmazonEksCredentials.token(provider.credentials, cluster.name) }
+ end
+
+ def kubeclient_ssl_options(ca_pem)
+ opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+ if ca_pem.present?
+ opts[:cert_store] = OpenSSL::X509::Store.new
+ opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+ end
+
+ opts
+ end
+
+ def cluster_stack
+ @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first
+ end
+
+ def stack_output_value(key)
+ cluster_stack.outputs.detect { |output| output.output_key == key }.output_value
+ end
+
+ def node_instance_role_arn
+ stack_output_value('NodeInstanceRole')
+ end
+
+ def cluster_endpoint
+ strong_memoize(:cluster_endpoint) do
+ stack_output_value('ClusterEndpoint')
+ end
+ end
+
+ def cluster_certificate
+ strong_memoize(:cluster_certificate) do
+ Base64.decode64(stack_output_value('ClusterCertificate'))
+ end
+ end
+
+ def configure_node_authentication!
+ kube_client.create_config_map(node_authentication_config)
+ end
+
+ def node_authentication_config
+ Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth.new(node_instance_role_arn).generate
+ end
+
+ def logger
+ @logger ||= Gitlab::Kubernetes::Logger.build
+ end
+
+ def log_service_error(exception, provider_id, message)
+ logger.error(
+ exception: exception.class.name,
+ service: self.class.name,
+ provider_id: provider_id,
+ message: message
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/aws/provision_service.rb b/app/services/clusters/aws/provision_service.rb
new file mode 100644
index 00000000000..35fe8433b4d
--- /dev/null
+++ b/app/services/clusters/aws/provision_service.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Aws
+ class ProvisionService
+ attr_reader :provider
+
+ def execute(provider)
+ @provider = provider
+
+ configure_provider_credentials
+ provision_cluster
+
+ if provider.make_creating
+ WaitForClusterCreationWorker.perform_in(
+ Clusters::Aws::VerifyProvisionStatusService::INITIAL_INTERVAL,
+ provider.cluster_id
+ )
+ else
+ provider.make_errored!("Failed to update provider record; #{provider.errors.full_messages}")
+ end
+ rescue Clusters::Aws::FetchCredentialsService::MissingRoleError
+ provider.make_errored!('Amazon role is not configured')
+ rescue ::Aws::Errors::MissingCredentialsError
+ provider.make_errored!('Amazon credentials are not configured')
+ rescue ::Aws::STS::Errors::ServiceError => e
+ provider.make_errored!("Amazon authentication failed; #{e.message}")
+ rescue ::Aws::CloudFormation::Errors::ServiceError => e
+ provider.make_errored!("Amazon CloudFormation request failed; #{e.message}")
+ end
+
+ private
+
+ def provision_role
+ provider.created_by_user&.aws_role
+ end
+
+ def credentials
+ @credentials ||= Clusters::Aws::FetchCredentialsService.new(
+ provision_role,
+ provider: provider,
+ region: provider.region
+ ).execute
+ end
+
+ def configure_provider_credentials
+ provider.update!(
+ access_key_id: credentials.access_key_id,
+ secret_access_key: credentials.secret_access_key,
+ session_token: credentials.session_token
+ )
+ end
+
+ def provision_cluster
+ provider.api_client.create_stack(
+ stack_name: provider.cluster.name,
+ template_body: stack_template,
+ parameters: parameters,
+ capabilities: ["CAPABILITY_IAM"]
+ )
+ end
+
+ def parameters
+ [
+ parameter('ClusterName', provider.cluster.name),
+ parameter('ClusterRole', provider.role_arn),
+ parameter('ClusterControlPlaneSecurityGroup', provider.security_group_id),
+ parameter('VpcId', provider.vpc_id),
+ parameter('Subnets', provider.subnet_ids.join(',')),
+ parameter('NodeAutoScalingGroupDesiredCapacity', provider.num_nodes.to_s),
+ parameter('NodeInstanceType', provider.instance_type),
+ parameter('KeyName', provider.key_name)
+ ]
+ end
+
+ def parameter(key, value)
+ { parameter_key: key, parameter_value: value }
+ end
+
+ def stack_template
+ File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/aws/proxy_service.rb b/app/services/clusters/aws/proxy_service.rb
new file mode 100644
index 00000000000..df8fc480005
--- /dev/null
+++ b/app/services/clusters/aws/proxy_service.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Aws
+ class ProxyService
+ DEFAULT_REGION = 'us-east-1'
+
+ BadRequest = Class.new(StandardError)
+ Response = Struct.new(:status, :body)
+
+ def initialize(role, params:)
+ @role = role
+ @params = params
+ end
+
+ def execute
+ api_response = request_from_api!
+
+ Response.new(:ok, api_response.to_hash)
+ rescue *service_errors
+ Response.new(:bad_request, {})
+ end
+
+ private
+
+ attr_reader :role, :params
+
+ def request_from_api!
+ case requested_resource
+ when 'key_pairs'
+ ec2_client.describe_key_pairs
+
+ when 'instance_types'
+ instance_types
+
+ when 'roles'
+ iam_client.list_roles
+
+ when 'regions'
+ ec2_client.describe_regions
+
+ when 'security_groups'
+ raise BadRequest unless vpc_id.present?
+
+ ec2_client.describe_security_groups(vpc_filter)
+
+ when 'subnets'
+ raise BadRequest unless vpc_id.present?
+
+ ec2_client.describe_subnets(vpc_filter)
+
+ when 'vpcs'
+ ec2_client.describe_vpcs
+
+ else
+ raise BadRequest
+ end
+ end
+
+ def requested_resource
+ params[:resource]
+ end
+
+ def vpc_id
+ params[:vpc_id]
+ end
+
+ def region
+ params[:region] || DEFAULT_REGION
+ end
+
+ def vpc_filter
+ {
+ filters: [{
+ name: "vpc-id",
+ values: [vpc_id]
+ }]
+ }
+ end
+
+ ##
+ # Unfortunately the EC2 API doesn't provide a list of
+ # possible instance types. There is a workaround, using
+ # the Pricing API, but instead of requiring the
+ # user to grant extra permissions for this we use the
+ # values that validate the CloudFormation template.
+ def instance_types
+ {
+ instance_types: cluster_stack_instance_types.map { |type| Hash(instance_type_name: type) }
+ }
+ end
+
+ def cluster_stack_instance_types
+ YAML.safe_load(stack_template).dig('Parameters', 'NodeInstanceType', 'AllowedValues')
+ end
+
+ def stack_template
+ File.read(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
+ end
+
+ def ec2_client
+ ::Aws::EC2::Client.new(client_options)
+ end
+
+ def iam_client
+ ::Aws::IAM::Client.new(client_options)
+ end
+
+ def credentials
+ Clusters::Aws::FetchCredentialsService.new(role, region: region).execute
+ end
+
+ def client_options
+ {
+ credentials: credentials,
+ region: region,
+ http_open_timeout: 5,
+ http_read_timeout: 10
+ }
+ end
+
+ def service_errors
+ [
+ BadRequest,
+ Clusters::Aws::FetchCredentialsService::MissingRoleError,
+ ::Aws::Errors::MissingCredentialsError,
+ ::Aws::EC2::Errors::ServiceError,
+ ::Aws::IAM::Errors::ServiceError,
+ ::Aws::STS::Errors::ServiceError
+ ]
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/aws/verify_provision_status_service.rb b/app/services/clusters/aws/verify_provision_status_service.rb
new file mode 100644
index 00000000000..99532662bc4
--- /dev/null
+++ b/app/services/clusters/aws/verify_provision_status_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Aws
+ class VerifyProvisionStatusService
+ attr_reader :provider
+
+ INITIAL_INTERVAL = 5.minutes
+ POLL_INTERVAL = 1.minute
+ TIMEOUT = 30.minutes
+
+ def execute(provider)
+ @provider = provider
+
+ case cluster_stack.stack_status
+ when 'CREATE_IN_PROGRESS'
+ continue_creation
+ when 'CREATE_COMPLETE'
+ finalize_creation
+ else
+ provider.make_errored!("Unexpected status; #{cluster_stack.stack_status}")
+ end
+ rescue ::Aws::CloudFormation::Errors::ServiceError => e
+ provider.make_errored!("Amazon CloudFormation request failed; #{e.message}")
+ end
+
+ private
+
+ def cluster_stack
+ @cluster_stack ||= provider.api_client.describe_stacks(stack_name: provider.cluster.name).stacks.first
+ end
+
+ def continue_creation
+ if timeout_threshold.future?
+ WaitForClusterCreationWorker.perform_in(POLL_INTERVAL, provider.cluster_id)
+ else
+ provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT })
+ end
+ end
+
+ def timeout_threshold
+ cluster_stack.creation_time + TIMEOUT
+ end
+
+ def finalize_creation
+ Clusters::Aws::FinalizeCreationService.new.execute(provider)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/destroy_service.rb b/app/services/clusters/destroy_service.rb
new file mode 100644
index 00000000000..a8de04683fa
--- /dev/null
+++ b/app/services/clusters/destroy_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Clusters
+ class DestroyService
+ attr_reader :current_user, :params
+
+ def initialize(user = nil, params = {})
+ @current_user, @params = user, params.dup
+ @response = {}
+ end
+
+ def execute(cluster)
+ cleanup? ? start_cleanup!(cluster) : destroy_cluster!(cluster)
+
+ @response
+ end
+
+ private
+
+ def cleanup?
+ Gitlab::Utils.to_boolean(params[:cleanup])
+ end
+
+ def start_cleanup!(cluster)
+ cluster.start_cleanup!
+ @response[:message] = _('Kubernetes cluster integration and resources are being removed.')
+ end
+
+ def destroy_cluster!(cluster)
+ cluster.destroy!
+ @response[:message] = _('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+end
diff --git a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
index 8b8ad924b64..d798dcdcfd3 100644
--- a/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
+++ b/app/services/clusters/kubernetes/create_or_update_service_account_service.rb
@@ -49,6 +49,8 @@ module Clusters
create_or_update_knative_serving_role
create_or_update_knative_serving_role_binding
+ create_or_update_crossplane_database_role
+ create_or_update_crossplane_database_role_binding
end
private
@@ -78,6 +80,14 @@ module Clusters
kubeclient.update_role_binding(knative_serving_role_binding_resource)
end
+ def create_or_update_crossplane_database_role
+ kubeclient.update_role(crossplane_database_role_resource)
+ end
+
+ def create_or_update_crossplane_database_role_binding
+ kubeclient.update_role_binding(crossplane_database_role_binding_resource)
+ end
+
def service_account_resource
Gitlab::Kubernetes::ServiceAccount.new(
service_account_name,
@@ -134,6 +144,28 @@ module Clusters
service_account_name: service_account_name
).generate
end
+
+ def crossplane_database_role_resource
+ Gitlab::Kubernetes::Role.new(
+ name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME,
+ namespace: service_account_namespace,
+ rules: [{
+ apiGroups: %w(database.crossplane.io),
+ resources: %w(postgresqlinstances),
+ verbs: %w(get list create watch)
+ }]
+ ).generate
+ end
+
+ def crossplane_database_role_binding_resource
+ Gitlab::Kubernetes::RoleBinding.new(
+ name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME,
+ role_name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME,
+ role_kind: :Role,
+ namespace: service_account_namespace,
+ service_account_name: service_account_name
+ ).generate
+ end
end
end
end
diff --git a/app/services/clusters/kubernetes/kubernetes.rb b/app/services/clusters/kubernetes/kubernetes.rb
index 7d5d0c2c1d6..d29519999b2 100644
--- a/app/services/clusters/kubernetes/kubernetes.rb
+++ b/app/services/clusters/kubernetes/kubernetes.rb
@@ -10,5 +10,7 @@ module Clusters
PROJECT_CLUSTER_ROLE_NAME = 'edit'
GITLAB_KNATIVE_SERVING_ROLE_NAME = 'gitlab-knative-serving-role'
GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME = 'gitlab-knative-serving-rolebinding'
+ GITLAB_CROSSPLANE_DATABASE_ROLE_NAME = 'gitlab-crossplane-database-role'
+ GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME = 'gitlab-crossplane-database-rolebinding'
end
end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
index 25d26e761b1..8cb77040b14 100644
--- a/app/services/clusters/update_service.rb
+++ b/app/services/clusters/update_service.rb
@@ -9,7 +9,55 @@ module Clusters
end
def execute(cluster)
- cluster.update(params)
+ if validate_params(cluster)
+ cluster.update(params)
+ else
+ false
+ end
+ end
+
+ private
+
+ def can_admin_pipeline_for_project?(project)
+ Ability.allowed?(current_user, :admin_pipeline, project)
+ end
+
+ def validate_params(cluster)
+ if params[:management_project_id].present?
+ management_project = management_project_scope(cluster).find_by_id(params[:management_project_id])
+
+ unless management_project
+ cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
+
+ return false
+ end
+
+ unless can_admin_pipeline_for_project?(management_project)
+ # Use same message as not found to prevent enumeration
+ cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
+
+ return false
+ end
+ end
+
+ true
+ end
+
+ def management_project_scope(cluster)
+ return ::Project.all if cluster.instance_type?
+
+ group =
+ if cluster.group_type?
+ cluster.first_group
+ elsif cluster.project_type?
+ cluster.first_project&.namespace
+ end
+
+ # Prevent users from selecting nested projects until
+ # https://gitlab.com/gitlab-org/gitlab/issues/34650 is resolved
+ include_subgroups = cluster.group_type?
+
+ ::GroupProjectsFinder.new(group: group, current_user: current_user, options: { only_owned: true, include_subgroups: include_subgroups }).execute
end
end
end
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
index 97fbb70f350..dbbe89ef260 100644
--- a/app/services/cohorts_service.rb
+++ b/app/services/cohorts_service.rb
@@ -88,7 +88,7 @@ class CohortsService
User
.where('created_at > ?', MONTHS_INCLUDED.months.ago.end_of_month)
.group(created_at_month, last_activity_on_month)
- .reorder("#{created_at_month} ASC", "#{last_activity_on_month} ASC")
+ .reorder(Arel.sql("#{created_at_month} ASC, #{last_activity_on_month} ASC"))
.count
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index fbf71f02837..661e654406e 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -23,14 +23,15 @@ module Commits
message,
start_project: @start_project,
start_branch_name: @start_branch)
- rescue Gitlab::Git::Repository::CreateTreeError
+ rescue Gitlab::Git::Repository::CreateTreeError => ex
act = action.to_s.dasherize
type = @commit.change_type_title(current_user)
error_msg = "Sorry, we cannot #{act} this #{type} automatically. " \
"This #{type} may already have been #{act}ed, or a more recent " \
"commit may have updated some of its content."
- raise ChangeError, error_msg
+
+ raise ChangeError.new(error_msg, ex.error_code)
end
end
end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index b5401a8ea37..b42494563b2 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -3,7 +3,15 @@
module Commits
class CreateService < ::BaseService
ValidationError = Class.new(StandardError)
- ChangeError = Class.new(StandardError)
+ class ChangeError < StandardError
+ attr_reader :error_code
+
+ def initialize(message, error_code = nil)
+ super(message)
+
+ @error_code = error_code
+ end
+ end
def initialize(*args)
super
@@ -21,8 +29,9 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
+ rescue ChangeError => ex
+ error(ex.message, pass_back: { error_code: ex.error_code })
rescue ValidationError,
- ChangeError,
Gitlab::Git::Index::IndexError,
Gitlab::Git::CommitError,
Gitlab::Git::PreReceiveError,
diff --git a/app/services/concerns/git/logger.rb b/app/services/concerns/git/logger.rb
new file mode 100644
index 00000000000..7c036212e66
--- /dev/null
+++ b/app/services/concerns/git/logger.rb
@@ -0,0 +1,10 @@
+# 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/create_branch_service.rb b/app/services/create_branch_service.rb
index 110e589e30d..d58cb0f9e2b 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
if new_branch
success(new_branch)
else
- error('Invalid reference name')
+ error("Invalid reference name: #{branch_name}")
end
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
index 2572802e6a1..e0a4e5419cc 100644
--- a/app/services/deployments/after_create_service.rb
+++ b/app/services/deployments/after_create_service.rb
@@ -33,12 +33,21 @@ module Deployments
if environment.save && !environment.stopped?
deployment.update_merge_request_metrics!
+ link_merge_requests(deployment)
end
end
end
private
+ def link_merge_requests(deployment)
+ unless Feature.enabled?(:deployment_merge_requests, deployment.project)
+ return
+ end
+
+ LinkMergeRequestsService.new(deployment).execute
+ end
+
def environment_options
options&.dig(:environment) || {}
end
diff --git a/app/services/deployments/link_merge_requests_service.rb b/app/services/deployments/link_merge_requests_service.rb
new file mode 100644
index 00000000000..71186659290
--- /dev/null
+++ b/app/services/deployments/link_merge_requests_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Deployments
+ # Service class for linking merge requests to deployments.
+ class LinkMergeRequestsService
+ attr_reader :deployment
+
+ # The number of commits per query for which to find merge requests.
+ COMMITS_PER_QUERY = 5_000
+
+ def initialize(deployment)
+ @deployment = deployment
+ end
+
+ def execute
+ return unless deployment.success?
+
+ if (prev = deployment.previous_environment_deployment)
+ link_merge_requests_for_range(prev.sha, deployment.sha)
+ else
+ # When no previous deployment is found we fall back to linking all merge
+ # requests merged into the deployed branch. This will not always be
+ # accurate, but it's better than having no data.
+ #
+ # We can't use the first commit in the repository as a base to compare
+ # to, as this will not scale to large repositories. For example, GitLab
+ # itself has over 150 000 commits.
+ link_all_merged_merge_requests
+ end
+ end
+
+ def link_merge_requests_for_range(from, to)
+ commits = project
+ .repository
+ .commits_between(from, to)
+ .map(&:id)
+
+ # For some projects the list of commits to deploy may be very large. To
+ # ensure we do not end up running SQL queries with thousands of WHERE IN
+ # values, we run one query per a certain number of commits.
+ #
+ # In most cases this translates to only a single query. For very large
+ # deployment we may end up running a handful of queries to get and insert
+ # the data.
+ commits.each_slice(COMMITS_PER_QUERY) do |slice|
+ merge_requests =
+ project.merge_requests.merged.by_merge_commit_sha(slice)
+
+ deployment.link_merge_requests(merge_requests)
+ end
+ end
+
+ def link_all_merged_merge_requests
+ merge_requests =
+ project.merge_requests.merged.by_target_branch(deployment.ref)
+
+ deployment.link_merge_requests(merge_requests)
+ end
+
+ private
+
+ def project
+ deployment.project
+ end
+ end
+end
diff --git a/app/services/deployments/update_service.rb b/app/services/deployments/update_service.rb
index 7c8215d28f2..97b233f16a7 100644
--- a/app/services/deployments/update_service.rb
+++ b/app/services/deployments/update_service.rb
@@ -10,7 +10,22 @@ module Deployments
end
def execute
- deployment.update(status: params[:status])
+ # A regular update() does not trigger the state machine transitions, which
+ # we need to ensure merge requests are linked when changing the status to
+ # success. To work around this we use this case statment, using the right
+ # event methods to trigger the transition hooks.
+ case params[:status]
+ when 'running'
+ deployment.run
+ when 'success'
+ deployment.succeed
+ when 'failed'
+ deployment.drop
+ when 'canceled'
+ deployment.cancel
+ else
+ false
+ end
end
end
end
diff --git a/app/services/error_tracking/base_service.rb b/app/services/error_tracking/base_service.rb
new file mode 100644
index 00000000000..430d9952332
--- /dev/null
+++ b/app/services/error_tracking/base_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class BaseService < ::BaseService
+ def execute
+ unauthorized = check_permissions
+ return unauthorized if unauthorized
+
+ begin
+ response = fetch
+ rescue Sentry::Client::Error => e
+ return error(e.message, :bad_request)
+ rescue Sentry::Client::MissingKeysError => e
+ return error(e.message, :internal_server_error)
+ end
+
+ errors = parse_errors(response)
+ return errors if errors
+
+ success(parse_response(response))
+ end
+
+ private
+
+ def fetch
+ raise NotImplementedError,
+ "#{self.class} does not implement #{__method__}"
+ end
+
+ def parse_response(response)
+ raise NotImplementedError,
+ "#{self.class} does not implement #{__method__}"
+ end
+
+ def check_permissions
+ return error('Error Tracking is not enabled') unless enabled?
+ return error('Access denied', :unauthorized) unless can_read?
+ end
+
+ def parse_errors(response)
+ return error('Not ready. Try again later', :no_content) unless response
+ return error(response[:error], http_status_for(response[:error_type])) if response[:error].present?
+ end
+
+ def http_status_for(error_type)
+ case error_type
+ when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
+ :internal_server_error
+ else
+ :bad_request
+ end
+ end
+
+ def project_error_tracking_setting
+ project.error_tracking_setting
+ end
+
+ def enabled?
+ project_error_tracking_setting&.enabled?
+ end
+
+ def can_read?
+ can?(current_user, :read_sentry_issue, project)
+ end
+ end
+end
diff --git a/app/services/error_tracking/issue_details_service.rb b/app/services/error_tracking/issue_details_service.rb
new file mode 100644
index 00000000000..368cd4517fc
--- /dev/null
+++ b/app/services/error_tracking/issue_details_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class IssueDetailsService < ErrorTracking::BaseService
+ private
+
+ def fetch
+ project_error_tracking_setting.issue_details(issue_id: params[:issue_id])
+ end
+
+ def parse_response(response)
+ { issue: response[:issue] }
+ end
+ end
+end
diff --git a/app/services/error_tracking/issue_latest_event_service.rb b/app/services/error_tracking/issue_latest_event_service.rb
new file mode 100644
index 00000000000..b6ad8f8028b
--- /dev/null
+++ b/app/services/error_tracking/issue_latest_event_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ErrorTracking
+ class IssueLatestEventService < ErrorTracking::BaseService
+ private
+
+ def fetch
+ project_error_tracking_setting.issue_latest_event(issue_id: params[:issue_id])
+ end
+
+ def parse_response(response)
+ { latest_event: response[:latest_event] }
+ end
+ end
+end
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index 86ab21fa865..2e8c401b8ef 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -1,46 +1,22 @@
# frozen_string_literal: true
module ErrorTracking
- class ListIssuesService < ::BaseService
+ class ListIssuesService < ErrorTracking::BaseService
DEFAULT_ISSUE_STATUS = 'unresolved'
DEFAULT_LIMIT = 20
- def execute
- return error('Error Tracking is not enabled') unless enabled?
- return error('Access denied', :unauthorized) unless can_read?
-
- result = project_error_tracking_setting
- .list_sentry_issues(issue_status: issue_status, limit: limit)
-
- # our results are not yet ready
- unless result
- return error('Not ready. Try again later', :no_content)
- end
-
- if result[:error].present?
- return error(result[:error], http_status_from_error_type(result[:error_type]))
- end
-
- success(issues: result[:issues])
- end
-
def external_url
project_error_tracking_setting&.sentry_external_url
end
private
- def http_status_from_error_type(error_type)
- case error_type
- when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
- :internal_server_error
- else
- :bad_request
- end
+ def fetch
+ project_error_tracking_setting.list_sentry_issues(issue_status: issue_status, limit: limit)
end
- def project_error_tracking_setting
- project.error_tracking_setting
+ def parse_response(response)
+ { issues: response[:issues] }
end
def issue_status
@@ -50,13 +26,5 @@ module ErrorTracking
def limit
params[:limit] || DEFAULT_LIMIT
end
-
- def enabled?
- project_error_tracking_setting&.enabled?
- end
-
- def can_read?
- can?(current_user, :read_sentry_issue, project)
- end
end
end
diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb
index 92d4ef85ecf..09a0b952e84 100644
--- a/app/services/error_tracking/list_projects_service.rb
+++ b/app/services/error_tracking/list_projects_service.rb
@@ -1,44 +1,38 @@
# frozen_string_literal: true
module ErrorTracking
- class ListProjectsService < ::BaseService
+ class ListProjectsService < ErrorTracking::BaseService
def execute
- return error('access denied') unless can_read?
-
- setting = project_error_tracking_setting
-
- unless setting.valid?
- return error(setting.errors.full_messages.join(', '), :bad_request)
+ unless project_error_tracking_setting.valid?
+ return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request)
end
- begin
- result = setting.list_sentry_projects
- rescue Sentry::Client::Error => e
- return error(e.message, :bad_request)
- rescue Sentry::Client::MissingKeysError => e
- return error(e.message, :internal_server_error)
- end
-
- success(projects: result[:projects])
+ super
end
private
- def project_error_tracking_setting
- (project.error_tracking_setting || project.build_error_tracking_setting).tap do |setting|
- setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
- api_host: params[:api_host],
- organization_slug: 'org',
- project_slug: 'proj'
- )
-
- setting.token = token(setting)
- setting.enabled = true
- end
+ def fetch
+ project_error_tracking_setting.list_sentry_projects
+ end
+
+ def parse_response(response)
+ { projects: response[:projects] }
end
- def can_read?
- can?(current_user, :read_sentry_issue, project)
+ def project_error_tracking_setting
+ @project_error_tracking_setting ||= begin
+ (super || project.build_error_tracking_setting).tap do |setting|
+ setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
+ api_host: params[:api_host],
+ organization_slug: 'org',
+ project_slug: 'proj'
+ )
+
+ setting.token = token(setting)
+ setting.enabled = true
+ end
+ end
end
def token(setting)
diff --git a/app/services/groups/group_links/create_service.rb b/app/services/groups/group_links/create_service.rb
new file mode 100644
index 00000000000..2ce53fcfe4a
--- /dev/null
+++ b/app/services/groups/group_links/create_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Groups
+ module GroupLinks
+ class CreateService < BaseService
+ def execute(shared_group)
+ unless group && shared_group &&
+ can?(current_user, :admin_group, shared_group) &&
+ can?(current_user, :read_group, group)
+ return error('Not Found', 404)
+ end
+
+ link = GroupGroupLink.new(
+ shared_group: shared_group,
+ shared_with_group: group,
+ group_access: params[:shared_group_access],
+ expires_at: params[:expires_at]
+ )
+
+ if link.save
+ group.refresh_members_authorized_projects
+ success(link: link)
+ else
+ error(link.errors.full_messages.to_sentence, 409)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/groups/group_links/destroy_service.rb b/app/services/groups/group_links/destroy_service.rb
new file mode 100644
index 00000000000..29aa8de4e68
--- /dev/null
+++ b/app/services/groups/group_links/destroy_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Groups
+ module GroupLinks
+ class DestroyService < BaseService
+ def execute(one_or_more_links)
+ links = Array(one_or_more_links)
+
+ GroupGroupLink.transaction do
+ GroupGroupLink.delete(links)
+
+ groups_to_refresh = links.map(&:shared_with_group)
+ groups_to_refresh.uniq.each do |group|
+ group.refresh_members_authorized_projects
+ end
+
+ Gitlab::AppLogger.info("GroupGroupLinks with ids: #{links.map(&:id)} have been deleted.")
+ rescue => ex
+ Gitlab::AppLogger.error(ex)
+
+ raise
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
new file mode 100644
index 00000000000..26886fc67dc
--- /dev/null
+++ b/app/services/groups/import_export/export_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Groups
+ module ImportExport
+ class ExportService
+ def initialize(group:, user:, params: {})
+ @group = group
+ @current_user = user
+ @params = params
+ @shared = @params[:shared] || Gitlab::ImportExport::Shared.new(@group)
+ end
+
+ def execute
+ save!
+ end
+
+ private
+
+ attr_accessor :shared
+
+ def save!
+ if savers.all?(&:save)
+ notify_success
+ else
+ cleanup_and_notify_error!
+ end
+ end
+
+ def savers
+ [tree_exporter, file_saver]
+ end
+
+ def tree_exporter
+ Gitlab::ImportExport::GroupTreeSaver.new(group: @group, current_user: @current_user, shared: @shared, params: @params)
+ end
+
+ def file_saver
+ Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
+ end
+
+ def cleanup_and_notify_error
+ FileUtils.rm_rf(shared.export_path)
+
+ notify_error
+ end
+
+ def cleanup_and_notify_error!
+ cleanup_and_notify_error
+
+ raise Gitlab::ImportExport::Error.new(shared.errors.to_sentence)
+ end
+
+ def notify_success
+ @shared.logger.info(
+ group_id: @group.id,
+ group_name: @group.name,
+ message: 'Group Import/Export: Export succeeded'
+ )
+ end
+
+ def notify_error
+ @shared.logger.error(
+ group_id: @group.id,
+ group_name: @group.name,
+ error: @shared.errors.join(', '),
+ message: 'Group Import/Export: Export failed'
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 6902b7bd529..24813f6ddf9 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -13,7 +13,7 @@ module Groups
TransferError = Class.new(StandardError)
- attr_reader :error
+ attr_reader :error, :new_parent_group
def initialize(group, user, params = {})
super
@@ -75,7 +75,7 @@ module Groups
# rubocop: enable CodeReuse/ActiveRecord
def group_projects_contain_registry_images?
- @group.has_container_repositories?
+ @group.has_container_repository_including_subgroups?
end
def update_group_attributes
@@ -115,3 +115,5 @@ module Groups
end
end
end
+
+Groups::TransferService.prepend_if_ee('EE::Groups::TransferService')
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index be7502a193e..8635b82461b 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -43,8 +43,9 @@ module Groups
def renaming_group_with_container_registry_images?
new_path = params[:path]
- new_path && new_path != group.path &&
- group.has_container_repositories?
+ new_path &&
+ new_path != group.path &&
+ group.has_container_repository_including_subgroups?
end
def container_images_error
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 3e17d75c02c..8a79c5f889d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -355,7 +355,7 @@ class IssuableBaseService < BaseService
associations =
{
labels: issuable.labels.to_a,
- mentioned_users: issuable.mentioned_users.to_a,
+ mentioned_users: issuable.mentioned_users(current_user).to_a,
assignees: issuable.assignees.to_a
}
associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 528b1ea61b3..b98a4d2567f 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -61,8 +61,6 @@ module Issues
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
-
- ZoomNotesService.new(issue, project, current_user, old_description: old_associations[:description]).execute
end
def handle_task_changes(issuable)
diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb
index 561c86475e5..023d7080e88 100644
--- a/app/services/issues/zoom_link_service.rb
+++ b/app/services/issues/zoom_link_service.rb
@@ -6,32 +6,37 @@ module Issues
super(issue.project, user)
@issue = issue
+ @added_meeting = ZoomMeeting.canonical_meeting(@issue)
end
def add_link(link)
if can_add_link? && (link = parse_link(link))
- track_meeting_added_event
- success(_('Zoom meeting added'), append_to_description(link))
+ begin
+ add_zoom_meeting(link)
+ success(_('Zoom meeting added'))
+ rescue ActiveRecord::RecordNotUnique
+ error(_('Failed to add a Zoom meeting'))
+ end
else
error(_('Failed to add a Zoom meeting'))
end
end
- def can_add_link?
- can? && !link_in_issue_description?
- end
-
def remove_link
if can_remove_link?
- track_meeting_removed_event
- success(_('Zoom meeting removed'), remove_from_description)
+ remove_zoom_meeting
+ success(_('Zoom meeting removed'))
else
error(_('Failed to remove a Zoom meeting'))
end
end
+ def can_add_link?
+ can_update_issue? && !@added_meeting
+ end
+
def can_remove_link?
- can? && link_in_issue_description?
+ can_update_issue? && !!@added_meeting
end
def parse_link(link)
@@ -42,10 +47,6 @@ module Issues
attr_reader :issue
- def issue_description
- issue.description || ''
- end
-
def track_meeting_added_event
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
end
@@ -54,39 +55,33 @@ module Issues
::Gitlab::Tracking.event('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
end
- def success(message, description)
- ServiceResponse
- .success(message: message, payload: { description: description })
- end
-
- def error(message)
- ServiceResponse.error(message: message)
+ def add_zoom_meeting(link)
+ ZoomMeeting.create(
+ issue: @issue,
+ project: @issue.project,
+ issue_status: :added,
+ url: link
+ )
+ track_meeting_added_event
+ SystemNoteService.zoom_link_added(@issue, @project, current_user)
end
- def append_to_description(link)
- "#{issue_description}\n\n#{link}"
+ def remove_zoom_meeting
+ @added_meeting.update(issue_status: :removed)
+ track_meeting_removed_event
+ SystemNoteService.zoom_link_removed(@issue, @project, current_user)
end
- def remove_from_description
- link = parse_link(issue_description)
- return issue_description unless link
-
- issue_description.delete_suffix(link).rstrip
+ def success(message)
+ ServiceResponse.success(message: message)
end
- def link_in_issue_description?
- link = extract_link_from_issue_description
- return unless link
-
- Gitlab::ZoomLinkExtractor.new(link).match?
- end
-
- def extract_link_from_issue_description
- issue_description[/(\S+)\z/, 1]
+ def error(message)
+ ServiceResponse.error(message: message)
end
- def can?
- current_user.can?(:update_issue, project)
+ def can_update_issue?
+ can?(current_user, :update_issue, project)
end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index aacc3d6831e..00bf69739ad 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -29,6 +29,19 @@ module MergeRequests
.execute_for_merge_request(merge_request)
end
+ def source_project
+ @source_project ||= merge_request.source_project
+ end
+
+ def target_project
+ @target_project ||= merge_request.target_project
+ end
+
+ # Don't try to print expensive instance variables.
+ def inspect
+ "#<#{self.class} #{merge_request.to_reference(full: true)}>"
+ end
+
private
def create(merge_request)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bf4da01723b..456cc589477 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -16,6 +16,14 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
+ # Source project sets the default source branch removal setting
+ merge_request.merge_params['force_remove_source_branch'] =
+ if params.key?(:force_remove_source_branch)
+ params.delete(:force_remove_source_branch)
+ else
+ merge_request.source_project.remove_source_branch_after_merge?
+ end
+
self.params = assign_allowed_merge_params(merge_request, params)
filter_params(merge_request)
diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
index 479e0fe6699..6f1fa607ef9 100644
--- a/app/services/merge_requests/ff_merge_service.rb
+++ b/app/services/merge_requests/ff_merge_service.rb
@@ -11,10 +11,16 @@ module MergeRequests
private
def commit
- repository.ff_merge(current_user,
- source,
- merge_request.target_branch,
- merge_request: merge_request)
+ ff_merge = repository.ff_merge(current_user,
+ source,
+ merge_request.target_branch,
+ merge_request: merge_request)
+
+ if merge_request.squash
+ merge_request.update_column(:squash_commit_sha, merge_request.in_progress_merge_commit_sha)
+ end
+
+ ff_merge
rescue Gitlab::Git::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
index 3f7f8bcdcbf..27b5e31faab 100644
--- a/app/services/merge_requests/merge_base_service.rb
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -19,10 +19,12 @@ module MergeRequests
end
def source
- if merge_request.squash
- squash_sha!
- else
- merge_request.diff_head_sha
+ strong_memoize(:source) do
+ if merge_request.squash
+ squash_sha!
+ else
+ merge_request.diff_head_sha
+ end
end
end
@@ -58,16 +60,14 @@ module MergeRequests
end
def squash_sha!
- strong_memoize(:squash_sha) do
- params[:merge_request] = merge_request
- squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute
-
- case squash_result[:status]
- when :success
- squash_result[:squash_sha]
- when :error
- raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
- end
+ params[:merge_request] = merge_request
+ squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute
+
+ case squash_result[:status]
+ when :success
+ squash_result[:squash_sha]
+ when :error
+ raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 6309052244d..a45b4f1142e 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -37,6 +37,7 @@ module MergeRequests
def validate!
authorization_check!
error_check!
+ updated_check!
end
def authorization_check!
@@ -60,6 +61,15 @@ module MergeRequests
raise_error(error) if error
end
+ def updated_check!
+ return unless Feature.enabled?(:validate_merge_sha, merge_request.target_project, default_enabled: false)
+
+ unless source_matches?
+ raise_error('Branch has been updated since the merge was requested. '\
+ 'Please review the changes.')
+ end
+ end
+
def commit
log_info("Git merge started on JID #{merge_jid}")
commit_id = try_merge
@@ -125,5 +135,11 @@ module MergeRequests
def merge_request_info
merge_request.to_reference(full: true)
end
+
+ def source_matches?
+ # params-keys are symbols coming from the controller, but when they get
+ # loaded from the database they're strings
+ params.with_indifferent_access[:sha] == merge_request.diff_head_sha
+ end
end
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 4d36dd4feae..7e9442c0c7c 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -1,9 +1,13 @@
# frozen_string_literal: true
module MergeRequests
- class RebaseService < MergeRequests::WorkingCopyBaseService
+ class RebaseService < MergeRequests::BaseService
+ include Git::Logger
+
REBASE_ERROR = 'Rebase failed. Please rebase locally'
+ attr_reader :merge_request
+
def execute(merge_request)
@merge_request = merge_request
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b32499629ff..bd3fcf85a62 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -152,7 +152,8 @@ module MergeRequests
def abort_ff_merge_requests_with_when_pipeline_succeeds
return unless @project.ff_merge_must_be_possible?
- requests_with_auto_merge_enabled_to(@push.branch_name).each do |merge_request|
+ merge_requests_with_auto_merge_enabled_to(@push.branch_name).each do |merge_request|
+ next unless merge_request.auto_merge_strategy == AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
next unless merge_request.should_be_rebased?
abort_auto_merge_with_todo(merge_request, 'target branch was updated')
@@ -167,11 +168,11 @@ module MergeRequests
todo_service.merge_request_became_unmergeable(merge_request)
end
- def requests_with_auto_merge_enabled_to(target_branch)
+ def merge_requests_with_auto_merge_enabled_to(target_branch)
@project
.merge_requests
.by_target_branch(target_branch)
- .with_open_merge_when_pipeline_succeeds
+ .with_auto_merge_enabled
end
def mark_pending_todos_done
diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb
index 88ca3b4f5a8..d25997c925e 100644
--- a/app/services/merge_requests/squash_service.rb
+++ b/app/services/merge_requests/squash_service.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module MergeRequests
- class SquashService < MergeRequests::WorkingCopyBaseService
+ class SquashService < MergeRequests::BaseService
+ include Git::Logger
+
def execute
# If performing a squash would result in no change, then
# immediately return a success message without performing a squash
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 7c9abb12b6e..8a6a7119508 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -88,9 +88,9 @@ module MergeRequests
merge_request.update(merge_error: nil)
if merge_request.head_pipeline && merge_request.head_pipeline.active?
- AutoMergeService.new(project, current_user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ AutoMergeService.new(project, current_user, { sha: last_diff_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
else
- merge_request.merge_async(current_user.id, {})
+ merge_request.merge_async(current_user.id, { sha: last_diff_sha })
end
end
diff --git a/app/services/merge_requests/working_copy_base_service.rb b/app/services/merge_requests/working_copy_base_service.rb
deleted file mode 100644
index 2d2be1f4c25..00000000000
--- a/app/services/merge_requests/working_copy_base_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class WorkingCopyBaseService < MergeRequests::BaseService
- attr_reader :merge_request
-
- def source_project
- @source_project ||= merge_request.source_project
- end
-
- def target_project
- @target_project ||= merge_request.target_project
- end
-
- 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
-
- # Don't try to print expensive instance variables.
- def inspect
- "#<#{self.class} #{merge_request.to_reference(full: true)}>"
- end
- end
-end
diff --git a/app/services/metrics/dashboard/custom_metric_embed_service.rb b/app/services/metrics/dashboard/custom_metric_embed_service.rb
index 50f070989fc..79a556b1695 100644
--- a/app/services/metrics/dashboard/custom_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/custom_metric_embed_service.rb
@@ -40,7 +40,7 @@ module Metrics
# All custom metrics are displayed on the system dashboard.
# Nil is acceptable as we'll default to the system dashboard.
def valid_dashboard?(dashboard)
- dashboard.nil? || SystemDashboardService.system_dashboard?(dashboard)
+ dashboard.nil? || ::Metrics::Dashboard::SystemDashboardService.system_dashboard?(dashboard)
end
end
@@ -77,15 +77,14 @@ module Metrics
# There may be multiple metrics, but they should be
# displayed in a single panel/chart.
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
- # rubocop: disable CodeReuse/ActiveRecord
def metrics
- project.prometheus_metrics.where(
+ PrometheusMetricsFinder.new(
+ project: project,
group: group_key,
title: title,
y_label: y_label
- )
+ ).execute
end
- # rubocop: enable CodeReuse/ActiveRecord
# Returns a symbol representing the group that
# the dashboard's group title belongs to.
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
new file mode 100644
index 00000000000..60591e9a6f3
--- /dev/null
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+# Responsible for returning a gitlab-compatible dashboard
+# containing info based on a grafana dashboard and datasource.
+#
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Metrics
+ module Dashboard
+ class GrafanaMetricEmbedService < ::Metrics::Dashboard::BaseService
+ include ReactiveCaching
+
+ SEQUENCE = [
+ ::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter
+ ].freeze
+
+ self.reactive_cache_key = ->(service) { service.cache_key }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.minutes
+ self.reactive_cache_lifetime = 30.minutes
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ class << self
+ # Determines whether the provided params are sufficient
+ # to uniquely identify a grafana dashboard.
+ def valid_params?(params)
+ [
+ params[:embedded],
+ params[:grafana_url]
+ ].all?
+ end
+
+ def from_cache(project_id, user_id, grafana_url)
+ project = Project.find(project_id)
+ user = User.find(user_id)
+
+ new(project, user, grafana_url: grafana_url)
+ end
+ end
+
+ def get_dashboard
+ with_reactive_cache(*cache_key) { |result| result }
+ end
+
+ # Inherits the primary logic from the parent class and
+ # maintains the service's API while including ReactiveCache
+ def calculate_reactive_cache(*)
+ # This is called with explicit parentheses to prevent
+ # the params passed to #calculate_reactive_cache from
+ # being passed to #get_dashboard (which accepts none)
+ ::Metrics::Dashboard::BaseService
+ .instance_method(:get_dashboard)
+ .bind(self)
+ .call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
+ end
+
+ def cache_key(*args)
+ [project.id, current_user.id, grafana_url]
+ end
+
+ # Required for ReactiveCaching; Usage overridden by
+ # self.reactive_cache_worker_finder
+ def id
+ nil
+ end
+
+ private
+
+ def get_raw_dashboard
+ raise MissingIntegrationError unless client
+
+ grafana_dashboard = fetch_dashboard
+ datasource = fetch_datasource(grafana_dashboard)
+
+ params.merge!(grafana_dashboard: grafana_dashboard, datasource: datasource)
+
+ {}
+ end
+
+ def fetch_dashboard
+ uid = GrafanaUidParser.new(grafana_url, project).parse
+ raise DashboardProcessingError.new('Dashboard uid not found') unless uid
+
+ response = client.get_dashboard(uid: uid)
+
+ parse_json(response.body)
+ end
+
+ def fetch_datasource(dashboard)
+ name = DatasourceNameParser.new(grafana_url, dashboard).parse
+ raise DashboardProcessingError.new('Datasource name not found') unless name
+
+ response = client.get_datasource(name: name)
+
+ parse_json(response.body)
+ end
+
+ def grafana_url
+ params[:grafana_url]
+ end
+
+ def client
+ project.grafana_integration&.client
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :read_project, project)
+ end
+
+ def sequence
+ SEQUENCE
+ end
+
+ def parse_json(json)
+ JSON.parse(json, symbolize_names: true)
+ rescue JSON::ParserError
+ raise DashboardProcessingError.new('Grafana response contains invalid json')
+ end
+ end
+
+ # Identifies the uid of the dashboard based on url format
+ class GrafanaUidParser
+ def initialize(grafana_url, project)
+ @grafana_url, @project = grafana_url, project
+ end
+
+ def parse
+ @grafana_url.match(uid_regex) { |m| m.named_captures['uid'] }
+ end
+
+ private
+
+ # URLs are expected to look like https://domain.com/d/:uid/other/stuff
+ def uid_regex
+ base_url = @project.grafana_integration.grafana_url.chomp('/')
+
+ %r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x
+ end
+ end
+
+ # Identifies the name of the datasource for a dashboard
+ # based on the panelId query parameter found in the url
+ class DatasourceNameParser
+ def initialize(grafana_url, grafana_dashboard)
+ @grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
+ end
+
+ def parse
+ @grafana_dashboard[:dashboard][:panels]
+ .find { |panel| panel[:id].to_s == query_params[:panelId] }
+ .try(:[], :datasource)
+ end
+
+ private
+
+ def query_params
+ Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
+ end
+ end
+ end
+end
diff --git a/app/services/metrics/dashboard/project_dashboard_service.rb b/app/services/metrics/dashboard/project_dashboard_service.rb
index 756d387c0e6..b0d54ee9347 100644
--- a/app/services/metrics/dashboard/project_dashboard_service.rb
+++ b/app/services/metrics/dashboard/project_dashboard_service.rb
@@ -16,7 +16,8 @@ module Metrics
{
path: filepath,
display_name: name_for_path(filepath),
- default: false
+ default: false,
+ system_dashboard: false
}
end
end
diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb
index ccfd9db8746..f8dbb8a705c 100644
--- a/app/services/metrics/dashboard/system_dashboard_service.rb
+++ b/app/services/metrics/dashboard/system_dashboard_service.rb
@@ -20,7 +20,8 @@ module Metrics
[{
path: SYSTEM_DASHBOARD_PATH,
display_name: SYSTEM_DASHBOARD_NAME,
- default: true
+ default: true,
+ system_dashboard: true
}]
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index c136803ef3b..9e6cbfa06fe 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -42,6 +42,10 @@ module Notes
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
increment_usage_counter(note)
+
+ if Feature.enabled?(:notes_create_service_tracking, project)
+ Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
+ end
end
if quick_actions_service.commands_executed_count.to_i > 0
@@ -59,5 +63,16 @@ module Notes
note
end
+
+ private
+
+ def tracking_data_for(note)
+ label = Gitlab.ee? && note.author == User.visual_review_bot ? 'anonymous_visual_review_note' : 'note'
+
+ {
+ label: label,
+ value: note.id
+ }
+ end
end
end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 48722cc2a79..53b3b57f4af 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -35,3 +35,5 @@ module Notes
end
end
end
+
+Notes::PostProcessService.prepend_if_ee('EE::Notes::PostProcessService')
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 9afbb678f5d..0bdf6a0e6bc 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -181,7 +181,7 @@ module NotificationRecipientService
def add_subscribed_users
return unless target.respond_to? :subscribers
- add_recipients(target.subscribers(project), :subscription, nil)
+ add_recipients(target.subscribers(project), :subscription, NotificationReason::SUBSCRIBED)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -240,7 +240,7 @@ module NotificationRecipientService
return unless target.respond_to? :labels
(labels || target.labels).each do |label|
- add_recipients(label.subscribers(project), :subscription, nil)
+ add_recipients(label.subscribers(project), :subscription, NotificationReason::SUBSCRIBED)
end
end
end
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 2b4c4ae68e2..afe2651b11a 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -16,8 +16,12 @@ class PreviewMarkdownService < BaseService
private
+ def quick_action_types
+ %w(Issue MergeRequest Commit)
+ end
+
def explain_quick_actions(text)
- return text, [] unless %w(Issue MergeRequest Commit).include?(target_type)
+ return text, [] unless quick_action_types.include?(target_type)
quick_actions_service = QuickActions::InterpretService.new(project, current_user)
quick_actions_service.explain(text, find_commands_target)
@@ -51,7 +55,7 @@ class PreviewMarkdownService < BaseService
def find_commands_target
QuickActions::TargetService
- .new(project, current_user)
+ .new(project, current_user, group: params[:group])
.execute(target_type, target_id)
end
@@ -63,3 +67,5 @@ class PreviewMarkdownService < BaseService
params[:target_id]
end
end
+
+PreviewMarkdownService.prepend_if_ee('EE::PreviewMarkdownService')
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index 5129e2269a8..48bd9394dc5 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -9,25 +9,11 @@ module Projects
tag_names = params[:tags]
return error('not tags specified') if tag_names.blank?
- if can_use?
- smart_delete(container_repository, tag_names)
- else
- unsafe_delete(container_repository, tag_names)
- end
+ smart_delete(container_repository, tag_names)
end
private
- def unsafe_delete(container_repository, tag_names)
- deleted_tags = tag_names.select do |tag_name|
- container_repository.tag(tag_name).unsafe_delete
- end
-
- return error('could not delete tags') if deleted_tags.empty?
-
- success(deleted: deleted_tags)
- end
-
# Replace a tag on the registry with a dummy tag.
# This is a hack as the registry doesn't support deleting individual
# tags. This code effectively pushes a dummy image and assigns the tag to it.
@@ -36,10 +22,18 @@ module Projects
def smart_delete(container_repository, tag_names)
# generates the blobs for the dummy image
dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
+ return error('could not generate manifest') if dummy_manifest.nil?
# update the manifests of the tags with the new dummy image
- tag_digests = tag_names.map do |name|
- container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
+ deleted_tags = []
+ tag_digests = []
+
+ tag_names.each do |name|
+ digest = container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
+ next unless digest
+
+ deleted_tags << name
+ tag_digests << digest
end
# make sure the digests are the same (it should always be)
@@ -51,16 +45,12 @@ module Projects
# Deletes the dummy image
# All created tag digests are the same since they all have the same dummy image.
# a single delete is sufficient to remove all tags with it
- if container_repository.delete_tag_by_digest(tag_digests.first)
- success(deleted: tag_names)
+ if tag_digests.any? && container_repository.delete_tag_by_digest(tag_digests.first)
+ success(deleted: deleted_tags)
else
error('could not delete tags')
end
end
-
- def can_use?
- Feature.enabled?(:container_registry_smart_delete, project, default_enabled: true)
- end
end
end
end
diff --git a/app/services/projects/hashed_storage/base_attachment_service.rb b/app/services/projects/hashed_storage/base_attachment_service.rb
index 828ab616bab..f8852c206e3 100644
--- a/app/services/projects/hashed_storage/base_attachment_service.rb
+++ b/app/services/projects/hashed_storage/base_attachment_service.rb
@@ -16,6 +16,12 @@ module Projects
# Returns the logger currently in use
attr_reader :logger
+ def initialize(project:, old_disk_path:, logger: nil)
+ @project = project
+ @old_disk_path = old_disk_path
+ @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
+ end
+
# Return whether this operation was skipped or not
#
# @return [Boolean] true if skipped of false otherwise
@@ -23,6 +29,14 @@ module Projects
@skipped
end
+ # Check if target path has discardable content
+ #
+ # @param [String] new_path
+ # @return [Boolean] whether we can discard the target path or not
+ def target_path_discardable?(new_path)
+ false
+ end
+
protected
def move_folder!(old_path, new_path)
@@ -34,8 +48,13 @@ module Projects
end
if File.exist?(new_path)
- logger.error("Cannot move attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
- raise AttachmentCannotMoveError, "Target path '#{new_path}' already exists"
+ if target_path_discardable?(new_path)
+ discard_path!(new_path)
+ else
+ logger.error("Cannot move attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
+
+ raise AttachmentCannotMoveError, "Target path '#{new_path}' already exists"
+ end
end
# Create base path folder on the new storage layout
@@ -46,6 +65,16 @@ module Projects
true
end
+
+ # Rename a path adding a suffix in order to prevent data-loss.
+ #
+ # @param [String] new_path
+ def discard_path!(new_path)
+ discarded_path = "#{new_path}-#{Time.now.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)
+ end
end
end
end
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
index b7e9d3e8791..8b1bcaf17b7 100644
--- a/app/services/projects/hashed_storage/base_repository_service.rb
+++ b/app/services/projects/hashed_storage/base_repository_service.rb
@@ -10,7 +10,7 @@ module Projects
attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki
- def initialize(project, old_disk_path, logger: nil)
+ def initialize(project:, old_disk_path:, logger: nil)
@project = project
@logger = logger || Gitlab::AppLogger
@old_disk_path = old_disk_path
diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb
index 0cbff283102..3d9d03c4a95 100644
--- a/app/services/projects/hashed_storage/migrate_attachments_service.rb
+++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb
@@ -3,18 +3,19 @@
module Projects
module HashedStorage
class MigrateAttachmentsService < BaseAttachmentService
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
- @old_disk_path = old_disk_path
+ extend ::Gitlab::Utils::Override
+
+ # List of paths that can be excluded while evaluation if a target can be discarded
+ DISCARDABLE_PATHS = %w(tmp tmp/cache tmp/work).freeze
+
+ def initialize(project:, old_disk_path:, logger: nil)
+ super
+
@skipped = false
end
def execute
- origin = FileUploader.absolute_base_dir(project)
- # It's possible that old_disk_path does not match project.disk_path.
- # For example, that happens when we rename a project
- origin.sub!(/#{Regexp.escape(project.full_path)}\z/, old_disk_path)
+ origin = find_old_attachments_path(project)
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
target = FileUploader.absolute_base_dir(project)
@@ -27,13 +28,38 @@ module Projects
project.save!(validate: false)
yield if block_given?
- else
- # Rollback changes
- project.rollback!
end
result
end
+
+ override :target_path_discardable?
+ # Check if target path has discardable content
+ #
+ # @param [String] new_path
+ # @return [Boolean] whether we can discard the target path or not
+ def target_path_discardable?(new_path)
+ return false unless File.directory?(new_path)
+
+ found = Dir.glob(File.join(new_path, '**', '**'))
+
+ (found - discardable_paths(new_path)).empty?
+ end
+
+ private
+
+ def discardable_paths(new_path)
+ DISCARDABLE_PATHS.collect { |path| File.join(new_path, path) }
+ end
+
+ def find_old_attachments_path(project)
+ origin = FileUploader.absolute_base_dir(project)
+
+ # It's possible that old_disk_path does not match project.disk_path.
+ # For example, that happens when we rename a project
+ #
+ origin.sub(/#{Regexp.escape(project.full_path)}\z/, old_disk_path)
+ end
end
end
end
diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb
index f132dca61c9..57a775a8f9e 100644
--- a/app/services/projects/hashed_storage/migration_service.rb
+++ b/app/services/projects/hashed_storage/migration_service.rb
@@ -14,12 +14,12 @@ module Projects
def execute
# Migrate repository from Legacy to Hashed Storage
unless project.hashed_storage?(:repository)
- return false unless migrate_repository
+ return false unless migrate_repository_service.execute
end
# Migrate attachments from Legacy to Hashed Storage
unless project.hashed_storage?(:attachments)
- return false unless migrate_attachments
+ return false unless migrate_attachments_service.execute
end
true
@@ -27,12 +27,12 @@ module Projects
private
- def migrate_repository
- HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute
+ def migrate_repository_service
+ HashedStorage::MigrateRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger)
end
- def migrate_attachments
- HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute
+ def migrate_attachments_service
+ HashedStorage::MigrateAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger)
end
end
end
diff --git a/app/services/projects/hashed_storage/rollback_attachments_service.rb b/app/services/projects/hashed_storage/rollback_attachments_service.rb
index fb09eaa4586..4bb8cb605a3 100644
--- a/app/services/projects/hashed_storage/rollback_attachments_service.rb
+++ b/app/services/projects/hashed_storage/rollback_attachments_service.rb
@@ -3,14 +3,9 @@
module Projects
module HashedStorage
class RollbackAttachmentsService < BaseAttachmentService
- def initialize(project, logger: nil)
- @project = project
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
- @old_disk_path = project.disk_path
- end
-
def execute
origin = FileUploader.absolute_base_dir(project)
+
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
target = FileUploader.absolute_base_dir(project)
diff --git a/app/services/projects/hashed_storage/rollback_service.rb b/app/services/projects/hashed_storage/rollback_service.rb
index ee41aae64a5..c437001c440 100644
--- a/app/services/projects/hashed_storage/rollback_service.rb
+++ b/app/services/projects/hashed_storage/rollback_service.rb
@@ -5,32 +5,26 @@ module Projects
class RollbackService < BaseService
attr_reader :logger, :old_disk_path
- def initialize(project, old_disk_path, logger: nil)
- @project = project
- @old_disk_path = old_disk_path
- @logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
- end
-
def execute
# Rollback attachments from Hashed Storage to Legacy
if project.hashed_storage?(:attachments)
- return false unless rollback_attachments
+ return false unless rollback_attachments_service.execute
end
# Rollback repository from Hashed Storage to Legacy
if project.hashed_storage?(:repository)
- rollback_repository
+ rollback_repository_service.execute
end
end
private
- def rollback_attachments
- HashedStorage::RollbackAttachmentsService.new(project, logger: logger).execute
+ def rollback_attachments_service
+ HashedStorage::RollbackAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger)
end
- def rollback_repository
- HashedStorage::RollbackRepositoryService.new(project, old_disk_path, logger: logger).execute
+ def rollback_repository_service
+ HashedStorage::RollbackRepositoryService.new(project: project, old_disk_path: old_disk_path, logger: logger)
end
end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d3638c57552..8344397f67d 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -24,7 +24,7 @@ module Projects
def save_all!
if save_exporters
- Gitlab::ImportExport::Saver.save(project: project, shared: shared)
+ Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
notify_success
else
cleanup_and_notify_error!
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 38de2af9c1e..a05c76f5e85 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -4,6 +4,9 @@
module Projects
module LfsPointers
class LfsLinkService < BaseService
+ TooManyOidsError = Class.new(StandardError)
+
+ MAX_OIDS = 100_000
BATCH_SIZE = 1000
# Accept an array of oids to link
@@ -12,6 +15,10 @@ module Projects
def execute(oids)
return [] unless project&.lfs_enabled?
+ if oids.size > MAX_OIDS
+ raise TooManyOidsError, 'Too many LFS object ids to link, please push them manually'
+ end
+
# Search and link existing LFS Object
link_existing_lfs_objects(oids)
end
@@ -20,22 +27,27 @@ module Projects
# rubocop: disable CodeReuse/ActiveRecord
def link_existing_lfs_objects(oids)
- all_existing_objects = []
+ linked_existing_objects = []
iterations = 0
- LfsObject.where(oid: oids).each_batch(of: BATCH_SIZE) do |existent_lfs_objects|
+ oids.each_slice(BATCH_SIZE) do |oids_batch|
+ # Load all existing LFS Objects immediately so we don't issue an extra
+ # query for the `.any?`
+ existent_lfs_objects = LfsObject.where(oid: oids_batch).load
next unless existent_lfs_objects.any?
+ rows = existent_lfs_objects
+ .not_linked_to_project(project)
+ .map { |existing_lfs_object| { project_id: project.id, lfs_object_id: existing_lfs_object.id } }
+ Gitlab::Database.bulk_insert(:lfs_objects_projects, rows)
iterations += 1
- not_linked_lfs_objects = existent_lfs_objects.where.not(id: project.all_lfs_objects)
- project.all_lfs_objects << not_linked_lfs_objects
- all_existing_objects += existent_lfs_objects.pluck(:oid)
+ linked_existing_objects += existent_lfs_objects.map(&:oid)
end
- log_lfs_link_results(all_existing_objects.count, iterations)
+ log_lfs_link_results(linked_existing_objects.count, iterations)
- all_existing_objects
+ linked_existing_objects
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 525fc18b312..718416a03d4 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -13,6 +13,8 @@ module Projects
include Gitlab::ShellAdapter
TransferError = Class.new(StandardError)
+ attr_reader :new_namespace
+
def execute(new_namespace)
@new_namespace = new_namespace
diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb
index 69464c3c1ae..4273acfbf8b 100644
--- a/app/services/quick_actions/target_service.rb
+++ b/app/services/quick_actions/target_service.rb
@@ -32,3 +32,5 @@ module QuickActions
end
end
end
+
+QuickActions::TargetService.prepend_if_ee('EE::QuickActions::TargetService')
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index b3eee01ea7a..25e3282d3fb 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -128,82 +128,37 @@ module SystemNoteService
# Called when 'merge when pipeline succeeds' is executed
def merge_when_pipeline_succeeds(noteable, project, author, sha)
- body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).merge_when_pipeline_succeeds(sha)
end
# Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_pipeline_succeeds(noteable, project, author)
- body = 'canceled the automatic merge'
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).cancel_merge_when_pipeline_succeeds
end
# Called when 'merge when pipeline succeeds' is aborted
def abort_merge_when_pipeline_succeeds(noteable, project, author, reason)
- body = "aborted the automatic merge because #{reason}"
-
- ##
- # TODO: Abort message should be sent by the system, not a particular user.
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63187.
- create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).abort_merge_when_pipeline_succeeds(reason)
end
def handle_merge_request_wip(noteable, project, author)
- prefix = noteable.work_in_progress? ? "marked" : "unmarked"
-
- body = "#{prefix} as a **Work In Progress**"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).handle_merge_request_wip
end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
- body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).add_merge_request_wip_from_commit(commit)
end
def resolve_all_discussions(merge_request, project, author)
- body = "resolved all threads"
-
- create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion'))
+ ::SystemNotes::MergeRequestsService.new(noteable: merge_request, project: project, author: author).resolve_all_discussions
end
def discussion_continued_in_issue(discussion, project, author, issue)
- body = "created #{issue.to_reference} to continue this discussion"
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
-
- note = Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp))
- note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
-
- note
+ ::SystemNotes::MergeRequestsService.new(project: project, author: author).discussion_continued_in_issue(discussion, issue)
end
def diff_discussion_outdated(discussion, project, author, change_position)
- merge_request = discussion.noteable
- diff_refs = change_position.diff_refs
- version_index = merge_request.merge_request_diffs.viewable.count
- position_on_text = change_position.on_text?
- text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"]
-
- if version_params = merge_request.version_params_for(diff_refs)
- repository = project.repository
- anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash
- url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor))
-
- text_parts << "[version #{version_index} of the diff](#{url})"
- else
- text_parts << "version #{version_index} of the diff"
- end
-
- body = text_parts.join(' ')
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
-
- note = Note.create(note_attributes.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new(action: 'outdated')
-
- note
+ ::SystemNotes::MergeRequestsService.new(project: project, author: author).diff_discussion_outdated(discussion, change_position)
end
def change_title(noteable, project, author, old_title)
@@ -233,9 +188,7 @@ module SystemNoteService
#
# Returns the created Note object
def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
- body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).change_branch(branch_type, old_branch, new_branch)
end
# Called when a branch in Noteable is added or deleted
@@ -253,16 +206,7 @@ module SystemNoteService
#
# Returns the created Note object
def change_branch_presence(noteable, project, author, branch_type, branch, presence)
- verb =
- if presence == :add
- 'restored'
- else
- 'deleted'
- end
-
- body = "#{verb} #{branch_type} branch `#{branch}`"
-
- create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).change_branch_presence(branch_type, branch, presence)
end
# Called when a branch is created from the 'new branch' button on a issue
@@ -270,18 +214,11 @@ module SystemNoteService
#
# "created branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch, branch_project: nil)
- branch_project ||= project
- link = url_helpers.project_compare_path(branch_project, from: branch_project.default_branch, to: branch)
-
- body = "created branch [`#{branch}`](#{link}) to address this issue"
-
- create_note(NoteSummary.new(issue, project, author, body, action: 'branch'))
+ ::SystemNotes::MergeRequestsService.new(noteable: issue, project: project, author: author).new_issue_branch(branch, branch_project: branch_project)
end
def new_merge_request(issue, project, author, merge_request)
- body = "created merge request #{merge_request.to_reference(project)} to address this issue"
-
- create_note(NoteSummary.new(issue, project, author, body, action: 'merge'))
+ ::SystemNotes::MergeRequestsService.new(noteable: issue, project: project, author: author).new_merge_request(merge_request)
end
def cross_reference(noteable, mentioner, author)
diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb
new file mode 100644
index 00000000000..1d17f0ded57
--- /dev/null
+++ b/app/services/system_notes/merge_requests_service.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class MergeRequestsService < ::SystemNotes::BaseService
+ # Called when 'merge when pipeline succeeds' is executed
+ def merge_when_pipeline_succeeds(sha)
+ body = "enabled an automatic merge when the pipeline for #{sha} succeeds"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ # Called when 'merge when pipeline succeeds' is canceled
+ def cancel_merge_when_pipeline_succeeds
+ body = 'canceled the automatic merge'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ # Called when 'merge when pipeline succeeds' is aborted
+ def abort_merge_when_pipeline_succeeds(reason)
+ body = "aborted the automatic merge because #{reason}"
+
+ ##
+ # TODO: Abort message should be sent by the system, not a particular user.
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/63187.
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+
+ def handle_merge_request_wip
+ prefix = noteable.work_in_progress? ? "marked" : "unmarked"
+
+ body = "#{prefix} as a **Work In Progress**"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ def add_merge_request_wip_from_commit(commit)
+ body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
+ end
+
+ def resolve_all_discussions
+ body = "resolved all threads"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'discussion'))
+ end
+
+ def discussion_continued_in_issue(discussion, issue)
+ body = "created #{issue.to_reference} to continue this discussion"
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
+
+ Note.create(note_attributes.merge(system: true, created_at: issue.system_note_timestamp)).tap do |note|
+ note.system_note_metadata = SystemNoteMetadata.new(action: 'discussion')
+ end
+ end
+
+ def diff_discussion_outdated(discussion, change_position)
+ merge_request = discussion.noteable
+ diff_refs = change_position.diff_refs
+ version_index = merge_request.merge_request_diffs.viewable.count
+ position_on_text = change_position.on_text?
+ text_parts = ["changed this #{position_on_text ? 'line' : 'file'} in"]
+
+ if version_params = merge_request.version_params_for(diff_refs)
+ repository = project.repository
+ anchor = position_on_text ? change_position.line_code(repository) : change_position.file_hash
+ url = url_helpers.diffs_project_merge_request_path(project, merge_request, version_params.merge(anchor: anchor))
+
+ text_parts << "[version #{version_index} of the diff](#{url})"
+ else
+ text_parts << "version #{version_index} of the diff"
+ end
+
+ body = text_parts.join(' ')
+ note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
+
+ Note.create(note_attributes.merge(system: true)).tap do |note|
+ note.system_note_metadata = SystemNoteMetadata.new(action: 'outdated')
+ end
+ end
+
+ # Called when a branch in Noteable is changed
+ #
+ # branch_type - 'source' or 'target'
+ # old_branch - old branch name
+ # new_branch - new branch name
+ #
+ # Example Note text:
+ #
+ # "changed target branch from `Old` to `New`"
+ #
+ # Returns the created Note object
+ def change_branch(branch_type, old_branch, new_branch)
+ body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ # Called when a branch in Noteable is added or deleted
+ #
+ # branch_type - :source or :target
+ # branch - branch name
+ # presence - :add or :delete
+ #
+ # Example Note text:
+ #
+ # "restored target branch `feature`"
+ #
+ # Returns the created Note object
+ def change_branch_presence(branch_type, branch, presence)
+ verb =
+ if presence == :add
+ 'restored'
+ else
+ 'deleted'
+ end
+
+ body = "#{verb} #{branch_type} branch `#{branch}`"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ # Called when a branch is created from the 'new branch' button on a issue
+ # Example note text:
+ #
+ # "created branch `201-issue-branch-button`"
+ def new_issue_branch(branch, branch_project: nil)
+ branch_project ||= project
+ link = url_helpers.project_compare_path(branch_project, from: branch_project.default_branch, to: branch)
+
+ body = "created branch [`#{branch}`](#{link}) to address this issue"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
+ end
+
+ def new_merge_request(merge_request)
+ body = "created merge request #{merge_request.to_reference(project)} to address this issue"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
+ end
+ end
+end
+
+SystemNotes::MergeRequestsService.prepend_if_ee('::EE::SystemNotes::MergeRequestsService')
diff --git a/app/services/users/signup_service.rb b/app/services/users/signup_service.rb
new file mode 100644
index 00000000000..1031cec44cb
--- /dev/null
+++ b/app/services/users/signup_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Users
+ class SignupService < BaseService
+ def initialize(current_user, params = {})
+ @user = current_user
+ @params = params.dup
+ end
+
+ def execute
+ assign_attributes
+ inject_validators
+
+ if @user.save
+ success
+ else
+ error(@user.errors.full_messages.join('. '))
+ end
+ end
+
+ private
+
+ def assign_attributes
+ @user.assign_attributes(params) unless params.empty?
+ end
+
+ def inject_validators
+ class << @user
+ validates :role, presence: true
+ validates :setup_for_company, inclusion: { in: [true, false], message: :blank }
+ end
+ end
+ end
+end
diff --git a/app/services/zoom_notes_service.rb b/app/services/zoom_notes_service.rb
deleted file mode 100644
index 983a7fcacd1..00000000000
--- a/app/services/zoom_notes_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-class ZoomNotesService
- def initialize(issue, project, current_user, old_description: nil)
- @issue = issue
- @project = project
- @current_user = current_user
- @old_description = old_description
- end
-
- def execute
- return if @issue.description == @old_description
-
- if zoom_link_added?
- zoom_link_added_notification
- elsif zoom_link_removed?
- zoom_link_removed_notification
- end
- end
-
- private
-
- def zoom_link_added?
- has_zoom_link?(@issue.description) && !has_zoom_link?(@old_description)
- end
-
- def zoom_link_removed?
- !has_zoom_link?(@issue.description) && has_zoom_link?(@old_description)
- end
-
- def has_zoom_link?(text)
- Gitlab::ZoomLinkExtractor.new(text).match?
- end
-
- def zoom_link_added_notification
- SystemNoteService.zoom_link_added(@issue, @project, @current_user)
- end
-
- def zoom_link_removed_notification
- SystemNoteService.zoom_link_removed(@issue, @project, @current_user)
- end
-end
diff --git a/app/validators/same_project_association_validator.rb b/app/validators/same_project_association_validator.rb
new file mode 100644
index 00000000000..2af2a21fa9a
--- /dev/null
+++ b/app/validators/same_project_association_validator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# SameProjectAssociationValidator
+#
+# Custom validator to validate that the same project associated with
+# the record is also associated with the value
+#
+# Example:
+# class ZoomMeeting < ApplicationRecord
+# belongs_to :project, optional: false
+# belongs_to :issue, optional: false
+
+# validates :issue, same_project_association: true
+# end
+class SameProjectAssociationValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if record.project == value&.project
+
+ record.errors[attribute] << 'must associate the same project'
+ end
+end
diff --git a/app/validators/zoom_url_validator.rb b/app/validators/zoom_url_validator.rb
new file mode 100644
index 00000000000..dc4ca6b9501
--- /dev/null
+++ b/app/validators/zoom_url_validator.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# ZoomUrlValidator
+#
+# Custom validator for zoom urls
+#
+class ZoomUrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if Gitlab::ZoomLinkExtractor.new(value).links.size == 1
+
+ record.errors.add(:url, 'must contain one valid Zoom URL')
+ end
+end
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index cc29657a439..e3d78b3058f 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,6 +1,18 @@
-- page_title 'Abuse Reports'
-%h3.page-title Abuse Reports
-%hr
+- page_title _('Abuse Reports')
+
+%h3.page-title= _('Abuse Reports')
+
+.row-content-block.second-block
+ = form_tag admin_abuse_reports_path, method: :get, class: 'filter-form' do
+ .filter-categories.flex-fill
+ .filter-item.inline
+ = dropdown_tag(user_dropdown_label(params[:user_id], 'User'),
+ options: { toggle_class: 'js-filter-submit js-user-search',
+ title: _('Filter by user'), filter: true, filterInput: 'input#user-search',
+ dropdown_class: 'dropdown-menu-selectable dropdown-menu-user js-filter-submit',
+ placeholder: _('Search users'),
+ data: { current_user: true, field_name: 'user_id' }})
+
.abuse-reports
- if @abuse_reports.present?
.table-holder
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 1f5bce19bc6..9806090c1a6 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -53,5 +53,11 @@
= s_('AdminSettings|Environment variables are protected by default')
.form-text.text-muted
= s_('AdminSettings|When creating a new environment variable it will be protected by default.')
+ .form-group
+ = f.label :ci_config_path, _('Default CI configuration path'), class: 'label-bold'
+ = f.text_field :default_ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
+ %p.form-text.text-muted
+ = _("The default CI configuration path for new projects.").html_safe
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml
new file mode 100644
index 00000000000..b1f7ed76281
--- /dev/null
+++ b/app/views/admin/application_settings/_eks.html.haml
@@ -0,0 +1,31 @@
+- expanded = integration_expanded?('eks_')
+%section.settings.as-eks.no-animate#js-eks-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Amazon EKS')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
+
+ .settings-content
+ = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :eks_integration_enabled, class: 'form-check-input'
+ = f.label :eks_integration_enabled, class: 'form-check-label' do
+ Enable Amazon EKS integration
+ .form-group
+ = f.label :eks_account_id, 'Account ID', class: 'label-bold'
+ = f.text_field :eks_account_id, class: 'form-control'
+ .form-group
+ = f.label :eks_access_key_id, 'Access key ID', class: 'label-bold'
+ = f.text_field :eks_access_key_id, class: 'form-control'
+ .form-group
+ = f.label :eks_secret_access_key, 'Secret access key', class: 'label-bold'
+ = f.password_field :eks_secret_access_key, value: @application_setting.eks_secret_access_key, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index ad26f52aea7..42528f40123 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -4,7 +4,7 @@
%fieldset
.form-group
.form-check
- = f.check_box :allow_local_requests_from_web_hooks_and_services, class: 'form-check-input'
+ = f.check_box :allow_local_requests_from_web_hooks_and_services, class: 'form-check-input', data: { qa_selector: 'allow_requests_from_services_checkbox' }
= f.label :allow_local_requests_from_web_hooks_and_services, class: 'form-check-label' do
= _('Allow requests to the local network from web hooks and services')
.form-check
@@ -27,4 +27,4 @@
%span.form-text.text-muted
= _('Resolves IP addresses once and uses them to submit requests')
- = 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/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 86dc289dd7c..d35774d330d 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -1,18 +1,27 @@
-= form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+- expanded = integration_expanded?('plantuml_')
+%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('PlantUML')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
+ .settings-content
+ = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting) if expanded
- %fieldset
- .form-group
- .form-check
- = f.check_box :plantuml_enabled, class: 'form-check-input'
- = f.label :plantuml_enabled, class: 'form-check-label' do
- Enable PlantUML
- .form-group
- = f.label :plantuml_url, 'PlantUML URL', class: 'label-bold'
- = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
- .form-text.text-muted
- Allow rendering of
- = link_to "PlantUML", "http://plantuml.com"
- diagrams in Asciidoc documents using an external PlantUML service.
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :plantuml_enabled, class: 'form-check-input'
+ = f.label :plantuml_enabled, _('Enable PlantUML'), class: 'form-check-label'
+ .form-group
+ = f.label :plantuml_url, 'PlantUML URL', class: 'label-bold'
+ = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+ .form-text.text-muted
+ Allow rendering of
+ = link_to "PlantUML", "http://plantuml.com"
+ diagrams in Asciidoc documents using an external PlantUML service.
- = f.submit 'Save changes', class: "btn btn-success"
+ = 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 362f4a42464..6e5fa6eb62c 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -7,9 +7,9 @@
.form-check
= f.check_box :mirror_available, class: 'form-check-input'
= f.label :mirror_available, class: 'form-check-label' do
- = _('Allow mirrors to be set up for projects')
+ = _('Allow repository mirroring to be configured by project maintainers')
%span.form-text.text-muted
- = _('If disabled, only admins will be able to set up mirrors in projects.')
+ = _('If disabled, only admins will be able to configure repository mirroring.')
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
= render_if_exists 'admin/application_settings/mirror_settings', form: f
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 31fd12d191e..a2597433270 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -1,4 +1,4 @@
-- expanded = true if !@application_setting.valid? && @application_setting.errors.any? { |k| k.to_s.start_with?('snowplow_') }
+- expanded = integration_expanded?('snowplow_')
%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
@@ -10,7 +10,7 @@
.settings-content
= form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
+ = form_errors(@application_setting) if expanded
%fieldset
.form-group
@@ -21,10 +21,13 @@
= f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light'
= f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com'
.form-group
- = f.label :snowplow_site_id, _('Site ID'), class: 'label-light'
- = f.text_field :snowplow_site_id, class: 'form-control'
+ = f.label :snowplow_app_id, _('App ID'), class: 'label-light'
+ = f.text_field :snowplow_app_id, class: 'form-control'
.form-group
= f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light'
= f.text_field :snowplow_cookie_domain, class: 'form-control'
+ .form-group
+ = f.label :snowplow_iglu_registry_url, _('Iglu registry URL (optional)'), class: 'label-light'
+ = f.text_field :snowplow_iglu_registry_url, class: 'form-control'
= f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
new file mode 100644
index 00000000000..23cda0334a2
--- /dev/null
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -0,0 +1,38 @@
+- return unless Gitlab::Sourcegraph.feature_available?
+- expanded = integration_expanded?('sourcegraph_')
+
+%section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Sourcegraph')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://sourcegraph.com/' }
+ - link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
+ = s_('SourcegraphAdmin|Enable code intelligence powered by %{link_start}Sourcegraph%{link_end} on your GitLab instance\'s code views and merge requests.').html_safe % { link_start: link_start, link_end: link_end }
+ %span
+ = link_to s_('SourcegraphAdmin|More information'), help_page_path('integration/sourcegraph.md'), target: '_blank'
+
+
+ .settings-content
+ = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :sourcegraph_enabled, class: 'form-check-input'
+ = f.label :sourcegraph_enabled, s_('SourcegraphAdmin|Enable Sourcegraph'), class: 'form-check-label'
+ .form-group
+ .form-check
+ = f.check_box :sourcegraph_public_only, class: 'form-check-input'
+ = f.label :sourcegraph_public_only, s_('SourcegraphAdmin|Block on private and internal projects'), class: 'form-check-label'
+ .form-text.text-muted
+ = s_('SourcegraphAdmin|If checked, only public projects will have code intelligence and communicate with Sourcegraph.')
+ .form-group
+ = f.label :sourcegraph_url, s_('SourcegraphAdmin|Sourcegraph URL'), class: 'label-bold'
+ = f.text_field :sourcegraph_url, class: 'form-control', placeholder: s_('SourcegraphAdmin|e.g. https://sourcegraph.example.com')
+ .form-text.text-muted
+ = s_('SourcegraphAdmin|Configure the URL to a Sourcegraph instance which can read your GitLab projects.')
+ = f.submit s_('SourcegraphAdmin|Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml
index adde09f75e4..256b1f74bfa 100644
--- a/app/views/admin/application_settings/_third_party_offers.html.haml
+++ b/app/views/admin/application_settings/_third_party_offers.html.haml
@@ -1,13 +1,21 @@
-- application_setting = local_assigns.fetch(:application_setting)
+- expanded = integration_expanded?('hide_third_party_')
+%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Third party offers')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Control the display of third party offers.')
+ .settings-content
-= form_for application_setting, url: integrations_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(application_setting)
+ = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting) if expanded
- %fieldset
- .form-group
- .form-check
- = f.check_box :hide_third_party_offers, class: 'form-check-input'
- = f.label :hide_third_party_offers, class: 'form-check-label' do
- Do not display offers from third parties within GitLab
+ %fieldset
+ .form-group
+ .form-check
+ = f.check_box :hide_third_party_offers, class: 'form-check-input'
+ = f.label :hide_third_party_offers, _('Do not display offers from third parties within GitLab'), class: 'form-check-label'
- = f.submit 'Save changes', class: "btn btn-success"
+ = 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 310e86b1377..0aa833e49a8 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -2,30 +2,11 @@
- page_title _("Integrations")
- @content_class = "limit-container-width" unless fluid_layout
-= render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded_by_default?
+= render_if_exists 'admin/application_settings/elasticsearch_form'
+= render 'admin/application_settings/plantuml'
+= render 'admin/application_settings/sourcegraph'
+= render_if_exists 'admin/application_settings/slack'
+= render 'admin/application_settings/third_party_offers'
+= render 'admin/application_settings/snowplow'
+= render 'admin/application_settings/eks' if Feature.enabled?(:create_eks_clusters)
-%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4
- = _('PlantUML')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded_by_default? ? _('Collapse') : _('Expand')
- %p
- = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
- .settings-content
- = render 'plantuml'
-
-= render_if_exists 'admin/application_settings/slack', expanded: expanded_by_default?
-
-%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4
- = _('Third party offers')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded_by_default? ? _('Collapse') : _('Expand')
- %p
- = _('Control the display of third party offers.')
- .settings-content
- = render 'third_party_offers', application_setting: @application_setting
-
-= render_if_exists 'admin/application_settings/snowplow', expanded: expanded_by_default?
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 092834b993c..7bd51172195 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -24,7 +24,7 @@
.settings-content
= render 'ip_limits'
-%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_section' } }
.settings-header
%h4
= _('Outbound requests')
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index 25f8b6541b5..b0934a9d9fb 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -5,11 +5,11 @@
%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
- = _('Repository mirror')
+ = _('Repository mirroring')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? 'Collapse' : 'Expand'
%p
- = _('Configure push mirrors.')
+ = _('Configure repository mirroring.')
.settings-content
= render partial: 'repository_mirrors_form'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 41147950c40..e5a3c0df9bf 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -41,17 +41,38 @@
.info-well
.well-segment.admin-well.admin-well-features
%h4 Features
- = feature_entry(_('Sign up'), href: admin_application_settings_path(anchor: 'js-signup-settings'), enabled: allow_signup?)
- = feature_entry(_('LDAP'), enabled: Gitlab.config.ldap.enabled)
- = feature_entry(_('Gravatar'), href: admin_application_settings_path(anchor: 'js-account-settings'), enabled: gravatar_enabled?)
- = feature_entry(_('OmniAuth'), href: admin_application_settings_path(anchor: 'js-signin-settings'), enabled: Gitlab::Auth.omniauth_enabled?)
- = feature_entry(_('Reply by email'), enabled: Gitlab::IncomingEmail.enabled?)
+ = feature_entry(_('Sign up'),
+ href: admin_application_settings_path(anchor: 'js-signup-settings'),
+ enabled: allow_signup?)
+
+ = feature_entry(_('LDAP'),
+ enabled: Gitlab.config.ldap.enabled)
+
+ = feature_entry(_('Gravatar'),
+ href: admin_application_settings_path(anchor: 'js-account-settings'),
+ enabled: gravatar_enabled?)
+
+ = feature_entry(_('OmniAuth'),
+ href: admin_application_settings_path(anchor: 'js-signin-settings'),
+ enabled: Gitlab::Auth.omniauth_enabled?)
+
+ = feature_entry(_('Reply by email'),
+ enabled: Gitlab::IncomingEmail.enabled?)
= render_if_exists 'admin/dashboard/elastic_and_geo'
- = feature_entry(_('Container Registry'), href: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'), enabled: Gitlab.config.registry.enabled)
- = feature_entry(_('Gitlab Pages'), href: help_instance_configuration_url, enabled: Gitlab.config.pages.enabled)
- = feature_entry(_('Shared Runners'), href: admin_runners_path, enabled: Gitlab.config.gitlab_ci.shared_runners_enabled)
+ = feature_entry(_('Container Registry'),
+ href: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
+ enabled: Gitlab.config.registry.enabled,
+ doc_href: help_page_path('user/packages/container_registry/index'))
+
+ = feature_entry(_('Gitlab Pages'),
+ enabled: Gitlab.config.pages.enabled,
+ doc_href: help_instance_configuration_url)
+
+ = feature_entry(_('Shared Runners'),
+ href: admin_runners_path,
+ enabled: Gitlab.config.gitlab_ci.shared_runners_enabled)
.col-md-4
.info-well
.well-segment.admin-well
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index 55aea0296e7..3d77a439d61 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -4,4 +4,4 @@
= password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
- = submit_tag _('Enter admin mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
+ = submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml
index 69baa76060e..1d19915d3c5 100644
--- a/app/views/admin/sessions/_signin_box.html.haml
+++ b/app/views/admin/sessions/_signin_box.html.haml
@@ -1,4 +1,4 @@
-- if form_based_providers.any?
+- if any_form_based_providers_enabled?
- if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
diff --git a/app/views/admin/sessions/_tabs_normal.html.haml b/app/views/admin/sessions/_tabs_normal.html.haml
index f5dedb5ad76..20830051d31 100644
--- a/app/views/admin/sessions/_tabs_normal.html.haml
+++ b/app/views/admin/sessions/_tabs_normal.html.haml
@@ -1,3 +1,3 @@
%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' }= _('Enter admin mode')
+ %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter Admin Mode')
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index ee06b4a1741..73028e78ea5 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -1,5 +1,5 @@
- @hide_breadcrumbs = true
-- page_title _('Enter admin mode')
+- page_title _('Enter Admin Mode')
.row.justify-content-center
.col-6.new-session-forms-container
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 706fa033c51..cd07fee8e59 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -152,7 +152,7 @@
- email = " (#{@user.unconfirmed_email})"
%p This user has an unconfirmed email address#{email}. You may force a confirmation.
%br
- = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
+ = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?', qa_selector: 'confirm_user_button' }
= render_if_exists 'admin/users/user_detail_note'
diff --git a/app/views/ci/group_variables/_content.html.haml b/app/views/ci/group_variables/_content.html.haml
new file mode 100644
index 00000000000..db5f1021f57
--- /dev/null
+++ b/app/views/ci/group_variables/_content.html.haml
@@ -0,0 +1 @@
+= _("These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables.")
diff --git a/app/views/ci/group_variables/_header.html.haml b/app/views/ci/group_variables/_header.html.haml
new file mode 100644
index 00000000000..71d123ec9f2
--- /dev/null
+++ b/app/views/ci/group_variables/_header.html.haml
@@ -0,0 +1,5 @@
+%h5
+ = _('Group variables (inherited)')
+
+%p
+ = render "ci/group_variables/content"
diff --git a/app/views/ci/group_variables/_index.html.haml b/app/views/ci/group_variables/_index.html.haml
new file mode 100644
index 00000000000..c350ba5caf7
--- /dev/null
+++ b/app/views/ci/group_variables/_index.html.haml
@@ -0,0 +1,13 @@
+- variables = @project.group.self_and_ancestors.map(&:variables).flatten
+
+.row
+ .col-lg-12
+ .group-variable-list
+ = render 'ci/group_variables/variable_header'
+ - variables.each do |variable|
+ .group-variable-row.d-flex.w-100.border-bottom.pt-2.pb-2
+ .table-section.section-40.append-right-10.key
+ = variable.key
+ .table-section.section-40.append-right-10
+ %a.group-origin-link{ href: group_settings_ci_cd_path(variable.group) }
+ = variable.group.name
diff --git a/app/views/ci/group_variables/_variable_header.html.haml b/app/views/ci/group_variables/_variable_header.html.haml
new file mode 100644
index 00000000000..1a3168cf781
--- /dev/null
+++ b/app/views/ci/group_variables/_variable_header.html.haml
@@ -0,0 +1,5 @@
+.group-variable-keys.d-flex.w-100.align-items-center.pb-2.border-bottom
+ .bold.table-section.section-40.append-right-10
+ = s_('Key')
+ .bold.table-section.section-40.append-right-10
+ = s_('Origin')
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
index dbfa0a9e5a1..ce4dd5a4877 100644
--- a/app/views/ci/variables/_header.html.haml
+++ b/app/views/ci/variables/_header.html.haml
@@ -7,5 +7,5 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
-%p.append-bottom-0
+%p
= render "ci/variables/content"
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 94102b4dcd0..7ae5c48b93c 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -24,3 +24,8 @@
= n_('Hide value', 'Hide values', @variables.size)
- else
= n_('Reveal value', 'Reveal values', @variables.size)
+ - if !@group && @project.group
+ .settings-header.border-top.prepend-top-20
+ = render 'ci/group_variables/header'
+ .settings-content.pr-0
+ = render 'ci/group_variables/index'
diff --git a/app/views/ci/variables/_url_query_variable_row.html.haml b/app/views/ci/variables/_url_query_variable_row.html.haml
new file mode 100644
index 00000000000..6672a8e5ea0
--- /dev/null
+++ b/app/views/ci/variables/_url_query_variable_row.html.haml
@@ -0,0 +1,28 @@
+- form_field = local_assigns.fetch(:form_field, nil)
+- variable = local_assigns.fetch(:variable, nil)
+
+- key = variable[0]
+- value = variable[1]
+- variable_type = variable[2] || "env_var"
+
+- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]"
+- variable_type_input_name = "#{form_field}[variables_attributes][][variable_type]"
+- key_input_name = "#{form_field}[variables_attributes][][key]"
+- value_input_name = "#{form_field}[variables_attributes][][secret_value]"
+
+%li.js-row.ci-variable-row
+ .ci-variable-row-body.border-bottom
+ %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
+ %select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
+ = options_for_select(ci_variable_type_options, variable_type)
+ %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text",
+ name: key_input_name,
+ value: key,
+ placeholder: s_('CiVariables|Input variable key') }
+ .ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
+ %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ rows: 1,
+ name: value_input_name,
+ placeholder: s_('CiVariables|Input variable value') }
+ = value
+ %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
+ = icon('minus-circle')
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index 8005dcbf65f..493d7a00854 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -1,3 +1,9 @@
+- group_id = @cluster.group.id if @cluster.group_type?
+
+- if @cluster.project_type?
+ - group_id = @cluster.project.group.id if @cluster.project.group
+ - user_id = @cluster.project.namespace.owner_id unless group_id
+
- if can?(current_user, :admin_cluster, @cluster)
- unless @cluster.provided_by_user?
.append-bottom-20
@@ -7,6 +13,21 @@
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
+ = form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'cluster_management_form' } do |field|
+
+ %h5
+ = s_('ClusterIntegration|Cluster management project (alpha)')
+
+ .form-group
+ .form-text.text-muted
+ = project_select_tag('cluster[management_project_id]', class: 'hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: _('Select project'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', simple_filter: true, allow_clear: true, include_groups: false, include_projects_in_subgroups: true, group_id: group_id, user_id: user_id }, value: @cluster.management_project_id)
+ .text-muted
+ = s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges.').html_safe
+ = link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank'
+ .form-group
+ = field.submit _('Save changes'), class: 'btn btn-success qa-save-domain'
+
.sub-section.form-group
%h4.text-danger
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 4b4278075a6..7d97aaccbcf 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -1,10 +1,10 @@
.hidden.js-cluster-error.bs-callout.bs-callout-danger{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
+ = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster')
%p.js-error-reason
.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' }
%span.spinner.spinner-dark.spinner-sm{ 'aria-label': 'Loading' }
- %span.prepend-left-4= s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
+ %span.prepend-left-4= s_('ClusterIntegration|Kubernetes cluster is being created...')
.hidden.row.js-cluster-api-unreachable.bs-callout.bs-callout-warning{ role: 'alert' }
.col-11
@@ -19,4 +19,4 @@
%button.js-close-banner.close.cluster-application-banner-close.h-100.m-0= "×"
.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
- = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine.")
+ = s_("ClusterIntegration|Kubernetes cluster was successfully created.")
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index a9299af8d78..617e5d1d5d3 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -7,6 +7,6 @@
.gcp-signup-offer--copy
%h4= s_('ClusterIntegration|Did you know?')
%p= s_('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.').html_safe % { sign_up_link: link }
- %a.btn.btn-default{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' }
+ %a.btn.btn-default{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
= s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml
new file mode 100644
index 00000000000..48467f88f52
--- /dev/null
+++ b/app/views/clusters/clusters/aws/_new.html.haml
@@ -0,0 +1,20 @@
+- if !Gitlab::CurrentSettings.eks_integration_enabled?
+ - documentation_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path("integration/amazon") }
+ = s_('Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service.').html_safe % { link_start: documentation_link_start, link_end: '<a/>'.html_safe }
+- else
+ .js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
+ 'create-role-path' => clusterable.authorize_aws_role_path,
+ 'sign-out-path' => clusterable.revoke_aws_role_path,
+ 'create-cluster-path' => clusterable.create_aws_clusters_path,
+ 'get-roles-path' => clusterable.aws_api_proxy_path('roles'),
+ 'get-regions-path' => clusterable.aws_api_proxy_path('regions'),
+ 'get-key-pairs-path' => clusterable.aws_api_proxy_path('key_pairs'),
+ 'get-vpcs-path' => clusterable.aws_api_proxy_path('vpcs'),
+ 'get-subnets-path' => clusterable.aws_api_proxy_path('subnets'),
+ 'get-security-groups-path' => clusterable.aws_api_proxy_path('security_groups'),
+ 'get-instance-types-path' => clusterable.aws_api_proxy_path('instance_types'),
+ 'account-id' => Gitlab::CurrentSettings.eks_account_id,
+ 'external-id' => @aws_role.role_external_id,
+ 'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
+ 'external-link-icon' => icon('external-link'),
+ 'has-credentials' => @aws_role.role_arn.present?.to_s } }
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
index d4999798c19..56d46580b9e 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_button.html.haml
@@ -1,8 +1,10 @@
- provider = local_assigns.fetch(:provider)
- logo_path = local_assigns.fetch(:logo_path)
- label = local_assigns.fetch(:label)
+- last = local_assigns.fetch(:last, false)
+- classes = ['btn btn-light btn-outline flex-fill d-inline-flex flex-column justify-content-center align-items-center', ('mr-3' unless last)]
-= link_to clusterable.new_path(provider: provider), class: 'btn gl-button btn-outline flex-fill d-inline-flex flex-column mr-3 justify-content-center align-items-center' do
- .svg-content= image_tag logo_path, alt: label, class: 'gl-w-13 gl-h-13'
+= link_to clusterable.new_path(provider: provider), class: classes do
+ .svg-content.p-2= image_tag logo_path, alt: label, class: 'gl-w-64 gl-h-64'
%span
= label
diff --git a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
index 7a93a7604f5..91925f5f96f 100644
--- a/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
+++ b/app/views/clusters/clusters/cloud_providers/_cloud_provider_selector.html.haml
@@ -2,10 +2,10 @@
- eks_label = s_('ClusterIntegration|Amazon EKS')
- create_cluster_label = s_('ClusterIntegration|Create cluster on')
.d-flex.flex-column
- %h5
+ %h5.mb-3
= create_cluster_label
.d-flex
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
- locals: { provider: 'eks', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' }
+ locals: { provider: 'aws', label: eks_label, logo_path: 'illustrations/logos/amazon_eks.svg' }
= render partial: 'clusters/clusters/cloud_providers/cloud_provider_button',
- locals: { provider: 'gke', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg' }
+ locals: { provider: 'gcp', label: gke_label, logo_path: 'illustrations/logos/google_gke.svg', last: true }
diff --git a/app/views/clusters/clusters/eks/_index.html.haml b/app/views/clusters/clusters/eks/_index.html.haml
deleted file mode 100644
index db64698a7f2..00000000000
--- a/app/views/clusters/clusters/eks/_index.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.js-create-eks-cluster-form-container{ data: { 'gitlab-managed-cluster-help-path' => help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'),
-'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index') } }
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index cca16ce7eda..95670a2ec87 100644
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -64,12 +64,13 @@
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
- .form-group
- = provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run on GKE (beta)'),
- label_class: 'label-bold' }
- .form-text.text-muted
- = s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'cloud-run-on-gke'), target: '_blank'
+ - if Feature.enabled?(:create_cloud_run_clusters, clusterable)
+ .form-group
+ = provider_gcp_field.check_box :cloud_run, { label: s_('ClusterIntegration|Enable Cloud Run on GKE (beta)'),
+ label_class: 'label-bold' }
+ .form-text.text-muted
+ = s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'cloud-run-on-gke'), target: '_blank'
.form-group
= field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
diff --git a/app/views/clusters/clusters/gcp/_new.html.haml b/app/views/clusters/clusters/gcp/_new.html.haml
new file mode 100644
index 00000000000..3d47f4bf2c3
--- /dev/null
+++ b/app/views/clusters/clusters/gcp/_new.html.haml
@@ -0,0 +1,7 @@
+= render 'clusters/clusters/gcp/header'
+- if @valid_gcp_token
+ = render 'clusters/clusters/gcp/form'
+- elsif @authorize_url
+ = render 'clusters/clusters/gcp/signin_with_google_button'
+- else
+ = render 'clusters/clusters/gcp/gcp_not_configured'
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 9bab3bf56aa..049010cadf4 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -16,7 +16,7 @@
.bs-callout.bs-callout-info
= s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.')
%strong
- = link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence')
+ = link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
.clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml
index 2c23426aaf9..cb8cbe4e6f2 100644
--- a/app/views/clusters/clusters/new.html.haml
+++ b/app/views/clusters/clusters/new.html.haml
@@ -2,9 +2,6 @@
- page_title _('Kubernetes Cluster')
- create_eks_enabled = Feature.enabled?(:create_eks_clusters)
- active_tab = local_assigns.fetch(:active_tab, 'create')
-- create_on_gke_tab_label = s_('ClusterIntegration|Create new Cluster on GKE')
-- create_on_eks_tab_label = s_('ClusterIntegration|Create new Cluster on EKS')
-- create_new_cluster_label = s_('ClusterIntegration|Create new Cluster')
= javascript_include_tag 'https://apis.google.com/js/api.js'
= render_gcp_signup_offer
@@ -18,14 +15,9 @@
%a.nav-link{ href: '#create-cluster-pane', id: 'create-cluster-tab', class: active_when(active_tab == 'create'), data: { toggle: 'tab' }, role: 'tab' }
%span
- if create_eks_enabled
- - if @gke_selected
- = create_on_gke_tab_label
- - elsif @eks_selected
- = create_on_eks_tab_label
- - else
- = create_new_cluster_label
+ = create_new_cluster_label(provider: params[:provider])
- else
- = create_on_gke_tab_label
+ = create_new_cluster_label(provider: 'gcp')
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#add-cluster-pane', id: 'add-cluster-tab', class: active_when(active_tab == 'add'), data: { toggle: 'tab' }, role: 'tab' }
%span Add existing cluster
@@ -33,27 +25,10 @@
.tab-content.gitlab-tab-content
- if create_eks_enabled
.tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
- - if @gke_selected
- = render 'clusters/clusters/gcp/header'
- - if @valid_gcp_token
- = render 'clusters/clusters/gcp/form'
- - elsif @authorize_url
- = render 'clusters/clusters/gcp/signin_with_google_button'
- - else
- = render 'clusters/clusters/gcp/gcp_not_configured'
- - elsif @eks_selected
- = render 'clusters/clusters/eks/index'
- - else
- = render 'clusters/clusters/cloud_providers/cloud_provider_selector'
+ = render new_cluster_partial(provider: params[:provider])
- else
.tab-pane{ id: 'create-cluster-pane', class: active_when(active_tab == 'create'), role: 'tabpanel' }
- = render 'clusters/clusters/gcp/header'
- - if @valid_gcp_token
- = render 'clusters/clusters/gcp/form'
- - elsif @authorize_url
- = render 'clusters/clusters/gcp/signin_with_google_button'
- - else
- = render 'clusters/clusters/gcp/gcp_not_configured'
+ = render new_cluster_partial(provider: 'gcp')
.tab-pane{ id: 'add-cluster-pane', class: active_when(active_tab == 'add'), role: 'tabpanel' }
= render 'clusters/clusters/user/header'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 31d5f592d75..5beeaf7259a 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -12,11 +12,13 @@
install_helm_path: clusterable.install_applications_cluster_path(@cluster, :helm),
install_ingress_path: clusterable.install_applications_cluster_path(@cluster, :ingress),
install_cert_manager_path: clusterable.install_applications_cluster_path(@cluster, :cert_manager),
+ install_crossplane_path: clusterable.install_applications_cluster_path(@cluster, :crossplane),
install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
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),
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/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index a6acf948ed4..39b6d74d9f9 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,7 +1,7 @@
-- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'add-existing-kubernetes-cluster'), target: '_blank'
-- rbac_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
+- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/add_remove_clusters.md',
+ anchor: 'add-existing-cluster'), target: '_blank'
+- rbac_help_link = link_to _('More information'), help_page_path('user/project/clusters/add_remove_clusters.md',
+ anchor: 'access-controls'), target: '_blank'
- api_url_help_text = s_('ClusterIntegration|The URL used to access the Kubernetes API.')
- ca_cert_help_text = s_('ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster.')
diff --git a/app/views/clusters/clusters/user/_header.html.haml b/app/views/clusters/clusters/user/_header.html.haml
index 3b9ceaa2b8a..b0a24ee464f 100644
--- a/app/views/clusters/clusters/user/_header.html.haml
+++ b/app/views/clusters/clusters/user/_header.html.haml
@@ -1,5 +1,5 @@
%h4
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
- - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/index', anchor: 'add-existing-kubernetes-cluster'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_help_page = link_to(s_('ClusterIntegration|documentation'), help_page_path('user/project/clusters/add_remove_clusters', anchor: 'add-existing-cluster'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes').html_safe % { link_to_help_page: link_to_help_page }
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index a2b1f0d9298..b5f5025b581 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -3,7 +3,7 @@
.container.section-body
.row
.blank-state-welcome.w-100
- %h2.blank-state-welcome-title
+ %h2.blank-state-welcome-title{ data: { qa_selector: 'welcome_title_content' } }
= _('Welcome to GitLab')
%p.blank-state-text
= _('Faster releases. Better code. Less pain.')
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 8f6c3ecbe58..fd6d8f3f769 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,13 +1,13 @@
- page_title "Sign in"
#signin-container
- - if form_based_providers.any?
+ - if any_form_based_providers_enabled?
= render 'devise/shared/tabs_ldap'
- else
- unless experiment_enabled?(:signup_flow)
= render 'devise/shared/tabs_normal'
.tab-content
- - if password_authentication_enabled_for_web? || ldap_enabled? || crowd_enabled?
+ - if password_authentication_enabled_for_web? || ldap_sign_in_enabled? || crowd_enabled?
= render 'devise/shared/signin_box'
-# Signup only makes sense if you can also sign-in
@@ -15,7 +15,7 @@
= render 'devise/shared/signup_box'
-# Show a message if none of the mechanisms above are enabled
- - if !password_authentication_enabled_for_web? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
+ - if !password_authentication_enabled_for_web? && !ldap_sign_in_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 746d43edbad..6ddb7e1ac48 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -1,4 +1,4 @@
-- if form_based_providers.any?
+- 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
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index db54c166a53..b8f0cd2a91a 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,4 +1,4 @@
-%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
+%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'
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index ae055f398ac..13f07e2f5d5 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -11,5 +11,5 @@
= form_tag search_path, method: :get, class: 'form-inline-flex' do |f|
.field
= search_field_tag :search, '', placeholder: _('Search for projects, issues, etc.'), class: 'form-control'
- = button_tag 'Search', class: 'btn btn-success', name: nil, type: 'submit'
+ = button_tag _('Search'), class: 'btn btn-sm btn-success', name: nil, type: 'submit'
= render 'errors/footer'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index bf077eb09d2..1cb1cc45bdb 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -22,4 +22,10 @@
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
- = render 'shared/issues'
+ - if Feature.enabled?(:vue_issuables_list, @group)
+ .js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)),
+ 'can-bulk-edit': @can_bulk_update.to_json,
+ 'empty-svg-path': image_path('illustrations/issues.svg'),
+ 'sort-key': @sort } }
+ - else
+ = render 'shared/issues'
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 23b1a22240f..33e68bc766e 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,4 @@
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
-= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.legacy_group_milestone?
+= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index a3f35b72cc6..81bd15ed287 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -31,7 +31,8 @@
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Register and see your runners for this group.')
+ = _("Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project.")
+ = link_to s_('More information'), help_page_path('ci/runners/README')
.settings-content
= render 'groups/runners/index'
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index f1ba804f920..5f8f2333e40 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -218,7 +218,7 @@
%tr
%td.shortcut
%kbd esc
- %td= _('Go back (while searching for files')
+ %td= _('Go back (while searching for files)')
%tr
%td.shortcut
%kbd y
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 78c7fadb019..b515ce084e4 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -13,7 +13,7 @@
.form-group
= label_tag :manifest, class: 'label-bold' do
= _('Manifest')
- = file_field_tag :manifest, class: 'form-control-file', required: true
+ = file_field_tag :manifest, class: 'form-control-file w-auto', required: true
.form-text.text-muted
= _('Import multiple repositories by uploading a manifest file.')
= link_to icon('question-circle'), help_page_path('user/project/import/manifest')
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 92572f0308c..a0b030fa3b2 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -3,7 +3,7 @@
- flash.each do |key, value|
-# Don't show a flash message if the message is nil
- if value
- %div{ class: "flash-content flash-#{key} rounded" }
+ %div{ class: "flash-#{key} mb-2" }
%span= value
%div{ class: "close-icon-wrapper js-close-icon" }
= sprite_icon('close', size: 16, css_class: 'close-icon')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index b8c9f0ae1e8..0060d8323b0 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -57,7 +57,7 @@
= yield :library_javascripts
= javascript_include_tag locale_path unless I18n.locale == :en
- = webpack_bundle_tag "raven" if Gitlab.config.sentry.enabled
+ = webpack_bundle_tag "sentry" if Gitlab.config.sentry.enabled
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
@@ -89,4 +89,4 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
- = render_if_exists 'layouts/snowplow'
+ = render 'layouts/snowplow'
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index 6e8294d6adc..24b8138078d 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -1,80 +1,48 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{ lang: "en" }
%head
%meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }/
%meta{ content: "width=device-width, initial-scale=1", name: "viewport" }/
%meta{ content: "IE=edge", "http-equiv" => "X-UA-Compatible" }/
%title= message.subject
- :css
- /* CLIENT-SPECIFIC STYLES */
- body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
- table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
- img { -ms-interpolation-mode: bicubic; }
- .hidden {
- display: none !important;
- visibility: hidden !important;
- }
- /* iOS BLUE LINKS */
- a[x-apple-data-detectors] {
- color: inherit !important;
- text-decoration: none !important;
- font-size: inherit !important;
- font-family: inherit !important;
- font-weight: inherit !important;
- line-height: inherit !important;
- }
+ -# Avoid premailer processing of client-specific styles (@media tag not supported)
+ -# We need to inline the contents here because mail clients (e.g. iOS Mail, Outlook)
+ -# do not support linked stylesheets.
+ %style{ type: 'text/css', 'data-premailer': 'ignore' }
+ = asset_to_string('mailer_client_specific.css').html_safe
- /* ANDROID MARGIN HACK */
- body { margin:0 !important; }
- div[style*="margin: 16px 0"] { margin:0 !important; }
-
- @media only screen and (max-width: 639px) {
- body, #body {
- min-width: 320px !important;
- }
- table.wrapper {
- width: 100% !important;
- min-width: 320px !important;
- }
- table.wrapper > tbody > tr > td {
- border-left: 0 !important;
- border-right: 0 !important;
- border-radius: 0 !important;
- padding-left: 10px !important;
- padding-right: 10px !important;
- }
- }
- %body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
+ = stylesheet_link_tag 'mailer.css'
+ %body
+ %table#body{ border: "0", cellpadding: "0", cellspacing: "0" }
%tbody
%tr.line
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#6b4fbb;height:4px;font-size:4px;line-height:4px;" }
+ %td
%tr.header
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %td
= html_header_message
= header_logo
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
- %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:640px;margin:0 auto;border-collapse:separate;border-spacing:0;" }
+ %td
+ %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0" }
%tbody
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;background-color:#ffffff;text-align:left;padding:18px 25px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
- %table.content{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:separate;border-spacing:0;" }
+ %td.wrapper-cell
+ %table.content{ border: "0", cellpadding: "0", cellspacing: "0" }
%tbody
= yield
= render_if_exists 'layouts/mailer/additional_text'
%tr.footer
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
- %img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
+ %td
+ %img{ alt: "GitLab", height: "33", width: "90", src: image_url('mailers/gitlab_footer_logo.gif') }
%div
- - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
- - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
+ - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link')
+ - help_link = link_to(_("Help"), help_url, class: 'help-link')
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
= yield :additional_footer
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
+ %td.footer-message
= html_footer_message
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 6cdb85456c3..443a73f5cce 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -17,6 +17,4 @@
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= render "layouts/flash", extra_flash_class: 'limit-container-width'
- - if Gitlab.com?
- = render_if_exists "layouts/privacy_policy_update_callout"
= yield
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index efe74ddd902..d15f0ae3228 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -18,6 +18,11 @@
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
+ - if current_user_menu?(:start_trial)
+ %li
+ %a.profile-link{ href: trials_link_url }
+ = s_("CurrentUser|Start a Gold trial")
+ = emoji_icon('rocket')
- if current_user_menu?(:settings)
%li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
@@ -35,8 +40,8 @@
%li.d-md-none
= render 'shared/user_dropdown_contributing_link'
= render_if_exists 'shared/user_dropdown_instance_review'
- - if Gitlab.com?
- %li.js-canary-link.d-md-none
+ - if Gitlab.com_but_not_canary?
+ %li.d-md-none
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
- if current_user_menu?(:sign_out)
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index d8697be7f7a..5719fb24b89 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -18,8 +18,8 @@
- if logo_text.present?
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
- - if Gitlab.com?
- = link_to 'https://next.gitlab.com', class: 'label-link js-canary-badge canary-badge bg-transparent hidden', target: :_blank do
+ - if Gitlab.com_and_canary?
+ = link_to 'https://next.gitlab.com', class: 'label-link canary-badge bg-transparent', target: :_blank do
%span.color-label.has-tooltip.badge.badge-pill.green-badge
= _('Next')
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 71977b23481..93854c212df 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -12,6 +12,6 @@
%li
= render 'shared/user_dropdown_contributing_link'
= render_if_exists 'shared/user_dropdown_instance_review'
- - if Gitlab.com?
- %li.js-canary-link
+ - if Gitlab.com_but_not_canary?
+ %li
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5122c2517aa..d339751848b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -55,15 +55,15 @@
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do
= _('Admin Area')
- - if Feature.enabled?(:user_mode_in_session)
- - if header_link?(:admin_mode)
- = nav_link(controller: 'admin/sessions') do
- = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
- = _('Leave admin mode')
- - elsif current_user.admin?
- = nav_link(controller: 'admin/sessions') do
- = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
- = _('Enter admin mode')
+ - if Feature.enabled?(:user_mode_in_session)
+ - if header_link?(:admin_mode)
+ = nav_link(controller: 'admin/sessions') do
+ = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
+ = _('Leave Admin Mode')
+ - elsif current_user.admin?
+ = nav_link(controller: 'admin/sessions') do
+ = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
+ = _('Enter Admin Mode')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, class: 'admin-icon' do
@@ -74,6 +74,15 @@
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin', size: 18)
+ - if Feature.enabled?(:user_mode_in_session)
+ - if header_link?(:admin_mode)
+ = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+ = link_to destroy_admin_session_path, title: _('Leave Admin Mode'), aria: { label: _('Leave Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = sprite_icon('lock-open', size: 18)
+ - elsif current_user.admin?
+ = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+ = link_to new_admin_session_path, title: _('Enter Admin Mode'), aria: { label: _('Enter Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = sprite_icon('lock', size: 18)
-# Shortcut to Dashboard > Projects
- if dashboard_nav_link?(:projects)
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 4930c6cf5f7..a6d2c894185 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -16,13 +16,19 @@
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
- = _('Overview')
+ - if @group.subgroup?
+ = _('Subgroup overview')
+ - else
+ = _('Group overview')
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#show', 'groups#details', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
= link_to group_path(@group) do
%strong.fly-out-top-item-name
- = _('Overview')
+ - if @group.subgroup?
+ = _('Subgroup overview')
+ - else
+ = _('Group overview')
%li.divider.fly-out-top-item
= nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index c84bc0b5cd4..9b3ad05d0c0 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,13 +13,13 @@
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
- = _('Project')
+ = _('Project overview')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
- = _('Project')
+ = _('Project overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
@@ -163,7 +163,7 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do
- = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines' do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines rspec-link-pipelines', data: { qa_selector: 'ci_cd_link' } do
.nav-icon-container
= sprite_icon('rocket')
%span.nav-item-name#js-onboarding-pipelines-link
@@ -247,6 +247,8 @@
%span
= _('Serverless')
+ = render_if_exists 'layouts/nav/sidebar/pod_logs_link' # EE-specific
+
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
@@ -264,7 +266,7 @@
dismiss_endpoint: user_callouts_path } }
- if show_cluster_hint
.feature-highlight-popover-content
- = image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration'
+ = image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration', lazy: false, alt: _('Kubernetes popover')
.feature-highlight-popover-sub-content
%p= _('Allows you to add and manage Kubernetes clusters.')
%p
@@ -347,7 +349,7 @@
= _('Members')
- if can_edit
= nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do
- = link_to project_settings_integrations_path(@project), title: _('Integrations') do
+ = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do
%span
= _('Integrations')
= nav_link(controller: :repository) do
diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml
index 71c9c50071a..11661a423dd 100644
--- a/app/views/notify/member_access_denied_email.html.haml
+++ b/app/views/notify/member_access_denied_email.html.haml
@@ -1,4 +1,7 @@
-%p
- Your request to join the
- #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}
- has been denied.
+%tr
+ %td.text-content
+ %p
+ Your request to join the
+ #{link_to member_source.human_name, member_source.web_url, class: :highlight} #{member_source.model_name.singular}
+ has been #{content_tag :span, 'denied', class: :highlight}.
+
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
index 1c50dba9c97..e28a10a243f 100644
--- a/app/views/notify/member_access_granted_email.html.haml
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -1,10 +1,14 @@
- link_end = '</a>'.html_safe
- source_type = member_source.model_name.singular
- leave_link = polymorphic_url([member_source], leave: 1)
-- source_link = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer')
+- source_link = link_to(member_source.human_name, member_source.web_url, target: '_blank', rel: 'noopener noreferrer', class: :highlight)
+- access_level = content_tag(:span, member.human_access, class: :highlight)
+
+%tr
+ %td.text-content
+ %p
+ = _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: access_level, source_link: source_link, source_type: source_type }
+ %p
+ - leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
+ = _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }
-%p
- = _('You have been granted %{access_level} access to the %{source_link} %{source_type}.').html_safe % { access_level: member.human_access, source_link: source_link, source_type: source_type }
-%p
- - leave_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: leave_link }
- = _('If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}.').html_safe % { source_type: source_type, leave_link_start: leave_link_start, link_end: link_end }
diff --git a/app/views/notify/member_access_requested_email.html.haml b/app/views/notify/member_access_requested_email.html.haml
index 76f1f08a0cb..43f25af3dba 100644
--- a/app/views/notify/member_access_requested_email.html.haml
+++ b/app/views/notify/member_access_requested_email.html.haml
@@ -1,3 +1,6 @@
-%p
- #{link_to member.user.name, member.user} requested #{member.human_access}
- access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members])} #{member_source.model_name.singular}.
+%tr
+ %td.text-content
+ %p
+ #{link_to member.user.name, member.user, class: :highlight} requested #{content_tag :span, member.human_access, class: :highlight}
+ access to the #{link_to member_source.human_name, polymorphic_url([member_source, :members]), class: :highlight} #{member_source.model_name.singular}.
+
diff --git a/app/views/notify/member_invite_accepted_email.html.haml b/app/views/notify/member_invite_accepted_email.html.haml
index 2d1d40881eb..0abb79000e0 100644
--- a/app/views/notify/member_invite_accepted_email.html.haml
+++ b/app/views/notify/member_invite_accepted_email.html.haml
@@ -1,5 +1,8 @@
-%p
- #{member.invite_email}, now known as
- #{link_to member.user.name, user_url(member.user)},
- has accepted your invitation to join the
- #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
+%tr
+ %td.text-content
+ %p
+ #{content_tag :span, member.invite_email, class: :highlight}, now known as
+ #{link_to member.user.name, user_url(member.user)},
+ has accepted your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url, class: :highlight} #{member_source.model_name.singular}.
+
diff --git a/app/views/notify/member_invite_declined_email.html.haml b/app/views/notify/member_invite_declined_email.html.haml
index aa1b373d1a6..5e626767235 100644
--- a/app/views/notify/member_invite_declined_email.html.haml
+++ b/app/views/notify/member_invite_declined_email.html.haml
@@ -1,4 +1,7 @@
-%p
- #{@invite_email}
- has declined your invitation to join the
- #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
+%tr
+ %td.text-content
+ %p
+ #{content_tag :span, @invite_email, class: :highlight}
+ has #{content_tag :span, 'declined', class: :highlight} your invitation to join the
+ #{link_to member_source.human_name, member_source.web_url, class: :highlight} #{member_source.model_name.singular}.
+
diff --git a/app/views/notify/member_invited_email.html.haml b/app/views/notify/member_invited_email.html.haml
index 6730172242b..ae3fecf404a 100644
--- a/app/views/notify/member_invited_email.html.haml
+++ b/app/views/notify/member_invited_email.html.haml
@@ -1,13 +1,16 @@
-%p
- You have been invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_url(member.created_by)
- to join the
- = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token)
- #{member_source.model_name.singular} as #{member.human_access}.
+%tr
+ %td.text-content
+ %p
+ You have been invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_url(member.created_by)
+ to join the
+ = link_to member_source.human_name, member_source.public? ? member_source.web_url : invite_url(@token), class: :highlight
+ #{member_source.model_name.singular} as #{content_tag :span, member.human_access, class: :highlight}.
+
+ %p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
-%p
- = link_to 'Accept invitation', invite_url(@token)
- or
- = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/profiles/preferences/_sourcegraph.html.haml b/app/views/profiles/preferences/_sourcegraph.html.haml
new file mode 100644
index 00000000000..20a904694ca
--- /dev/null
+++ b/app/views/profiles/preferences/_sourcegraph.html.haml
@@ -0,0 +1,26 @@
+- return unless Gitlab::Sourcegraph::feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled
+- sourcegraph_url = Gitlab::CurrentSettings.sourcegraph_url
+
+.col-sm-12
+ %hr
+
+.col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = s_('Preferences|Integrations')
+ %p
+ = s_('Preferences|Customize integrations with third party services.')
+ = succeed '.' do
+ = link_to _('Learn more'), help_page_path('user/profile/preferences.md', anchor: 'integrations'), target: '_blank'
+.col-lg-8
+ %label.label-bold
+ = s_('Preferences|Sourcegraph')
+ = link_to icon('question-circle'), help_page_path('user/profile/preferences.md', anchor: 'sourcegraph'), target: '_blank', class: 'has-tooltip', title: _('More information')
+ .form-group.form-check
+ = f.check_box :sourcegraph_enabled, class: 'form-check-input'
+ = f.label :sourcegraph_enabled, class: 'form-check-label' do
+ - link_start = '<a href="%{url}">'.html_safe % { url: sourcegraph_url }
+ - link_end = '</a>'.html_safe
+ = s_('Preferences|Enable integrated code intelligence on code views').html_safe % { link_start: link_start, link_end: link_end }
+ .form-text.text-muted
+ = sourcegraph_url_message
+ = sourcegraph_experimental_message
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 84657592cd8..bf76b7379dd 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -111,6 +111,9 @@
= time_display_label
.form-text.text-muted
= s_('Preferences|For example: 30 mins ago.')
+
+ = render 'sourcegraph', f: f
+
.col-lg-4.profile-settings-sidebar
.col-lg-8
.form-group
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 68b7efc6fb4..cfad274f91d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -94,7 +94,7 @@
- else
= f.text_field :name, label: s_('Profiles|Full name'), required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name rspec-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you")
= f.text_field :id, readonly: true, label: s_('Profiles|User ID'), wrapper: { class: 'col-md-3' }
- = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'input-md'
+ = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { prompt: _('Select your role') }, required: true, class: 'input-md'
= render_if_exists 'profiles/email_settings', form: f
= f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username")
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index e4129a91daf..2e00632892b 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -14,7 +14,7 @@
%li= desc
%p= _('The following items will NOT be exported:')
%ul
- %li= _('Job traces and artifacts')
+ %li= _('Job logs and artifacts')
%li= _('Container registry images')
%li= _('CI variables')
%li= _('Webhooks')
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 95fdad125a7..20d4084f428 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -15,15 +15,13 @@
= render 'shared/commit_well', commit: commit, ref: ref, project: project
- if is_project_overview
- .project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list_enabled?) }
+ .project-buttons.append-bottom-default{ class: ("js-show-on-project-root" if vue_file_list_enabled?) }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
- if vue_file_list_enabled?
- #js-tree-list{ data: { project_path: @project.full_path, project_short_path: @project.path, ref: ref, full_name: @project.name_with_namespace } }
+ #js-tree-list{ data: vue_file_list_data(project, ref) }
- if can_edit_tree?
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
= render 'projects/blob/new_dir'
- - if @tree.readme
- = render "projects/tree/readme", readme: @tree.readme
- else
= render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 4783b10cf6d..b7c4114d485 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -3,7 +3,7 @@
- max_project_topic_length = 15
- emails_disabled = @project.emails_disabled?
-.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
+.project-home-panel{ class: [("empty-project" if empty_repo), ("js-show-on-project-root" if vue_file_list_enabled?)] }
.row.append-bottom-8
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
diff --git a/app/views/projects/_merge_request_merge_options_settings.html.haml b/app/views/projects/_merge_request_merge_options_settings.html.haml
index 5ab475822de..047b4dafbfc 100644
--- a/app/views/projects/_merge_request_merge_options_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_options_settings.html.haml
@@ -12,3 +12,9 @@
= form.check_box :printing_merge_request_link_enabled, class: 'form-check-input'
= form.label :printing_merge_request_link_enabled, class: 'form-check-label' do
= s_('ProjectSettings|Show link to create/view merge request when pushing from the command line')
+ .form-check.mb-2
+ = form.check_box :remove_source_branch_after_merge, class: 'form-check-input'
+ = form.label :remove_source_branch_after_merge, class: 'form-check-label' do
+ = s_("ProjectSettings|Enable 'Delete source branch' option by default")
+ .descr.text-secondary
+ = s_('ProjectSettings|Existing merge requests and protected branches are not affected')
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index f2215765974..30fe5622ebd 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -19,7 +19,7 @@
= author_avatar(commit, size: 36, has_tooltip: false)
.commit-row-title
%span.item-title.str-truncated-100
- = link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
+ = link_to commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
.float-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 84ccd816d80..77245114772 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -6,17 +6,18 @@
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
- = copy_blob_source_button(blob) unless blame
- = open_raw_blob_button(blob)
- = download_blob_button(blob)
- = view_on_environment_button(@commit.sha, @path, @environment) if @environment
- .btn-group{ role: "group" }<
- = render_if_exists 'projects/blob/header_file_locks_link'
= edit_blob_button
= ide_edit_button
+ .btn-group{ role: "group" }<
+ = render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
= replace_blob_link
= delete_blob_link
+ .btn-group{ role: "group" }<
+ = copy_blob_source_button(blob) unless blame
+ = open_raw_blob_button(blob)
+ = download_blob_button(blob)
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
= render 'projects/fork_suggestion'
= render_if_exists 'projects/blob/header_file_locks', project: @project, path: @path
diff --git a/app/views/projects/blob/_markdown_buttons.html.haml b/app/views/projects/blob/_markdown_buttons.html.haml
index 28d1ff97825..44ec2fa69cb 100644
--- a/app/views/projects/blob/_markdown_buttons.html.haml
+++ b/app/views/projects/blob/_markdown_buttons.html.haml
@@ -6,7 +6,7 @@
= markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
- = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: _("Add a task list") })
+ = markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: _("Add a task list") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index abef33ca01c..ed22573b23e 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -25,5 +25,3 @@
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
-
-= render_if_exists 'shared/geo_info_modal', project: project
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 96df3cd18fe..e8aff58b505 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -12,11 +12,14 @@
%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 directory? && Feature.enabled?(:git_archive_path, default_enabled: true)
- %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 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 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/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index b256d94065b..990f3ff526b 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -1,6 +1,4 @@
-- formats = [['zip', 'btn-primary'], ['tar.gz'], ['tar.bz2'], ['tar']]
-
.btn-group.ml-0.w-100
- - formats.each do |(fmt, extra_class)|
+ - Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
- = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
+ = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "btn btn-xs #{"btn-primary" if index == 0}"
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 9744d293c8b..c8c96297672 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -8,8 +8,9 @@
.input-group-text
= s_("CompareBranches|Source")
= hidden_field_tag :to, params[:to]
- = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip monospace", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
- .dropdown-toggle-text.str-truncated= params[:to] || _("Select branch/tag")
+ = button_tag type: 'button', title: params[:to], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag")
+ = sprite_icon('arrow-down', size: 16, css_class: 'float-right')
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
@@ -18,8 +19,9 @@
.input-group-text
= s_("CompareBranches|Target")
= hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip monospace", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag")
+ = button_tag type: 'button', title: params[:from], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag")
+ = sprite_icon('arrow-down', size: 16, css_class: 'float-right')
= render 'shared/ref_dropdown'
&nbsp;
= button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn"
diff --git a/app/views/projects/deployments/_confirm_rollback_modal.html.haml b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
index ff40e404e5f..9162827b501 100644
--- a/app/views/projects/deployments/_confirm_rollback_modal.html.haml
+++ b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
@@ -13,7 +13,7 @@
%p= s_('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?').html_safe % {commit_id: commit_sha}
- else
%p
- = s_('Environments|This action will run the job defined by staging 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?').html_safe % {commit_id: commit_sha}
+ = s_('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?').html_safe % {commit_id: commit_sha, environment_name: @environment.name}
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-danger' do
diff --git a/app/views/projects/environments/empty_logs.html.haml b/app/views/projects/environments/empty_logs.html.haml
new file mode 100644
index 00000000000..602dc908b75
--- /dev/null
+++ b/app/views/projects/environments/empty_logs.html.haml
@@ -0,0 +1,14 @@
+- page_title _('Pod logs')
+
+.row.empty-state
+ .col-sm-12
+ .svg-content
+ = image_tag 'illustrations/operations_log_pods_empty.svg'
+ .col-12
+ .text-content
+ %h4.text-center
+ = s_('Environments|No deployed environments')
+ %p.state-description.text-center
+ = s_('Logs|To see the pod logs, deploy your code to an environment.')
+ .text-center
+ = link_to s_('Environments|Learn about environments'), help_page_path('ci/environments'), class: 'btn btn-success'
diff --git a/app/views/projects/environments/empty.html.haml b/app/views/projects/environments/empty_metrics.html.haml
index 129dbbf4e56..dad93290fbd 100644
--- a/app/views/projects/environments/empty.html.haml
+++ b/app/views/projects/environments/empty_metrics.html.haml
@@ -7,8 +7,8 @@
.col-12
.text-content
%h4.text-center
- = s_('Metrics|No deployed environments')
+ = s_('Environments|No deployed environments')
%p.state-description
= s_('Metrics|Check out the CI/CD documentation on deploying to an environment')
.text-center
- = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
+ = link_to s_("Environments|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success'
diff --git a/app/views/projects/error_tracking/details.html.haml b/app/views/projects/error_tracking/details.html.haml
new file mode 100644
index 00000000000..640746ad8f6
--- /dev/null
+++ b/app/views/projects/error_tracking/details.html.haml
@@ -0,0 +1,4 @@
+- page_title _('Error Details')
+- add_to_breadcrumbs 'Errors', project_error_tracking_index_path(@project)
+
+#js-error_details{ data: error_details_data(@current_user, @project) }
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 6e5e4607232..a952db0eea3 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,26 +1,8 @@
- page_title _('Contributors')
-.js-graphs-show{ 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
- .sub-header-block
- .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'
+.sub-header-block.bg-gray-light.gl-p-3
+ .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'
- .loading-graph
- .center
- %h3.page-title
- %i.fa.fa-spinner.fa-spin
- = s_('ContributorsPage|Building repository graph.')
- %p.slead
- = s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
-
- .stat-graph.hide
- .header.clearfix
- %h3#date_header.page-title
- %p.light
- = s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
- %input#brush_change{ :type => "hidden" }
- .graphs.row
- #contributors-master.svg-w-100
- #contributors.clearfix
- %ol.contributors-list.svg-w-100.row
+.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json),'data-project-branch': current_ref }
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 367b8c1138e..c8ab47888d0 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,4 +1,5 @@
-%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
+-# DANGER: Any changes to this file need to be reflected in issuables_list/components/issuable.vue!
+%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue', qa_issue_title: issue.title } }
.issue-box
- if @can_bulk_update
.issue-check.hidden
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 0328751c68c..0373e37818d 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -26,7 +26,7 @@
= render partial: 'shared/label', collection: @prioritized_labels, as: :label, locals: { force_priority: true, subject: @project }
- elsif search.present?
.nothing-here-block
- = _('No prioritised labels with such name or description')
+ = _('No prioritized labels with such name or description')
- if @labels.present?
.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 57205682bda..9cdbbe7204b 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -12,8 +12,8 @@
= clipboard_button(target: "pre#merge-info-1", title: _("Copy commands"))
%pre.dark#merge-info-1
- if @merge_request.for_fork?
+ -# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch)
:preserve
- -# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch)
git fetch "#{h default_url_to_repo(@merge_request.source_project)}" "#{h @merge_request.source_branch}"
git checkout -b "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" FETCH_HEAD
- else
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index 543441b9479..15c83f92474 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -1,15 +1,5 @@
%h3.page-title
New Merge Request
-%p.slead
- - source_title, target_title = format_mr_branch_names(@merge_request)
- From
- %strong.ref-name= source_title
- %span into
- %strong.ref-name= target_title
-
- %span.float-right
- = link_to 'Change branches', mr_change_branches_path(@merge_request)
-%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter
= f.hidden_field :source_project_id
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 03159f123f3..318c9d809c1 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -2,5 +2,4 @@
%h3.page-title
Edit Merge Request #{@merge_request.to_reference}
-%hr
= render 'form'
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 49d3039d0c9..5f244d3a6c3 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,57 +3,8 @@
- page_title @milestone.title, _('Milestones')
- page_description @milestone.description
-.detail-page-header.milestone-page-header
- .status-box{ class: status_box_class(@milestone) }
- - if @milestone.closed?
- = _('Closed')
- - elsif @milestone.expired?
- = _('Past due')
- - elsif @milestone.upcoming?
- = _('Upcoming')
- - else
- = _('Open')
- .header-text-content
- %span.identifier
- %strong
- = _('Milestone')
- - if @milestone.due_date || @milestone.start_date
- = milestone_date_range(@milestone)
- .milestone-buttons
- - if can?(current_user, :admin_milestone, @project)
- = link_to edit_project_milestone_path(@project, @milestone), class: 'btn btn-grouped btn-nr' do
- = _('Edit')
-
- - if @project.group
- %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
- target: '#promote-milestone-modal',
- milestone_title: @milestone.title,
- group_name: @project.group.name,
- url: promote_project_milestone_path(@milestone.project, @milestone),
- container: 'body' },
- disabled: true,
- type: 'button' }
- = _('Promote')
- #promote-milestone-modal
-
- - if @milestone.active?
- = link_to _('Close milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: 'btn btn-close btn-nr btn-grouped'
- - else
- = link_to _('Reopen milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: 'btn btn-reopen btn-nr btn-grouped'
-
- = render 'shared/milestones/delete_button'
-
- %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: '#' }
- = icon('angle-double-left')
-
-.detail-page-description.milestone-detail
- %h2.title.qa-milestone-title
- = markdown_field(@milestone, :title)
-
- %div
- - if @milestone.description.present?
- .description.md
- = markdown_field(@milestone, :description)
+= render 'shared/milestones/header', milestone: @milestone
+= render 'shared/milestones/description', milestone: @milestone
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index ee82d68d398..a6978cba495 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -3,10 +3,12 @@
.form-group
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
- = f.select :auth_method,
- options_for_select(auth_options, mirror.auth_method),
- {}, { class: "form-control js-mirror-auth-type qa-authentication-method" }
- = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
+ .select-wrapper
+ = f.select :auth_method,
+ options_for_select(auth_options, mirror.auth_method),
+ {}, { class: "form-control select-control js-mirror-auth-type qa-authentication-method" }
+ = icon('chevron-down')
+ = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
.collapse.js-well-changing-auth
diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml
index b49f1d9315e..dd794e03f48 100644
--- a/app/views/projects/mirrors/_mirror_repos_form.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml
@@ -1,5 +1,7 @@
.form-group
= label_tag :mirror_direction, _('Mirror direction'), class: 'label-light'
- = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction qa-mirror-direction', disabled: true
+ .select-wrapper
+ = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control select-control js-mirror-direction qa-mirror-direction', disabled: true
+ = icon('chevron-down')
= render partial: "projects/mirrors/mirror_repos_push", locals: { f: f }
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 178f0acc5b9..08dcba2afd7 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -13,5 +13,11 @@
- @project.pages_domains.each do |domain|
%p
= external_link(domain.url, domain.url)
+ - unless @project.public_pages?
+ .card-footer.alert-warning
+ - help_page = help_page_path('/user/project/pages/pages_access_control')
+ - link_start = '<a href="%{url}" target="_blank" class="alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page }
+ - link_end = '</a>'.html_safe
+ = s_('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.').html_safe % { link_start: link_start, link_end: link_end, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
.card-footer.alert-primary
= s_('GitLabPages|It may take up to 30 minutes before the site is available after the first deployment.')
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index b05491f2c6e..4676c7399f1 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -21,11 +21,11 @@
%span.badge.badge-danger
= s_('GitLabPages|Expired')
%div
- = link_to s_('GitLabPages|Details'), project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped"
+ = link_to s_('GitLabPages|Edit'), edit_project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped btn-success btn-inverted"
= link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- if verification_enabled && domain.unverified?
%li.list-group-item.bs-callout-warning
- - details_link_start = "<a href='#{project_pages_domain_path(@project, domain)}'>".html_safe
+ - details_link_start = "<a href='#{edit_project_pages_domain_path(@project, domain)}'>".html_safe
- details_link_end = '</a>'.html_safe
= s_('GitLabPages|%{domain} is not verified. To learn how to verify ownership, visit your %{link_start}domain details%{link_end}.').html_safe % { domain: domain.domain,
link_start: details_link_start,
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 0e1f281410a..3ec87597849 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,23 +1,27 @@
- page_title 'Pages'
-%h3.page-title.with-button
- = s_('GitLabPages|Pages')
+- if @project.pages_enabled?
+ %h3.page-title.with-button
+ = s_('GitLabPages|Pages')
- - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
- = link_to new_project_pages_domain_path(@project), class: 'btn btn-success float-right', title: s_('GitLabPages|New Domain') do
- = s_('GitLabPages|New Domain')
+ - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
+ = link_to new_project_pages_domain_path(@project), class: 'btn btn-success float-right', title: s_('GitLabPages|New Domain') do
+ = s_('GitLabPages|New Domain')
-%p.light
- = s_('GitLabPages|With GitLab Pages you can host your static websites on GitLab. Combined with the power of GitLab CI and the help of GitLab Runner you can deploy static pages for your individual projects, your user or your group.')
-- if Gitlab.config.pages.external_https
- = render 'https_only'
+ %p.light
+ = s_('GitLabPages|With GitLab Pages you can host your static websites on GitLab. Combined with the power of GitLab CI and the help of GitLab Runner you can deploy static pages for your individual projects, your user or your group.')
+ - if Gitlab.config.pages.external_https
+ = render 'https_only'
-%hr.clearfix
+ %hr.clearfix
-= render 'access'
-= render 'use'
-- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
- = render 'list'
+ = render 'access'
+ = render 'use'
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
+ = render 'list'
+ - else
+ = render 'no_domains'
+ = render 'destroy'
- else
- = render 'no_domains'
-= render 'destroy'
+ .bs-callout.bs-callout-warning
+ = s_('GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project\'s %{strong_start}Settings > General > Visibility%{strong_end} page.').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
index 42631fca5e8..92d30e0b056 100644
--- a/app/views/projects/pages_domains/_certificate.html.haml
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -1,18 +1,63 @@
-- if @domain.auto_ssl_enabled?
- - if @domain.enabled?
- - if @domain.certificate_text
- %pre
- = @domain.certificate_text
- - else
- .bs-callout.bs-callout-info
- = _("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.")
+- auto_ssl_available = ::Gitlab::LetsEncrypt.enabled?
+- auto_ssl_enabled = @domain.auto_ssl_enabled?
+- auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled
+- has_user_defined_certificate = @domain.certificate && @domain.certificate_user_provided?
+
+- if auto_ssl_available
+ .form-group.border-section
+ .row
+ .col-sm-2
+ = _('Certificate')
+ .col-sm-10.js-auto-ssl-toggle-container
+ %label{ for: "pages_domain_auto_ssl_enabled_button" }
+ - lets_encrypt_link_url = "https://letsencrypt.org/"
+ - lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url }
+ - lets_encrypt_link_end = "</a>".html_safe
+ = _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end }
+ %button{ type: "button", id: "pages_domain_auto_ssl_enabled_button",
+ class: "js-project-feature-toggle project-feature-toggle mt-2 #{"is-checked" if auto_ssl_available_and_enabled}",
+ "aria-label": _("Automatic certificate management using Let's Encrypt") }
+ = f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
+ %span.toggle-icon
+ = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked")
+ = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked")
+ %p.text-secondary.mt-3
+ - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
+ - docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
+ - docs_link_end = "</a>".html_safe
+ = _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+
+.form-group.border-section.js-shown-unless-auto-ssl{ class: ("d-none" if auto_ssl_available_and_enabled) }
+ - if has_user_defined_certificate
+ .row
+ .col-sm-10.offset-sm-2
+ .card
+ .card-header
+ = _('Certificate')
+ .d-flex.justify-content-between.align-items-center.p-3
+ %span
+ = @domain.subject || _('missing')
+ = link_to _('Remove'),
+ clean_certificate_project_pages_domain_path(@project, @domain),
+ data: { confirm: _('Are you sure?') },
+ class: 'btn btn-remove btn-sm',
+ method: :delete
- else
- .bs-callout.bs-callout-warning
- = _("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.")
-- else
- - if @domain.certificate_text
- %pre
- = @domain.certificate_text
- - else
- .light
- = _("missing")
+ .row
+ .col-sm-10.offset-sm-2
+ = f.label :user_provided_certificate, _("Certificate (PEM)")
+ = f.text_area :user_provided_certificate,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a certificate for your domain with all intermediates")
+ .row
+ .col-sm-10.offset-sm-2
+ = f.label :user_provided_key, _("Key (PEM)")
+ = f.text_area :user_provided_key,
+ rows: 5,
+ class: "form-control js-enabled-unless-auto-ssl",
+ disabled: auto_ssl_available_and_enabled
+ %span.help-inline.text-muted= _("Upload a private key for your certificate")
+
+= render 'lets_encrypt_callout', auto_ssl_available_and_enabled: auto_ssl_available_and_enabled
diff --git a/app/views/projects/pages_domains/_dns.html.haml b/app/views/projects/pages_domains/_dns.html.haml
new file mode 100644
index 00000000000..e4e590f0a98
--- /dev/null
+++ b/app/views/projects/pages_domains/_dns.html.haml
@@ -0,0 +1,33 @@
+- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
+- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}."
+
+.form-group.border-section
+ .row
+ .col-sm-2
+ = _("DNS")
+ .col-sm-10
+ .input-group
+ = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-append
+ = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
+ %p.form-text.text-muted
+ = _("To access this domain create a new DNS record")
+- if verification_enabled
+ - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
+ .form-group.border-section
+ .row
+ .col-sm-2
+ = _("Verification status")
+ .col-sm-10
+ .status-badge
+ - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
+ .badge{ class: status }
+ = text
+ = link_to sprite_icon("redo"), verify_project_pages_domain_path(@project, @domain), method: :post, class: "btn has-tooltip", title: _("Retry verification")
+ .input-group
+ = text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-append
+ = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
+ %p.form-text.text-muted
+ - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
+ = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help }
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index 4aa1e574d93..e06dab9be06 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -3,62 +3,25 @@
- @domain.errors.full_messages.each do |msg|
= msg
-.form-group.row
- .col-sm-2.col-form-label
- = f.label :domain, _("Domain")
- .col-sm-10
- = f.text_field :domain, required: true, autocomplete: "off", class: "form-control", disabled: @domain.persisted?
-
-- if Gitlab.config.pages.external_https
-
- - auto_ssl_available = ::Gitlab::LetsEncrypt.enabled?
- - auto_ssl_enabled = @domain.auto_ssl_enabled?
- - auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled
-
- - if auto_ssl_available
- .form-group.row
- .col-sm-2.col-form-label
- %label{ for: "pages_domain_auto_ssl_enabled_button" }
- - lets_encrypt_link_url = "https://letsencrypt.org/"
- - lets_encrypt_link_start = "<a href=\"%{lets_encrypt_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { lets_encrypt_link_url: lets_encrypt_link_url }
- - lets_encrypt_link_end = "</a>".html_safe
- = _("Automatic certificate management using %{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end}").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: lets_encrypt_link_end }
-
- .col-sm-10.js-auto-ssl-toggle-container
- %button{ type: "button", id: "pages_domain_auto_ssl_enabled_button",
- class: "js-project-feature-toggle project-feature-toggle mt-2 #{"is-checked" if auto_ssl_available_and_enabled}",
- "aria-label": _("Automatic certificate management using Let's Encrypt") }
- = f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
- %span.toggle-icon
- = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked")
- = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked")
- %p.text-secondary.mt-3
- - docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
- - docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
- - docs_link_end = "</a>".html_safe
- = _("Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}.").html_safe % { docs_link_url: docs_link_url, docs_link_start: docs_link_start, docs_link_end: docs_link_end }
-
- .js-shown-unless-auto-ssl{ class: ("d-none" if auto_ssl_available_and_enabled) }
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :user_provided_certificate, _("Certificate (PEM)")
+.form-group.border-section
+ .row
+ - if @domain.persisted?
+ .col-sm-2
+ = _("Domain")
.col-sm-10
- = f.text_area :user_provided_certificate,
- rows: 5,
- class: "form-control js-enabled-unless-auto-ssl",
- disabled: auto_ssl_available_and_enabled
- %span.help-inline.text-muted= _("Upload a certificate for your domain with all intermediates")
-
- .form-group.row
- .col-sm-2.col-form-label
- = f.label :user_provided_key, _("Key (PEM)")
+ = external_link(@domain.url, @domain.url)
+ - else
+ .col-sm-2
+ = f.label :domain, _("Domain")
.col-sm-10
- = f.text_area :user_provided_key,
- rows: 5,
- class: "form-control js-enabled-unless-auto-ssl",
- disabled: auto_ssl_available_and_enabled
- %span.help-inline.text-muted= _("Upload a private key for your certificate")
+ .input-group
+ = f.text_field :domain, required: true, autocomplete: "off", class: "form-control"
+- if @domain.persisted?
+ = render 'dns'
+
+- if Gitlab.config.pages.external_https
+ = render 'certificate', f: f
- else
- .nothing-here-block
+ .border-section.nothing-here-block
= _("Support for custom certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/projects/pages_domains/_lets_encrypt_callout.html.haml b/app/views/projects/pages_domains/_lets_encrypt_callout.html.haml
new file mode 100644
index 00000000000..d6406a78fca
--- /dev/null
+++ b/app/views/projects/pages_domains/_lets_encrypt_callout.html.haml
@@ -0,0 +1,13 @@
+- if @domain.enabled?
+ - if @domain.auto_ssl_enabled && !@domain.certificate
+ .form-group.border-section.js-shown-if-auto-ssl{ class: ("d-none" unless auto_ssl_available_and_enabled) }
+ .row
+ .col-sm-10.offset-sm-2
+ .bs-callout.bs-callout-info.mt-0
+ = _("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.")
+- else
+ .form-group.border-section.js-shown-if-auto-ssl{ class: ("d-none" unless auto_ssl_available_and_enabled) }
+ .row
+ .col-sm-10.offset-sm-2
+ .bs-callout.bs-callout-warning.mt-0
+ = _("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.")
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
index 7c0777e5496..a08be65d7e4 100644
--- a/app/views/projects/pages_domains/edit.html.haml
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -1,12 +1,21 @@
- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
- page_title @domain.domain
+
+- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
+
+- if verification_enabled && @domain.unverified?
+ = content_for :flash_message do
+ .alert.alert-warning
+ .container-fluid.container-limited
+ = _("This domain is not verified. You will need to verify ownership before access is enabled.")
+
%h3.page-title
- = @domain.domain
+ = _('Pages Domain')
= render 'projects/pages_domains/helper_text'
-%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
- .form-actions
+ .form-actions.d-flex.justify-content-between
= f.submit _('Save Changes'), class: "btn btn-success"
+ = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-default btn-inverse'
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index e23ccb5d4c6..3210bfe9231 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -3,7 +3,6 @@
%h3.page-title
= _("New Pages Domain")
= render 'projects/pages_domains/helper_text'
-%hr.clearfix
%div
= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 33837e21c8d..8eec3d51835 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -58,4 +58,4 @@
%td
= _("Certificate")
%td
- = render 'certificate'
+ = render 'lets_encrypt_callout', auto_ssl_available_and_enabled: false
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 8c3518e3a29..4d8cba5168d 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,3 +1,5 @@
+- test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
+
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
@@ -12,6 +14,11 @@
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _('Failed Jobs')
%span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
+ - if test_reports_enabled
+ %li.js-tests-tab-link
+ = link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
+ = s_('TestReports|Tests')
+ %span.badge.badge-pill= pipeline.test_reports.total_count
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
@@ -32,10 +39,6 @@
%th
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- - elsif pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
- .bs-callout.bs-callout-warning
- = _("%{gitlab_ci_yml} not found in this commit") % { gitlab_ci_yml: ".gitlab-ci.yml" }
-
- if @pipeline.failed_builds.present?
#js-tab-failures.build-failures.tab-pane.build-page
%table.table.responsive-table.ci-table.responsive-table-sm-rounded
@@ -71,4 +74,7 @@
%pre.build-trace.build-trace-rounded
%code.bash.js-build-output
= build_summary(build)
+
+ #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/new.html.haml b/app/views/projects/pipelines/new.html.haml
index bfcaa09ae8c..a3e46a0939c 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -23,6 +23,13 @@
%label
= s_('Pipeline|Variables')
%ul.ci-variable-list
+ - if params[:var]
+ - params[:var].each do |variable|
+ = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
+ - if params[:file_var]
+ - params[:file_var].each do |variable|
+ - variable.push("file")
+ = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
= render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
.form-text.text-muted
= (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2b2133b8296..f0b3ab24ea0 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -20,4 +20,5 @@
- else
= render "projects/pipelines/with_tabs", pipeline: @pipeline
-.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
+.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json),
+ test_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 9dff251101b..f07de81d7fd 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -5,6 +5,7 @@
%p.settings-message.text-center
= s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.")
- else
+ .flash-container
%table.table.table-bordered
%colgroup
%col{ width: "20%" }
@@ -27,8 +28,6 @@
- if can_admin_project
%th
%tbody
- %tr
- %td.flash-container{ colspan: 5 }
= yield
= paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 959a2423e02..582f3d6fce4 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -6,7 +6,6 @@
- hide_class = 'd-none' if @service.activated? != value
%span.js-service-active-status{ class: hide_class, data: { value: value.to_s } }
= boolean_to_icon value
- %p= #{@service.description}.
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
index 7748a7a6a8e..3f33d72d3ec 100644
--- a/app/views/projects/services/_index.html.haml
+++ b/app/views/projects/services/_index.html.haml
@@ -21,7 +21,7 @@
%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) do
+ = 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
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index fc20bc52d1c..1e7903535c6 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,6 +1,7 @@
-- breadcrumb_title s_("ProjectService|Integrations")
+- breadcrumb_title @service.title
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
+- add_to_breadcrumbs(s_("ProjectService|Integrations"), namespace_project_settings_integrations_path)
= render 'deprecated_message' if @service.deprecation_message
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 66ed1cadf6a..ea815be23c1 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -98,7 +98,7 @@
%span.input-group-append
.input-group-text /
%p.form-text.text-muted
- = _("A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable")
+ = _("A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable")
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing'), target: '_blank'
.bs-callout.bs-callout-info
%p= _("Below are examples of regex for existing tools:")
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 87000e8270b..862db23e856 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -37,7 +37,8 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _("Register and see your runners for this project.")
+ = _("Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project.")
+ = link_to s_('More information'), help_page_path('ci/runners/README')
.settings-content
= render 'projects/runners/index'
diff --git a/app/views/projects/settings/operations/_grafana_integration.html.haml b/app/views/projects/settings/operations/_grafana_integration.html.haml
new file mode 100644
index 00000000000..cd5b5abd9ce
--- /dev/null
+++ b/app/views/projects/settings/operations/_grafana_integration.html.haml
@@ -0,0 +1,2 @@
+.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
+ grafana_integration: { url: grafana_integration_url, token: grafana_integration_token, enabled: grafana_integration_enabled?.to_s } } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 0a7a155bc12..3c955e5f558 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -5,4 +5,5 @@
= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/external_dashboard'
+= render 'projects/settings/operations/grafana_integration'
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index b58af545439..c5653c3dd5a 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -6,7 +6,7 @@
= render partial: 'flash_messages', locals: { project: @project }
-- if !@project.empty_repo? && can?(current_user, :download_code, @project)
+- if !@project.empty_repo? && can?(current_user, :download_code, @project) && !vue_file_list_enabled?
- signatures_path = project_signatures_path(@project, @project.default_branch)
.js-signature-container{ data: { 'signatures-path': signatures_path } }
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 4f6c7e1f9a6..fef019e1b69 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
- if readme.rich_viewer
- %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if vue_file_list_enabled?)] }
+ %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-show-on-root" if vue_file_list_enabled?)] }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 38422d4533d..127734ddfd7 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -77,15 +77,21 @@
.tree-controls
= render_if_exists 'projects/tree/lock_link'
- = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
+ - if vue_file_list_enabled?
+ #js-tree-history-link.d-inline-block{ data: { history_link: project_commits_path(@project, @ref) } }
+ - else
+ = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
- if can_create_mr_from_fork
= succeed " " do
- if can_collaborate || current_user&.already_forked?(@project)
- = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
- = _('Web IDE')
+ - if vue_file_list_enabled?
+ #js-tree-web-ide-link.d-inline-block
+ - else
+ = link_to ide_edit_path(@project, @ref, @path), class: 'btn btn-default qa-web-ide-button' do
+ = _('Web IDE')
- else
= link_to '#modal-confirm-fork', class: 'btn btn-default qa-web-ide-button', data: { target: '#modal-confirm-fork', toggle: 'modal'} do
= _('Web IDE')
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 39b29a20df6..65f5bc31d2e 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,7 +6,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
-.js-signature-container{ data: { 'signatures-path': signatures_path } }
+- unless vue_file_list_enabled?
+ .js-signature-container{ data: { 'signatures-path': signatures_path } }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/registrations/welcome.html.haml b/app/views/registrations/welcome.html.haml
index 02ab974ecc0..7b92f5070df 100644
--- a/app/views/registrations/welcome.html.haml
+++ b/app/views/registrations/welcome.html.haml
@@ -1,10 +1,10 @@
-- content_for(:page_title, _('Welcome to GitLab<br>%{username}!' % { username: html_escape(current_user.username) }).html_safe)
+- content_for(:page_title, _('Welcome to GitLab @%{username}!') % { username: current_user.username })
- max_name_length = 128
.text-center.mb-3
- = _('In order to tailor your experience with GitLab<br>we would like to know a bit more about you.').html_safe
+ = _('In order to tailor your experience with GitLab we<br>would like to know a bit more about you.').html_safe
.signup-box.p-3.mb-2
.signup-body
- = form_for(current_user, url: users_sign_up_update_role_path, html: { class: 'new_new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
+ = form_for(current_user, url: users_sign_up_update_registration_path, html: { class: 'new_new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
.devise-errors.mt-0
= render 'devise/shared/error_messages', resource: current_user
.name.form-group
@@ -13,5 +13,14 @@
.form-group
= f.label :role, _('Role'), class: 'label-bold'
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control'
+ .form-group
+ = f.label :setup_for_company, _('Are you setting up GitLab for a company?'), class: 'label-bold'
+ .d-flex.justify-content-center
+ .w-25
+ = f.radio_button :setup_for_company, true
+ = f.label :setup_for_company, _('Yes'), value: 'true'
+ .w-25
+ = f.radio_button :setup_for_company, false
+ = f.label :setup_for_company, _('No'), value: 'false'
.submit-container.mt-3
= f.submit _('Get started!'), class: 'btn-register btn btn-block mb-0 p-2'
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index eae2a491ceb..84198489e41 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,5 +1,5 @@
- users = capture_haml do
- - if search_tabs?(:members)
+ - if show_user_search_tab?
= search_filter_link 'users', _("Users")
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index de9947528cf..629a5a045b1 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,6 +1,7 @@
- if @search_objects.to_a.empty?
= render partial: "search/results/empty"
= render_if_exists 'shared/promotions/promote_advanced_search'
+ = render_if_exists 'search/form_revert_to_basic'
- else
.row-content-block.d-md-flex.text-left.align-items-center
- unless @search_objects.is_a?(Kaminari::PaginatableWithoutCount)
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index bdad07f36d1..4fb72b26955 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -2,6 +2,6 @@
- return unless project
- blob = parse_search_result(blob)
-- blob_link = project_blob_path(project, tree_join(blob.ref, blob.filename))
+- blob_link = project_blob_path(project, tree_join(blob.ref, blob.path))
-= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: blob.filename, blob_link: blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, path: blob.path, blob_link: blob_link }
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index 36b6ea7bd37..01e42224428 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -4,7 +4,7 @@
= link_to blob_link do
%i.fa.fa-file
%strong
- = search_blob_title(project, file_name)
+ = 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
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index f17dae0a94c..37f4efee9d2 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -1,6 +1,7 @@
- snippet_blob = chunk_snippet(snippet_blob, @search_term)
- snippet = snippet_blob[:snippet_object]
- snippet_chunks = snippet_blob[:snippet_chunks]
+- snippet_path = reliable_snippet_path(snippet)
.search-result-row
%span
@@ -11,7 +12,6 @@
= snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- - snippet_path = reliable_snippet_path(snippet)
.file-holder
.js-file-title.file-title
= link_to snippet_path do
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 1e01088d9e6..7280146720e 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -2,10 +2,7 @@
%h4.snippet-title.term
= link_to reliable_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
- - if snippet_title.private?
- %span.badge.badge-gray
- %i.fa.fa-lock
- = _("private")
+ = snippet_badge(snippet_title)
%span.cgray.monospace.tiny.float-right.term
= snippet_title.file_name
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index b351ecd4edf..9afed2bbecc 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -2,4 +2,4 @@
- wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
-= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
+= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, path: wiki_blob.path, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index cb834878276..3e805189055 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -22,6 +22,3 @@
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
- = render_if_exists 'shared/geo_modal_button'
-
-= render_if_exists 'shared/geo_modal', project: project
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 606d0f241aa..a7ad6d6f2c4 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -16,7 +16,7 @@
= 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
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled, 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
- elsif type == 'checkbox'
@@ -24,6 +24,6 @@
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled}
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, disabled: disabled
+ = 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" }
- 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 959792718ca..9a65981ed58 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -22,11 +22,16 @@
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
- = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control',
+ = 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.'),
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
+ %p.validation-error.gl-field-error.field-validation.hide
+ = _('Group path is already taken. Suggestions: ')
+ %span.gl-path-suggestions
+ %p.validation-success.gl-field-success.field-validation.hide= _('Group path is available.')
+ %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking group path availability...')
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index 1e6b6f7c79b..2887acf7cd7 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -4,7 +4,7 @@
.btn-group.mobile-git-clone.js-mobile-git-clone.btn-block
= clipboard_button(button_text: default_clone_label, text: default_url_to_repo(project), hide_button_icon: true, class: "btn-primary flex-fill bold justify-content-center input-group-text clone-dropdown-btn js-clone-dropdown-label")
- %button.btn.btn-primary.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } }
+ %button.btn.btn-primary.dropdown-toggle.js-dropdown-toggle.flex-grow-0.d-flex-center{ type: "button", data: { toggle: "dropdown" } }
= sprite_icon("arrow-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
- if ssh_enabled?
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index 1d96feda3b0..ca0b473addf 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -19,7 +19,6 @@
= f.label :expires_at, _('Expires at'), class: 'label-bold'
.input-icon-wrapper
= f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
- = icon('calendar', { class: 'input-icon-right' })
.form-group
= f.label :scopes, _('Scopes'), class: 'label-bold'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 6fa61c15493..627a1eb6eae 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -12,7 +12,7 @@
.form-group.row
= form.label :active, "Active", class: "col-form-label col-sm-2"
.col-sm-10
- = form.check_box :active, disabled: disable_fields_service?(@service)
+ = form.check_box :active, disabled: disable_fields_service?(@service), data: { qa_selector: 'active_checkbox' }
- if @service.configurable_events.present?
.form-group.row
diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml
index be78fd0ccfb..9db6184ebca 100644
--- a/app/views/shared/form_elements/_description.html.haml
+++ b/app/views/shared/form_elements/_description.html.haml
@@ -2,6 +2,7 @@
- model = local_assigns.fetch(:model)
- form = local_assigns.fetch(:form)
+- placeholder = model.is_a?(MergeRequest) ? _('Describe the goal of the changes and what reviewers should be aware of.') : _('Write a comment or drag your files here…')
- supports_quick_actions = model.new_record?
- if supports_quick_actions
@@ -16,7 +17,7 @@
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description',
- placeholder: "Write a comment or drag your files here…",
+ placeholder: placeholder,
supports_quick_actions: supports_quick_actions
= render 'shared/notes/hints', supports_quick_actions: supports_quick_actions
.clearfix
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 5e2b5f95ee3..0fb23adc31f 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -12,6 +12,8 @@
= link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
+= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
+
.form-group.row
= form.label :title, class: 'col-form-label col-sm-2'
@@ -34,8 +36,6 @@
= render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form
-= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
-
= render 'shared/issuable/form/merge_params', issuable: issuable
= render 'shared/issuable/form/contribution', issuable: issuable, form: form
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 9d580930fb8..d341520e4a2 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -5,155 +5,170 @@
- user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent)
.issues-filters{ class: ("w-100" if type == :boards_modal) }
- .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-md-row{ class: block_css_class, "v-pre" => type == :boards_modal }
- - if type == :boards
- = render "shared/boards/switcher", board: board
- = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
- - if params[:search].present?
- = hidden_field_tag :search, params[:search]
- - if @can_bulk_update
- .check-all-holder.d-none.d-sm-block.hidden
- = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
- .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
- .filtered-search-box
- - if type != :boards_modal && type != :boards
- = dropdown_tag(_('Recent searches'),
- options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
- toggle_class: "filtered-search-history-dropdown-toggle-button",
- dropdown_class: "filtered-search-history-dropdown",
- content_class: "filtered-search-history-dropdown-content" }) do
- .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
- .filtered-search-box-input-container.droplab-dropdown
- .scroll-container
- %ul.tokens-container.list-unstyled
- %li.input-token
- %input.form-control.filtered-search{ search_filter_input_options(type) }
- #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { action: 'submit' } }
- %button.btn.btn-link{ type: 'button' }
- = sprite_icon('search')
- %span
- = _('Press Enter or click to search')
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link{ type: 'button' }
- -# Encapsulate static class name `{{icon}}` inside #{} to bypass
- -# haml lint's ClassAttributeWithStaticValue
- %svg
- %use{ 'xlink:href': "#{'{{icon}}'}" }
- %span.js-filter-hint
- {{hint}}
- %span.js-filter-tag.dropdown-light-content
- {{tag}}
- #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- - if current_user
+ .issues-details-filters.filtered-search-block.d-flex.flex-column.flex-lg-row{ class: block_css_class, "v-pre" => type == :boards_modal }
+ .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0
+ - if type == :boards
+ = render "shared/boards/switcher", board: board
+ = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do
+ - if params[:search].present?
+ = hidden_field_tag :search, params[:search]
+ - if @can_bulk_update
+ .check-all-holder.d-none.d-sm-block.hidden
+ = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
+ .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
+ .filtered-search-box
+ - if type != :boards_modal && type != :boards
+ = dropdown_tag(_('Recent searches'),
+ options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
+ toggle_class: "filtered-search-history-dropdown-toggle-button",
+ dropdown_class: "filtered-search-history-dropdown",
+ content_class: "filtered-search-history-dropdown-content" }) do
+ .js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
+ .filtered-search-box-input-container.droplab-dropdown
+ .scroll-container
+ %ul.tokens-container.list-unstyled
+ %li.input-token
+ %input.form-control.filtered-search{ search_filter_input_options(type) }
+ #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
- = render 'shared/issuable/user_dropdown_item',
- user: current_user
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- = render 'shared/issuable/user_dropdown_item',
- user: User.new(username: '{{username}}', name: '{{name}}'),
- avatar: { lazy: true, url: '{{avatar_url}}' }
- #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'None' } }
- %button.btn.btn-link{ type: 'button' }
- = _('None')
- %li.filter-dropdown-item{ data: { value: 'Any' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Any')
- %li.divider.droplab-item-ignore
+ %li.filter-dropdown-item{ data: { action: 'submit' } }
+ %button.btn.btn-link{ type: 'button' }
+ = sprite_icon('search')
+ %span
+ = _('Press Enter or click to search')
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link{ type: 'button' }
+ -# Encapsulate static class name `{{icon}}` inside #{} to bypass
+ -# haml lint's ClassAttributeWithStaticValue
+ %svg
+ %use{ 'xlink:href': "#{'{{icon}}'}" }
+ %span.js-filter-hint
+ {{hint}}
+ %span.js-filter-tag.dropdown-light-content
+ {{tag}}
+ #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user
+ %ul{ data: { dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
+ #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ - if current_user
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
- user: current_user
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- = render 'shared/issuable/user_dropdown_item',
- user: User.new(username: '{{username}}', name: '{{name}}'),
- avatar: { lazy: true, url: '{{avatar_url}}' }
- = render_if_exists 'shared/issuable/approver_dropdown'
- #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'None' } }
- %button.btn.btn-link{ type: 'button' }
- = _('None')
- %li.filter-dropdown-item{ data: { value: 'Any' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Any')
- %li.filter-dropdown-item{ data: { value: 'Upcoming' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Upcoming')
- %li.filter-dropdown-item{ data: { value: 'Started' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Started')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.js-data-value{ type: 'button' }
- {{title}}
- #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'None' } }
- %button.btn.btn-link{ type: 'button' }
- = _('None')
- %li.filter-dropdown-item{ data: { value: 'Any' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Any')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link{ type: 'button' }
- %span.dropdown-label-box{ style: 'background: {{color}}' }
- %span.label-title.js-data-value
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
+ = render_if_exists 'shared/issuable/approver_dropdown'
+ #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.filter-dropdown-item{ data: { value: 'Upcoming' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Upcoming')
+ %li.filter-dropdown-item{ data: { value: 'Started' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Started')
+ %li.divider.droplab-item-ignore
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value{ type: 'button' }
+ {{title}}
+ #js-dropdown-release.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value{ type: 'button' }
+ {{title}}
+ #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link{ type: 'button' }
+ %span.dropdown-label-box{ style: 'background: {{color}}' }
+ %span.label-title.js-data-value
+ {{title}}
+ #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link{ type: 'button' }
+ %gl-emoji
+ %span.js-data-value.prepend-left-10
+ {{name}}
+ #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Yes')
+ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('No')
+ #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Yes')
+ %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('No')
+ #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value.monospace
{{title}}
- #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
- %ul{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'None' } }
- %button.btn.btn-link{ type: 'button' }
- = _('None')
- %li.filter-dropdown-item{ data: { value: 'Any' } }
- %button.btn.btn-link{ type: 'button' }
- = _('Any')
- %li.divider.droplab-item-ignore
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link{ type: 'button' }
- %gl-emoji
- %span.js-data-value.prepend-left-10
- {{name}}
- #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
- %button.btn.btn-link{ type: 'button' }
- = _('Yes')
- %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
- %button.btn.btn-link{ type: 'button' }
- = _('No')
- #js-dropdown-confidential.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dropdown: true } }
- %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } }
- %button.btn.btn-link{ type: 'button' }
- = _('Yes')
- %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
- %button.btn.btn-link{ type: 'button' }
- = _('No')
- #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
- %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.js-data-value.monospace
- {{title}}
- = render_if_exists 'shared/issuable/filter_weight', type: type
+ = render_if_exists 'shared/issuable/filter_weight', type: type
- %button.clear-search.hidden{ type: 'button' }
- = icon('times')
- .filter-dropdown-container.d-flex.flex-column.flex-md-row
- - if type == :boards
- .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
- - if user_can_admin_list
- = render 'shared/issuable/board_create_list_dropdown', board: board
- - if @project
- #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
- #js-toggle-focus-btn
- - elsif is_not_boards_modal_or_productivity_analytics
- = render 'shared/issuable/sort_dropdown'
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
+ .filter-dropdown-container.d-flex.flex-column.flex-md-row
+ #js-board-labels-toggle
+ - if type == :boards
+ .js-board-config{ data: { can_admin_list: user_can_admin_list, has_scope: board.scoped? } }
+ - if user_can_admin_list
+ = render 'shared/issuable/board_create_list_dropdown', board: board
+ - if @project
+ #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
+ #js-toggle-focus-btn
+ - elsif is_not_boards_modal_or_productivity_analytics
+ = render 'shared/issuable/sort_dropdown'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c8b2adcf084..2170b88c7c3 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -141,13 +141,7 @@
.js-sidebar-participants-entry-point
- if signed_in
- - if issuable_sidebar[:project_emails_disabled]
- .block.js-emails-disabled
- .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } }
- = notification_setting_icon
- .hide-collapsed= notification_description(:owner_disabled)
- - else
- .js-sidebar-subscriptions-entry-point
+ .js-sidebar-subscriptions-entry-point
- project_ref = issuable_sidebar[:reference]
.block.project-reference
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index fbc96baa0f7..29ac17c43b9 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -4,21 +4,20 @@
- return unless issuable.is_a?(MergeRequest)
- return if issuable.closed_without_fork?
-%hr
-- if issuable.new_record?
- .form-group.row
- = form.label :source_branch, class: 'col-form-label col-sm-2'
- .col-sm-10
- .issuable-form-select-holder
- = form.select(:source_branch, [issuable.source_branch], {}, { class: 'source_branch select2 ref-name', disabled: true })
-.form-group.row
- = form.label :target_branch, class: 'col-form-label col-sm-2'
- .col-sm-10.target-branch-select-dropdown-container
- .issuable-form-select-holder
- = form.hidden_field(:target_branch,
- { class: 'target_branch js-target-branch-select ref-name',
- disabled: issuable.new_record?,
- data: { placeholder: "Select branch", endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
+- source_title, target_title = format_mr_branch_names(@merge_request)
+
+.form-group.row.d-flex.gl-pl-3.gl-pr-3.branch-selector
+ .align-self-center
+ %span= s_('From %{source_title} into').html_safe % { source_title: "<code>#{source_title}</code>".html_safe }
- if issuable.new_record?
+ %code= target_title
&nbsp;
- = link_to 'Change branches', mr_change_branches_path(issuable)
+ = link_to _('Change branches'), mr_change_branches_path(issuable)
+ - elsif issuable.for_fork?
+ %code= issuable.target_project_path + ":"
+ - unless issuable.new_record?
+ %span.dropdown.prepend-left-5.d-inline-block
+ = form.hidden_field(:target_branch,
+ { class: 'target_branch js-target-branch-select ref-name mw-xl',
+ data: { placeholder: _('Select branch'), endpoint: refs_project_path(@project, sort: 'updated_desc', find: 'branches') }})
+%hr
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index f0c4acdd07f..1b557214e02 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -3,17 +3,17 @@
- return unless issuable.is_a?(MergeRequest)
- return if issuable.closed_without_fork?
-- if issuable.can_remove_source_branch?(current_user)
- .form-group.row
- .col-sm-10.offset-sm-2
- .form-check
+.form-group.row
+ .col-sm-2.col-form-label.pt-sm-0
+ %label
+ = _('Merge options')
+ .col-sm-10
+ - if issuable.can_remove_source_branch?(current_user)
+ .form-check.append-bottom-default
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input'
= label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do
Delete source branch when merge request is accepted.
-
-.form-group.row
- .col-sm-10.offset-sm-2
.form-check
= hidden_field_tag 'merge_request[squash]', '0', id: nil
= check_box_tag 'merge_request[squash]', '1', issuable.squash, class: 'form-check-input'
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
index eac743b5206..b4b06640bd9 100644
--- a/app/views/shared/members/_access_request_links.html.haml
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -4,7 +4,7 @@
- link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project')
= link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
- data: { confirm: leave_confirmation_message(source) },
+ data: { confirm: leave_confirmation_message(source), qa_selector: 'leave_group_link' },
class: 'access-request-link js-leave-link'
- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord
= link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]),
diff --git a/app/views/shared/milestones/_description.html.haml b/app/views/shared/milestones/_description.html.haml
new file mode 100644
index 00000000000..5ff110bf94b
--- /dev/null
+++ b/app/views/shared/milestones/_description.html.haml
@@ -0,0 +1,8 @@
+.detail-page-description.milestone-detail
+ %h2.title
+ = markdown_field(milestone, :title)
+
+ - if milestone.try(:description).present?
+ %div
+ .description.md
+ = markdown_field(milestone, :description)
diff --git a/app/views/shared/milestones/_header.html.haml b/app/views/shared/milestones/_header.html.haml
new file mode 100644
index 00000000000..2da857261d1
--- /dev/null
+++ b/app/views/shared/milestones/_header.html.haml
@@ -0,0 +1,38 @@
+.detail-page-header.milestone-page-header
+ .status-box{ class: status_box_class(milestone) }
+ = milestone_status_string(milestone)
+
+ .header-text-content
+ %span.identifier
+ %strong
+ = _('Milestone')
+ - if milestone.due_date || milestone.start_date
+ = milestone_date_range(milestone)
+
+ .milestone-buttons
+ - if can?(current_user, :admin_milestone, @group || @project)
+ - unless milestone.legacy_group_milestone?
+ = link_to _('Edit'), edit_milestone_path(milestone), class: 'btn btn-grouped'
+
+ - if milestone.project_milestone? && milestone.project.group
+ %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
+ target: '#promote-milestone-modal',
+ milestone_title: milestone.title,
+ group_name: milestone.project.group.name,
+ url: promote_project_milestone_path(milestone.project, milestone),
+ container: 'body' },
+ disabled: true,
+ type: 'button' }
+ = _('Promote')
+ #promote-milestone-modal
+
+ - if milestone.active?
+ = link_to _('Close milestone'), update_milestone_path(milestone, { state_event: :close }), method: :put, class: 'btn btn-grouped btn-close'
+ - else
+ = link_to _('Reopen milestone'), update_milestone_path(milestone, { state_event: :activate }), method: :put, class: 'btn btn-grouped btn-reopen'
+
+ - unless milestone.legacy_group_milestone?
+ = render 'shared/milestones/delete_button'
+
+ %button.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ type: 'button' }
+ = icon('angle-double-left')
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index e99aa3f1ee4..b324f35c338 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -12,8 +12,20 @@
- if @project || milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
- if milestone.due_date || milestone.start_date
- .milestone-range.append-bottom-5
+ .text-tertiary.append-bottom-5
= milestone_date_range(milestone)
+ - recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
+ - unless total_count.zero?
+ .text-tertiary.append-bottom-5.milestone-release-links
+ = icon('rocket')
+ = n_('Release', 'Releases', total_count)
+ - recent_releases.each do |release|
+ = link_to release.name, project_releases_path(release.project, anchor: release.tag)
+ - unless release == recent_releases.last
+ &bull;
+ - if total_count > recent_releases.count
+ &bull;
+ = link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project)
%div
= render('shared/milestone_expired', milestone: milestone)
- if milestone.group_milestone?
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 22a6d5e33f0..b6656e6283c 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -138,6 +138,27 @@
Merged:
= milestone.merge_requests.merged.count
+ - if project
+ - recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
+ .block.releases
+ .sidebar-collapsed-icon.has-tooltip{ title: milestone_releases_tooltip_text(milestone), data: { container: 'body', placement: 'left', boundary: 'viewport' } }
+ %strong
+ = icon('rocket')
+ %span= total_count
+ .title.hide-collapsed= n_('Release', 'Releases', total_count)
+ .hide-collapsed
+ - if total_count.zero?
+ .no-value= _('None')
+ - else
+ .font-weight-bold
+ - recent_releases.each do |release|
+ = link_to release.name, project_releases_path(project, :anchor => release.tag)
+ - unless release == recent_releases.last
+ %span.font-weight-normal &bull;
+ - if more_count > 0
+ %span.font-weight-normal &bull;
+ = link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(project), class: 'font-weight-normal'
+
- milestone_ref = milestone.try(:to_reference, full: true)
- if milestone_ref.present?
.block.reference
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index b877f66c71e..f718c5767d1 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -1,30 +1,22 @@
-- issues_accessible = milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
-
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs.js-milestone-tabs.nav.nav-tabs
- - if issues_accessible
- %li.nav-item
- = link_to '#tab-issues', class: 'nav-link active', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Issues
- %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
- %li.nav-item
- = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
- Merge Requests
- %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- - else
- %li.nav-item
- = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
- Merge Requests
- %span.badge.badge-pill= milestone.merge_requests.size
%li.nav-item
- = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
- Participants
+ = link_to '#tab-issues', class: 'nav-link active', data: { toggle: 'tab', show: '.tab-issues-buttons' } do
+ = _('Issues')
+ %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).size
+ %li.nav-item
+ = link_to '#tab-merge-requests', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'merge_requests') } do
+ = _('Merge Requests')
+ %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
+ %li.nav-item
+ = link_to '#tab-participants', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'participants') } do
+ = _('Participants')
%span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
%li.nav-item
- = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
- Labels
+ = link_to '#tab-labels', class: 'nav-link', data: { toggle: 'tab', endpoint: milestone_tab_path(milestone, 'labels') } do
+ = _('Labels')
%span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user)
@@ -32,16 +24,11 @@
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
.tab-content.milestone-content
- - if issues_accessible
- .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
- = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
- .tab-pane#tab-merge-requests
- -# loaded async
- = render "shared/milestones/tab_loading"
- - else
- .tab-pane.active#tab-merge-requests
- -# loaded async
- = render "shared/milestones/tab_loading"
+ .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } }
+ = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ .tab-pane#tab-merge-requests
+ -# loaded async
+ = render "shared/milestones/tab_loading"
.tab-pane#tab-participants
-# loaded async
= render "shared/milestones/tab_loading"
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index fd3317341f6..12575b30a6c 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -4,54 +4,15 @@
- group = local_assigns[:group]
- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone?
-.detail-page-header.milestone-page-header
- .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
- - if milestone.closed?
- Closed
- - elsif milestone.expired?
- Expired
- - else
- Open
-
- .header-text-content
- %span.identifier
- Milestone #{milestone.title}
- - if milestone.due_date || milestone.start_date
- %span.creator
- &nbsp;&middot;
- = milestone_date_range(milestone)
-
- .milestone-buttons
- - if group
- - if can?(current_user, :admin_milestone, group)
- - if milestone.group_milestone?
- = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
- Edit
- - if milestone.active?
- = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- - else
- = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-
- - unless is_dynamic_milestone
- = render 'shared/milestones/delete_button'
-
- %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" }
- = icon('angle-double-left')
-
+= render 'shared/milestones/header', milestone: milestone
= render 'shared/milestones/deprecation_message' if is_dynamic_milestone
-
-.detail-page-description.milestone-detail
- %h2.title
- = markdown_field(milestone, :title)
- - if milestone.group_milestone? && milestone.description.present?
- %div
- .description.md
- = markdown_field(milestone, :description)
+= render 'shared/milestones/description', milestone: milestone
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
- %span All issues for this milestone are closed. #{close_msg}
+ %span
+ = _('All issues for this milestone are closed.')
+ = group ? _('You may close the milestone now.') : _('Navigate to the project to close the milestone.')
= render_if_exists 'shared/milestones/burndown', milestone: milestone, project: @project
@@ -77,10 +38,3 @@
Open
%td
= milestone.expires_at
-- elsif milestone.group_milestone?
- %br
- View
- = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
- or
- = link_to 'Merge Requests', merge_requests_group_path(@group, milestone_title: milestone.title)
- in this milestone
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 441abd57334..2b3e986a841 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -17,14 +17,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
- %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.btn.dropdown-toggle.d-flex{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.btn-xs.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 43a87fd8397..1fef43c0c37 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -1,3 +1,5 @@
+- hide_label = local_assigns.fetch(:hide_label, false)
+
.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), "aria-labelledby": "custom-notifications-title" }
.modal-dialog
.modal-content
@@ -11,6 +13,7 @@
.container-fluid
= form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
= hidden_setting_source_input(notification_setting)
+ = hidden_field_tag("hide_label", true) if hide_label
.row
.col-lg-4
%h4.prepend-top-0= _('Notification events')
diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
index 3c8cc023848..363053b5e35 100644
--- a/app/views/shared/notifications/_new_button.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -31,4 +31,4 @@
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
- = render "shared/notifications/custom_notifications", notification_setting: notification_setting
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting, hide_label: true
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index d70a1631010..59b4facdbe5 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -32,7 +32,7 @@
- explore_groups_button_label = _('Explore groups')
- explore_groups_button_link = explore_groups_path
-.js-projects-list-holder
+.js-projects-list-holder{ data: { qa_selector: 'projects_list' } }
- if any_projects?(projects)
- load_pipeline_status(projects) if pipeline_status
%ul.projects-list{ class: css_classes }
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
index 5935750ca06..a47bbd55325 100644
--- a/app/views/shared/runners/_runner_description.html.haml
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -1,6 +1,6 @@
.light.prepend-top-default
%p
- = _("A 'Runner' is a process which runs a job. You can set up as many Runners as you need.")
+ = _("You can set up as many Runners as you need to run your jobs.")
%br
= _('Runners can be placed on separate users, servers, and even on your local machine.')
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 2132fcbccc5..6a5e777706c 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -8,7 +8,6 @@
.btn-group{ role: "group" }<
= copy_blob_source_button(blob)
= open_raw_blob_button(blob)
-
- = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
+ = download_raw_snippet_button(@snippet)
= render 'projects/blob/content', blob: blob
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index c7f0511d1de..d2e35511b32 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -17,7 +17,7 @@
.file-actions.d-none.d-sm-block
.btn-group{ role: "group" }<
- = embedded_snippet_raw_button
+ = embedded_raw_snippet_button
= embedded_snippet_download_button
%article.file-holder.snippet-file-content
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 8d94a87a775..67f177288f0 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -44,7 +44,7 @@
%li
%button.js-share-btn.btn.btn-transparent{ type: 'button' }
%strong.embed-toggle-list-item= _("Share")
- %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed }
+ %input.js-snippet-url-area.snippet-embed-input.form-control{ type: "text", autocomplete: 'off', value: snippet_embed_tag(@snippet) }
.input-group-append
= clipboard_button(title: _('Copy'), class: 'js-clipboard-btn snippet-clipboard-btn btn btn-default', target: '.js-snippet-url-area')
.clearfix
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 0ef626868a2..5602ea37b5c 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -7,8 +7,9 @@
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- - if snippet.file_name
- %span.snippet-filename.monospace.d-none.d-sm-inline-block
+ - if snippet.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')
= snippet.file_name
%ul.controls
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b161cc65602..66b5214cfcb 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -45,6 +45,9 @@
- gcp_cluster:cluster_project_configure
- gcp_cluster:clusters_applications_wait_for_uninstall_app
- gcp_cluster:clusters_applications_uninstall
+- gcp_cluster:clusters_cleanup_app
+- gcp_cluster:clusters_cleanup_project_namespace
+- gcp_cluster:clusters_cleanup_service_account
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -176,3 +179,4 @@
- import_issues_csv
- project_daily_statistics
- create_evidence
+- group_export
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 577c439f4a2..9492cfe217c 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -5,6 +5,7 @@ class AuthorizedProjectsWorker
prepend WaitableWorker
feature_category :authentication_and_authorization
+ latency_sensitive_worker!
# This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore the
# visibility of prepended modules. See https://github.com/rspec/rspec-mocks/issues/1231
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index e95b6b38d28..e61f37ddce1 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -5,6 +5,8 @@ class BuildFinishedWorker
include PipelineQueue
queue_namespace :pipeline_processing
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index 15b31acf3e5..fa55769e486 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -6,6 +6,7 @@ class BuildHooksWorker
queue_namespace :pipeline_hooks
feature_category :continuous_integration
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index 6584fba4c65..6f75f403e6e 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -6,6 +6,8 @@ class BuildQueueWorker
queue_namespace :pipeline_processing
feature_category :continuous_integration
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index ac947f3cf38..b7dbd367fee 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -5,6 +5,7 @@ class BuildSuccessWorker
include PipelineQueue
queue_namespace :pipeline_processing
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
diff --git a/app/workers/chat_notification_worker.rb b/app/workers/chat_notification_worker.rb
index 3bc2edad62c..42a23cd472a 100644
--- a/app/workers/chat_notification_worker.rb
+++ b/app/workers/chat_notification_worker.rb
@@ -4,6 +4,11 @@ class ChatNotificationWorker
include ApplicationWorker
feature_category :chatops
+ latency_sensitive_worker!
+ # TODO: break this into multiple jobs
+ # as the `responder` uses external dependencies
+ # See https://gitlab.com/gitlab-com/gl-infra/scalability/issues/34
+ # worker_has_external_dependencies!
RESCHEDULE_INTERVAL = 2.seconds
diff --git a/app/workers/ci/build_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb
index f22ec4c7810..e34f16f46c2 100644
--- a/app/workers/ci/build_schedule_worker.rb
+++ b/app/workers/ci/build_schedule_worker.rb
@@ -7,6 +7,7 @@ module Ci
queue_namespace :pipeline_processing
feature_category :continuous_integration
+ worker_resource_boundary :cpu
def perform(build_id)
::Ci::Build.find_by_id(build_id).try do |build|
diff --git a/app/workers/cluster_install_app_worker.rb b/app/workers/cluster_install_app_worker.rb
index 32e2ea7996c..0e075b295dd 100644
--- a/app/workers/cluster_install_app_worker.rb
+++ b/app/workers/cluster_install_app_worker.rb
@@ -5,6 +5,8 @@ class ClusterInstallAppWorker
include ClusterQueue
include ClusterApplications
+ worker_has_external_dependencies!
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::InstallService.new(app).execute
diff --git a/app/workers/cluster_patch_app_worker.rb b/app/workers/cluster_patch_app_worker.rb
index 0549e81ed05..3f95a764567 100644
--- a/app/workers/cluster_patch_app_worker.rb
+++ b/app/workers/cluster_patch_app_worker.rb
@@ -5,6 +5,8 @@ class ClusterPatchAppWorker
include ClusterQueue
include ClusterApplications
+ worker_has_external_dependencies!
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::PatchService.new(app).execute
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
index ad2437a77e9..614029c2b5c 100644
--- a/app/workers/cluster_project_configure_worker.rb
+++ b/app/workers/cluster_project_configure_worker.rb
@@ -4,6 +4,8 @@ class ClusterProjectConfigureWorker
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
diff --git a/app/workers/cluster_provision_worker.rb b/app/workers/cluster_provision_worker.rb
index 59de7903c1c..c34284319dd 100644
--- a/app/workers/cluster_provision_worker.rb
+++ b/app/workers/cluster_provision_worker.rb
@@ -4,10 +4,16 @@ class ClusterProvisionWorker
include ApplicationWorker
include ClusterQueue
+ worker_has_external_dependencies!
+
def perform(cluster_id)
Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
cluster.provider.try do |provider|
- Clusters::Gcp::ProvisionService.new.execute(provider) if cluster.gcp?
+ if cluster.gcp?
+ Clusters::Gcp::ProvisionService.new.execute(provider)
+ elsif cluster.aws?
+ Clusters::Aws::ProvisionService.new.execute(provider)
+ end
end
end
end
diff --git a/app/workers/cluster_upgrade_app_worker.rb b/app/workers/cluster_upgrade_app_worker.rb
index d1a538859b4..cd06f0a2224 100644
--- a/app/workers/cluster_upgrade_app_worker.rb
+++ b/app/workers/cluster_upgrade_app_worker.rb
@@ -5,6 +5,8 @@ class ClusterUpgradeAppWorker
include ClusterQueue
include ClusterApplications
+ worker_has_external_dependencies!
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::UpgradeService.new(app).execute
diff --git a/app/workers/cluster_wait_for_app_installation_worker.rb b/app/workers/cluster_wait_for_app_installation_worker.rb
index e8d7e52f70f..7155dc6f835 100644
--- a/app/workers/cluster_wait_for_app_installation_worker.rb
+++ b/app/workers/cluster_wait_for_app_installation_worker.rb
@@ -8,6 +8,9 @@ class ClusterWaitForAppInstallationWorker
INTERVAL = 10.seconds
TIMEOUT = 20.minutes
+ worker_has_external_dependencies!
+ worker_resource_boundary :cpu
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckInstallationProgressService.new(app).execute
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
index 6865384df44..14b1651cc72 100644
--- a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -5,6 +5,8 @@ class ClusterWaitForIngressIpAddressWorker
include ClusterQueue
include ClusterApplications
+ worker_has_external_dependencies!
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckIngressIpAddressService.new(app).execute
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
index 85e8ecc4ad5..6180998c8d9 100644
--- a/app/workers/clusters/applications/uninstall_worker.rb
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -7,6 +7,8 @@ module Clusters
include ClusterQueue
include ClusterApplications
+ worker_has_external_dependencies!
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::UninstallService.new(app).execute
diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
index 163c99d3c3c..7907aa8dfff 100644
--- a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
+++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
@@ -10,6 +10,9 @@ module Clusters
INTERVAL = 10.seconds
TIMEOUT = 20.minutes
+ worker_has_external_dependencies!
+ worker_resource_boundary :cpu
+
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckUninstallProgressService.new(app).execute
diff --git a/app/workers/clusters/cleanup/app_worker.rb b/app/workers/clusters/cleanup/app_worker.rb
new file mode 100644
index 00000000000..1eedf510ba1
--- /dev/null
+++ b/app/workers/clusters/cleanup/app_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Cleanup
+ class AppWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ # TODO: Merge with https://gitlab.com/gitlab-org/gitlab/merge_requests/16954
+ # We're splitting the above MR in smaller chunks to facilitate reviews
+ def perform
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/cleanup/project_namespace_worker.rb b/app/workers/clusters/cleanup/project_namespace_worker.rb
new file mode 100644
index 00000000000..09f2abf5d8a
--- /dev/null
+++ b/app/workers/clusters/cleanup/project_namespace_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Cleanup
+ class ProjectNamespaceWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ # TODO: Merge with https://gitlab.com/gitlab-org/gitlab/merge_requests/16954
+ # We're splitting the above MR in smaller chunks to facilitate reviews
+ def perform
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/cleanup/service_account_worker.rb b/app/workers/clusters/cleanup/service_account_worker.rb
new file mode 100644
index 00000000000..fab6318a807
--- /dev/null
+++ b/app/workers/clusters/cleanup/service_account_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Cleanup
+ class ServiceAccountWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ # TODO: Merge with https://gitlab.com/gitlab-org/gitlab/merge_requests/16954
+ # We're splitting the above MR in smaller chunks to facilitate reviews
+ def perform
+ end
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index b856a9329dd..bd0b566658e 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -14,6 +14,7 @@ module Gitlab
include NotifyUponDeath
feature_category :importers
+ worker_has_external_dependencies!
end
# project - An instance of `Project` to import the data into.
diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb
index 70412ffd095..a75cc643038 100644
--- a/app/workers/create_pipeline_worker.rb
+++ b/app/workers/create_pipeline_worker.rb
@@ -6,6 +6,8 @@ class CreatePipelineWorker
queue_namespace :pipeline_creation
feature_category :continuous_integration
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(project_id, user_id, ref, source, params = {})
project = Project.find(project_id)
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
index 79a1caccc92..90bbc193651 100644
--- a/app/workers/deployments/finished_worker.rb
+++ b/app/workers/deployments/finished_worker.rb
@@ -6,6 +6,7 @@ module Deployments
queue_namespace :deployment
feature_category :continuous_delivery
+ worker_resource_boundary :cpu
def perform(deployment_id)
Deployment.find_by_id(deployment_id).try(:execute_hooks)
diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb
index f6520307186..4a29f1aef52 100644
--- a/app/workers/deployments/success_worker.rb
+++ b/app/workers/deployments/success_worker.rb
@@ -6,6 +6,7 @@ module Deployments
queue_namespace :deployment
feature_category :continuous_delivery
+ worker_resource_boundary :cpu
def perform(deployment_id)
Deployment.find_by_id(deployment_id).try do |deployment|
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index c82728be329..b56bf4ed833 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -4,6 +4,7 @@ class EmailReceiverWorker
include ApplicationWorker
feature_category :issue_tracking
+ latency_sensitive_worker!
def perform(raw)
return unless Gitlab::IncomingEmail.enabled?
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 2231c91a720..f523f5953e1 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -6,6 +6,8 @@ class EmailsOnPushWorker
attr_reader :email, :skip_premailer
feature_category :source_code_management
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(project_id, recipients, push_data, options = {})
options.symbolize_keys!
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 9545227fa31..383fd30e098 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -7,25 +7,6 @@ class ExpireBuildArtifactsWorker
feature_category :continuous_integration
def perform
- if Feature.enabled?(:ci_new_expire_job_artifacts_service, default_enabled: true)
- perform_efficient_artifacts_removal
- else
- perform_legacy_artifacts_removal
- end
- end
-
- def perform_efficient_artifacts_removal
Ci::DestroyExpiredJobArtifactsService.new.execute
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def perform_legacy_artifacts_removal
- Rails.logger.info 'Scheduling removal of build artifacts' # rubocop:disable Gitlab/RailsLogger
-
- build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
- build_ids = build_ids.map { |build_id| [build_id] }
-
- ExpireBuildInstanceArtifactsWorker.bulk_perform_async(build_ids)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index b09d0a5d121..0363429587e 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -5,6 +5,7 @@ class ExpireJobCacheWorker
include PipelineQueue
queue_namespace :pipeline_cache
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 78e68d7bf46..ab57c59ffda 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -5,6 +5,8 @@ class ExpirePipelineCacheWorker
include PipelineQueue
queue_namespace :pipeline_cache
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index 9766331cf4b..57e64570c09 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -5,8 +5,11 @@ class GitlabShellWorker
include Gitlab::ShellAdapter
feature_category :source_code_management
+ latency_sensitive_worker!
def perform(action, *arg)
- gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
+ Gitlab::GitalyClient::NamespaceService.allow do
+ gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
+ end
end
end
diff --git a/app/workers/group_export_worker.rb b/app/workers/group_export_worker.rb
new file mode 100644
index 00000000000..51dbdc95661
--- /dev/null
+++ b/app/workers/group_export_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class GroupExportWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ feature_category :source_code_management
+
+ def perform(current_user_id, group_id, params = {})
+ current_user = User.find(current_user_id)
+ group = Group.find(group_id)
+
+ ::Groups::ImportExport::ExportService.new(group: group, user: current_user, params: params).execute
+ end
+end
diff --git a/app/workers/hashed_storage/project_migrate_worker.rb b/app/workers/hashed_storage/project_migrate_worker.rb
index f00a459a097..8c0ec97638f 100644
--- a/app/workers/hashed_storage/project_migrate_worker.rb
+++ b/app/workers/hashed_storage/project_migrate_worker.rb
@@ -16,7 +16,7 @@ module HashedStorage
project = Project.without_deleted.find_by(id: project_id)
break unless project
- old_disk_path ||= project.disk_path
+ old_disk_path ||= Storage::LegacyProject.new(project).disk_path
::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute
end
diff --git a/app/workers/import_issues_csv_worker.rb b/app/workers/import_issues_csv_worker.rb
index d9834320318..d2733dc5f56 100644
--- a/app/workers/import_issues_csv_worker.rb
+++ b/app/workers/import_issues_csv_worker.rb
@@ -4,6 +4,7 @@ class ImportIssuesCsvWorker
include ApplicationWorker
feature_category :issue_tracking
+ worker_resource_boundary :cpu
sidekiq_retries_exhausted do |job|
Upload.find(job['args'][2]).destroy
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 0d06dab3b2e..4130ce25878 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -8,6 +8,7 @@ module MailScheduler
include MailSchedulerQueue
feature_category :issue_tracking
+ worker_resource_boundary :cpu
def perform(meth, *args)
check_arguments!(args)
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 70b909afea8..ed88c57e8d4 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -4,6 +4,7 @@ class MergeWorker
include ApplicationWorker
feature_category :source_code_management
+ latency_sensitive_worker!
def perform(merge_request_id, current_user_id, params)
params = params.with_indifferent_access
diff --git a/app/workers/namespaces/prune_aggregation_schedules_worker.rb b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
index 16259ffbfa6..9a5f533fe9a 100644
--- a/app/workers/namespaces/prune_aggregation_schedules_worker.rb
+++ b/app/workers/namespaces/prune_aggregation_schedules_worker.rb
@@ -6,6 +6,7 @@ module Namespaces
include CronjobQueue
feature_category :source_code_management
+ worker_resource_boundary :cpu
# Worker to prune pending rows on Namespace::AggregationSchedule
# It's scheduled to run once a day at 1:05am.
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 1b0fec597e7..af9ca332d3c 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -5,6 +5,8 @@ class NewIssueWorker
include NewIssuable
feature_category :issue_tracking
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(issue_id, user_id)
return unless objects_found?(issue_id, user_id)
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 0a5b2f86331..aa3f85c157b 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -5,6 +5,8 @@ class NewMergeRequestWorker
include NewIssuable
feature_category :source_code_management
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(merge_request_id, user_id)
return unless objects_found?(merge_request_id, user_id)
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index d0d2a563738..2a5988a7e32 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -4,6 +4,8 @@ class NewNoteWorker
include ApplicationWorker
feature_category :issue_tracking
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
# Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb
index 28d2517238e..a3a882f9343 100644
--- a/app/workers/new_release_worker.rb
+++ b/app/workers/new_release_worker.rb
@@ -7,7 +7,7 @@ class NewReleaseWorker
feature_category :release_orchestration
def perform(release_id)
- release = Release.with_project_and_namespace.find_by_id(release_id)
+ release = Release.preloaded.find_by_id(release_id)
return unless release
NotificationService.new.send_new_release_notifications(release)
diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb
index 9c5161fd55a..ddd002eabb8 100644
--- a/app/workers/object_pool/join_worker.rb
+++ b/app/workers/object_pool/join_worker.rb
@@ -5,6 +5,8 @@ module ObjectPool
include ApplicationWorker
include ObjectPoolQueue
+ worker_resource_boundary :cpu
+
# The use of pool id is deprecated. Keeping the argument allows old jobs to
# still be performed.
def perform(_pool_id, project_id)
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
index 25e747c78d0..b1506831056 100644
--- a/app/workers/pages_domain_removal_cron_worker.rb
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -5,6 +5,7 @@ class PagesDomainRemovalCronWorker
include CronjobQueue
feature_category :pages
+ worker_resource_boundary :cpu
def perform
PagesDomain.for_removal.find_each do |domain|
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index eae1115e60c..04abc9c88fd 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -5,6 +5,8 @@ class PipelineHooksWorker
include PipelineQueue
queue_namespace :pipeline_hooks
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 0ddad43b8d5..3830522aaa1 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -4,6 +4,8 @@ class PipelineMetricsWorker
include ApplicationWorker
include PipelineQueue
+ latency_sensitive_worker!
+
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
index e4a18573d20..62ecbc8a047 100644
--- a/app/workers/pipeline_notification_worker.rb
+++ b/app/workers/pipeline_notification_worker.rb
@@ -4,6 +4,9 @@ class PipelineNotificationWorker
include ApplicationWorker
include PipelineQueue
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
+
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, recipients = nil)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 96f3725dbbe..2a36ab992e9 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -6,6 +6,7 @@ class PipelineProcessWorker
queue_namespace :pipeline_processing
feature_category :continuous_integration
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id, build_ids = nil)
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index f500ea08353..19c3c5fcc2f 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -5,6 +5,7 @@ class PipelineScheduleWorker
include CronjobQueue
feature_category :continuous_integration
+ worker_resource_boundary :cpu
def perform
Ci::PipelineSchedule.runnable_schedules.preloaded.find_in_batches do |schedules|
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index 666331e6cd4..5c24f00e0c3 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -5,6 +5,7 @@ class PipelineSuccessWorker
include PipelineQueue
queue_namespace :pipeline_processing
+ latency_sensitive_worker!
def perform(pipeline_id)
# no-op
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 13a748e1551..5b742461f7a 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -5,6 +5,7 @@ class PipelineUpdateWorker
include PipelineQueue
queue_namespace :pipeline_processing
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index a3bc7e5b9c9..334a98a0017 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -4,6 +4,8 @@ class PostReceive
include ApplicationWorker
feature_category :source_code_management
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(gl_repository, identifier, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 1e4561fc6ea..8b4d66ae493 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -11,6 +11,7 @@ class ProcessCommitWorker
include ApplicationWorker
feature_category :source_code_management
+ latency_sensitive_worker!
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 57a01c0dd8e..ae1d57aa124 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -3,6 +3,9 @@
# Worker for updating any project specific caches.
class ProjectCacheWorker
include ApplicationWorker
+
+ latency_sensitive_worker!
+
LEASE_TIMEOUT = 15.minutes.to_i
feature_category :source_code_management
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index bbcf3b72718..11f3fed82cd 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -6,6 +6,7 @@ class ProjectExportWorker
sidekiq_options retry: 3
feature_category :source_code_management
+ worker_resource_boundary :memory
def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb
index 8041404fc71..38a2a7414a5 100644
--- a/app/workers/project_service_worker.rb
+++ b/app/workers/project_service_worker.rb
@@ -5,6 +5,7 @@ class ProjectServiceWorker
sidekiq_options dead: false
feature_category :integrations
+ worker_has_external_dependencies!
def perform(hook_id, data)
data = data.with_indifferent_access
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index af4a3def062..f3a83e0e8d4 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -5,6 +5,14 @@ class ReactiveCachingWorker
feature_category_not_owned!
+ # TODO: The reactive caching worker should be split into
+ # two different workers, one for latency_sensitive jobs without external dependencies
+ # and another worker without latency_sensitivity, but with external dependencies
+ # https://gitlab.com/gitlab-com/gl-infra/scalability/issues/34
+ # This worker should also have `worker_has_external_dependencies!` enabled
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
+
def perform(class_name, id, *args)
klass = begin
class_name.constantize
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 147b412b772..a43e6fd11d5 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -8,5 +8,9 @@ class RemoveExpiredGroupLinksWorker
def perform
ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll
+
+ GroupGroupLink.expired.find_in_batches do |link_batch|
+ Groups::GroupLinks::DestroyService.new(nil, nil).execute(link_batch)
+ end
end
end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 75f06fd9f6b..bf209fcec9f 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -5,6 +5,7 @@ class RemoveExpiredMembersWorker
include CronjobQueue
feature_category :authentication_and_authorization
+ worker_resource_boundary :cpu
def perform
Member.expired.find_each do |member|
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index bc2d0366fdd..15677fb0a95 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -7,6 +7,7 @@ class RepositoryImportWorker
include ProjectImportOptions
feature_category :importers
+ worker_has_external_dependencies!
# technical debt: https://gitlab.com/gitlab-org/gitlab/issues/33991
sidekiq_options memory_killer_memory_growth_kb: ENV.fetch('MEMORY_KILLER_REPOSITORY_IMPORT_WORKER_MEMORY_GROWTH_KB', 50).to_i
diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb
index b4d96546fa4..d1dec4cb732 100644
--- a/app/workers/repository_update_remote_mirror_worker.rb
+++ b/app/workers/repository_update_remote_mirror_worker.rb
@@ -6,6 +6,8 @@ class RepositoryUpdateRemoteMirrorWorker
include ApplicationWorker
include Gitlab::ExclusiveLeaseHelpers
+ worker_has_external_dependencies!
+
sidekiq_options retry: 3, dead: false
feature_category :source_code_management
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index ea587789d03..de2454128f6 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -5,6 +5,7 @@ class StageUpdateWorker
include PipelineQueue
queue_namespace :pipeline_processing
+ latency_sensitive_worker!
# rubocop: disable CodeReuse/ActiveRecord
def perform(stage_id)
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 971edb1f14f..b116965d105 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -5,6 +5,7 @@ class StuckCiJobsWorker
include CronjobQueue
feature_category :continuous_integration
+ worker_resource_boundary :cpu
EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'
@@ -72,5 +73,19 @@ class StuckCiJobsWorker
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
b.drop(reason)
end
+ rescue => ex
+ build.doom!
+
+ track_exception_for_build(ex, build)
+ end
+
+ def track_exception_for_build(ex, build)
+ Gitlab::Sentry.track_acceptable_exception(ex, extra: {
+ build_id: build.id,
+ build_name: build.name,
+ build_stage: build.stage,
+ pipeline_id: build.pipeline_id,
+ project_id: build.project_id
+ })
end
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index 4993cd1220c..d9a9a613ca9 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -5,6 +5,7 @@ class StuckImportJobsWorker
include CronjobQueue
feature_category :importers
+ worker_resource_boundary :cpu
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
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 77859abfea4..e069b16eb90 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -6,6 +6,8 @@ class UpdateHeadPipelineForMergeRequestWorker
queue_namespace :pipeline_processing
feature_category :continuous_integration
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
def perform(merge_request_id)
MergeRequest.find_by_id(merge_request_id).try do |merge_request|
diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb
index 8e1703cdd0b..acb95353983 100644
--- a/app/workers/update_merge_requests_worker.rb
+++ b/app/workers/update_merge_requests_worker.rb
@@ -4,6 +4,8 @@ class UpdateMergeRequestsWorker
include ApplicationWorker
feature_category :source_code_management
+ latency_sensitive_worker!
+ worker_resource_boundary :cpu
LOG_TIME_THRESHOLD = 90 # seconds
diff --git a/app/workers/wait_for_cluster_creation_worker.rb b/app/workers/wait_for_cluster_creation_worker.rb
index 8aa1d9290fd..621125c8503 100644
--- a/app/workers/wait_for_cluster_creation_worker.rb
+++ b/app/workers/wait_for_cluster_creation_worker.rb
@@ -4,10 +4,16 @@ class WaitForClusterCreationWorker
include ApplicationWorker
include ClusterQueue
+ worker_has_external_dependencies!
+
def perform(cluster_id)
Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
cluster.provider.try do |provider|
- Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider) if cluster.gcp?
+ if cluster.gcp?
+ Clusters::Gcp::VerifyProvisionStatusService.new.execute(provider)
+ elsif cluster.aws?
+ Clusters::Aws::VerifyProvisionStatusService.new.execute(provider)
+ end
end
end
end
diff --git a/app/workers/web_hook_worker.rb b/app/workers/web_hook_worker.rb
index fd7ca93683e..c3fa3162c14 100644
--- a/app/workers/web_hook_worker.rb
+++ b/app/workers/web_hook_worker.rb
@@ -4,6 +4,8 @@ class WebHookWorker
include ApplicationWorker
feature_category :integrations
+ worker_has_external_dependencies!
+
sidekiq_options retry: 4, dead: false
def perform(hook_id, data, hook_name)
diff --git a/bin/secpick b/bin/secpick
index a44867846d0..963172987f4 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -103,7 +103,7 @@ module Secpick
options[:branch] = branch
end
- opts.on('-s', '--sha abcd', 'SHA to cherry pick') do |sha|
+ opts.on('-s', '--sha abcd', 'SHA or SHA range to cherry pick') do |sha|
options[:sha] = sha
end
diff --git a/changelogs/unreleased/10242-move-old-vulns-api-to-vuln-findings.yml b/changelogs/unreleased/10242-move-old-vulns-api-to-vuln-findings.yml
new file mode 100644
index 00000000000..08e22948add
--- /dev/null
+++ b/changelogs/unreleased/10242-move-old-vulns-api-to-vuln-findings.yml
@@ -0,0 +1,5 @@
+---
+title: Rename Vulnerabilities API to Vulnerability Findings API
+merge_request: 19029
+author:
+type: changed
diff --git a/changelogs/unreleased/11137-propagate-all-env-vars-to-sast-containers.yml b/changelogs/unreleased/11137-propagate-all-env-vars-to-sast-containers.yml
new file mode 100644
index 00000000000..e9096ac1d72
--- /dev/null
+++ b/changelogs/unreleased/11137-propagate-all-env-vars-to-sast-containers.yml
@@ -0,0 +1,5 @@
+---
+title: Propagate custom environment variables to SAST analyzers
+merge_request: 18193
+author:
+type: changed
diff --git a/changelogs/unreleased/11930-handle-multiple-entries-dast-report.yml b/changelogs/unreleased/11930-handle-multiple-entries-dast-report.yml
new file mode 100644
index 00000000000..5fd6b539bcd
--- /dev/null
+++ b/changelogs/unreleased/11930-handle-multiple-entries-dast-report.yml
@@ -0,0 +1,5 @@
+---
+title: The Security Dashboard displays DAST vulnerabilities for all the scanned sites, not just the first
+merge_request: 17779
+author:
+type: added
diff --git a/changelogs/unreleased/12769-milestone-blank-name-causes-issues.yml b/changelogs/unreleased/12769-milestone-blank-name-causes-issues.yml
new file mode 100644
index 00000000000..44ec08e611b
--- /dev/null
+++ b/changelogs/unreleased/12769-milestone-blank-name-causes-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure milestone titles are never empty
+merge_request: 19985
+author:
+type: fixed
diff --git a/changelogs/unreleased/12881-security-dashboard-leveraging-sparklines-to-show-vulnerability-tren.yml b/changelogs/unreleased/12881-security-dashboard-leveraging-sparklines-to-show-vulnerability-tren.yml
new file mode 100644
index 00000000000..15839300343
--- /dev/null
+++ b/changelogs/unreleased/12881-security-dashboard-leveraging-sparklines-to-show-vulnerability-tren.yml
@@ -0,0 +1,5 @@
+---
+title: Vulnerabilities history chart - use sparklines
+merge_request: 19745
+author:
+type: changed
diff --git a/changelogs/unreleased/13055-show-the-sync-information-for-design-repositories.yml b/changelogs/unreleased/13055-show-the-sync-information-for-design-repositories.yml
new file mode 100644
index 00000000000..2f807170f17
--- /dev/null
+++ b/changelogs/unreleased/13055-show-the-sync-information-for-design-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: 'Geo: Add resigns-related fields to Geo Node Status table'
+merge_request: 18379
+author:
+type: other
diff --git a/changelogs/unreleased/13135-call-to-validate_query-in-custom-metrics-form-is-not-retried.yml b/changelogs/unreleased/13135-call-to-validate_query-in-custom-metrics-form-is-not-retried.yml
new file mode 100644
index 00000000000..2728d5213c1
--- /dev/null
+++ b/changelogs/unreleased/13135-call-to-validate_query-in-custom-metrics-form-is-not-retried.yml
@@ -0,0 +1,5 @@
+---
+title: Fix query validation in custom metrics form
+merge_request: 18769
+author:
+type: fixed
diff --git a/changelogs/unreleased/13401-close-gitlab-issue-on-recovery-alerts-from-prometheus.yml b/changelogs/unreleased/13401-close-gitlab-issue-on-recovery-alerts-from-prometheus.yml
new file mode 100644
index 00000000000..466dfd2f50b
--- /dev/null
+++ b/changelogs/unreleased/13401-close-gitlab-issue-on-recovery-alerts-from-prometheus.yml
@@ -0,0 +1,5 @@
+---
+title: Close issues on Prometheus alert recovery
+merge_request: 18431
+author:
+type: added
diff --git a/changelogs/unreleased/13492-design-comments-width.yml b/changelogs/unreleased/13492-design-comments-width.yml
new file mode 100644
index 00000000000..0929483a37d
--- /dev/null
+++ b/changelogs/unreleased/13492-design-comments-width.yml
@@ -0,0 +1,5 @@
+---
+title: Smaller width for design comments layout, truncate image title
+merge_request: 17547
+author:
+type: fixed
diff --git a/changelogs/unreleased/13539-license-compliance-approval-required.yml b/changelogs/unreleased/13539-license-compliance-approval-required.yml
new file mode 100644
index 00000000000..8fa91775dac
--- /dev/null
+++ b/changelogs/unreleased/13539-license-compliance-approval-required.yml
@@ -0,0 +1,5 @@
+---
+title: Show approval required status in license compliance
+merge_request: 19114
+author:
+type: changed
diff --git a/changelogs/unreleased/14707-add-modsec-logging-sidecar-to-ingress-controller.yml b/changelogs/unreleased/14707-add-modsec-logging-sidecar-to-ingress-controller.yml
new file mode 100644
index 00000000000..2ed6c45b5e3
--- /dev/null
+++ b/changelogs/unreleased/14707-add-modsec-logging-sidecar-to-ingress-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add modsecurity logging sidecar to ingress controller
+merge_request: 19600
+author:
+type: added
diff --git a/changelogs/unreleased/14770-ignore-deprecated-column-on-projects.yml b/changelogs/unreleased/14770-ignore-deprecated-column-on-projects.yml
new file mode 100644
index 00000000000..26bcf121f57
--- /dev/null
+++ b/changelogs/unreleased/14770-ignore-deprecated-column-on-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore deprecated column and remove references to it
+merge_request: 18911
+author:
+type: deprecated
diff --git a/changelogs/unreleased/18126-change-tag-url-for-tag-push-events-in-chat-msg-integration.yaml b/changelogs/unreleased/18126-change-tag-url-for-tag-push-events-in-chat-msg-integration.yaml
new file mode 100644
index 00000000000..c576b57f7cb
--- /dev/null
+++ b/changelogs/unreleased/18126-change-tag-url-for-tag-push-events-in-chat-msg-integration.yaml
@@ -0,0 +1,5 @@
+---
+title: "Show tag link whenever it's a tag in chat message integration for push events and pipeline events"
+merge_request: 18126
+author: Mats Estensen
+type: fixed
diff --git a/changelogs/unreleased/18797-support-for-crossplane-as-a-managed-app.yml b/changelogs/unreleased/18797-support-for-crossplane-as-a-managed-app.yml
new file mode 100644
index 00000000000..1f8d8093fdc
--- /dev/null
+++ b/changelogs/unreleased/18797-support-for-crossplane-as-a-managed-app.yml
@@ -0,0 +1,5 @@
+---
+title: Support for Crossplane as a managed app
+merge_request: 18797
+author: Mahendra Bagul
+type: added
diff --git a/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml b/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml
new file mode 100644
index 00000000000..3e08b80282a
--- /dev/null
+++ b/changelogs/unreleased/18986-allow-to-use-commit-sha-in-cache-key.yml
@@ -0,0 +1,5 @@
+---
+title: Build CI cache key from commit SHAs that changed given files
+merge_request: 19392
+author:
+type: added
diff --git a/changelogs/unreleased/19054-upgrade-helm.yml b/changelogs/unreleased/19054-upgrade-helm.yml
new file mode 100644
index 00000000000..cb4f887a6ed
--- /dev/null
+++ b/changelogs/unreleased/19054-upgrade-helm.yml
@@ -0,0 +1,5 @@
+---
+title: 'Updated Auto-DevOps to kubectl v1.13.12 and helm v2.15.1'
+merge_request: 19054
+author: Leo Antunes
+type: changed
diff --git a/changelogs/unreleased/19445-native-group-milestone-page-needs-same-info-as-project-milestones.yml b/changelogs/unreleased/19445-native-group-milestone-page-needs-same-info-as-project-milestones.yml
new file mode 100644
index 00000000000..0e804e1322f
--- /dev/null
+++ b/changelogs/unreleased/19445-native-group-milestone-page-needs-same-info-as-project-milestones.yml
@@ -0,0 +1,5 @@
+---
+title: Add issues, MRs, participants, and labels tabs in group milestone page
+merge_request: 18818
+author:
+type: added
diff --git a/changelogs/unreleased/20081-add-mb-2-class-to-global-alerts.yml b/changelogs/unreleased/20081-add-mb-2-class-to-global-alerts.yml
new file mode 100644
index 00000000000..2aea916402e
--- /dev/null
+++ b/changelogs/unreleased/20081-add-mb-2-class-to-global-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Add mb-2 class to global alerts
+merge_request: 20081
+author: 2knal
+type: other
diff --git a/changelogs/unreleased/22392-add-eks-clusters-to-usage-data.yml b/changelogs/unreleased/22392-add-eks-clusters-to-usage-data.yml
new file mode 100644
index 00000000000..cac10925020
--- /dev/null
+++ b/changelogs/unreleased/22392-add-eks-clusters-to-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Add EKS cluster count to usage data
+merge_request: 17059
+author:
+type: other
diff --git a/changelogs/unreleased/22392-capture-aws-role-details.yml b/changelogs/unreleased/22392-capture-aws-role-details.yml
new file mode 100644
index 00000000000..51b70d94f6a
--- /dev/null
+++ b/changelogs/unreleased/22392-capture-aws-role-details.yml
@@ -0,0 +1,5 @@
+---
+title: Add ApplicationSetting entries for EKS integration
+merge_request: 18307
+author:
+type: other
diff --git a/changelogs/unreleased/22392-eks-create-cluster-fe.yml b/changelogs/unreleased/22392-eks-create-cluster-fe.yml
new file mode 100644
index 00000000000..133154de03f
--- /dev/null
+++ b/changelogs/unreleased/22392-eks-create-cluster-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Create AWS EKS cluster
+merge_request: 19578
+author:
+type: added
diff --git a/changelogs/unreleased/24082-double-escaping-in-tableflip-quick-action.yml b/changelogs/unreleased/24082-double-escaping-in-tableflip-quick-action.yml
new file mode 100644
index 00000000000..b7a2a41c93e
--- /dev/null
+++ b/changelogs/unreleased/24082-double-escaping-in-tableflip-quick-action.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double escaping in /tableflip quick action
+merge_request: 19271
+author: Brian T
+type: fixed
diff --git a/changelogs/unreleased/24146-query-string-params.yml b/changelogs/unreleased/24146-query-string-params.yml
new file mode 100644
index 00000000000..80cab30e255
--- /dev/null
+++ b/changelogs/unreleased/24146-query-string-params.yml
@@ -0,0 +1,5 @@
+---
+title: Populate new pipeline CI vars from params
+merge_request: 19023
+author:
+type: added
diff --git a/changelogs/unreleased/24172-group-vars.yml b/changelogs/unreleased/24172-group-vars.yml
new file mode 100644
index 00000000000..14f604de1a4
--- /dev/null
+++ b/changelogs/unreleased/24172-group-vars.yml
@@ -0,0 +1,5 @@
+---
+title: Show inherited group variables in project view
+merge_request: 18759
+author:
+type: added
diff --git a/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml b/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml
new file mode 100644
index 00000000000..9deb1f23d2a
--- /dev/null
+++ b/changelogs/unreleased/24792-artifact-based-view-for-junit-xml.yml
@@ -0,0 +1,6 @@
+---
+title: Added Tests tab to pipeline detail that contains a UI for browsing test reports
+ produced by JUnit
+merge_request: 18255
+author:
+type: added
diff --git a/changelogs/unreleased/25188-unable-to-expand-collapse-files-in-merge-request-by-clicking-caret.yml b/changelogs/unreleased/25188-unable-to-expand-collapse-files-in-merge-request-by-clicking-caret.yml
new file mode 100644
index 00000000000..59a0efee013
--- /dev/null
+++ b/changelogs/unreleased/25188-unable-to-expand-collapse-files-in-merge-request-by-clicking-caret.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unable to expand or collapse files in merge request by clicking caret
+merge_request: 19222
+author: Brian T
+type: fixed
diff --git a/changelogs/unreleased/26138-replace-raven-js-with-sentry-browser.yml b/changelogs/unreleased/26138-replace-raven-js-with-sentry-browser.yml
new file mode 100644
index 00000000000..87db6917ab7
--- /dev/null
+++ b/changelogs/unreleased/26138-replace-raven-js-with-sentry-browser.yml
@@ -0,0 +1,5 @@
+---
+title: Replace raven-js with @sentry/browser
+merge_request: 17715
+author:
+type: changed
diff --git a/changelogs/unreleased/26207-issue-board-loading-infinite-if-closing-the-closed-row.yml b/changelogs/unreleased/26207-issue-board-loading-infinite-if-closing-the-closed-row.yml
new file mode 100644
index 00000000000..f1b7e8a948e
--- /dev/null
+++ b/changelogs/unreleased/26207-issue-board-loading-infinite-if-closing-the-closed-row.yml
@@ -0,0 +1,5 @@
+---
+title: Fix closed board list loading issue
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/26380-personal-snippets.yml b/changelogs/unreleased/26380-personal-snippets.yml
new file mode 100644
index 00000000000..6ea38a4623c
--- /dev/null
+++ b/changelogs/unreleased/26380-personal-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Allow admins to administer personal snippets
+merge_request: 19693
+author: Oren Kanner
+type: fixed
diff --git a/changelogs/unreleased/27034-new-branch-length.yml b/changelogs/unreleased/27034-new-branch-length.yml
new file mode 100644
index 00000000000..44f91de9812
--- /dev/null
+++ b/changelogs/unreleased/27034-new-branch-length.yml
@@ -0,0 +1,5 @@
+---
+title: Truncate recommended branch name to a sane length
+merge_request: 18821
+author:
+type: changed
diff --git a/changelogs/unreleased/27433-fix-vulnerable-code-copied-from-devise.yml b/changelogs/unreleased/27433-fix-vulnerable-code-copied-from-devise.yml
new file mode 100644
index 00000000000..0332d85c352
--- /dev/null
+++ b/changelogs/unreleased/27433-fix-vulnerable-code-copied-from-devise.yml
@@ -0,0 +1,5 @@
+---
+title: Update incrementing of failed logins to be thread-safe
+merge_request: 19614
+author:
+type: security
diff --git a/changelogs/unreleased/27464-misleading-input-controller-for-dates.yml b/changelogs/unreleased/27464-misleading-input-controller-for-dates.yml
new file mode 100644
index 00000000000..5c5c699122b
--- /dev/null
+++ b/changelogs/unreleased/27464-misleading-input-controller-for-dates.yml
@@ -0,0 +1,5 @@
+---
+title: Remove calendar icon from personal access tokens
+merge_request: 20183
+author:
+type: other
diff --git a/changelogs/unreleased/28258-fix-project-clone-dropdrown-button-width.yml b/changelogs/unreleased/28258-fix-project-clone-dropdrown-button-width.yml
new file mode 100644
index 00000000000..d72a64de6e3
--- /dev/null
+++ b/changelogs/unreleased/28258-fix-project-clone-dropdrown-button-width.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project clone dropdown button width
+merge_request: 19551
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/28302-move-add-license-button.yml b/changelogs/unreleased/28302-move-add-license-button.yml
new file mode 100644
index 00000000000..d9a5c15990f
--- /dev/null
+++ b/changelogs/unreleased/28302-move-add-license-button.yml
@@ -0,0 +1,5 @@
+---
+title: Move add license button to project buttons
+merge_request: 19370
+author:
+type: changed
diff --git a/changelogs/unreleased/28336-dropdown-icon-missing-on-compare-page.yml b/changelogs/unreleased/28336-dropdown-icon-missing-on-compare-page.yml
new file mode 100644
index 00000000000..75f40d1ac8a
--- /dev/null
+++ b/changelogs/unreleased/28336-dropdown-icon-missing-on-compare-page.yml
@@ -0,0 +1,5 @@
+---
+title: Adding dropdown arrow icon and updated text alignment
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/28338-different-dropdown-styles-on-settings-page.yml b/changelogs/unreleased/28338-different-dropdown-styles-on-settings-page.yml
new file mode 100644
index 00000000000..c70ca59a847
--- /dev/null
+++ b/changelogs/unreleased/28338-different-dropdown-styles-on-settings-page.yml
@@ -0,0 +1,5 @@
+---
+title: Change selects from default browser style to custom style
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/28350-manifest-error-file-attach.yml b/changelogs/unreleased/28350-manifest-error-file-attach.yml
new file mode 100644
index 00000000000..f1c11d5f1bd
--- /dev/null
+++ b/changelogs/unreleased/28350-manifest-error-file-attach.yml
@@ -0,0 +1,5 @@
+---
+title: Add max width on manifest file attachment input
+merge_request: 19028
+author:
+type: fixed
diff --git a/changelogs/unreleased/28801-fix-canary-inconsistency.yml b/changelogs/unreleased/28801-fix-canary-inconsistency.yml
new file mode 100644
index 00000000000..fae9dd241fe
--- /dev/null
+++ b/changelogs/unreleased/28801-fix-canary-inconsistency.yml
@@ -0,0 +1,5 @@
+---
+title: Fix canary badge and favicon inconsistency
+merge_request: 19645
+author:
+type: fixed
diff --git a/changelogs/unreleased/28985-links-to-notes-in-collapsed-discussions-dont-work.yml b/changelogs/unreleased/28985-links-to-notes-in-collapsed-discussions-dont-work.yml
new file mode 100644
index 00000000000..46d1e94829b
--- /dev/null
+++ b/changelogs/unreleased/28985-links-to-notes-in-collapsed-discussions-dont-work.yml
@@ -0,0 +1,5 @@
+---
+title: Fix expanding collapsed threads when reference link clicked
+merge_request: 20148
+author:
+type: fixed
diff --git a/changelogs/unreleased/29121-rename-trace.yml b/changelogs/unreleased/29121-rename-trace.yml
new file mode 100644
index 00000000000..14c724e8356
--- /dev/null
+++ b/changelogs/unreleased/29121-rename-trace.yml
@@ -0,0 +1,5 @@
+---
+title: Replace wording trace with log
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/29451-webide-currentsha.yml b/changelogs/unreleased/29451-webide-currentsha.yml
new file mode 100644
index 00000000000..566da597413
--- /dev/null
+++ b/changelogs/unreleased/29451-webide-currentsha.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve: Web IDE Throws Error When Viewing Diff for Renamed Files'
+merge_request: 19348
+author:
+type: fixed
diff --git a/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml b/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml
new file mode 100644
index 00000000000..38a02a027de
--- /dev/null
+++ b/changelogs/unreleased/29713-graphql-add-issue-relative-position-sort-2.yml
@@ -0,0 +1,5 @@
+---
+title: 'Graphql query for issues can now be sorted by relative_position'
+merge_request: 19713
+author:
+type: added
diff --git a/changelogs/unreleased/29986-remove-leaky-401-responses.yml b/changelogs/unreleased/29986-remove-leaky-401-responses.yml
new file mode 100644
index 00000000000..3d60011b63f
--- /dev/null
+++ b/changelogs/unreleased/29986-remove-leaky-401-responses.yml
@@ -0,0 +1,5 @@
+---
+title: Standardize error response when route is missing
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/30131-breadcrumb-back-to-integrations-not-correct.yml b/changelogs/unreleased/30131-breadcrumb-back-to-integrations-not-correct.yml
new file mode 100644
index 00000000000..5b8a46b4a88
--- /dev/null
+++ b/changelogs/unreleased/30131-breadcrumb-back-to-integrations-not-correct.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing breadcrumb in Project > Settings > Integrations
+merge_request: 18990
+author:
+type: fixed
diff --git a/changelogs/unreleased/30161-expose-merge-request-status-in-api.yml b/changelogs/unreleased/30161-expose-merge-request-status-in-api.yml
new file mode 100644
index 00000000000..aaac2d31b0c
--- /dev/null
+++ b/changelogs/unreleased/30161-expose-merge-request-status-in-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose mergeable state of a merge request
+merge_request: 18888
+author: briankabiro
+type: added
diff --git a/changelogs/unreleased/30182-checkbox-with-no-text-causing-following-checkboxes-not-to-be-saved.yml b/changelogs/unreleased/30182-checkbox-with-no-text-causing-following-checkboxes-not-to-be-saved.yml
new file mode 100644
index 00000000000..5ea21bbd983
--- /dev/null
+++ b/changelogs/unreleased/30182-checkbox-with-no-text-causing-following-checkboxes-not-to-be-saved.yml
@@ -0,0 +1,5 @@
+---
+title: Fix checking task item when previous tasks contain only spaces
+merge_request: 19724
+author:
+type: fixed
diff --git a/changelogs/unreleased/30229-gitlab-backgroundmigration-pruneorphanedgeoevents-did-you-mean-prun.yml b/changelogs/unreleased/30229-gitlab-backgroundmigration-pruneorphanedgeoevents-did-you-mean-prun.yml
new file mode 100644
index 00000000000..daaf051ea0c
--- /dev/null
+++ b/changelogs/unreleased/30229-gitlab-backgroundmigration-pruneorphanedgeoevents-did-you-mean-prun.yml
@@ -0,0 +1,5 @@
+---
+title: "[Geo] Fix: undefined Gitlab::BackgroundMigration::PruneOrphanedGeoEvents"
+merge_request: 19638
+author:
+type: fixed
diff --git a/changelogs/unreleased/30533-asciidoc-image-link-lazy-loading.yml b/changelogs/unreleased/30533-asciidoc-image-link-lazy-loading.yml
new file mode 100644
index 00000000000..47320c32719
--- /dev/null
+++ b/changelogs/unreleased/30533-asciidoc-image-link-lazy-loading.yml
@@ -0,0 +1,5 @@
+---
+title: Enable image link and lazy loading in AsciiDoc documents
+merge_request: 18164
+author: Guillaume Grossetie
+type: fixed
diff --git a/changelogs/unreleased/30616-improve-merge-request-description-placeholder.yml b/changelogs/unreleased/30616-improve-merge-request-description-placeholder.yml
new file mode 100644
index 00000000000..942450313be
--- /dev/null
+++ b/changelogs/unreleased/30616-improve-merge-request-description-placeholder.yml
@@ -0,0 +1,5 @@
+---
+title: Improve merge request description placeholder
+merge_request: 20032
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/30660-allow-to-enable-disable-auto-ssl-letsencrypt-support-via-api.yml b/changelogs/unreleased/30660-allow-to-enable-disable-auto-ssl-letsencrypt-support-via-api.yml
new file mode 100644
index 00000000000..7ec23301eeb
--- /dev/null
+++ b/changelogs/unreleased/30660-allow-to-enable-disable-auto-ssl-letsencrypt-support-via-api.yml
@@ -0,0 +1,5 @@
+---
+title: Require explicit null parameters to remove pages domain certificate and allow to use Let's Encrypt certificates through API
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml b/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml
new file mode 100644
index 00000000000..5ef0c9ef67a
--- /dev/null
+++ b/changelogs/unreleased/30695-remove-add-btn-in-approval-rule.yml
@@ -0,0 +1,5 @@
+---
+title: Can directly add approvers to approval rule
+merge_request: 18965
+author:
+type: changed
diff --git a/changelogs/unreleased/30810-blob-view-buttons.yml b/changelogs/unreleased/30810-blob-view-buttons.yml
new file mode 100644
index 00000000000..e8599817192
--- /dev/null
+++ b/changelogs/unreleased/30810-blob-view-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Change blob edit view button styling
+merge_request: 19566
+author:
+type: other
diff --git a/changelogs/unreleased/31134-add-usage-ping-data-for-project-services.yml b/changelogs/unreleased/31134-add-usage-ping-data-for-project-services.yml
new file mode 100644
index 00000000000..52846a6a915
--- /dev/null
+++ b/changelogs/unreleased/31134-add-usage-ping-data-for-project-services.yml
@@ -0,0 +1,5 @@
+---
+title: Add usage ping data for project services
+merge_request: 19687
+author:
+type: added
diff --git a/changelogs/unreleased/31184-refactor-disabled-sidebar-notification-to-vue.yml b/changelogs/unreleased/31184-refactor-disabled-sidebar-notification-to-vue.yml
new file mode 100644
index 00000000000..8ef877ff2fc
--- /dev/null
+++ b/changelogs/unreleased/31184-refactor-disabled-sidebar-notification-to-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor disabled sidebar notifications to Vue
+merge_request: 20007
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/31222-follow-up-from-adjusts-snowplow-to-use-cookies-for-sessions.yml b/changelogs/unreleased/31222-follow-up-from-adjusts-snowplow-to-use-cookies-for-sessions.yml
new file mode 100644
index 00000000000..6d62663a0bc
--- /dev/null
+++ b/changelogs/unreleased/31222-follow-up-from-adjusts-snowplow-to-use-cookies-for-sessions.yml
@@ -0,0 +1,5 @@
+---
+title: Rename snowplow_site_id to snowplow_app_id in application_settings table
+merge_request: 19252
+author:
+type: other
diff --git a/changelogs/unreleased/31309-add-ability-for-users-to-organize-projects-on-the-operations-dashbo.yml b/changelogs/unreleased/31309-add-ability-for-users-to-organize-projects-on-the-operations-dashbo.yml
new file mode 100644
index 00000000000..cd70b5f7864
--- /dev/null
+++ b/changelogs/unreleased/31309-add-ability-for-users-to-organize-projects-on-the-operations-dashbo.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to reorder projects on operations dashboard
+merge_request: 18855
+author:
+type: added
diff --git a/changelogs/unreleased/31364-error-when-attempting-to-view-gitlab-com-group-billing-page.yml b/changelogs/unreleased/31364-error-when-attempting-to-view-gitlab-com-group-billing-page.yml
new file mode 100644
index 00000000000..22f69005dd5
--- /dev/null
+++ b/changelogs/unreleased/31364-error-when-attempting-to-view-gitlab-com-group-billing-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error when viewing group billing page
+merge_request: 18740
+author:
+type: fixed
diff --git a/changelogs/unreleased/31390-remove-pointer-cursor-from-memory-usage-chart.yml b/changelogs/unreleased/31390-remove-pointer-cursor-from-memory-usage-chart.yml
new file mode 100644
index 00000000000..0bbbcb11523
--- /dev/null
+++ b/changelogs/unreleased/31390-remove-pointer-cursor-from-memory-usage-chart.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pointer cursor from MemoryUsage chart on MR widget deployment
+merge_request: 18599
+author:
+type: fixed
diff --git a/changelogs/unreleased/31411-add-loading-indicator-when-connecting-to-error-tracking-server.yml b/changelogs/unreleased/31411-add-loading-indicator-when-connecting-to-error-tracking-server.yml
new file mode 100644
index 00000000000..7e62b9f90b4
--- /dev/null
+++ b/changelogs/unreleased/31411-add-loading-indicator-when-connecting-to-error-tracking-server.yml
@@ -0,0 +1,5 @@
+---
+title: Add loading icon to error tracking settings page
+merge_request: 19539
+author:
+type: changed
diff --git a/changelogs/unreleased/31658-add-rollback-dialog-environment.yml b/changelogs/unreleased/31658-add-rollback-dialog-environment.yml
new file mode 100644
index 00000000000..d50feb434a2
--- /dev/null
+++ b/changelogs/unreleased/31658-add-rollback-dialog-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix environment name in rollback dialog
+merge_request: 19209
+author:
+type: fixed
diff --git a/changelogs/unreleased/31843-contextual-documentation-to-help-users-download-npm-packages.yml b/changelogs/unreleased/31843-contextual-documentation-to-help-users-download-npm-packages.yml
new file mode 100644
index 00000000000..d301f6fed5b
--- /dev/null
+++ b/changelogs/unreleased/31843-contextual-documentation-to-help-users-download-npm-packages.yml
@@ -0,0 +1,5 @@
+---
+title: Added installation commands for npm and yarn packages to package detail page
+merge_request: 18999
+author:
+type: added
diff --git a/changelogs/unreleased/31868-make-name-optional-parameter-of-release-entity.yml b/changelogs/unreleased/31868-make-name-optional-parameter-of-release-entity.yml
new file mode 100644
index 00000000000..4ba2af01dfa
--- /dev/null
+++ b/changelogs/unreleased/31868-make-name-optional-parameter-of-release-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Made `name` optional parameter of Release entity
+merge_request: 19705
+author:
+type: changed
diff --git a/changelogs/unreleased/31912-epic-labels.yml b/changelogs/unreleased/31912-epic-labels.yml
new file mode 100644
index 00000000000..ec4ca31f837
--- /dev/null
+++ b/changelogs/unreleased/31912-epic-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Manage and display labels from epic in the GraphQL API
+merge_request: 19642
+author:
+type: added
diff --git a/changelogs/unreleased/31914-graphql-todos-mark-todo-as-done-pd.yml b/changelogs/unreleased/31914-graphql-todos-mark-todo-as-done-pd.yml
new file mode 100644
index 00000000000..2a5a7a2ec5e
--- /dev/null
+++ b/changelogs/unreleased/31914-graphql-todos-mark-todo-as-done-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Mark todo done by GraphQL API
+merge_request: 18581
+author:
+type: added
diff --git a/changelogs/unreleased/31919-graphql-MR-assignee-mutation.yml b/changelogs/unreleased/31919-graphql-MR-assignee-mutation.yml
new file mode 100644
index 00000000000..02c2e4421dc
--- /dev/null
+++ b/changelogs/unreleased/31919-graphql-MR-assignee-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: Add MergeRequestSetAssignees GraphQL mutation
+merge_request: 19272
+author:
+type: added
diff --git a/changelogs/unreleased/31919-graphql-MR-label-mutation.yml b/changelogs/unreleased/31919-graphql-MR-label-mutation.yml
new file mode 100644
index 00000000000..41a1a91713d
--- /dev/null
+++ b/changelogs/unreleased/31919-graphql-MR-label-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: 'GraphQL: Create MR mutations needed for the sidebar'
+merge_request: 19913
+author:
+type: added
diff --git a/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml b/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml
new file mode 100644
index 00000000000..e8d47abd169
--- /dev/null
+++ b/changelogs/unreleased/31919-graphql-MR-sidebar-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: 'GraphQL: Add Merge Request milestone mutation'
+merge_request: 19257
+author:
+type: added
diff --git a/changelogs/unreleased/31964-make-snippet-list-easier-to-scan.yml b/changelogs/unreleased/31964-make-snippet-list-easier-to-scan.yml
new file mode 100644
index 00000000000..d10165438d4
--- /dev/null
+++ b/changelogs/unreleased/31964-make-snippet-list-easier-to-scan.yml
@@ -0,0 +1,5 @@
+---
+title: Make snippet list easier to scan
+merge_request: 19490
+author:
+type: other
diff --git a/changelogs/unreleased/32052-add-pa-start-date-limit.yml b/changelogs/unreleased/32052-add-pa-start-date-limit.yml
new file mode 100644
index 00000000000..fa2cf796edb
--- /dev/null
+++ b/changelogs/unreleased/32052-add-pa-start-date-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Add productivity analytics merge date filtering limit
+merge_request: 32052
+author:
+type: fixed
diff --git a/changelogs/unreleased/32198-banner-alerting-of-project-move-is-showing-up-everywhere.yml b/changelogs/unreleased/32198-banner-alerting-of-project-move-is-showing-up-everywhere.yml
new file mode 100644
index 00000000000..a6d211d59ca
--- /dev/null
+++ b/changelogs/unreleased/32198-banner-alerting-of-project-move-is-showing-up-everywhere.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "project or group was moved" alerts showing up in the wrong pages
+merge_request: 18985
+author:
+type: fixed
diff --git a/changelogs/unreleased/32358-add-modsec-feature-flag-to-usage-ping.yml b/changelogs/unreleased/32358-add-modsec-feature-flag-to-usage-ping.yml
new file mode 100644
index 00000000000..9bfdf4eb513
--- /dev/null
+++ b/changelogs/unreleased/32358-add-modsec-feature-flag-to-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Add modsecurity feature flag to usage ping
+merge_request: 20194
+author:
+type: added
diff --git a/changelogs/unreleased/32367-hashed-storage-migration-handle-failed-attachment-migrations-wth-ex.yml b/changelogs/unreleased/32367-hashed-storage-migration-handle-failed-attachment-migrations-wth-ex.yml
new file mode 100644
index 00000000000..6690318d8ac
--- /dev/null
+++ b/changelogs/unreleased/32367-hashed-storage-migration-handle-failed-attachment-migrations-wth-ex.yml
@@ -0,0 +1,6 @@
+---
+title: 'Hashed Storage Migration: Handle failed attachment migrations with existing
+ target path'
+merge_request: 19061
+author:
+type: fixed
diff --git a/changelogs/unreleased/32398-geo-rake-gitlab-geo-check-on-the-primary-is-cluttered.yml b/changelogs/unreleased/32398-geo-rake-gitlab-geo-check-on-the-primary-is-cluttered.yml
new file mode 100644
index 00000000000..e8a8828b929
--- /dev/null
+++ b/changelogs/unreleased/32398-geo-rake-gitlab-geo-check-on-the-primary-is-cluttered.yml
@@ -0,0 +1,5 @@
+---
+title: "[Geo] Fix: rake gitlab:geo:check on the primary is cluttered"
+merge_request: 19460
+author:
+type: changed
diff --git a/changelogs/unreleased/32419-growth-conversion-experiment-test-new-admin-upgrade-design-copy-for.yml b/changelogs/unreleased/32419-growth-conversion-experiment-test-new-admin-upgrade-design-copy-for.yml
new file mode 100644
index 00000000000..12de1e6aadd
--- /dev/null
+++ b/changelogs/unreleased/32419-growth-conversion-experiment-test-new-admin-upgrade-design-copy-for.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade design/copy for issue weights locked feature
+merge_request: 17352
+author:
+type: changed
diff --git a/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml b/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml
new file mode 100644
index 00000000000..53a7331b4d9
--- /dev/null
+++ b/changelogs/unreleased/32458-update-group-creation-url-so-it-is-always-unique-and-does-not-gener.yml
@@ -0,0 +1,5 @@
+---
+title: New group path uniqueness check
+merge_request: 17394
+author:
+type: added
diff --git a/changelogs/unreleased/32464-backend-sentry-error-details.yml b/changelogs/unreleased/32464-backend-sentry-error-details.yml
new file mode 100644
index 00000000000..aa7b44f9d08
--- /dev/null
+++ b/changelogs/unreleased/32464-backend-sentry-error-details.yml
@@ -0,0 +1,5 @@
+---
+title: API for stack trace & detail view of Sentry error in GitLab
+merge_request: 19137
+author:
+type: added
diff --git a/changelogs/unreleased/32464-detail-view-of-sentry-error.yml b/changelogs/unreleased/32464-detail-view-of-sentry-error.yml
new file mode 100644
index 00000000000..d23a2b08419
--- /dev/null
+++ b/changelogs/unreleased/32464-detail-view-of-sentry-error.yml
@@ -0,0 +1,5 @@
+---
+title: Detail view of Sentry error in GitLab
+merge_request: 18878
+author:
+type: added
diff --git a/changelogs/unreleased/32534-gitlab-rake-gitlab-cleanup-orphan_job_artifact_files-dry_run-false-is-not-removing-artifacts.yml b/changelogs/unreleased/32534-gitlab-rake-gitlab-cleanup-orphan_job_artifact_files-dry_run-false-is-not-removing-artifacts.yml
new file mode 100644
index 00000000000..aa0e5fee47a
--- /dev/null
+++ b/changelogs/unreleased/32534-gitlab-rake-gitlab-cleanup-orphan_job_artifact_files-dry_run-false-is-not-removing-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly cleanup orphan job artifacts
+merge_request: 17679
+author: Adam Mulvany
+type: fixed
diff --git a/changelogs/unreleased/32562-dynamic-db-pool-size-in-puma.yml b/changelogs/unreleased/32562-dynamic-db-pool-size-in-puma.yml
new file mode 100644
index 00000000000..e90de9a4d6a
--- /dev/null
+++ b/changelogs/unreleased/32562-dynamic-db-pool-size-in-puma.yml
@@ -0,0 +1,5 @@
+---
+title: 'Puma only: database connection pool now always >= number of worker threads'
+merge_request: 19286
+author:
+type: performance
diff --git a/changelogs/unreleased/32685-remove-epics-tree-feature-flag.yml b/changelogs/unreleased/32685-remove-epics-tree-feature-flag.yml
new file mode 100644
index 00000000000..5c30150e66a
--- /dev/null
+++ b/changelogs/unreleased/32685-remove-epics-tree-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Show Tree UI containing child Epics and Issues within an Epic
+merge_request: 19812
+author:
+type: added
diff --git a/changelogs/unreleased/32935-preventing-accidental-project-deletion-db-changes.yml b/changelogs/unreleased/32935-preventing-accidental-project-deletion-db-changes.yml
new file mode 100644
index 00000000000..15b9f0d55cb
--- /dev/null
+++ b/changelogs/unreleased/32935-preventing-accidental-project-deletion-db-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Add migrations and changes for soft-delete for projects
+merge_request: 18791
+author:
+type: added
diff --git a/changelogs/unreleased/32935-preventing-accidental-project-deletion-index.yml b/changelogs/unreleased/32935-preventing-accidental-project-deletion-index.yml
new file mode 100644
index 00000000000..7f85da35f83
--- /dev/null
+++ b/changelogs/unreleased/32935-preventing-accidental-project-deletion-index.yml
@@ -0,0 +1,5 @@
+---
+title: Add index on marked_for_deletion_at in projects table
+merge_request: 19788
+author:
+type: other
diff --git a/changelogs/unreleased/32951-secure-modal-mobile-issue.yml b/changelogs/unreleased/32951-secure-modal-mobile-issue.yml
new file mode 100644
index 00000000000..d3c4399cb35
--- /dev/null
+++ b/changelogs/unreleased/32951-secure-modal-mobile-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes mobile styling issues on security modals
+merge_request: 19391
+author:
+type: fixed
diff --git a/changelogs/unreleased/32962-update-gcp-credit-url.yml b/changelogs/unreleased/32962-update-gcp-credit-url.yml
new file mode 100644
index 00000000000..87e3e7ff364
--- /dev/null
+++ b/changelogs/unreleased/32962-update-gcp-credit-url.yml
@@ -0,0 +1,5 @@
+---
+title: Update GCP credit URLs
+merge_request: 19683
+author:
+type: fixed
diff --git a/changelogs/unreleased/33042-persist-zoom-meetings-added-to-issues-in-the-database-2.yml b/changelogs/unreleased/33042-persist-zoom-meetings-added-to-issues-in-the-database-2.yml
new file mode 100644
index 00000000000..dd7df055c71
--- /dev/null
+++ b/changelogs/unreleased/33042-persist-zoom-meetings-added-to-issues-in-the-database-2.yml
@@ -0,0 +1,5 @@
+---
+title: Store Zoom URLs in a table rather than in the issue description
+merge_request: 18620
+author:
+type: changed
diff --git a/changelogs/unreleased/33054-share_groups_with_groups.yml b/changelogs/unreleased/33054-share_groups_with_groups.yml
new file mode 100644
index 00000000000..0a1f9e5cb3b
--- /dev/null
+++ b/changelogs/unreleased/33054-share_groups_with_groups.yml
@@ -0,0 +1,5 @@
+---
+title: Share groups with groups
+merge_request: 17117
+author:
+type: added
diff --git a/changelogs/unreleased/33101-add-deployments-api-updated-after-param.yml b/changelogs/unreleased/33101-add-deployments-api-updated-after-param.yml
new file mode 100644
index 00000000000..f3bc1dd9751
--- /dev/null
+++ b/changelogs/unreleased/33101-add-deployments-api-updated-after-param.yml
@@ -0,0 +1,5 @@
+---
+title: Allow order_by updated_at in Deployments API
+merge_request: 19658
+author:
+type: added
diff --git a/changelogs/unreleased/33121-refactor-user-counts.yml b/changelogs/unreleased/33121-refactor-user-counts.yml
new file mode 100644
index 00000000000..6e3ee5f18f3
--- /dev/null
+++ b/changelogs/unreleased/33121-refactor-user-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor maximum user counts in license
+merge_request: 19071
+author: briankabiro
+type: changed
diff --git a/changelogs/unreleased/33182-fix-productivity-analytics-multiple-labels-bug.yml b/changelogs/unreleased/33182-fix-productivity-analytics-multiple-labels-bug.yml
new file mode 100644
index 00000000000..4c306c539d4
--- /dev/null
+++ b/changelogs/unreleased/33182-fix-productivity-analytics-multiple-labels-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix productivity analytics listing with multiple labels
+merge_request: 33182
+author:
+type: fixed
diff --git a/changelogs/unreleased/33268-updated-operations-metrics-charts-do-not-load-properly.yml b/changelogs/unreleased/33268-updated-operations-metrics-charts-do-not-load-properly.yml
new file mode 100644
index 00000000000..e1a24404372
--- /dev/null
+++ b/changelogs/unreleased/33268-updated-operations-metrics-charts-do-not-load-properly.yml
@@ -0,0 +1,5 @@
+---
+title: Fix empty chart in collapsed sections
+merge_request: 18699
+author:
+type: fixed
diff --git a/changelogs/unreleased/33306-missing-field-discussions.yml b/changelogs/unreleased/33306-missing-field-discussions.yml
new file mode 100644
index 00000000000..6506b6ad1c6
--- /dev/null
+++ b/changelogs/unreleased/33306-missing-field-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Prevents console warning on design upload
+merge_request: 19297
+author:
+type: fixed
diff --git a/changelogs/unreleased/33460-webide-line-endings.yml b/changelogs/unreleased/33460-webide-line-endings.yml
new file mode 100644
index 00000000000..62fe15c051b
--- /dev/null
+++ b/changelogs/unreleased/33460-webide-line-endings.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve: Web IDE does not create POSIX Compliant Files'
+merge_request: 19339
+author:
+type: fixed
diff --git a/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml b/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml
new file mode 100644
index 00000000000..c85faa9e7a2
--- /dev/null
+++ b/changelogs/unreleased/33533-go-to-root-if-no-path-on-branch.yml
@@ -0,0 +1,6 @@
+---
+title: When a user views a file's blame or blob and switches to a branch where the
+ current file does not exist, they will now be redirected to the root of the repository.
+merge_request: 18169
+author: Jesse Hall @jessehall3
+type: changed
diff --git a/changelogs/unreleased/33557-make-pages-settings-e-g-enablement-access-control-more-visible.yml b/changelogs/unreleased/33557-make-pages-settings-e-g-enablement-access-control-more-visible.yml
new file mode 100644
index 00000000000..d04771f47a8
--- /dev/null
+++ b/changelogs/unreleased/33557-make-pages-settings-e-g-enablement-access-control-more-visible.yml
@@ -0,0 +1,5 @@
+---
+title: Add warnings about pages access control settings
+merge_request: 19067
+author:
+type: added
diff --git a/changelogs/unreleased/33672-add-enable-checkbox-for-grafana-authentication-settings.yml b/changelogs/unreleased/33672-add-enable-checkbox-for-grafana-authentication-settings.yml
new file mode 100644
index 00000000000..bb45c5ec4d2
--- /dev/null
+++ b/changelogs/unreleased/33672-add-enable-checkbox-for-grafana-authentication-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate enabled flag on grafana_integrations table
+merge_request: 19234
+author:
+type: changed
diff --git a/changelogs/unreleased/33805-add_serverless_framework_template.yml b/changelogs/unreleased/33805-add_serverless_framework_template.yml
new file mode 100644
index 00000000000..3f6ceddc84e
--- /dev/null
+++ b/changelogs/unreleased/33805-add_serverless_framework_template.yml
@@ -0,0 +1,5 @@
+---
+title: Add template for Serverless Framework/JS
+merge_request: 33805
+author:
+type: added
diff --git a/changelogs/unreleased/33896-security-dashboard-projects.yml b/changelogs/unreleased/33896-security-dashboard-projects.yml
new file mode 100644
index 00000000000..9f93e1b3497
--- /dev/null
+++ b/changelogs/unreleased/33896-security-dashboard-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Create a users_security_dashboard_projects table to store the projects a user has added to their personal security dashboard
+merge_request: 18708
+author:
+type: added
diff --git a/changelogs/unreleased/33897-make-arrow-buttons-work-within-search-box.yml b/changelogs/unreleased/33897-make-arrow-buttons-work-within-search-box.yml
new file mode 100644
index 00000000000..8132343322b
--- /dev/null
+++ b/changelogs/unreleased/33897-make-arrow-buttons-work-within-search-box.yml
@@ -0,0 +1,5 @@
+---
+title: Fix keyboard shortcuts in header search autocomplete
+merge_request: 18685
+author:
+type: fixed
diff --git a/changelogs/unreleased/33902-fix-private-group-todo-mentions.yml b/changelogs/unreleased/33902-fix-private-group-todo-mentions.yml
new file mode 100644
index 00000000000..52405fe7553
--- /dev/null
+++ b/changelogs/unreleased/33902-fix-private-group-todo-mentions.yml
@@ -0,0 +1,5 @@
+---
+title: Do not generate To-Dos additional when editing group mentions
+merge_request: 19037
+author:
+type: fixed
diff --git a/changelogs/unreleased/34078-extend-audit-events-api-for-gitlab-com.yml b/changelogs/unreleased/34078-extend-audit-events-api-for-gitlab-com.yml
new file mode 100644
index 00000000000..8b4d640c3f8
--- /dev/null
+++ b/changelogs/unreleased/34078-extend-audit-events-api-for-gitlab-com.yml
@@ -0,0 +1,5 @@
+---
+title: Add Group Audit Events API
+merge_request: 19868
+author:
+type: added
diff --git a/changelogs/unreleased/34132-graphql-epic-subscribtions.yml b/changelogs/unreleased/34132-graphql-epic-subscribtions.yml
new file mode 100644
index 00000000000..a35da2e5212
--- /dev/null
+++ b/changelogs/unreleased/34132-graphql-epic-subscribtions.yml
@@ -0,0 +1,5 @@
+---
+title: Graphql mutation for (un)subscribing to an epic
+merge_request: 19083
+author:
+type: added
diff --git a/changelogs/unreleased/34149-hide-delete-selected-in-designs-when-viewing-an-old-version.yml b/changelogs/unreleased/34149-hide-delete-selected-in-designs-when-viewing-an-old-version.yml
new file mode 100644
index 00000000000..73e24b856fc
--- /dev/null
+++ b/changelogs/unreleased/34149-hide-delete-selected-in-designs-when-viewing-an-old-version.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Hide Delete selected in designs when viewing an old version
+merge_request: 19889
+author:
+type: fixed
diff --git a/changelogs/unreleased/34230-fix-popover-image.yml b/changelogs/unreleased/34230-fix-popover-image.yml
new file mode 100644
index 00000000000..c9cf230ac6c
--- /dev/null
+++ b/changelogs/unreleased/34230-fix-popover-image.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cluster feature highlight popover image
+merge_request: 19372
+author:
+type: fixed
diff --git a/changelogs/unreleased/34258-embedding-sentry-stacktrace.yml b/changelogs/unreleased/34258-embedding-sentry-stacktrace.yml
new file mode 100644
index 00000000000..8cbd16a0e40
--- /dev/null
+++ b/changelogs/unreleased/34258-embedding-sentry-stacktrace.yml
@@ -0,0 +1,5 @@
+---
+title: Sentry error stacktrace
+merge_request: 19492
+author:
+type: added
diff --git a/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
new file mode 100644
index 00000000000..546e6bc6b63
--- /dev/null
+++ b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
@@ -0,0 +1,5 @@
+---
+title: Enable the color chip in AsciiDoc documents
+merge_request: 18723
+author:
+type: added
diff --git a/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
new file mode 100644
index 00000000000..b727bc7f85e
--- /dev/null
+++ b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Error when uploading a few designs in a row
+merge_request: 18811
+author:
+type: fixed
diff --git a/changelogs/unreleased/34335-move-subscribed-field-to-issue-type.yml b/changelogs/unreleased/34335-move-subscribed-field-to-issue-type.yml
new file mode 100644
index 00000000000..d411874e62a
--- /dev/null
+++ b/changelogs/unreleased/34335-move-subscribed-field-to-issue-type.yml
@@ -0,0 +1,5 @@
+---
+title: Expose subscribed field in issue lists queried with GraphQL
+merge_request: 19458
+author: briankabiro
+type: changed
diff --git a/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml b/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml
new file mode 100644
index 00000000000..0a7bfd5fb4f
--- /dev/null
+++ b/changelogs/unreleased/34372-serverless-function-description-does-not-show-up-for-newly-created-.yml
@@ -0,0 +1,5 @@
+---
+title: Fix serverless function descriptions not showing on Knative 0.7
+merge_request: 18973
+author:
+type: fixed
diff --git a/changelogs/unreleased/34416-subscribed-notification-header.yml b/changelogs/unreleased/34416-subscribed-notification-header.yml
new file mode 100644
index 00000000000..dd1fca4255b
--- /dev/null
+++ b/changelogs/unreleased/34416-subscribed-notification-header.yml
@@ -0,0 +1,5 @@
+---
+title: Set X-GitLab-NotificationReason header if notification reason is explicit subscription
+merge_request: 18812
+author:
+type: added
diff --git a/changelogs/unreleased/34423-user-popover-immediately-closed-when-hovering-over-certain-areas.yml b/changelogs/unreleased/34423-user-popover-immediately-closed-when-hovering-over-certain-areas.yml
new file mode 100644
index 00000000000..56a36830b54
--- /dev/null
+++ b/changelogs/unreleased/34423-user-popover-immediately-closed-when-hovering-over-certain-areas.yml
@@ -0,0 +1,5 @@
+---
+title: Fix user popover not being displayed when the user has a status message
+merge_request: 19519
+author:
+type: fixed
diff --git a/changelogs/unreleased/34426-use-new-list-task-icon-in-text-editor.yml b/changelogs/unreleased/34426-use-new-list-task-icon-in-text-editor.yml
new file mode 100644
index 00000000000..2b66bd02e93
--- /dev/null
+++ b/changelogs/unreleased/34426-use-new-list-task-icon-in-text-editor.yml
@@ -0,0 +1,6 @@
+---
+title: Replace task-done icon with list-task icon to better align with other toolbar
+ list icons
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/34431-add-report-type-vulnerabilities.yml b/changelogs/unreleased/34431-add-report-type-vulnerabilities.yml
new file mode 100644
index 00000000000..36d21162d20
--- /dev/null
+++ b/changelogs/unreleased/34431-add-report-type-vulnerabilities.yml
@@ -0,0 +1,5 @@
+---
+title: Added report_type attribute to Vulnerabilities
+merge_request: 19179
+author:
+type: changed
diff --git a/changelogs/unreleased/34443-fix-template-bug.yml b/changelogs/unreleased/34443-fix-template-bug.yml
new file mode 100644
index 00000000000..57881debcad
--- /dev/null
+++ b/changelogs/unreleased/34443-fix-template-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix template selector filename bug
+merge_request: 19376
+author:
+type: fixed
diff --git a/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml b/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml
new file mode 100644
index 00000000000..669a0c8bd93
--- /dev/null
+++ b/changelogs/unreleased/34519-move-planels-in-dashboard-save-to-the-vuex-store.yml
@@ -0,0 +1,5 @@
+---
+title: Save dashboard changes by the user into the vuex store
+merge_request: 18862
+author:
+type: changed
diff --git a/changelogs/unreleased/34564-vulnerability-issue-links.yml b/changelogs/unreleased/34564-vulnerability-issue-links.yml
new file mode 100644
index 00000000000..0f6f5610159
--- /dev/null
+++ b/changelogs/unreleased/34564-vulnerability-issue-links.yml
@@ -0,0 +1,5 @@
+---
+title: Update the DB schema to allow linking between Vulnerabilities and Issues
+merge_request: 19852
+author:
+type: added
diff --git a/changelogs/unreleased/34577-add-dep-scanner-var-maven.yml b/changelogs/unreleased/34577-add-dep-scanner-var-maven.yml
new file mode 100644
index 00000000000..8b28cead295
--- /dev/null
+++ b/changelogs/unreleased/34577-add-dep-scanner-var-maven.yml
@@ -0,0 +1,5 @@
+---
+title: Add maven cli opts flag to maven security analyzer (part of dependency scanning)
+merge_request: 19174
+author:
+type: changed
diff --git a/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml b/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml
new file mode 100644
index 00000000000..9cb44979f3e
--- /dev/null
+++ b/changelogs/unreleased/34606-Remove-IIFEs-from-merge_request-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove IIFEs from merge_request.js
+merge_request: 19294
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/34607-Remove-IIFEs-from-branch_graph-js.yml b/changelogs/unreleased/34607-Remove-IIFEs-from-branch_graph-js.yml
new file mode 100644
index 00000000000..6c902549c4c
--- /dev/null
+++ b/changelogs/unreleased/34607-Remove-IIFEs-from-branch_graph-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove IIFEs from branch_graph.js
+merge_request: 20008
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/34610-Remove-IIFEs-from-new_branch_form-js.yml b/changelogs/unreleased/34610-Remove-IIFEs-from-new_branch_form-js.yml
new file mode 100644
index 00000000000..7875772222f
--- /dev/null
+++ b/changelogs/unreleased/34610-Remove-IIFEs-from-new_branch_form-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove IIFEs from new_branch_form.js
+merge_request: 20009
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml b/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml
new file mode 100644
index 00000000000..a5acaf7c5dc
--- /dev/null
+++ b/changelogs/unreleased/34624-remove-IIFEs-from-project_select-js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove IIFEs from project_select.js
+merge_request: 19288
+author: minghuan lei
+type: other
diff --git a/changelogs/unreleased/34717-update-expired-trial-copy.yml b/changelogs/unreleased/34717-update-expired-trial-copy.yml
new file mode 100644
index 00000000000..d1f77d3e4cb
--- /dev/null
+++ b/changelogs/unreleased/34717-update-expired-trial-copy.yml
@@ -0,0 +1,5 @@
+---
+title: Update expired trial status copy
+merge_request: 18962
+author:
+type: changed
diff --git a/changelogs/unreleased/34755-refactor-getdateinpast-to-return-date-object.yml b/changelogs/unreleased/34755-refactor-getdateinpast-to-return-date-object.yml
new file mode 100644
index 00000000000..0a374daa7d2
--- /dev/null
+++ b/changelogs/unreleased/34755-refactor-getdateinpast-to-return-date-object.yml
@@ -0,0 +1,5 @@
+---
+title: Change return type of getDateInPast to Date
+merge_request: 19081
+author:
+type: changed
diff --git a/changelogs/unreleased/34757-bugfix-graphql-missing-todo-types.yml b/changelogs/unreleased/34757-bugfix-graphql-missing-todo-types.yml
new file mode 100644
index 00000000000..94ed1963f53
--- /dev/null
+++ b/changelogs/unreleased/34757-bugfix-graphql-missing-todo-types.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors in GraphQL Todos API due to missing TargetTypeEnum values
+merge_request: 19052
+author:
+type: fixed
diff --git a/changelogs/unreleased/34770-error-500-for-api-v4-projects-id-services-jira-nomethoderror-undefi.yml b/changelogs/unreleased/34770-error-500-for-api-v4-projects-id-services-jira-nomethoderror-undefi.yml
new file mode 100644
index 00000000000..68de843402d
--- /dev/null
+++ b/changelogs/unreleased/34770-error-500-for-api-v4-projects-id-services-jira-nomethoderror-undefi.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project service API 500 error
+merge_request: 19367
+author:
+type: fixed
diff --git a/changelogs/unreleased/34779-editing-metric-dashboard-using-yml-file.yml b/changelogs/unreleased/34779-editing-metric-dashboard-using-yml-file.yml
new file mode 100644
index 00000000000..f4a4d67175e
--- /dev/null
+++ b/changelogs/unreleased/34779-editing-metric-dashboard-using-yml-file.yml
@@ -0,0 +1,5 @@
+---
+title: Add edit button to metrics dashboard
+merge_request: 19279
+author:
+type: added
diff --git a/changelogs/unreleased/34827-delete-container-registry-tag-undefined-method-success-for-nil.yml b/changelogs/unreleased/34827-delete-container-registry-tag-undefined-method-success-for-nil.yml
new file mode 100644
index 00000000000..97f5fae95bb
--- /dev/null
+++ b/changelogs/unreleased/34827-delete-container-registry-tag-undefined-method-success-for-nil.yml
@@ -0,0 +1,5 @@
+---
+title: Fix crash when docker fails deleting tags
+merge_request: 19208
+author:
+type: fixed
diff --git a/changelogs/unreleased/34850-fix-graphql-todo-ids.yml b/changelogs/unreleased/34850-fix-graphql-todo-ids.yml
new file mode 100644
index 00000000000..ba3d63a2ee5
--- /dev/null
+++ b/changelogs/unreleased/34850-fix-graphql-todo-ids.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Todo IDs in GraphQL API
+merge_request: 19068
+author:
+type: fixed
diff --git a/changelogs/unreleased/34855-dependency-list-is-not-up-to-date-frontend.yml b/changelogs/unreleased/34855-dependency-list-is-not-up-to-date-frontend.yml
new file mode 100644
index 00000000000..aa1858a1b98
--- /dev/null
+++ b/changelogs/unreleased/34855-dependency-list-is-not-up-to-date-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add pipeline information to dependency list header
+merge_request: 19352
+author:
+type: added
diff --git a/changelogs/unreleased/34944-fix-kubernetes-help-text-link.yml b/changelogs/unreleased/34944-fix-kubernetes-help-text-link.yml
new file mode 100644
index 00000000000..5fd87c43bbc
--- /dev/null
+++ b/changelogs/unreleased/34944-fix-kubernetes-help-text-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Kubernetes help text link
+merge_request: 19121
+author:
+type: fixed
diff --git a/changelogs/unreleased/35217-add_fields_to_all_dashboards.yml b/changelogs/unreleased/35217-add_fields_to_all_dashboards.yml
new file mode 100644
index 00000000000..36ee8977ab1
--- /dev/null
+++ b/changelogs/unreleased/35217-add_fields_to_all_dashboards.yml
@@ -0,0 +1,5 @@
+---
+title: Add can_edit and project_blob_path to metrics_dashboard endpoint
+merge_request: 19663
+author:
+type: added
diff --git a/changelogs/unreleased/35440-Hide-start-trial-buttons-for-expired-namespaces.yml b/changelogs/unreleased/35440-Hide-start-trial-buttons-for-expired-namespaces.yml
new file mode 100644
index 00000000000..d2d60e8c498
--- /dev/null
+++ b/changelogs/unreleased/35440-Hide-start-trial-buttons-for-expired-namespaces.yml
@@ -0,0 +1,5 @@
+---
+title: Hide repeated trial offers on self-hosted instances
+merge_request: 19511
+author:
+type: changed
diff --git a/changelogs/unreleased/35440-for-com-namespaces-with-expired-trials-remove-start-trial-ctas.yml b/changelogs/unreleased/35440-for-com-namespaces-with-expired-trials-remove-start-trial-ctas.yml
new file mode 100644
index 00000000000..3b4ec67334f
--- /dev/null
+++ b/changelogs/unreleased/35440-for-com-namespaces-with-expired-trials-remove-start-trial-ctas.yml
@@ -0,0 +1,5 @@
+---
+title: Hide trial banner for namespaces with expired trials
+merge_request: 19510
+author:
+type: changed
diff --git a/changelogs/unreleased/35528-add-ui-event-tracking-for-container-registry.yml b/changelogs/unreleased/35528-add-ui-event-tracking-for-container-registry.yml
new file mode 100644
index 00000000000..9b1ae378fef
--- /dev/null
+++ b/changelogs/unreleased/35528-add-ui-event-tracking-for-container-registry.yml
@@ -0,0 +1,5 @@
+---
+title: Add event tracking to container registry
+merge_request: 19772
+author:
+type: changed
diff --git a/changelogs/unreleased/35534-broken-scroll-to-bottom.yml b/changelogs/unreleased/35534-broken-scroll-to-bottom.yml
new file mode 100644
index 00000000000..a7b6d06a8f8
--- /dev/null
+++ b/changelogs/unreleased/35534-broken-scroll-to-bottom.yml
@@ -0,0 +1,5 @@
+---
+title: Fix scroll to bottom with new job log
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/35537-button-regression-fix.yml b/changelogs/unreleased/35537-button-regression-fix.yml
new file mode 100644
index 00000000000..05c2e7a5a62
--- /dev/null
+++ b/changelogs/unreleased/35537-button-regression-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Revert btn-xs styling in projects scss
+merge_request: 19640
+author:
+type: fixed
diff --git a/changelogs/unreleased/35547-add-documentation-for-sign-in-application-setting.yml b/changelogs/unreleased/35547-add-documentation-for-sign-in-application-setting.yml
new file mode 100644
index 00000000000..d13fa1d8fc5
--- /dev/null
+++ b/changelogs/unreleased/35547-add-documentation-for-sign-in-application-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Add documentation for sign-in application setting
+merge_request: 19561
+author: Horatiu Eugen Vlad
+type: added
diff --git a/changelogs/unreleased/35618-add-clipboard-button-to-package-registry-information.yml b/changelogs/unreleased/35618-add-clipboard-button-to-package-registry-information.yml
new file mode 100644
index 00000000000..faf22561fb0
--- /dev/null
+++ b/changelogs/unreleased/35618-add-clipboard-button-to-package-registry-information.yml
@@ -0,0 +1,5 @@
+---
+title: Adds a copy button next to package metadata on the details page
+merge_request: 19881
+author:
+type: added
diff --git a/changelogs/unreleased/35637-add-start-a-trial-option-in-top-right-drop-down.yml b/changelogs/unreleased/35637-add-start-a-trial-option-in-top-right-drop-down.yml
new file mode 100644
index 00000000000..f07de3cc104
--- /dev/null
+++ b/changelogs/unreleased/35637-add-start-a-trial-option-in-top-right-drop-down.yml
@@ -0,0 +1,5 @@
+---
+title: Add start a trial option in the top-right user dropdown
+merge_request: 19632
+author:
+type: added
diff --git a/changelogs/unreleased/35709-update-squash-commit-sha-only-on-successful-merge.yml b/changelogs/unreleased/35709-update-squash-commit-sha-only-on-successful-merge.yml
new file mode 100644
index 00000000000..8ab549c4b06
--- /dev/null
+++ b/changelogs/unreleased/35709-update-squash-commit-sha-only-on-successful-merge.yml
@@ -0,0 +1,5 @@
+---
+title: Update squash_commit_sha only on successful merge
+merge_request: 19688
+author:
+type: fixed
diff --git a/changelogs/unreleased/35731-fix-snippets-with-emoji-import.yml b/changelogs/unreleased/35731-fix-snippets-with-emoji-import.yml
new file mode 100644
index 00000000000..bf977958ca3
--- /dev/null
+++ b/changelogs/unreleased/35731-fix-snippets-with-emoji-import.yml
@@ -0,0 +1,5 @@
+---
+title: Fix import of snippets having `award_emoji` (Project Export/Import)
+merge_request: 19690
+author:
+type: fixed
diff --git a/changelogs/unreleased/35749-improve-performance-of-lfs_object-queries.yml b/changelogs/unreleased/35749-improve-performance-of-lfs_object-queries.yml
new file mode 100644
index 00000000000..ea2582805c4
--- /dev/null
+++ b/changelogs/unreleased/35749-improve-performance-of-lfs_object-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of linking LFS objects during import
+merge_request: 19709
+author:
+type: performance
diff --git a/changelogs/unreleased/35751-new-trial-flow-for-logged-in-user-should-not-take-them-to-the-custo.yml b/changelogs/unreleased/35751-new-trial-flow-for-logged-in-user-should-not-take-them-to-the-custo.yml
new file mode 100644
index 00000000000..4e3a1479f11
--- /dev/null
+++ b/changelogs/unreleased/35751-new-trial-flow-for-logged-in-user-should-not-take-them-to-the-custo.yml
@@ -0,0 +1,5 @@
+---
+title: Use new trial registration URL in billing
+merge_request: 19978
+author:
+type: fixed
diff --git a/changelogs/unreleased/35908-embedded-videos-not-scaled-correctly.yml b/changelogs/unreleased/35908-embedded-videos-not-scaled-correctly.yml
new file mode 100644
index 00000000000..a5234533dc2
--- /dev/null
+++ b/changelogs/unreleased/35908-embedded-videos-not-scaled-correctly.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed the scale of embedded videos to fit the page
+merge_request: 20056
+author:
+type: fixed
diff --git a/changelogs/unreleased/36113-visual-design-of-edit-and-web-ide-button-in-blob-view.yml b/changelogs/unreleased/36113-visual-design-of-edit-and-web-ide-button-in-blob-view.yml
new file mode 100644
index 00000000000..30583144a9e
--- /dev/null
+++ b/changelogs/unreleased/36113-visual-design-of-edit-and-web-ide-button-in-blob-view.yml
@@ -0,0 +1,5 @@
+---
+title: Visual design for edit buttons in blob view
+merge_request: 19932
+author:
+type: other
diff --git a/changelogs/unreleased/36141-update-start-a-trial-option-in-top-right-drop-down-to-include-gold.yml b/changelogs/unreleased/36141-update-start-a-trial-option-in-top-right-drop-down-to-include-gold.yml
new file mode 100644
index 00000000000..6952e630e2d
--- /dev/null
+++ b/changelogs/unreleased/36141-update-start-a-trial-option-in-top-right-drop-down-to-include-gold.yml
@@ -0,0 +1,5 @@
+---
+title: Update start a trial option in top right drop down to include Gold
+merge_request: 19971
+author:
+type: changed
diff --git a/changelogs/unreleased/36142-update-saas-trial-header-to-include-the-tier-gold.yml b/changelogs/unreleased/36142-update-saas-trial-header-to-include-the-tier-gold.yml
new file mode 100644
index 00000000000..d68b5b1f4b9
--- /dev/null
+++ b/changelogs/unreleased/36142-update-saas-trial-header-to-include-the-tier-gold.yml
@@ -0,0 +1,5 @@
+---
+title: Update SaaS trial header to include the tier Gold
+merge_request: 19970
+author:
+type: changed
diff --git a/changelogs/unreleased/36213-update-codequality-to-12-5.yml b/changelogs/unreleased/36213-update-codequality-to-12-5.yml
new file mode 100644
index 00000000000..5b4429af81e
--- /dev/null
+++ b/changelogs/unreleased/36213-update-codequality-to-12-5.yml
@@ -0,0 +1,5 @@
+---
+title: Update registry.gitlab.com/gitlab-org/security-products/codequality to 12-5-stable
+merge_request: 20046
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/36262-monitor-cluster-health-charts-does-not-load.yml b/changelogs/unreleased/36262-monitor-cluster-health-charts-does-not-load.yml
new file mode 100644
index 00000000000..95817e6010e
--- /dev/null
+++ b/changelogs/unreleased/36262-monitor-cluster-health-charts-does-not-load.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken monitor cluster health dashboard
+merge_request: 20120
+author:
+type: fixed
diff --git a/changelogs/unreleased/36453-flashcontainer-causing-page-header-to-be-off-center.yml b/changelogs/unreleased/36453-flashcontainer-causing-page-header-to-be-off-center.yml
new file mode 100644
index 00000000000..12f44a5f661
--- /dev/null
+++ b/changelogs/unreleased/36453-flashcontainer-causing-page-header-to-be-off-center.yml
@@ -0,0 +1,5 @@
+---
+title: Move margin-top from flash container to flash
+merge_request: 20211
+author:
+type: other
diff --git a/changelogs/unreleased/36460-metrics-dashboard-fails-to-load-script-doesn-t-stop.yml b/changelogs/unreleased/36460-metrics-dashboard-fails-to-load-script-doesn-t-stop.yml
new file mode 100644
index 00000000000..313df8ba4fb
--- /dev/null
+++ b/changelogs/unreleased/36460-metrics-dashboard-fails-to-load-script-doesn-t-stop.yml
@@ -0,0 +1,5 @@
+---
+title: Remove update hook from date filter to prevent js from getting stuck
+merge_request: 20215
+author:
+type: fixed
diff --git a/changelogs/unreleased/48-add-company-question-to-profile-information.yml b/changelogs/unreleased/48-add-company-question-to-profile-information.yml
new file mode 100644
index 00000000000..255f2289cdc
--- /dev/null
+++ b/changelogs/unreleased/48-add-company-question-to-profile-information.yml
@@ -0,0 +1,5 @@
+---
+title: Ask if the user is setting up GitLab for a company during signup
+merge_request: 17999
+author:
+type: changed
diff --git a/changelogs/unreleased/5366-display-anomaly-deviation-boundaries-on-dashboard-ce.yml b/changelogs/unreleased/5366-display-anomaly-deviation-boundaries-on-dashboard-ce.yml
new file mode 100644
index 00000000000..08b85156259
--- /dev/null
+++ b/changelogs/unreleased/5366-display-anomaly-deviation-boundaries-on-dashboard-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Added new chart component to display an anomaly boundary
+merge_request: 16530
+author:
+type: added
diff --git a/changelogs/unreleased/63778-graphql-add-issue-due-date-sortring.yml b/changelogs/unreleased/63778-graphql-add-issue-due-date-sortring.yml
new file mode 100644
index 00000000000..805c0f3c6b2
--- /dev/null
+++ b/changelogs/unreleased/63778-graphql-add-issue-due-date-sortring.yml
@@ -0,0 +1,5 @@
+---
+title: 'Issues queried in GraphQL now sortable by due date'
+merge_request: 18094
+author:
+type: added
diff --git a/changelogs/unreleased/7104-show-timeframe-dates-in-epics-list.yml b/changelogs/unreleased/7104-show-timeframe-dates-in-epics-list.yml
new file mode 100644
index 00000000000..529c3858fad
--- /dev/null
+++ b/changelogs/unreleased/7104-show-timeframe-dates-in-epics-list.yml
@@ -0,0 +1,5 @@
+---
+title: Show start and end dates in Epics list page
+merge_request: 19006
+author:
+type: added
diff --git a/changelogs/unreleased/7816-commits-diff-total-api.yml b/changelogs/unreleased/7816-commits-diff-total-api.yml
new file mode 100644
index 00000000000..95b8afda682
--- /dev/null
+++ b/changelogs/unreleased/7816-commits-diff-total-api.yml
@@ -0,0 +1,5 @@
+---
+title: Show correct total number of commit diff's changes
+merge_request: 19424
+author:
+type: fixed
diff --git a/changelogs/unreleased/8199-epic-quick-actions-preview.yml b/changelogs/unreleased/8199-epic-quick-actions-preview.yml
new file mode 100644
index 00000000000..640c2b47c6f
--- /dev/null
+++ b/changelogs/unreleased/8199-epic-quick-actions-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix previewing quick actions for epics
+merge_request: 19042
+author:
+type: fixed
diff --git a/changelogs/unreleased/8558-bump-ado-image-for-modsec-secruleengine.yml b/changelogs/unreleased/8558-bump-ado-image-for-modsec-secruleengine.yml
new file mode 100644
index 00000000000..615ae1452d0
--- /dev/null
+++ b/changelogs/unreleased/8558-bump-ado-image-for-modsec-secruleengine.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Auto-Deploy image to v0.3.0
+merge_request: 18809
+author:
+type: added
diff --git a/changelogs/unreleased/8558-use-custom-modsecurity-ingress-config.yml b/changelogs/unreleased/8558-use-custom-modsecurity-ingress-config.yml
new file mode 100644
index 00000000000..180715e89d6
--- /dev/null
+++ b/changelogs/unreleased/8558-use-custom-modsecurity-ingress-config.yml
@@ -0,0 +1,5 @@
+---
+title: Add modsecurity template for ingress-controller
+merge_request: 18485
+author:
+type: changed
diff --git a/changelogs/unreleased/CauhxMilloy-gitlab-addStartEndMarkersForTagsSearch.yml b/changelogs/unreleased/CauhxMilloy-gitlab-addStartEndMarkersForTagsSearch.yml
new file mode 100644
index 00000000000..e06f7868a34
--- /dev/null
+++ b/changelogs/unreleased/CauhxMilloy-gitlab-addStartEndMarkersForTagsSearch.yml
@@ -0,0 +1,5 @@
+---
+title: Adding support for searching tags using '^' and '$'
+merge_request: 19435
+author: Cauhx Milloy
+type: added
diff --git a/changelogs/unreleased/Remove-IIFEs-from-image_file-js.yml b/changelogs/unreleased/Remove-IIFEs-from-image_file-js.yml
new file mode 100644
index 00000000000..4a5f8a892a7
--- /dev/null
+++ b/changelogs/unreleased/Remove-IIFEs-from-image_file-js.yml
@@ -0,0 +1,5 @@
+---
+title: Removed IIFEs from image_file.js
+merge_request: 19548
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Remove-IIFEs-from-network-js.yml b/changelogs/unreleased/Remove-IIFEs-from-network-js.yml
new file mode 100644
index 00000000000..2db5ab239d2
--- /dev/null
+++ b/changelogs/unreleased/Remove-IIFEs-from-network-js.yml
@@ -0,0 +1,5 @@
+---
+title: Removed IIFEs from network.js file
+merge_request: 19254
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Update-boards-components-board_form-vue.yml b/changelogs/unreleased/Update-boards-components-board_form-vue.yml
new file mode 100644
index 00000000000..e754aaf3b94
--- /dev/null
+++ b/changelogs/unreleased/Update-boards-components-board_form-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Remove all reference to BoardService in board_form.vue
+merge_request: 20158
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Update-boards-components-models-index-vue.yml b/changelogs/unreleased/Update-boards-components-models-index-vue.yml
new file mode 100644
index 00000000000..e328c6ca0da
--- /dev/null
+++ b/changelogs/unreleased/Update-boards-components-models-index-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Remove all references to BoardsService in index.vue
+merge_request: 20152
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml b/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml
new file mode 100644
index 00000000000..95234bd9069
--- /dev/null
+++ b/changelogs/unreleased/Update-boards_selector-vue-to-use-boardsStore.yml
@@ -0,0 +1,5 @@
+---
+title: remove all references of BoardService in boards_selector.vue
+merge_request: 20147
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/ab-projects-api-indexes-authenticated.yml b/changelogs/unreleased/ab-projects-api-indexes-authenticated.yml
new file mode 100644
index 00000000000..7bfb2b8d166
--- /dev/null
+++ b/changelogs/unreleased/ab-projects-api-indexes-authenticated.yml
@@ -0,0 +1,5 @@
+---
+title: Add index for authenticated requests to projects API default endpoint
+merge_request: 19993
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-projects-api-indexes.yml b/changelogs/unreleased/ab-projects-api-indexes.yml
new file mode 100644
index 00000000000..90b67c08fef
--- /dev/null
+++ b/changelogs/unreleased/ab-projects-api-indexes.yml
@@ -0,0 +1,5 @@
+---
+title: Add index for unauthenticated requests to projects API default endpoint
+merge_request: 19989
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-projects-id-filter.yml b/changelogs/unreleased/ab-projects-id-filter.yml
new file mode 100644
index 00000000000..6bc21ac4452
--- /dev/null
+++ b/changelogs/unreleased/ab-projects-id-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add id_before, id_after filter param to projects API
+merge_request: 19949
+author:
+type: added
diff --git a/changelogs/unreleased/add-dead-jobs-to-api-sidekiq-metrics.yml b/changelogs/unreleased/add-dead-jobs-to-api-sidekiq-metrics.yml
new file mode 100644
index 00000000000..ac06e83bc73
--- /dev/null
+++ b/changelogs/unreleased/add-dead-jobs-to-api-sidekiq-metrics.yml
@@ -0,0 +1,5 @@
+---
+title: Add dead jobs to Sidekiq metrics API
+merge_request: 19350
+author: Marco Peterseil
+type: added
diff --git a/changelogs/unreleased/add-default-plan.yml b/changelogs/unreleased/add-default-plan.yml
new file mode 100644
index 00000000000..0bcab9b2919
--- /dev/null
+++ b/changelogs/unreleased/add-default-plan.yml
@@ -0,0 +1,5 @@
+---
+title: Create explicit Default and Free plans
+merge_request: 19033
+author:
+type: other
diff --git a/changelogs/unreleased/add-inheritable-mixin.yml b/changelogs/unreleased/add-inheritable-mixin.yml
new file mode 100644
index 00000000000..e0fd2326cc5
--- /dev/null
+++ b/changelogs/unreleased/add-inheritable-mixin.yml
@@ -0,0 +1,5 @@
+---
+title: Make `Job`, `Bridge` and `Default` inheritable
+merge_request: 18867
+author:
+type: added
diff --git a/changelogs/unreleased/add-missing-bottom-padding-in-settings.yml b/changelogs/unreleased/add-missing-bottom-padding-in-settings.yml
new file mode 100644
index 00000000000..b87f83ce33a
--- /dev/null
+++ b/changelogs/unreleased/add-missing-bottom-padding-in-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing bottom padding in CI/CD settings
+merge_request: 19284
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/add-pendo-snippet.yml b/changelogs/unreleased/add-pendo-snippet.yml
new file mode 100644
index 00000000000..c78db1f5d83
--- /dev/null
+++ b/changelogs/unreleased/add-pendo-snippet.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Application Settings and ui settings in the integration admin area for Pendo
+merge_request: 15086
+author:
+type: added
diff --git a/changelogs/unreleased/add-rails-parser-for-new-cs-report-format.yml b/changelogs/unreleased/add-rails-parser-for-new-cs-report-format.yml
new file mode 100644
index 00000000000..ae76ad3a4e6
--- /dev/null
+++ b/changelogs/unreleased/add-rails-parser-for-new-cs-report-format.yml
@@ -0,0 +1,5 @@
+---
+title: Handle new Container Scanning report format
+merge_request: 19123
+author:
+type: changed
diff --git a/changelogs/unreleased/add-slack-slash-command-issue-comment.yml b/changelogs/unreleased/add-slack-slash-command-issue-comment.yml
new file mode 100644
index 00000000000..ccd82830303
--- /dev/null
+++ b/changelogs/unreleased/add-slack-slash-command-issue-comment.yml
@@ -0,0 +1,5 @@
+---
+title: Add a Slack slash command to add a comment to an issue
+merge_request: 18946
+author:
+type: added
diff --git a/changelogs/unreleased/add-snowplow-iglu-registry-application-setting.yml b/changelogs/unreleased/add-snowplow-iglu-registry-application-setting.yml
new file mode 100644
index 00000000000..b1e7447eaad
--- /dev/null
+++ b/changelogs/unreleased/add-snowplow-iglu-registry-application-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Add ApplicationSetting for snowplow_iglu_registry_url
+merge_request: 18449
+author:
+type: added
diff --git a/changelogs/unreleased/ak-add-elastic-cluster-app.yml b/changelogs/unreleased/ak-add-elastic-cluster-app.yml
new file mode 100644
index 00000000000..b8fd0880553
--- /dev/null
+++ b/changelogs/unreleased/ak-add-elastic-cluster-app.yml
@@ -0,0 +1,4 @@
+title: Create table for elastic stack.
+merge_request: 18015
+author:
+type: added
diff --git a/changelogs/unreleased/ak-fix-undefined-value.yml b/changelogs/unreleased/ak-fix-undefined-value.yml
new file mode 100644
index 00000000000..d85f5ddc23f
--- /dev/null
+++ b/changelogs/unreleased/ak-fix-undefined-value.yml
@@ -0,0 +1,5 @@
+---
+title: Fix uninitialized constant SystemDashboardService
+merge_request: 19453
+author:
+type: fixed
diff --git a/changelogs/unreleased/allow-adding-requests-to-performance-bar-manually.yml b/changelogs/unreleased/allow-adding-requests-to-performance-bar-manually.yml
new file mode 100644
index 00000000000..0ec1167d9eb
--- /dev/null
+++ b/changelogs/unreleased/allow-adding-requests-to-performance-bar-manually.yml
@@ -0,0 +1,5 @@
+---
+title: Allow adding requests to performance bar manually
+merge_request: 18464
+author:
+type: other
diff --git a/changelogs/unreleased/allow-container-scanning-to-run-offline.yml b/changelogs/unreleased/allow-container-scanning-to-run-offline.yml
new file mode 100644
index 00000000000..5cce0565d28
--- /dev/null
+++ b/changelogs/unreleased/allow-container-scanning-to-run-offline.yml
@@ -0,0 +1,5 @@
+---
+title: Allow container scanning to run offline by specifying the Clair DB image to use.
+merge_request: 19161
+author:
+type: changed
diff --git a/changelogs/unreleased/an-mark-jobs-as-latency-sensitive.yml b/changelogs/unreleased/an-mark-jobs-as-latency-sensitive.yml
new file mode 100644
index 00000000000..0ed019b8500
--- /dev/null
+++ b/changelogs/unreleased/an-mark-jobs-as-latency-sensitive.yml
@@ -0,0 +1,5 @@
+---
+title: Attribute Sidekiq workers according to their workloads
+merge_request: 18066
+author:
+type: other
diff --git a/changelogs/unreleased/an-sidekiq-records-failure-durations.yml b/changelogs/unreleased/an-sidekiq-records-failure-durations.yml
new file mode 100644
index 00000000000..bbb0b230c6b
--- /dev/null
+++ b/changelogs/unreleased/an-sidekiq-records-failure-durations.yml
@@ -0,0 +1,5 @@
+---
+title: Record latencies for Sidekiq failures
+merge_request: 18909
+author:
+type: performance
diff --git a/changelogs/unreleased/andr3-fix-designmanagement-upload-limit.yml b/changelogs/unreleased/andr3-fix-designmanagement-upload-limit.yml
new file mode 100644
index 00000000000..71f9a9ba319
--- /dev/null
+++ b/changelogs/unreleased/andr3-fix-designmanagement-upload-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Apply correctly the limit of 10 designs per upload
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/auto-deploy-image-0-7-0.yml b/changelogs/unreleased/auto-deploy-image-0-7-0.yml
new file mode 100644
index 00000000000..ddb5b3d808d
--- /dev/null
+++ b/changelogs/unreleased/auto-deploy-image-0-7-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Auto DevOps deploy image to v0.7.0
+merge_request: 20250
+author:
+type: other
diff --git a/changelogs/unreleased/backstage-remove-export-designs-feature-flag.yml b/changelogs/unreleased/backstage-remove-export-designs-feature-flag.yml
new file mode 100644
index 00000000000..8e44173ba60
--- /dev/null
+++ b/changelogs/unreleased/backstage-remove-export-designs-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Removes `export_designs` feature flag
+merge_request: 18507
+author: nate geslin
+type: other
diff --git a/changelogs/unreleased/blackst0ne-make-sidekiq-testing-fake-default.yml b/changelogs/unreleased/blackst0ne-make-sidekiq-testing-fake-default.yml
new file mode 100644
index 00000000000..658ead4c4fb
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-make-sidekiq-testing-fake-default.yml
@@ -0,0 +1,5 @@
+---
+title: Make 'Sidekiq::Testing.fake!' mode as default
+merge_request: 31662
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/bug-fix-27164-Image-cannot-be-collapsed-on-merge-request-changes-tab.yml b/changelogs/unreleased/bug-fix-27164-Image-cannot-be-collapsed-on-merge-request-changes-tab.yml
new file mode 100644
index 00000000000..22956631131
--- /dev/null
+++ b/changelogs/unreleased/bug-fix-27164-Image-cannot-be-collapsed-on-merge-request-changes-tab.yml
@@ -0,0 +1,5 @@
+---
+title: 'fixed #27164 Image cannot be collapsed on merge request changes tab'
+merge_request: 18917
+author: Jannik Lehmann
+type: fixed
diff --git a/changelogs/unreleased/bvl-robust-gpg-homedir-cleanup.yml b/changelogs/unreleased/bvl-robust-gpg-homedir-cleanup.yml
new file mode 100644
index 00000000000..766e9f16968
--- /dev/null
+++ b/changelogs/unreleased/bvl-robust-gpg-homedir-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Improve handling of gpg-agent processes
+merge_request: 19311
+author:
+type: changed
diff --git a/changelogs/unreleased/change-default-factor-on-merge-train.yml b/changelogs/unreleased/change-default-factor-on-merge-train.yml
new file mode 100644
index 00000000000..7228366e44c
--- /dev/null
+++ b/changelogs/unreleased/change-default-factor-on-merge-train.yml
@@ -0,0 +1,5 @@
+---
+title: Change the default concurrency factor of merge train to 20
+merge_request: 20201
+author:
+type: changed
diff --git a/changelogs/unreleased/chore-slugify-duplication-removal.yml b/changelogs/unreleased/chore-slugify-duplication-removal.yml
new file mode 100644
index 00000000000..a076e7f5429
--- /dev/null
+++ b/changelogs/unreleased/chore-slugify-duplication-removal.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplication from slugifyWithUnderscore function
+merge_request: 20016
+author: Arun Kumar Mohan
+type: other
diff --git a/changelogs/unreleased/cluster_management_projects_updating.yml b/changelogs/unreleased/cluster_management_projects_updating.yml
new file mode 100644
index 00000000000..8f1876fc5a1
--- /dev/null
+++ b/changelogs/unreleased/cluster_management_projects_updating.yml
@@ -0,0 +1,5 @@
+---
+title: Adds ability to set management project for cluster via API
+merge_request: 18429
+author:
+type: added
diff --git a/changelogs/unreleased/consider-location-fingerprint-in-mr-widget.yml b/changelogs/unreleased/consider-location-fingerprint-in-mr-widget.yml
new file mode 100644
index 00000000000..5d5edf25a3c
--- /dev/null
+++ b/changelogs/unreleased/consider-location-fingerprint-in-mr-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Use fingerprint when comparing security reports in MR widget
+merge_request: 19654
+author:
+type: fixed
diff --git a/changelogs/unreleased/defect-diff-file-size.yml b/changelogs/unreleased/defect-diff-file-size.yml
new file mode 100644
index 00000000000..9bc24cadf6b
--- /dev/null
+++ b/changelogs/unreleased/defect-diff-file-size.yml
@@ -0,0 +1,5 @@
+---
+title: Re-add missing file sizes in 2-Up diff file viewer
+merge_request: 19710
+author:
+type: fixed
diff --git a/changelogs/unreleased/deployment-commit-tracking.yml b/changelogs/unreleased/deployment-commit-tracking.yml
new file mode 100644
index 00000000000..357bf6af60f
--- /dev/null
+++ b/changelogs/unreleased/deployment-commit-tracking.yml
@@ -0,0 +1,5 @@
+---
+title: Add deployment_merge_requests table
+merge_request: 18755
+author:
+type: other
diff --git a/changelogs/unreleased/do-not-abort-merge-trains-on-ff.yml b/changelogs/unreleased/do-not-abort-merge-trains-on-ff.yml
new file mode 100644
index 00000000000..cdaf004c553
--- /dev/null
+++ b/changelogs/unreleased/do-not-abort-merge-trains-on-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Abort only MWPS when FF only merge is impossible
+merge_request: 18591
+author:
+type: fixed
diff --git a/changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml b/changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml
new file mode 100644
index 00000000000..3071269df2c
--- /dev/null
+++ b/changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml
@@ -0,0 +1,5 @@
+---
+title: Drop `id` column from `ci_build_trace_sections` table
+merge_request: 18741
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-abuse-reports-filter.yml b/changelogs/unreleased/dz-abuse-reports-filter.yml
new file mode 100644
index 00000000000..99211d84b58
--- /dev/null
+++ b/changelogs/unreleased/dz-abuse-reports-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add user filtering to abuse reports page
+merge_request: 19365
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-add-indices-to-abuse-reports.yml b/changelogs/unreleased/dz-add-indices-to-abuse-reports.yml
new file mode 100644
index 00000000000..8ea5dfce12d
--- /dev/null
+++ b/changelogs/unreleased/dz-add-indices-to-abuse-reports.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of admin/abuse_reports page
+merge_request: 19630
+author:
+type: performance
diff --git a/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml b/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml
new file mode 100644
index 00000000000..ca8842190cf
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-clusters-api-doc-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fix api docs for deleting project cluster
+merge_request: 19558
+author:
+type: other
diff --git a/changelogs/unreleased/dz-improve-admin-features.yml b/changelogs/unreleased/dz-improve-admin-features.yml
new file mode 100644
index 00000000000..a15b593d04e
--- /dev/null
+++ b/changelogs/unreleased/dz-improve-admin-features.yml
@@ -0,0 +1,5 @@
+---
+title: Improve admin dashboard features
+merge_request: 18666
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-move-project-routes.yml b/changelogs/unreleased/dz-move-project-routes.yml
new file mode 100644
index 00000000000..713f6d90f32
--- /dev/null
+++ b/changelogs/unreleased/dz-move-project-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Move some project routes under - scope
+merge_request: 19954
+author:
+type: deprecated
diff --git a/changelogs/unreleased/enable-environment-dashboard-on-prod.yml b/changelogs/unreleased/enable-environment-dashboard-on-prod.yml
new file mode 100644
index 00000000000..47da49ca8aa
--- /dev/null
+++ b/changelogs/unreleased/enable-environment-dashboard-on-prod.yml
@@ -0,0 +1,5 @@
+---
+title: Enable environments dashboard by default
+merge_request: 19838
+author:
+type: added
diff --git a/changelogs/unreleased/enable-group-events.yml b/changelogs/unreleased/enable-group-events.yml
new file mode 100644
index 00000000000..8a7af8ab170
--- /dev/null
+++ b/changelogs/unreleased/enable-group-events.yml
@@ -0,0 +1,5 @@
+---
+title: Show epic events on group activity page.
+merge_request: 18869
+author:
+type: added
diff --git a/changelogs/unreleased/environments-dashboard-ux-tweaks.yml b/changelogs/unreleased/environments-dashboard-ux-tweaks.yml
new file mode 100644
index 00000000000..bc45951538b
--- /dev/null
+++ b/changelogs/unreleased/environments-dashboard-ux-tweaks.yml
@@ -0,0 +1,5 @@
+---
+title: Minor UX improvements to Environments Dashboard page
+merge_request: 18280
+author:
+type: changed
diff --git a/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml b/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml
new file mode 100644
index 00000000000..24113325feb
--- /dev/null
+++ b/changelogs/unreleased/expose-artifacts-to-merge-request-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Expose arbitrary job artifacts in Merge Request widget
+merge_request: 18385
+author:
+type: added
diff --git a/changelogs/unreleased/fe-cluster-management-project.yml b/changelogs/unreleased/fe-cluster-management-project.yml
new file mode 100644
index 00000000000..43b4ddc8724
--- /dev/null
+++ b/changelogs/unreleased/fe-cluster-management-project.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to select a Cluster management project
+merge_request: 18928
+author:
+type: added
diff --git a/changelogs/unreleased/feat-unify-html-email-layouts.yml b/changelogs/unreleased/feat-unify-html-email-layouts.yml
new file mode 100644
index 00000000000..ff3a54e9c17
--- /dev/null
+++ b/changelogs/unreleased/feat-unify-html-email-layouts.yml
@@ -0,0 +1,5 @@
+---
+title: Unify html email layout for member html emails
+merge_request: 17699
+author: Diego Louzán
+type: added
diff --git a/changelogs/unreleased/feature-cluster-cleanup-state-machine.yml b/changelogs/unreleased/feature-cluster-cleanup-state-machine.yml
new file mode 100644
index 00000000000..0de86de090d
--- /dev/null
+++ b/changelogs/unreleased/feature-cluster-cleanup-state-machine.yml
@@ -0,0 +1,5 @@
+---
+title: Add cleanup status to clusters
+merge_request: 18144
+author:
+type: added
diff --git a/changelogs/unreleased/feature-reduce-cluster-ip-size.yml b/changelogs/unreleased/feature-reduce-cluster-ip-size.yml
new file mode 100644
index 00000000000..62bdd3b8592
--- /dev/null
+++ b/changelogs/unreleased/feature-reduce-cluster-ip-size.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce the allocated IP for Cluster and Services
+merge_request: 18341
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
new file mode 100644
index 00000000000..4014b5e7ab2
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing admin mode UI buttons on bigger screen sizes
+merge_request: 18585
+author: Diego Louzán
+type: fixed
diff --git a/changelogs/unreleased/fix-dropzone-no-element-exception.yml b/changelogs/unreleased/fix-dropzone-no-element-exception.yml
new file mode 100644
index 00000000000..80dfb44c297
--- /dev/null
+++ b/changelogs/unreleased/fix-dropzone-no-element-exception.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent Dropzone.js initialisation error by checking target element existence
+merge_request: 20256
+author: Fabio Huser
+type: fixed
diff --git a/changelogs/unreleased/fix-job-log-style-reset.yml b/changelogs/unreleased/fix-job-log-style-reset.yml
new file mode 100644
index 00000000000..eea41af23f6
--- /dev/null
+++ b/changelogs/unreleased/fix-job-log-style-reset.yml
@@ -0,0 +1,5 @@
+---
+title: Fix style reset in job log when empty ANSI sequence is encoutered
+merge_request: 20367
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-merge-train-is-not-refreshed-when-aborted.yml b/changelogs/unreleased/fix-merge-train-is-not-refreshed-when-aborted.yml
new file mode 100644
index 00000000000..ab548fe6c97
--- /dev/null
+++ b/changelogs/unreleased/fix-merge-train-is-not-refreshed-when-aborted.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge train is not refreshed when the system aborts/drops a merge request
+merge_request: 19763
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-stuck-ci-jobs-worker.yml b/changelogs/unreleased/fix-stuck-ci-jobs-worker.yml
new file mode 100644
index 00000000000..e5e160f5743
--- /dev/null
+++ b/changelogs/unreleased/fix-stuck-ci-jobs-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Properly handle exceptions in StuckCiJobsWorker
+merge_request: 19465
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix_group_container_repositories_n_1.yml b/changelogs/unreleased/fix_group_container_repositories_n_1.yml
new file mode 100644
index 00000000000..1ec2148b274
--- /dev/null
+++ b/changelogs/unreleased/fix_group_container_repositories_n_1.yml
@@ -0,0 +1,5 @@
+---
+title: Fix N+1 for group container repositories view
+merge_request: 18979
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-24837-codesanbox-usage-ping.yml b/changelogs/unreleased/fj-24837-codesanbox-usage-ping.yml
new file mode 100644
index 00000000000..480fdbd51f4
--- /dev/null
+++ b/changelogs/unreleased/fj-24837-codesanbox-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Add Codesandbox metrics to usage ping
+merge_request: 19075
+author:
+type: other
diff --git a/changelogs/unreleased/fj-32057-web-ide-preview-markdown-image-bug.yml b/changelogs/unreleased/fj-32057-web-ide-preview-markdown-image-bug.yml
new file mode 100644
index 00000000000..237f6bb6840
--- /dev/null
+++ b/changelogs/unreleased/fj-32057-web-ide-preview-markdown-image-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken images when previewing markdown files in Web IDE
+merge_request: 18899
+author:
+type: fixed
diff --git a/changelogs/unreleased/gdk-672-bump-puma-killer-limits-for-dev.yml b/changelogs/unreleased/gdk-672-bump-puma-killer-limits-for-dev.yml
new file mode 100644
index 00000000000..67b93cf95df
--- /dev/null
+++ b/changelogs/unreleased/gdk-672-bump-puma-killer-limits-for-dev.yml
@@ -0,0 +1,5 @@
+---
+title: Increase PumaWorkerKiller memory limit in development environment
+merge_request: 20039
+author:
+type: performance
diff --git a/changelogs/unreleased/georgekoltsov-fix-group-export-descendants.yml b/changelogs/unreleased/georgekoltsov-fix-group-export-descendants.yml
new file mode 100644
index 00000000000..b826e4bbac4
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-fix-group-export-descendants.yml
@@ -0,0 +1,5 @@
+---
+title: Fix sub group export to export direct children
+merge_request: 20172
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-group-structure-export-api-endp.yml b/changelogs/unreleased/georgekoltsov-group-structure-export-api-endp.yml
new file mode 100644
index 00000000000..ed14e958b7f
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-group-structure-export-api-endp.yml
@@ -0,0 +1,5 @@
+---
+title: Add API endpoint to trigger Group Structure Export
+merge_request: 19779
+author:
+type: added
diff --git a/changelogs/unreleased/gitaly-version-v1.71.0.yml b/changelogs/unreleased/gitaly-version-v1.71.0.yml
new file mode 100644
index 00000000000..306153ff4d7
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.71.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.71.0
+merge_request: 19611
+author:
+type: changed
diff --git a/changelogs/unreleased/gitlab_ci_path.yml b/changelogs/unreleased/gitlab_ci_path.yml
new file mode 100644
index 00000000000..900d1cccbab
--- /dev/null
+++ b/changelogs/unreleased/gitlab_ci_path.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to define a default CI configuration path for new projects
+merge_request: 18073
+author: Mathieu Parent
+type: added
diff --git a/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml b/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml
new file mode 100644
index 00000000000..739d865b516
--- /dev/null
+++ b/changelogs/unreleased/growth-91-when-users-update-their-profile-do-not-let-them-complete-the-up.yml
@@ -0,0 +1,5 @@
+---
+title: Make role required when editing profile
+merge_request: 19636
+author:
+type: changed
diff --git a/changelogs/unreleased/harishsr-emoticon-commit-links.yml b/changelogs/unreleased/harishsr-emoticon-commit-links.yml
new file mode 100644
index 00000000000..0ad9dd1e101
--- /dev/null
+++ b/changelogs/unreleased/harishsr-emoticon-commit-links.yml
@@ -0,0 +1,5 @@
+---
+title: Allow emojis to be linkable
+merge_request: 18014
+author:
+type: fixed
diff --git a/changelogs/unreleased/helm-v2-16-1.yml b/changelogs/unreleased/helm-v2-16-1.yml
new file mode 100644
index 00000000000..15abf254915
--- /dev/null
+++ b/changelogs/unreleased/helm-v2-16-1.yml
@@ -0,0 +1,5 @@
+---
+title: Helm v2.16.1
+merge_request: 19981
+author:
+type: fixed
diff --git a/changelogs/unreleased/hide-projects-when-admin-mode-is-disabled.yml b/changelogs/unreleased/hide-projects-when-admin-mode-is-disabled.yml
new file mode 100644
index 00000000000..4957b104e2e
--- /dev/null
+++ b/changelogs/unreleased/hide-projects-when-admin-mode-is-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Hide projects without access to admin user when admin mode is disabled
+merge_request: 18530
+author: Diego Louzán
+type: changed
diff --git a/changelogs/unreleased/id-avoid-preloading-merge-request-commits.yml b/changelogs/unreleased/id-avoid-preloading-merge-request-commits.yml
new file mode 100644
index 00000000000..e937b8f2e6e
--- /dev/null
+++ b/changelogs/unreleased/id-avoid-preloading-merge-request-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Execute limited request for diff commits instead of preloading
+merge_request: 19485
+author:
+type: performance
diff --git a/changelogs/unreleased/id-conditional-check-mergeability.yml b/changelogs/unreleased/id-conditional-check-mergeability.yml
new file mode 100644
index 00000000000..1b52c86df59
--- /dev/null
+++ b/changelogs/unreleased/id-conditional-check-mergeability.yml
@@ -0,0 +1,5 @@
+---
+title: Run check_mergeability only if merge status requires it
+merge_request: 19364
+author:
+type: performance
diff --git a/changelogs/unreleased/id-fix-any-approver-rule-for-projects.yml b/changelogs/unreleased/id-fix-any-approver-rule-for-projects.yml
new file mode 100644
index 00000000000..e435d4357e4
--- /dev/null
+++ b/changelogs/unreleased/id-fix-any-approver-rule-for-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Fix any approver project rule records
+merge_request: 18265
+author:
+type: changed
diff --git a/changelogs/unreleased/id-nil-short-commit-sha.yml b/changelogs/unreleased/id-nil-short-commit-sha.yml
new file mode 100644
index 00000000000..3d925e10616
--- /dev/null
+++ b/changelogs/unreleased/id-nil-short-commit-sha.yml
@@ -0,0 +1,5 @@
+---
+title: Serialize short sha as nil if head commit is blank
+merge_request: 19014
+author:
+type: fixed
diff --git a/changelogs/unreleased/id-optimize-mergeable-discussions-state.yml b/changelogs/unreleased/id-optimize-mergeable-discussions-state.yml
new file mode 100644
index 00000000000..db7f6712d5c
--- /dev/null
+++ b/changelogs/unreleased/id-optimize-mergeable-discussions-state.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize MergeRequest#mergeable_discussions_state? method
+merge_request: 19988
+author:
+type: performance
diff --git a/changelogs/unreleased/increase-certmanager-install-timeout.yml b/changelogs/unreleased/increase-certmanager-install-timeout.yml
new file mode 100644
index 00000000000..0f2987b0476
--- /dev/null
+++ b/changelogs/unreleased/increase-certmanager-install-timeout.yml
@@ -0,0 +1,6 @@
+---
+title: Increase the timeout for GitLab-managed cert-manager installation to 90 seconds
+ (was 30 seconds)
+merge_request: 19447
+author:
+type: fixed
diff --git a/changelogs/unreleased/infinite-scroll.yml b/changelogs/unreleased/infinite-scroll.yml
new file mode 100644
index 00000000000..825eaacad4d
--- /dev/null
+++ b/changelogs/unreleased/infinite-scroll.yml
@@ -0,0 +1,5 @@
+---
+title: Add Infinite scroll to Add Projects modal in the operations dashboard
+merge_request: 17842
+author:
+type: fixed
diff --git a/changelogs/unreleased/introduce-feature-flag-api-enable-disable.yml b/changelogs/unreleased/introduce-feature-flag-api-enable-disable.yml
new file mode 100644
index 00000000000..1c60b87d7b2
--- /dev/null
+++ b/changelogs/unreleased/introduce-feature-flag-api-enable-disable.yml
@@ -0,0 +1,5 @@
+---
+title: Support Enable/Disable operations in Feature Flag API
+merge_request: 18368
+author:
+type: added
diff --git a/changelogs/unreleased/issue-63160.yml b/changelogs/unreleased/issue-63160.yml
new file mode 100644
index 00000000000..b76ea0805bf
--- /dev/null
+++ b/changelogs/unreleased/issue-63160.yml
@@ -0,0 +1,5 @@
+---
+title: Use initial commit SHA instead of branch id to request IDE files and contents
+merge_request: 19348
+author: David Palubin
+type: fixed
diff --git a/changelogs/unreleased/jc-add-internal-socket-dir-to-setup.yml b/changelogs/unreleased/jc-add-internal-socket-dir-to-setup.yml
new file mode 100644
index 00000000000..119e836b1ee
--- /dev/null
+++ b/changelogs/unreleased/jc-add-internal-socket-dir-to-setup.yml
@@ -0,0 +1,5 @@
+---
+title: Add internal_socket_dir to gitaly config in setup helper
+merge_request: 19170
+author:
+type: other
diff --git a/changelogs/unreleased/jc-dont-render-commit-links.yml b/changelogs/unreleased/jc-dont-render-commit-links.yml
new file mode 100644
index 00000000000..6146e354702
--- /dev/null
+++ b/changelogs/unreleased/jc-dont-render-commit-links.yml
@@ -0,0 +1,5 @@
+---
+title: Do not render links in commit message on blame page
+merge_request: 19128
+author:
+type: performance
diff --git a/changelogs/unreleased/jc-dont-try-to-movedirs-unless-legacy.yml b/changelogs/unreleased/jc-dont-try-to-movedirs-unless-legacy.yml
new file mode 100644
index 00000000000..b1fd8a29a4f
--- /dev/null
+++ b/changelogs/unreleased/jc-dont-try-to-movedirs-unless-legacy.yml
@@ -0,0 +1,5 @@
+---
+title: Only move repos for legacy project storage
+merge_request: 19410
+author:
+type: fixed
diff --git a/changelogs/unreleased/jej-group-saml-test-button-shows-response.yml b/changelogs/unreleased/jej-group-saml-test-button-shows-response.yml
new file mode 100644
index 00000000000..03dcfcfb0c8
--- /dev/null
+++ b/changelogs/unreleased/jej-group-saml-test-button-shows-response.yml
@@ -0,0 +1,5 @@
+---
+title: Users can verify SAML configuration and view SamlResponse XML
+merge_request: 18362
+author:
+type: added
diff --git a/changelogs/unreleased/jej-prevent-ldap-sign-in.yml b/changelogs/unreleased/jej-prevent-ldap-sign-in.yml
new file mode 100644
index 00000000000..cc4625d8b5a
--- /dev/null
+++ b/changelogs/unreleased/jej-prevent-ldap-sign-in.yml
@@ -0,0 +1,5 @@
+---
+title: Add prevent_ldap_sign_in option so LDAP can be used exclusively for sync
+merge_request: 18749
+author:
+type: added
diff --git a/changelogs/unreleased/jh_flash_messages_styling_22992.yml b/changelogs/unreleased/jh_flash_messages_styling_22992.yml
new file mode 100644
index 00000000000..efbc2e3cd1a
--- /dev/null
+++ b/changelogs/unreleased/jh_flash_messages_styling_22992.yml
@@ -0,0 +1,5 @@
+---
+title: Update flash messages color sitewide
+merge_request: 18369
+author:
+type: changed
diff --git a/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml b/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml
new file mode 100644
index 00000000000..e9a13868559
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-add-support-heatmap-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Add heatmap chart support
+merge_request: 32424
+author:
+type: added
diff --git a/changelogs/unreleased/jj-ramirez-master-patch-71805.yml b/changelogs/unreleased/jj-ramirez-master-patch-71805.yml
new file mode 100644
index 00000000000..b3ca8261601
--- /dev/null
+++ b/changelogs/unreleased/jj-ramirez-master-patch-71805.yml
@@ -0,0 +1,5 @@
+---
+title: Update Runners Settings Text + Link to Docs
+merge_request: 18534
+author:
+type: changed
diff --git a/changelogs/unreleased/jlenny-master-patch-21737.yml b/changelogs/unreleased/jlenny-master-patch-21737.yml
new file mode 100644
index 00000000000..f1c5b3a8645
--- /dev/null
+++ b/changelogs/unreleased/jlenny-master-patch-21737.yml
@@ -0,0 +1,5 @@
+---
+title: Improve clarity of text for merge train position
+merge_request: 19031
+author:
+type: changed
diff --git a/changelogs/unreleased/job-log-support-mac-line-break.yml b/changelogs/unreleased/job-log-support-mac-line-break.yml
new file mode 100644
index 00000000000..12be5be687d
--- /dev/null
+++ b/changelogs/unreleased/job-log-support-mac-line-break.yml
@@ -0,0 +1,5 @@
+---
+title: Let ANSI \r code replace the current job log line
+merge_request: 18933
+author:
+type: fixed
diff --git a/changelogs/unreleased/jramsay-admin-mirror-helptext.yml b/changelogs/unreleased/jramsay-admin-mirror-helptext.yml
new file mode 100644
index 00000000000..56e1f6ca45b
--- /dev/null
+++ b/changelogs/unreleased/jramsay-admin-mirror-helptext.yml
@@ -0,0 +1,5 @@
+---
+title: Improve instance mirroring help text
+merge_request: 19047
+author:
+type: other
diff --git a/changelogs/unreleased/jramsay-configure-default-branch-deletion.yml b/changelogs/unreleased/jramsay-configure-default-branch-deletion.yml
new file mode 100644
index 00000000000..811a78c9dc0
--- /dev/null
+++ b/changelogs/unreleased/jramsay-configure-default-branch-deletion.yml
@@ -0,0 +1,5 @@
+---
+title: Add project option for deleting source branch
+merge_request: 18408
+author: Zsolt Kovari
+type: added
diff --git a/changelogs/unreleased/lm-grafana-auth-checkbox.yml b/changelogs/unreleased/lm-grafana-auth-checkbox.yml
new file mode 100644
index 00000000000..39642f656ec
--- /dev/null
+++ b/changelogs/unreleased/lm-grafana-auth-checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: Add grafana integration active status checkbox
+merge_request: 19255
+author:
+type: added
diff --git a/changelogs/unreleased/lm-search-list-of-sentry-errors.yml b/changelogs/unreleased/lm-search-list-of-sentry-errors.yml
new file mode 100644
index 00000000000..619c2b0c276
--- /dev/null
+++ b/changelogs/unreleased/lm-search-list-of-sentry-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Search list of Sentry errors by title in Gitlab
+merge_request: 18772
+author:
+type: added
diff --git a/changelogs/unreleased/maintenance-move-branch-selector-to-top.yml b/changelogs/unreleased/maintenance-move-branch-selector-to-top.yml
new file mode 100644
index 00000000000..78455c49116
--- /dev/null
+++ b/changelogs/unreleased/maintenance-move-branch-selector-to-top.yml
@@ -0,0 +1,6 @@
+---
+title: Reduce new MR page redundancy by moving the source/target branch selector to
+ the top
+merge_request: 17559
+author:
+type: changed
diff --git a/changelogs/unreleased/make-register-job-service-to-be-resillient.yml b/changelogs/unreleased/make-register-job-service-to-be-resillient.yml
new file mode 100644
index 00000000000..7587447c252
--- /dev/null
+++ b/changelogs/unreleased/make-register-job-service-to-be-resillient.yml
@@ -0,0 +1,5 @@
+---
+title: Make `jobs/request` to be resillient
+merge_request: 19150
+author:
+type: fixed
diff --git a/changelogs/unreleased/make_cluster_management_project_ff_default_enabled.yml b/changelogs/unreleased/make_cluster_management_project_ff_default_enabled.yml
new file mode 100644
index 00000000000..faabcca665e
--- /dev/null
+++ b/changelogs/unreleased/make_cluster_management_project_ff_default_enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Specify management project for a Kubernetes cluster
+merge_request: 20216
+author:
+type: added
diff --git a/changelogs/unreleased/mc-bug-omips-not-blocking-on-skipped-pipelines.yml b/changelogs/unreleased/mc-bug-omips-not-blocking-on-skipped-pipelines.yml
new file mode 100644
index 00000000000..560b4de81a0
--- /dev/null
+++ b/changelogs/unreleased/mc-bug-omips-not-blocking-on-skipped-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Block MR with OMIPS on skipped pipelines.
+merge_request: 18838
+author:
+type: fixed
diff --git a/changelogs/unreleased/mc-feature-flatten-ci-scripts.yml b/changelogs/unreleased/mc-feature-flatten-ci-scripts.yml
new file mode 100644
index 00000000000..3dfcc5e871c
--- /dev/null
+++ b/changelogs/unreleased/mc-feature-flatten-ci-scripts.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for YAML anchors in CI scripts.
+merge_request: 18849
+author:
+type: changed
diff --git a/changelogs/unreleased/mermaid-update.yml b/changelogs/unreleased/mermaid-update.yml
new file mode 100644
index 00000000000..33b0c1a551c
--- /dev/null
+++ b/changelogs/unreleased/mermaid-update.yml
@@ -0,0 +1,5 @@
+---
+title: Update to Mermaid v8.4.2 to support more graph types
+merge_request: 19444
+author:
+type: changed
diff --git a/changelogs/unreleased/most-affected-projects.yml b/changelogs/unreleased/most-affected-projects.yml
new file mode 100644
index 00000000000..1835f62e533
--- /dev/null
+++ b/changelogs/unreleased/most-affected-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoint for a group's vulnerable projects
+merge_request: 15317
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-release-links-to-milestone-detail-page.yml b/changelogs/unreleased/nfriend-add-release-links-to-milestone-detail-page.yml
new file mode 100644
index 00000000000..e2d57b3a65f
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-release-links-to-milestone-detail-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add links to associated release(s) to the milestone detail page
+merge_request: 17278
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-release-to-milestone-list-page.yml b/changelogs/unreleased/nfriend-add-release-to-milestone-list-page.yml
new file mode 100644
index 00000000000..46bffa3476d
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-release-to-milestone-list-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add links to associated releases on the Milestones page
+merge_request: 16558
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-releases-filter-for-issues.yml b/changelogs/unreleased/nfriend-add-releases-filter-for-issues.yml
new file mode 100644
index 00000000000..cc709eacb83
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-releases-filter-for-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Add "release" filter to issue search page
+merge_request: 18761
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-add-releases-filter-for-merge-requests.yml b/changelogs/unreleased/nfriend-add-releases-filter-for-merge-requests.yml
new file mode 100644
index 00000000000..87e987a73b4
--- /dev/null
+++ b/changelogs/unreleased/nfriend-add-releases-filter-for-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Add "release" filter to merge request search page
+merge_request: 19315
+author:
+type: added
diff --git a/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml b/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml
new file mode 100644
index 00000000000..f593db40382
--- /dev/null
+++ b/changelogs/unreleased/nfriend-edit-release-ux-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Update help text of "Tag name" field on Edit Release page
+merge_request: 19321
+author:
+type: changed
diff --git a/changelogs/unreleased/nfriend-fix-edit_url-property.yml b/changelogs/unreleased/nfriend-fix-edit_url-property.yml
new file mode 100644
index 00000000000..fa5d24c9ad6
--- /dev/null
+++ b/changelogs/unreleased/nfriend-fix-edit_url-property.yml
@@ -0,0 +1,5 @@
+---
+title: Allow release block edit button to be visible
+merge_request: 19226
+author:
+type: fixed
diff --git a/changelogs/unreleased/nfriend-move-release-data-into-footer.yml b/changelogs/unreleased/nfriend-move-release-data-into-footer.yml
new file mode 100644
index 00000000000..64772924d26
--- /dev/null
+++ b/changelogs/unreleased/nfriend-move-release-data-into-footer.yml
@@ -0,0 +1,5 @@
+---
+title: Move release meta-data into footer on Releases page
+merge_request: 19451
+author:
+type: changed
diff --git a/changelogs/unreleased/nicolasdular-confirm-email-before-ci-usage.yml b/changelogs/unreleased/nicolasdular-confirm-email-before-ci-usage.yml
new file mode 100644
index 00000000000..f24ffb6c368
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-confirm-email-before-ci-usage.yml
@@ -0,0 +1,5 @@
+---
+title: Only allow confirmed users to run pipelines
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-remove-n-plus-ones-from-branches-api-call.yml b/changelogs/unreleased/osw-remove-n-plus-ones-from-branches-api-call.yml
new file mode 100644
index 00000000000..5e491c6847e
--- /dev/null
+++ b/changelogs/unreleased/osw-remove-n-plus-ones-from-branches-api-call.yml
@@ -0,0 +1,5 @@
+---
+title: Remove N+1 DB calls from branches API
+merge_request: 19661
+author:
+type: performance
diff --git a/changelogs/unreleased/patch-35.yml b/changelogs/unreleased/patch-35.yml
new file mode 100644
index 00000000000..22dce87919a
--- /dev/null
+++ b/changelogs/unreleased/patch-35.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search button height on 404 page
+merge_request: 19080
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-fixAdminGeoSidebarFlyOut.yml b/changelogs/unreleased/ph-fixAdminGeoSidebarFlyOut.yml
new file mode 100644
index 00000000000..daafd2539fd
--- /dev/null
+++ b/changelogs/unreleased/ph-fixAdminGeoSidebarFlyOut.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed admin geo collapsed sidebar fly out not showing
+merge_request: 19012
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-fixProtectedBranchesFlash.yml b/changelogs/unreleased/ph-fixProtectedBranchesFlash.yml
new file mode 100644
index 00000000000..0307570ec24
--- /dev/null
+++ b/changelogs/unreleased/ph-fixProtectedBranchesFlash.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed protected branches flash styling
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml b/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml
new file mode 100644
index 00000000000..3ced037b74a
--- /dev/null
+++ b/changelogs/unreleased/pokstad1-gitaly-1-70-0.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 1.70.0 and remove cache invalidation feature flag
+merge_request: 18766
+author:
+type: other
diff --git a/changelogs/unreleased/record-sidekiq-queuing-latency.yml b/changelogs/unreleased/record-sidekiq-queuing-latency.yml
new file mode 100644
index 00000000000..72bacd90e33
--- /dev/null
+++ b/changelogs/unreleased/record-sidekiq-queuing-latency.yml
@@ -0,0 +1,5 @@
+---
+title: Adds a Sidekiq queue duration metric
+merge_request: 19005
+author:
+type: other
diff --git a/changelogs/unreleased/remove-dind-for-ds.yml b/changelogs/unreleased/remove-dind-for-ds.yml
new file mode 100644
index 00000000000..b60c983d91e
--- /dev/null
+++ b/changelogs/unreleased/remove-dind-for-ds.yml
@@ -0,0 +1,5 @@
+---
+title: Dependency Scanning template that doesn't rely on Docker-in-Docker
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/remove-domain-details.yml b/changelogs/unreleased/remove-domain-details.yml
new file mode 100644
index 00000000000..a9eedd580f6
--- /dev/null
+++ b/changelogs/unreleased/remove-domain-details.yml
@@ -0,0 +1,5 @@
+---
+title: Merge Details Page and Edit Page for Page Domains
+merge_request: 16687
+author:
+type: added
diff --git a/changelogs/unreleased/remove-empty-github-service-templates.yml b/changelogs/unreleased/remove-empty-github-service-templates.yml
new file mode 100644
index 00000000000..6f5bb3ddcf1
--- /dev/null
+++ b/changelogs/unreleased/remove-empty-github-service-templates.yml
@@ -0,0 +1,5 @@
+---
+title: Remove empty Github service templates from database
+merge_request: 18868
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove_local_qualifier_from_geo_sync_indicators.yml b/changelogs/unreleased/remove_local_qualifier_from_geo_sync_indicators.yml
new file mode 100644
index 00000000000..1ec7ace1740
--- /dev/null
+++ b/changelogs/unreleased/remove_local_qualifier_from_geo_sync_indicators.yml
@@ -0,0 +1,5 @@
+---
+title: Remove local qualifier from geo sync indicators
+merge_request: 20034
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/remove_unused_image_screenshot.yml b/changelogs/unreleased/remove_unused_image_screenshot.yml
new file mode 100644
index 00000000000..c24704bb6fe
--- /dev/null
+++ b/changelogs/unreleased/remove_unused_image_screenshot.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused image/screenshot
+merge_request: 20030
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/remove_var_from_bootstrap_jquery_spec_js.yml b/changelogs/unreleased/remove_var_from_bootstrap_jquery_spec_js.yml
new file mode 100644
index 00000000000..940404031c9
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_bootstrap_jquery_spec_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from bootstrap_jquery_spec.js
+merge_request: 20089
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_issue_js.yml b/changelogs/unreleased/remove_var_from_issue_js.yml
new file mode 100644
index 00000000000..ee5cf59fb56
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_issue_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from issue.js
+merge_request: 20098
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_labels_select_js.yml b/changelogs/unreleased/remove_var_from_labels_select_js.yml
new file mode 100644
index 00000000000..7186fe4b2f8
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_labels_select_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from labels_select.js
+merge_request: 20153
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_line_highlighter_js.yml b/changelogs/unreleased/remove_var_from_line_highlighter_js.yml
new file mode 100644
index 00000000000..1de400f807b
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_line_highlighter_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from line_highlighter.js
+merge_request: 20108
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_merge_request_tabs_spec_js.yml b/changelogs/unreleased/remove_var_from_merge_request_tabs_spec_js.yml
new file mode 100644
index 00000000000..826a9da0d04
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_merge_request_tabs_spec_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from merge_request_tabs_spec.js
+merge_request: 20087
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_new_branch_Form_js.yml b/changelogs/unreleased/remove_var_from_new_branch_Form_js.yml
new file mode 100644
index 00000000000..2f4bf8e44b5
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_new_branch_Form_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from new_branch_form.js
+merge_request: 20099
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_new_commit_form_js.yml b/changelogs/unreleased/remove_var_from_new_commit_form_js.yml
new file mode 100644
index 00000000000..0abac1eb7c9
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_new_commit_form_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from new_commit_form.js
+merge_request: 20095
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_preview_markdown_js.yml b/changelogs/unreleased/remove_var_from_preview_markdown_js.yml
new file mode 100644
index 00000000000..ea85fb8f275
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_preview_markdown_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from preview_markdown.js
+merge_request: 20115
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_project_select_js.yml b/changelogs/unreleased/remove_var_from_project_select_js.yml
new file mode 100644
index 00000000000..736f8adc5da
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_project_select_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from project_select.js
+merge_request: 20091
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_syntax_highlight_spec_js.yml b/changelogs/unreleased/remove_var_from_syntax_highlight_spec_js.yml
new file mode 100644
index 00000000000..e68c9c9f1e6
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_syntax_highlight_spec_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from syntax_highlight_spec.js
+merge_request: 20086
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_var_from_tree_js.yml b/changelogs/unreleased/remove_var_from_tree_js.yml
new file mode 100644
index 00000000000..9738a44c4c8
--- /dev/null
+++ b/changelogs/unreleased/remove_var_from_tree_js.yml
@@ -0,0 +1,5 @@
+---
+title: Remove var from tree.js
+merge_request: 20103
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/render-html-tags-in-job-log.yml b/changelogs/unreleased/render-html-tags-in-job-log.yml
new file mode 100644
index 00000000000..cbdcf4e6610
--- /dev/null
+++ b/changelogs/unreleased/render-html-tags-in-job-log.yml
@@ -0,0 +1,5 @@
+---
+title: Do not escape HTML tags in Ansi2json as they are escaped in the frontend
+merge_request: 19610
+author:
+type: fixed
diff --git a/changelogs/unreleased/retrigger-license-compliance.yml b/changelogs/unreleased/retrigger-license-compliance.yml
new file mode 100644
index 00000000000..de02b0417ed
--- /dev/null
+++ b/changelogs/unreleased/retrigger-license-compliance.yml
@@ -0,0 +1,5 @@
+---
+title: Triggers the correct endpoint on licence approval
+merge_request: 19078
+author:
+type: fixed
diff --git a/changelogs/unreleased/rs-change-service-failure-reasons.yml b/changelogs/unreleased/rs-change-service-failure-reasons.yml
new file mode 100644
index 00000000000..28a04d28680
--- /dev/null
+++ b/changelogs/unreleased/rs-change-service-failure-reasons.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add an `error_code` attribute to the API response when a cherry-pick or revert fails.'
+merge_request: 19518
+author:
+type: added
diff --git a/changelogs/unreleased/security-2914-labels-visible-despite-no-access-to-issues-repositories.yml b/changelogs/unreleased/security-2914-labels-visible-despite-no-access-to-issues-repositories.yml
new file mode 100644
index 00000000000..59af202a3bd
--- /dev/null
+++ b/changelogs/unreleased/security-2914-labels-visible-despite-no-access-to-issues-repositories.yml
@@ -0,0 +1,5 @@
+---
+title: Do not display project labels that are not visible for user accessing group labels
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-2920-fix-notes-with-label-cross-reference.yml b/changelogs/unreleased/security-2920-fix-notes-with-label-cross-reference.yml
new file mode 100644
index 00000000000..b2901411729
--- /dev/null
+++ b/changelogs/unreleased/security-2920-fix-notes-with-label-cross-reference.yml
@@ -0,0 +1,5 @@
+---
+title: Show cross-referenced label and milestones in issues' activities only to authorized users
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-64519-nested-graphql-query-can-cause-denial-of-service.yml b/changelogs/unreleased/security-64519-nested-graphql-query-can-cause-denial-of-service.yml
new file mode 100644
index 00000000000..5ce37b0d032
--- /dev/null
+++ b/changelogs/unreleased/security-64519-nested-graphql-query-can-cause-denial-of-service.yml
@@ -0,0 +1,5 @@
+---
+title: Analyze incoming GraphQL queries and check for recursion
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-65756-ex-admin-attacker-can-comment-in-internal.yml b/changelogs/unreleased/security-65756-ex-admin-attacker-can-comment-in-internal.yml
new file mode 100644
index 00000000000..3d9f480ba11
--- /dev/null
+++ b/changelogs/unreleased/security-65756-ex-admin-attacker-can-comment-in-internal.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow unprivileged users from commenting on private repository commits
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-bvl-validate-force-remove-branch-on-mrs.yml b/changelogs/unreleased/security-bvl-validate-force-remove-branch-on-mrs.yml
new file mode 100644
index 00000000000..50dc9c32c5d
--- /dev/null
+++ b/changelogs/unreleased/security-bvl-validate-force-remove-branch-on-mrs.yml
@@ -0,0 +1,6 @@
+---
+title: Don't allow maintainers of a target project to delete the source branch of
+ a merge request from a fork
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-developer-transfer-project.yml b/changelogs/unreleased/security-developer-transfer-project.yml
new file mode 100644
index 00000000000..fe533fc099a
--- /dev/null
+++ b/changelogs/unreleased/security-developer-transfer-project.yml
@@ -0,0 +1,5 @@
+---
+title: Require Maintainer permission on group where project is transferred to
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-hide-private-members-in-project-member-autocomplete.yml b/changelogs/unreleased/security-hide-private-members-in-project-member-autocomplete.yml
new file mode 100644
index 00000000000..5992e93bda2
--- /dev/null
+++ b/changelogs/unreleased/security-hide-private-members-in-project-member-autocomplete.yml
@@ -0,0 +1,3 @@
+---
+title: "Don't leak private members in project member autocomplete suggestions"
+type: security
diff --git a/changelogs/unreleased/security-id-fix-disclosure-of-private-repo-names.yml b/changelogs/unreleased/security-id-fix-disclosure-of-private-repo-names.yml
new file mode 100644
index 00000000000..dfd7a2d11f9
--- /dev/null
+++ b/changelogs/unreleased/security-id-fix-disclosure-of-private-repo-names.yml
@@ -0,0 +1,5 @@
+---
+title: Return 404 on LFS request if project doesn't exist
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-mask-sentry-token-ce.yml b/changelogs/unreleased/security-mask-sentry-token-ce.yml
new file mode 100644
index 00000000000..e9fe780a488
--- /dev/null
+++ b/changelogs/unreleased/security-mask-sentry-token-ce.yml
@@ -0,0 +1,4 @@
+---
+title: Mask sentry auth token in Error Tracking dashboard
+author:
+type: security
diff --git a/changelogs/unreleased/security-remove-deploy-access-levels-on-project-group-link-deletion.yml b/changelogs/unreleased/security-remove-deploy-access-levels-on-project-group-link-deletion.yml
new file mode 100644
index 00000000000..b570d182a53
--- /dev/null
+++ b/changelogs/unreleased/security-remove-deploy-access-levels-on-project-group-link-deletion.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deploy access level when project/group link is deleted
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-stored-xss-using-find-file.yml b/changelogs/unreleased/security-stored-xss-using-find-file.yml
new file mode 100644
index 00000000000..41cd2f9494f
--- /dev/null
+++ b/changelogs/unreleased/security-stored-xss-using-find-file.yml
@@ -0,0 +1,5 @@
+---
+title: Sanitize search text to prevent XSS
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/security-wiki-rdoc-content.yml b/changelogs/unreleased/security-wiki-rdoc-content.yml
new file mode 100644
index 00000000000..f40f1abcd94
--- /dev/null
+++ b/changelogs/unreleased/security-wiki-rdoc-content.yml
@@ -0,0 +1,5 @@
+---
+title: Sanitize all wiki markup formats with GitLab sanitization pipelines
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/sh-add-api-exception-to-logs.yml b/changelogs/unreleased/sh-add-api-exception-to-logs.yml
new file mode 100644
index 00000000000..273f342d8a1
--- /dev/null
+++ b/changelogs/unreleased/sh-add-api-exception-to-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Include exception and backtrace in API logs
+merge_request: 19671
+author:
+type: other
diff --git a/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml b/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml
new file mode 100644
index 00000000000..05e17892611
--- /dev/null
+++ b/changelogs/unreleased/sh-add-exception-backtrace-production-log.yml
@@ -0,0 +1,5 @@
+---
+title: Add backtrace to production_json.log
+merge_request: 20122
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-bitbucket-importer-handle-superseded.yml b/changelogs/unreleased/sh-bitbucket-importer-handle-superseded.yml
new file mode 100644
index 00000000000..35f169cae43
--- /dev/null
+++ b/changelogs/unreleased/sh-bitbucket-importer-handle-superseded.yml
@@ -0,0 +1,5 @@
+---
+title: Make Bitbucket Cloud superseded pull requests as closed
+merge_request: 19193
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-ensure-short-ttl-sessions.yml b/changelogs/unreleased/sh-ensure-short-ttl-sessions.yml
new file mode 100644
index 00000000000..86c8fb74e21
--- /dev/null
+++ b/changelogs/unreleased/sh-ensure-short-ttl-sessions.yml
@@ -0,0 +1,5 @@
+---
+title: Set shorter TTL for all unauthenticated requests
+merge_request: 19064
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-bitbucket-importer-pr-state.yml b/changelogs/unreleased/sh-fix-bitbucket-importer-pr-state.yml
new file mode 100644
index 00000000000..f5b90a296c1
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-bitbucket-importer-pr-state.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Bitbucket Cloud importer pull request state
+merge_request: 19734
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-protected-paths.yml b/changelogs/unreleased/sh-fix-protected-paths.yml
new file mode 100644
index 00000000000..1cc0e704506
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-protected-paths.yml
@@ -0,0 +1,5 @@
+---
+title: Only enable protected paths for POST requests
+merge_request: 19184
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-gitaly-duration-measurement.yml b/changelogs/unreleased/sh-gitaly-duration-measurement.yml
new file mode 100644
index 00000000000..60d00e7d9ab
--- /dev/null
+++ b/changelogs/unreleased/sh-gitaly-duration-measurement.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Gitaly call duration measurements
+merge_request: 18785
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-guard-repository-mirrors-read-only.yml b/changelogs/unreleased/sh-guard-repository-mirrors-read-only.yml
new file mode 100644
index 00000000000..5dc916e1082
--- /dev/null
+++ b/changelogs/unreleased/sh-guard-repository-mirrors-read-only.yml
@@ -0,0 +1,5 @@
+---
+title: Disable pull mirror if repository is in read-only state
+merge_request: 19182
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-set-admin-default-visibilities.yml b/changelogs/unreleased/sh-set-admin-default-visibilities.yml
new file mode 100644
index 00000000000..46e0b5c0ca1
--- /dev/null
+++ b/changelogs/unreleased/sh-set-admin-default-visibilities.yml
@@ -0,0 +1,5 @@
+---
+title: Enforce default, global project and snippet visibilities
+merge_request: 19188
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-set-httponly-experimentation-subject-id.yml b/changelogs/unreleased/sh-set-httponly-experimentation-subject-id.yml
new file mode 100644
index 00000000000..00cd50d43fd
--- /dev/null
+++ b/changelogs/unreleased/sh-set-httponly-experimentation-subject-id.yml
@@ -0,0 +1,5 @@
+---
+title: Enable the HttpOnly flag for experimentation_subject_id cookie
+merge_request: 19189
+author:
+type: security
diff --git a/changelogs/unreleased/sh-support-project-template-id-in-api.yml b/changelogs/unreleased/sh-support-project-template-id-in-api.yml
new file mode 100644
index 00000000000..5087c6c711a
--- /dev/null
+++ b/changelogs/unreleased/sh-support-project-template-id-in-api.yml
@@ -0,0 +1,5 @@
+---
+title: Support template_project_id parameter in project creation API
+merge_request: 20258
+author:
+type: added
diff --git a/changelogs/unreleased/sh-time-limit-merge-rebase-lock.yml b/changelogs/unreleased/sh-time-limit-merge-rebase-lock.yml
new file mode 100644
index 00000000000..c96db4d2ea9
--- /dev/null
+++ b/changelogs/unreleased/sh-time-limit-merge-rebase-lock.yml
@@ -0,0 +1,5 @@
+---
+title: Time limit the database lock when rebasing a merge request
+merge_request: 18481
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-aws-sdk.yml b/changelogs/unreleased/sh-update-aws-sdk.yml
new file mode 100644
index 00000000000..608cda98d86
--- /dev/null
+++ b/changelogs/unreleased/sh-update-aws-sdk.yml
@@ -0,0 +1,5 @@
+---
+title: Update AWS SDK to 2.11.374
+merge_request: 18601
+author:
+type: other
diff --git a/changelogs/unreleased/sh-update-openid-connect.yml b/changelogs/unreleased/sh-update-openid-connect.yml
new file mode 100644
index 00000000000..34341b6a385
--- /dev/null
+++ b/changelogs/unreleased/sh-update-openid-connect.yml
@@ -0,0 +1,5 @@
+---
+title: Update omniauth_openid_connect to v0.3.3
+merge_request: 19525
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-upgrade-grpc.yml b/changelogs/unreleased/sh-upgrade-grpc.yml
new file mode 100644
index 00000000000..d0c3034eb93
--- /dev/null
+++ b/changelogs/unreleased/sh-upgrade-grpc.yml
@@ -0,0 +1,5 @@
+---
+title: Update gRPC to v1.24.0
+merge_request: 18837
+author:
+type: other
diff --git a/changelogs/unreleased/sh-use-rails-redis-store.yml b/changelogs/unreleased/sh-use-rails-redis-store.yml
new file mode 100644
index 00000000000..cf20c23b415
--- /dev/null
+++ b/changelogs/unreleased/sh-use-rails-redis-store.yml
@@ -0,0 +1,5 @@
+---
+title: Use Rails 5.2 Redis caching store
+merge_request: 19202
+author:
+type: other
diff --git a/changelogs/unreleased/sh-use-template-project-id.yml b/changelogs/unreleased/sh-use-template-project-id.yml
new file mode 100644
index 00000000000..7784007f536
--- /dev/null
+++ b/changelogs/unreleased/sh-use-template-project-id.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect selection of custom templates
+merge_request: 17205
+author:
+type: fixed
diff --git a/changelogs/unreleased/show-prometheus-is-updating.yml b/changelogs/unreleased/show-prometheus-is-updating.yml
new file mode 100644
index 00000000000..a62c0eb26ac
--- /dev/null
+++ b/changelogs/unreleased/show-prometheus-is-updating.yml
@@ -0,0 +1,5 @@
+---
+title: Expose prometheus status to monitor dashboard
+merge_request: 18289
+author:
+type: fixed
diff --git a/changelogs/unreleased/sort-vulnerabilities-for-pipeline-dashboard.yml b/changelogs/unreleased/sort-vulnerabilities-for-pipeline-dashboard.yml
new file mode 100644
index 00000000000..ffbfc652b81
--- /dev/null
+++ b/changelogs/unreleased/sort-vulnerabilities-for-pipeline-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Pipeline vulnerability dashboard sort vulnerabilities by severity then confidence
+merge_request: 18863
+author:
+type: fixed
diff --git a/changelogs/unreleased/stein_ma-gitlab-patch-32.yml b/changelogs/unreleased/stein_ma-gitlab-patch-32.yml
new file mode 100644
index 00000000000..3e747408e15
--- /dev/null
+++ b/changelogs/unreleased/stein_ma-gitlab-patch-32.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed a typo in the "Keyboard Shortcuts" pop-up
+merge_request: 19217
+author: Manuel Stein
+type: fixed
diff --git a/changelogs/unreleased/tr-remove-grafana-ff.yml b/changelogs/unreleased/tr-remove-grafana-ff.yml
new file mode 100644
index 00000000000..2b62f57872a
--- /dev/null
+++ b/changelogs/unreleased/tr-remove-grafana-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Grafana charts to be embedded in Gitlab Flavored Markdown
+merge_request: 18486
+author:
+type: added
diff --git a/changelogs/unreleased/tracking-experimental-signup-flow.yml b/changelogs/unreleased/tracking-experimental-signup-flow.yml
new file mode 100644
index 00000000000..e0effd396cb
--- /dev/null
+++ b/changelogs/unreleased/tracking-experimental-signup-flow.yml
@@ -0,0 +1,5 @@
+---
+title: Track the starting and stopping of the current signup flow and the experimental signup flow
+merge_request: 17521
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-0.yml
new file mode 100644
index 00000000000..610eafe7be6
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.10.0
+merge_request: 18879
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-1.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-1.yml
new file mode 100644
index 00000000000..99e868f59f0
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-10-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.10.1
+merge_request: 19232
+author:
+type: other
diff --git a/changelogs/unreleased/update-pages-1-12.yml b/changelogs/unreleased/update-pages-1-12.yml
new file mode 100644
index 00000000000..36d61acaf15
--- /dev/null
+++ b/changelogs/unreleased/update-pages-1-12.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade pages to 1.12.0
+merge_request: 20217
+author:
+type: added
diff --git a/changelogs/unreleased/visual-review-api.yml b/changelogs/unreleased/visual-review-api.yml
new file mode 100644
index 00000000000..667ec553fb2
--- /dev/null
+++ b/changelogs/unreleased/visual-review-api.yml
@@ -0,0 +1,5 @@
+---
+title: New API endpoint for creating anonymous merge request discussions from Visual Review Tools
+merge_request: 18710
+author:
+type: added
diff --git a/changelogs/unreleased/zj-pg-connection-ff-gitaly.yml b/changelogs/unreleased/zj-pg-connection-ff-gitaly.yml
new file mode 100644
index 00000000000..39b335b76b9
--- /dev/null
+++ b/changelogs/unreleased/zj-pg-connection-ff-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Remove required dependecy of Postgresql for Gitaly
+merge_request: 18659
+author:
+type: other
diff --git a/config/application.rb b/config/application.rb
index 5d7c52c5d81..cad5c8bbe76 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -157,6 +157,8 @@ module Gitlab
config.assets.paths << "#{config.root}/vendor/assets/fonts"
config.assets.precompile << "print.css"
+ config.assets.precompile << "mailer.css"
+ config.assets.precompile << "mailer_client_specific.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "page_bundles/ide.css"
@@ -247,15 +249,18 @@ module Gitlab
end
# Use caching across all environments
+ # Full list of options:
+ # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
caching_config_hash = Gitlab::Redis::Cache.params
+ caching_config_hash[:compress] = false
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
- if Sidekiq.server? # threaded context
- caching_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5
+ if Sidekiq.server? || defined?(::Puma) # threaded context
+ caching_config_hash[:pool_size] = Gitlab::Redis::Cache.pool_size
caching_config_hash[:pool_timeout] = 1
end
- config.cache_store = :redis_store, caching_config_hash
+ config.cache_store = :redis_cache_store, caching_config_hash
config.active_job.queue_adapter = :sidekiq
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 5346bf45473..84db15d6535 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -620,3 +620,9 @@
:why: https://github.com/hexorx/countries/blob/master/LICENSE
:versions: []
:when: 2019-09-11 13:08:28.431132000 Z
+- - :whitelist
+ - "(MIT OR CC0-1.0)"
+ - :who:
+ :why:
+ :versions: []
+ :when: 2019-11-08 10:03:31.787226000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index f6814262b7a..a5486e450d4 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -8,7 +8,7 @@
# If a setting requires an application restart say so in that screen. #
# If you change this file in a Merge Request, please also create #
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests. #
-# For more details see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/0928cfb09f43993fd9454b0b14dbd1924b1407bc/doc/settings/gitlab.yml.md #
+# For more details see https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/gitlab.yml.md #
########################################################################
#
#
@@ -467,6 +467,13 @@ production: &base
# enabled: true
# primary_api_url: http://localhost:5000/ # internal address to the primary registry, will be used by GitLab to directly communicate with primary registry API
+ ## Feature Flag https://docs.gitlab.com/ee/user/project/operations/feature_flags.html
+ feature_flags:
+ unleash:
+ # enabled: false
+ # url: https://gitlab.com/api/v4/feature_flags/unleash/<project_id>
+ # app_name: gitlab.com # Environment name of your GitLab instance
+ # instance_id: INSTANCE_ID
#
# 2. GitLab CI settings
@@ -494,6 +501,7 @@ production: &base
# bundle exec rake gitlab:ldap:check RAILS_ENV=production
ldap:
enabled: false
+ prevent_ldap_sign_in: false
# This setting controls the number of seconds between LDAP permission checks
# for each user. After this time has expired for a given user, their next
@@ -1024,12 +1032,6 @@ production: &base
# enabled: true
# address: localhost
# port: 8083
- # # blackout_seconds:
- # # defines an interval to block healthcheck,
- # # but continue accepting application requests
- # # this allows Load Balancer to notice service
- # # being shutdown and not interrupt any of the clients
- # blackout_seconds: 10
## Prometheus settings
# Do not modify these settings here. They should be modified in /etc/gitlab/gitlab.rb
@@ -1041,6 +1043,14 @@ production: &base
# enable: true
# listen_address: 'localhost:9090'
+ shutdown:
+ # # blackout_seconds:
+ # # defines an interval to block healthcheck,
+ # # but continue accepting application requests
+ # # this allows Load Balancer to notice service
+ # # being shutdown and not interrupt any of the clients
+ # blackout_seconds: 10
+
#
# 5. Extra customization
# ==========================
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index c0afa207ac3..7690eafdc6b 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -12,18 +12,19 @@
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(
award_emoji
- project_statistics
- system_note_metadata
+ container_repository_registry
+ design_registry
event_log
- project_auto_devops
- project_registry
file_registry
+ group_view
job_artifact_registry
- container_repository_registry
- design_registry
- vulnerability_feedback
+ lfs_object_registry
+ project_auto_devops
+ project_registry
+ project_statistics
+ system_note_metadata
vulnerabilities_feedback
- group_view
+ vulnerability_feedback
)
inflect.acronym 'EE'
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 7ee4a4e3610..df4f49524bc 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -5,6 +5,7 @@ require_relative '../smime_signature_settings'
# Default settings
Settings['ldap'] ||= Settingslogic.new({})
Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil?
+Settings.ldap['prevent_ldap_sign_in'] = false if Settings.ldap['prevent_ldap_sign_in'].blank?
Gitlab.ee do
Settings.ldap['sync_time'] = 3600 if Settings.ldap['sync_time'].nil?
@@ -308,6 +309,13 @@ Gitlab.ee do
end
#
+# Unleash
+#
+Settings['feature_flags'] ||= Settingslogic.new({})
+Settings.feature_flags['unleash'] ||= Settingslogic.new({})
+Settings.feature_flags.unleash['enabled'] = false if Settings.feature_flags.unleash['enabled'].nil?
+
+#
# External merge request diffs
#
Settings['external_diffs'] ||= Settingslogic.new({})
@@ -668,7 +676,12 @@ Settings.monitoring['web_exporter'] ||= Settingslogic.new({})
Settings.monitoring.web_exporter['enabled'] ||= false
Settings.monitoring.web_exporter['address'] ||= 'localhost'
Settings.monitoring.web_exporter['port'] ||= 8083
-Settings.monitoring.web_exporter['blackout_seconds'] ||= 10
+
+#
+# Shutdown settings
+#
+Settings['shutdown'] ||= Settingslogic.new({})
+Settings.shutdown['blackout_seconds'] ||= 10
#
# Testing settings
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 974eff1a528..d40049970c1 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -70,8 +70,15 @@ if defined?(::Unicorn) || defined?(::Puma)
Gitlab::Metrics::Exporter::WebExporter.instance.start
end
- Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
- # We need to ensure that before we re-exec server
+ # DEPRECATED: TO BE REMOVED
+ # This is needed to implement blackout period of `web_exporter`
+ # https://gitlab.com/gitlab-org/gitlab/issues/35343#note_238479057
+ Gitlab::Cluster::LifecycleEvents.on_before_blackout_period do
+ Gitlab::Metrics::Exporter::WebExporter.instance.mark_as_not_running!
+ end
+
+ Gitlab::Cluster::LifecycleEvents.on_before_graceful_shutdown do
+ # We need to ensure that before we re-exec or shutdown server
# we do stop the exporter
Gitlab::Metrics::Exporter::WebExporter.instance.stop
end
diff --git a/config/initializers/database_config.rb b/config/initializers/database_config.rb
new file mode 100644
index 00000000000..d8c2821066b
--- /dev/null
+++ b/config/initializers/database_config.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# when running on puma, scale connection pool size with the number
+# of threads per worker process
+if defined?(::Puma)
+ db_config = Gitlab::Database.config ||
+ Rails.application.config.database_configuration[Rails.env]
+ puma_options = Puma.cli_config.options
+
+ # We use either the maximum number of threads per worker process, or
+ # the user specified value, whichever is larger.
+ desired_pool_size = [db_config['pool'].to_i, puma_options[:max_threads]].max
+
+ db_config['pool'] = desired_pool_size
+
+ # recreate the connection pool from the new config
+ ActiveRecord::Base.establish_connection(db_config)
+end
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 9f466dc39de..1496f20afc1 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -8,3 +8,15 @@ HealthCheck.setup do |config|
end
end
end
+
+Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ Gitlab::HealthChecks::MasterCheck.register_master
+end
+
+Gitlab::Cluster::LifecycleEvents.on_before_blackout_period do
+ Gitlab::HealthChecks::MasterCheck.finish_master
+end
+
+Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ Gitlab::HealthChecks::MasterCheck.register_worker
+end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index d5d4c589884..769ef2af0e7 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -10,6 +10,11 @@ unless Sidekiq.server?
# unmaintained gem that monkey patches `Time`
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.logger = ActiveSupport::Logger.new(filename)
+ config.lograge.before_format = lambda do |data, payload|
+ data.delete(:error)
+ data
+ end
+
# Add request parameters to log output
config.lograge.custom_options = lambda do |event|
params = event.payload[:params]
@@ -36,6 +41,20 @@ unless Sidekiq.server?
payload[:cpu_s] = cpu_s
end
+ # https://github.com/roidrage/lograge#logging-errors--exceptions
+ exception = event.payload[:exception_object]
+
+ if exception
+ payload[:exception] = {
+ class: exception.class.name,
+ message: exception.message
+ }
+
+ if exception.backtrace
+ payload[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(exception.backtrace)
+ end
+ end
+
payload
end
end
diff --git a/config/initializers/rack_attack_git_basic_auth.rb b/config/initializers/rack_attack_git_basic_auth.rb
index 6a721826170..71e5e2969ce 100644
--- a/config/initializers/rack_attack_git_basic_auth.rb
+++ b/config/initializers/rack_attack_git_basic_auth.rb
@@ -1,14 +1,14 @@
-rack_attack_enabled = Gitlab.config.rack_attack.git_basic_auth['enabled']
+# Tell the Rack::Attack Rack middleware to maintain an IP blacklist.
+# We update the blacklist in Gitlab::Auth::IpRateLimiter.
+Rack::Attack.blocklist('Git HTTP Basic Auth') do |req|
+ rate_limiter = Gitlab::Auth::IpRateLimiter.new(req.ip)
-unless Rails.env.test? || !rack_attack_enabled
- # Tell the Rack::Attack Rack middleware to maintain an IP blacklist. We will
- # update the blacklist from Grack::Auth#authenticate_user.
- Rack::Attack.blacklist('Git HTTP Basic Auth') do |req|
- Rack::Attack::Allow2Ban.filter(req.ip, Gitlab.config.rack_attack.git_basic_auth) do
- # This block only gets run if the IP was not already banned.
- # Return false, meaning that we do not see anything wrong with the
- # request at this time
- false
- end
+ next false if !rate_limiter.enabled? || rate_limiter.trusted_ip?
+
+ Rack::Attack::Allow2Ban.filter(req.ip, Gitlab.config.rack_attack.git_basic_auth) do
+ # This block only gets run if the IP was not already banned.
+ # Return false, meaning that we do not see anything wrong with the
+ # request at this time
+ false
end
end
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
index be7c2175cb2..a95cb09755b 100644
--- a/config/initializers/rack_attack_logging.rb
+++ b/config/initializers/rack_attack_logging.rb
@@ -2,8 +2,10 @@
#
# Adds logging for all Rack Attack blocks and throttling events.
-ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
- if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
+ActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, request_id, payload|
+ req = payload[:request]
+
+ if [:throttle, :blocklist].include? req.env['rack.attack.match_type']
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
diff --git a/config/initializers/rack_attack_new.rb b/config/initializers/rack_attack_new.rb
index b0f7febe427..92a8bf79432 100644
--- a/config/initializers/rack_attack_new.rb
+++ b/config/initializers/rack_attack_new.rb
@@ -39,45 +39,65 @@ module Gitlab::Throttle
end
class Rack::Attack
+ # Order conditions by how expensive they are:
+ # 1. The most expensive is the `req.unauthenticated?` and
+ # `req.authenticated_user_id` as it performs an expensive
+ # DB/Redis query to validate the request
+ # 2. Slightly less expensive is the need to query DB/Redis
+ # to unmarshal settings (`Gitlab::Throttle.settings`)
+ #
+ # We deliberately skip `/-/health|liveness|readiness`
+ # from Rack Attack as they need to always be accessible
+ # by Load Balancer and additional measure is implemented
+ # (token and whitelisting) to prevent abuse.
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
- Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
- req.unauthenticated? &&
- !req.should_be_skipped? &&
+ if !req.should_be_skipped? &&
+ Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
+ req.unauthenticated?
req.ip
+ end
end
throttle('throttle_authenticated_api', Gitlab::Throttle.authenticated_api_options) do |req|
- Gitlab::Throttle.settings.throttle_authenticated_api_enabled &&
- req.api_request? &&
+ if req.api_request? &&
+ Gitlab::Throttle.settings.throttle_authenticated_api_enabled
req.authenticated_user_id([:api])
+ end
end
throttle('throttle_authenticated_web', Gitlab::Throttle.authenticated_web_options) do |req|
- Gitlab::Throttle.settings.throttle_authenticated_web_enabled &&
- req.web_request? &&
+ if req.web_request? &&
+ Gitlab::Throttle.settings.throttle_authenticated_web_enabled
req.authenticated_user_id([:api, :rss, :ics])
+ end
end
throttle('throttle_unauthenticated_protected_paths', Gitlab::Throttle.protected_paths_options) do |req|
- Gitlab::Throttle.protected_paths_enabled? &&
- req.unauthenticated? &&
- !req.should_be_skipped? &&
- req.protected_path? &&
+ if req.post? &&
+ !req.should_be_skipped? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled? &&
+ req.unauthenticated?
req.ip
+ end
end
throttle('throttle_authenticated_protected_paths_api', Gitlab::Throttle.protected_paths_options) do |req|
- Gitlab::Throttle.protected_paths_enabled? &&
- req.api_request? &&
- req.protected_path? &&
+ if req.post? &&
+ req.api_request? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled?
req.authenticated_user_id([:api])
+ end
end
throttle('throttle_authenticated_protected_paths_web', Gitlab::Throttle.protected_paths_options) do |req|
- Gitlab::Throttle.protected_paths_enabled? &&
- req.web_request? &&
- req.protected_path? &&
+ if req.post? &&
+ req.web_request? &&
+ req.protected_path? &&
+ Gitlab::Throttle.protected_paths_enabled?
req.authenticated_user_id([:api, :rss, :ics])
+ end
end
class Request
@@ -97,12 +117,16 @@ class Rack::Attack
path =~ %r{^/api/v\d+/internal/}
end
+ def health_check_request?
+ path =~ %r{^/-/(health|liveness|readiness)}
+ end
+
def should_be_skipped?
- api_internal_request?
+ api_internal_request? || health_check_request?
end
def web_request?
- !api_request?
+ !api_request? && !health_check_request?
end
def protected_path?
diff --git a/config/initializers/validate_puma.rb b/config/initializers/validate_puma.rb
new file mode 100644
index 00000000000..64bd6e7bbc1
--- /dev/null
+++ b/config/initializers/validate_puma.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+if defined?(::Puma) && ::Puma.cli_config.options[:workers].to_i.zero?
+ raise 'Puma is only supported in Cluster-mode: workers > 0'
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index eff015459e3..950529f0355 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -19,6 +19,7 @@ en:
project/grafana_integration:
token: "Grafana HTTP API Token"
grafana_url: "Grafana API URL"
+ grafana_enabled: "Grafana integration enabled"
views:
pagination:
previous: "Prev"
diff --git a/config/puma.example.development.rb b/config/puma.example.development.rb
index f23ccc23c9a..6f686437f88 100644
--- a/config/puma.example.development.rb
+++ b/config/puma.example.development.rb
@@ -14,9 +14,13 @@ rackup 'config.ru'
pidfile '/home/git/gitlab/tmp/pids/puma.pid'
state_path '/home/git/gitlab/tmp/pids/puma.state'
-stdout_redirect '/home/git/gitlab/log/puma.stdout.log',
- '/home/git/gitlab/log/puma.stderr.log',
- true
+## Uncomment the lines if you would like to write puma stdout & stderr streams
+## to a different location than rails logs.
+## When using GitLab Development Kit, by default, these logs will be consumed
+## by runit and can be accessed using `gdk tail rails-web`
+# stdout_redirect '/home/git/gitlab/log/puma.stdout.log',
+# '/home/git/gitlab/log/puma.stderr.log',
+# true
# Configure "min" to be the minimum number of threads to use to answer
# requests and "max" the maximum.
diff --git a/config/routes.rb b/config/routes.rb
index 5bfae777f17..9fb4d94f068 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -57,7 +57,7 @@ Rails.application.routes.draw do
# Sign up
get 'users/sign_up/welcome' => 'registrations#welcome'
- patch 'users/sign_up/update_role' => 'registrations#update_role'
+ patch 'users/sign_up/update_registration' => 'registrations#update_registration'
# Search
get 'search' => 'search#show'
@@ -142,6 +142,13 @@ Rails.application.routes.draw do
collection do
post :create_user
post :create_gcp
+ post :create_aws
+ post :authorize_aws_role
+ delete :revoke_aws_role
+
+ scope :aws do
+ get 'api/:resource', to: 'clusters#aws_proxy', as: :aws_proxy
+ end
end
member do
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 093cde64c85..437c80b8c92 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -62,6 +62,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
delete :leave, on: :collection
end
+ resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
+
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7d51cfd6dee..3f913683b00 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -179,7 +179,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :releases, only: [:index]
+ resources :releases, only: [:index, :edit], param: :tag, constraints: { tag: %r{[^/]+} }
resources :starrers, only: [:index]
resources :forks, only: [:index, :new, :create]
resources :group_links, only: [:index, :create, :update, :destroy], constraints: { id: /\d+/ }
@@ -187,9 +187,35 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :import, only: [:new, :create, :show]
resource :avatar, only: [:show, :destroy]
- get 'grafana/proxy/:datasource_id/*proxy_path',
- to: 'grafana_api#proxy',
- as: :grafana_api
+ scope :grafana, as: :grafana_api do
+ get 'proxy/:datasource_id/*proxy_path', to: 'grafana_api#proxy'
+ get :metrics_dashboard, to: 'grafana_api#metrics_dashboard'
+ end
+
+ resource :mattermost, only: [:new, :create]
+ resource :variables, only: [:show, :update]
+ resources :triggers, only: [:index, :create, :edit, :update, :destroy]
+
+ resource :mirror, only: [:show, :update] do
+ member do
+ get :ssh_host_keys, constraints: { format: :json }
+ post :update_now
+ end
+ end
+
+ resource :cycle_analytics, only: [:show]
+
+ namespace :cycle_analytics do
+ scope :events, controller: 'events' do
+ get :issue
+ get :plan
+ get :code
+ get :test
+ get :review
+ get :staging
+ get :production
+ end
+ end
end
# End of the /-/ scope.
@@ -222,6 +248,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
member do
post :verify
+ delete :clean_certificate
end
end
end
@@ -233,8 +260,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resource :mattermost, only: [:new, :create]
-
namespace :prometheus do
resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do
get :active_common, on: :collection
@@ -274,6 +299,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :discussions, format: :json
post :rebase
get :test_reports
+ get :exposed_artifacts
scope constraints: { format: nil }, action: :show do
get :commits, defaults: { tab: 'commits' }
@@ -361,17 +387,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
put '/service_desk' => 'service_desk#update', as: :service_desk_refresh
end
- resource :variables, only: [:show, :update]
-
- resources :triggers, only: [:index, :create, :edit, :update, :destroy]
-
- resource :mirror, only: [:show, :update] do
- member do
- get :ssh_host_keys, constraints: { format: :json }
- post :update_now
- end
- end
-
Gitlab.ee do
resources :push_rules, constraints: { id: /\d+/ }, only: [:update]
end
@@ -430,6 +445,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
Gitlab.ee do
get :logs
+ get '/pods/(:pod_name)/containers/(:container_name)/logs', to: 'environments#k8s_pod_logs', as: :k8s_pod_logs
end
end
@@ -437,6 +453,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :metrics, action: :metrics_redirect
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
get :search
+
+ Gitlab.ee do
+ get :logs, action: :logs_redirect
+ end
end
resources :deployments, only: [:index] do
@@ -455,20 +475,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resource :cycle_analytics, only: [:show]
-
- namespace :cycle_analytics do
- scope :events, controller: 'events' do
- get :issue
- get :plan
- get :code
- get :test
- get :review
- get :staging
- get :production
- end
- end
-
namespace :serverless do
scope :functions do
get '/:environment_id/:id', to: 'functions#show'
@@ -609,10 +615,20 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :error_tracking, only: [:index], controller: :error_tracking do
collection do
+ get ':issue_id/details',
+ to: 'error_tracking#details',
+ as: 'details'
+ get ':issue_id/stack_trace',
+ to: 'error_tracking#stack_trace',
+ as: 'stack_trace'
post :list_projects
end
end
+ scope :usage_ping, controller: :usage_ping do
+ post :web_ide_clientside_preview
+ end
+
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
draw :wiki
@@ -648,7 +664,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# Legacy routes.
# Introduced in 12.0.
- # Should be removed after 12.1
+ # Should be removed with https://gitlab.com/gitlab-org/gitlab/issues/28848.
scope(path: '*namespace_id',
as: :namespace,
namespace_id: Gitlab::PathRegex.full_namespace_route_regex) do
@@ -660,7 +676,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
:network, :graphs, :autocomplete_sources,
:project_members, :deploy_keys, :deploy_tokens,
:labels, :milestones, :services, :boards, :releases,
- :forks, :group_links, :import, :avatar)
+ :forks, :group_links, :import, :avatar, :mirror,
+ :cycle_analytics, :mattermost, :variables, :triggers)
end
end
end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index d4616c8080d..31af321d2b2 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -13,7 +13,7 @@ def override_omniauth(provider, controller, path_prefix = '/users/auth')
end
# Use custom controller for LDAP omniauth callback
-if Gitlab::Auth::LDAP::Config.enabled?
+if Gitlab::Auth::LDAP::Config.sign_in_enabled?
devise_scope :user do
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
@@ -55,6 +55,7 @@ scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) d
get :starred, as: :starred_projects
get :snippets
get :exists
+ get :suggests
get :activity
get '/', to: redirect('%{username}'), as: nil
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index b97e8ad67c9..b4be61d8a3d 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -98,8 +98,10 @@
- [update_namespace_statistics, 1]
- [chaos, 2]
- [create_evidence, 2]
+ - [group_export, 1]
# EE-specific queues
+ - [analytics, 1]
- [ldap_group_sync, 2]
- [create_github_webhook, 2]
- [geo, 1]
@@ -120,3 +122,4 @@
- [update_external_pull_requests, 3]
- [refresh_license_compliance_checks, 2]
- [design_management_new_version, 1]
+ - [epics, 2]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 25fb6cc5f5a..9c7a3f42c97 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -73,7 +73,7 @@ function generateEntries() {
const manualEntries = {
default: defaultEntries,
- raven: './raven/index.js',
+ sentry: './sentry/index.js',
};
return Object.assign(manualEntries, autoEntries);
@@ -300,6 +300,11 @@ module.exports = {
to: path.join(ROOT_PATH, 'public/assets/webpack/cmaps/'),
},
{
+ from: path.join(ROOT_PATH, 'node_modules/@sourcegraph/code-host-integration/'),
+ to: path.join(ROOT_PATH, 'public/assets/webpack/sourcegraph/'),
+ ignore: ['package.json'],
+ },
+ {
from: path.join(
ROOT_PATH,
'node_modules/@gitlab/visual-review-tools/dist/visual_review_toolbar.js',
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index 064b8c94805..60bc90139ab 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -86,6 +86,12 @@ def unicode_emoji_regex
))x
end
+def count_filtered_commits(commits)
+ commits.count do |commit|
+ !commit.message.start_with?('fixup!', 'squash!')
+ end
+end
+
def lint_commit(commit) # rubocop:disable Metrics/AbcSize
# For now we'll ignore merge commits, as getting rid of those is a problem
# separate from enforcing good commit messages.
@@ -234,7 +240,7 @@ def lint_commit(commit) # rubocop:disable Metrics/AbcSize
fail_commit(
commit,
'Use full URLs instead of short references ' \
- '(`gitlab-org/gitlab-ce#123` or `!123`), as short references are ' \
+ '(`gitlab-org/gitlab#123` or `!123`), as short references are ' \
'displayed as plain text outside of GitLab'
)
@@ -285,7 +291,7 @@ def lint_commits(commits)
end
end
-if git.commits.length > 10 && !ce_upstream?
+if count_filtered_commits(git.commits) > 10 && !ce_upstream?
warn(
'This merge request includes more than 10 commits. ' \
'Please rebase these commits into a smaller number of commits.'
diff --git a/db/fixtures/development/02_users.rb b/db/fixtures/development/02_users.rb
new file mode 100644
index 00000000000..6e0b37d7258
--- /dev/null
+++ b/db/fixtures/development/02_users.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+class Gitlab::Seeder::Users
+ include ActionView::Helpers::NumberHelper
+
+ RANDOM_USERS_COUNT = 20
+ MASS_USERS_COUNT = ENV['CI'] ? 10 : 1_000_000
+ MASS_INSERT_USERNAME_START = 'mass_insert_user_'
+
+ attr_reader :opts
+
+ def initialize(opts = {})
+ @opts = opts
+ end
+
+ def seed!
+ Sidekiq::Testing.inline! do
+ create_mass_users!
+ create_random_users!
+ end
+ end
+
+ private
+
+ def create_mass_users!
+ encrypted_password = Devise::Encryptor.digest(User, '12345678')
+
+ Gitlab::Seeder.with_mass_insert(MASS_USERS_COUNT, User) do
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO users (username, name, email, confirmed_at, projects_limit, encrypted_password)
+ SELECT
+ '#{MASS_INSERT_USERNAME_START}' || seq,
+ 'Seed user ' || seq,
+ 'seed_user' || seq || '@example.com',
+ to_timestamp(seq),
+ #{MASS_USERS_COUNT},
+ '#{encrypted_password}'
+ FROM generate_series(1, #{MASS_USERS_COUNT}) AS seq
+ SQL
+ end
+
+ relation = User.where(admin: false)
+ Gitlab::Seeder.with_mass_insert(relation.count, Namespace) do
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO namespaces (name, path, owner_id)
+ SELECT
+ username,
+ username,
+ id
+ FROM users WHERE NOT admin
+ SQL
+ end
+ end
+
+ def create_random_users!
+ RANDOM_USERS_COUNT.times do |i|
+ begin
+ User.create!(
+ username: FFaker::Internet.user_name,
+ name: FFaker::Name.name,
+ email: FFaker::Internet.email,
+ confirmed_at: DateTime.now,
+ password: '12345678'
+ )
+
+ print '.'
+ rescue ActiveRecord::RecordInvalid
+ print 'F'
+ end
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ users = Gitlab::Seeder::Users.new
+ users.seed!
+end
diff --git a/db/fixtures/development/03_project.rb b/db/fixtures/development/03_project.rb
index 46018cf68aa..87ef65276eb 100644
--- a/db/fixtures/development/03_project.rb
+++ b/db/fixtures/development/03_project.rb
@@ -1,137 +1,210 @@
require './spec/support/sidekiq'
-# rubocop:disable Rails/Output
-
-Sidekiq::Testing.inline! do
- Gitlab::Seeder.quiet do
- Gitlab::Seeder.without_gitaly_timeout do
- project_urls = %w[
- https://gitlab.com/gitlab-org/gitlab-test.git
- https://gitlab.com/gitlab-org/gitlab-shell.git
- https://gitlab.com/gnuwget/wget2.git
- https://gitlab.com/Commit451/LabCoat.git
- https://github.com/jashkenas/underscore.git
- https://github.com/flightjs/flight.git
- https://github.com/twitter/typeahead.js.git
- https://github.com/h5bp/html5-boilerplate.git
- https://github.com/google/material-design-lite.git
- https://github.com/jlevy/the-art-of-command-line.git
- https://github.com/FreeCodeCamp/freecodecamp.git
- https://github.com/google/deepdream.git
- https://github.com/jtleek/datasharing.git
- https://github.com/WebAssembly/design.git
- https://github.com/airbnb/javascript.git
- https://github.com/tessalt/echo-chamber-js.git
- https://github.com/atom/atom.git
- https://github.com/mattermost/mattermost-server.git
- https://github.com/purifycss/purifycss.git
- https://github.com/facebook/nuclide.git
- https://github.com/wbkd/awesome-d3.git
- https://github.com/kilimchoi/engineering-blogs.git
- https://github.com/gilbarbara/logos.git
- https://github.com/reduxjs/redux.git
- https://github.com/awslabs/s2n.git
- https://github.com/arkency/reactjs_koans.git
- https://github.com/twbs/bootstrap.git
- https://github.com/chjj/ttystudio.git
- https://github.com/MostlyAdequate/mostly-adequate-guide.git
- https://github.com/octocat/Spoon-Knife.git
- https://github.com/opencontainers/runc.git
- https://github.com/googlesamples/android-topeka.git
- ]
-
- large_project_urls = %w[
- https://github.com/torvalds/linux.git
- https://gitlab.gnome.org/GNOME/gimp.git
- https://gitlab.gnome.org/GNOME/gnome-mud.git
- https://gitlab.com/fdroid/fdroidclient.git
- https://gitlab.com/inkscape/inkscape.git
- https://github.com/gnachman/iTerm2.git
- ]
-
- def create_project(url, force_latest_storage: false)
- group_path, project_path = url.split('/')[-2..-1]
-
- group = Group.find_by(path: group_path)
-
- unless group
- group = Group.new(
- name: group_path.titleize,
- path: group_path
- )
- group.description = FFaker::Lorem.sentence
- group.save!
-
- group.add_owner(User.first)
- end
+class Gitlab::Seeder::Projects
+ include ActionView::Helpers::NumberHelper
+
+ PROJECT_URLS = %w[
+ https://gitlab.com/gitlab-org/gitlab-test.git
+ https://gitlab.com/gitlab-org/gitlab-shell.git
+ https://gitlab.com/gnuwget/wget2.git
+ https://gitlab.com/Commit451/LabCoat.git
+ https://github.com/jashkenas/underscore.git
+ https://github.com/flightjs/flight.git
+ https://github.com/twitter/typeahead.js.git
+ https://github.com/h5bp/html5-boilerplate.git
+ https://github.com/google/material-design-lite.git
+ https://github.com/jlevy/the-art-of-command-line.git
+ https://github.com/FreeCodeCamp/freecodecamp.git
+ https://github.com/google/deepdream.git
+ https://github.com/jtleek/datasharing.git
+ https://github.com/WebAssembly/design.git
+ https://github.com/airbnb/javascript.git
+ https://github.com/tessalt/echo-chamber-js.git
+ https://github.com/atom/atom.git
+ https://github.com/mattermost/mattermost-server.git
+ https://github.com/purifycss/purifycss.git
+ https://github.com/facebook/nuclide.git
+ https://github.com/wbkd/awesome-d3.git
+ https://github.com/kilimchoi/engineering-blogs.git
+ https://github.com/gilbarbara/logos.git
+ https://github.com/reduxjs/redux.git
+ https://github.com/awslabs/s2n.git
+ https://github.com/arkency/reactjs_koans.git
+ https://github.com/twbs/bootstrap.git
+ https://github.com/chjj/ttystudio.git
+ https://github.com/MostlyAdequate/mostly-adequate-guide.git
+ https://github.com/octocat/Spoon-Knife.git
+ https://github.com/opencontainers/runc.git
+ https://github.com/googlesamples/android-topeka.git
+ ]
+ LARGE_PROJECT_URLS = %w[
+ https://github.com/torvalds/linux.git
+ https://gitlab.gnome.org/GNOME/gimp.git
+ https://gitlab.gnome.org/GNOME/gnome-mud.git
+ https://gitlab.com/fdroid/fdroidclient.git
+ https://gitlab.com/inkscape/inkscape.git
+ https://github.com/gnachman/iTerm2.git
+ ]
+ # Consider altering MASS_USERS_COUNT for less
+ # users with projects.
+ MASS_PROJECTS_COUNT_PER_USER = {
+ private: 3, # 3m projects +
+ internal: 1, # 1m projects +
+ public: 1 # 1m projects = 5m total
+ }
+ MASS_INSERT_NAME_START = 'mass_insert_project_'
+
+ def seed!
+ Sidekiq::Testing.inline! do
+ create_real_projects!
+ create_large_projects!
+ create_mass_projects!
+ end
+ end
- project_path.gsub!(".git", "")
+ private
- params = {
- import_url: url,
- namespace_id: group.id,
- name: project_path.titleize,
- description: FFaker::Lorem.sentence,
- visibility_level: Gitlab::VisibilityLevel.values.sample,
- skip_disk_validation: true
- }
+ def create_real_projects!
+ # You can specify how many projects you need during seed execution
+ size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
- if force_latest_storage
- params[:storage_version] = Project::LATEST_STORAGE_VERSION
- end
+ PROJECT_URLS.first(size).each_with_index do |url, i|
+ create_real_project!(url, force_latest_storage: i.even?)
+ end
+ end
- project = nil
+ def create_large_projects!
+ return unless ENV['LARGE_PROJECTS'].present?
- Sidekiq::Worker.skipping_transaction_check do
- project = Projects::CreateService.new(User.first, params).execute
+ LARGE_PROJECT_URLS.each(&method(:create_real_project!))
- # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
- # hook won't run until after the fixture is loaded. That is too late
- # since the Sidekiq::Testing block has already exited. Force clearing
- # the `after_commit` queue to ensure the job is run now.
- project.send(:_run_after_commit_queue)
- project.import_state.send(:_run_after_commit_queue)
- end
+ if ENV['FORK'].present?
+ puts "\nGenerating forks"
- if project.valid? && project.valid_repo?
+ project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK']
+
+ project = Project.find_by_full_path(project_name)
+
+ User.offset(1).first(5).each do |user|
+ new_project = ::Projects::ForkService.new(project, user).execute
+
+ if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
print '.'
else
- puts project.errors.full_messages
+ new_project.errors.full_messages.each do |error|
+ puts "#{new_project.full_path}: #{error}"
+ end
print 'F'
end
end
+ end
+ end
- # You can specify how many projects you need during seed execution
- size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8
+ def create_real_project!(url, force_latest_storage: false)
+ group_path, project_path = url.split('/')[-2..-1]
- project_urls.first(size).each_with_index do |url, i|
- create_project(url, force_latest_storage: i.even?)
- end
+ group = Group.find_by(path: group_path)
- if ENV['LARGE_PROJECTS'].present?
- large_project_urls.each(&method(:create_project))
+ unless group
+ group = Group.new(
+ name: group_path.titleize,
+ path: group_path
+ )
+ group.description = FFaker::Lorem.sentence
+ group.save!
- if ENV['FORK'].present?
- puts "\nGenerating forks"
+ group.add_owner(User.first)
+ end
- project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK']
+ project_path.gsub!(".git", "")
- project = Project.find_by_full_path(project_name)
+ params = {
+ import_url: url,
+ namespace_id: group.id,
+ name: project_path.titleize,
+ description: FFaker::Lorem.sentence,
+ visibility_level: Gitlab::VisibilityLevel.values.sample,
+ skip_disk_validation: true
+ }
- User.offset(1).first(5).each do |user|
- new_project = Projects::ForkService.new(project, user).execute
+ if force_latest_storage
+ params[:storage_version] = Project::LATEST_STORAGE_VERSION
+ end
- if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?)
- print '.'
- else
- new_project.errors.full_messages.each do |error|
- puts "#{new_project.full_path}: #{error}"
- end
- print 'F'
- end
- end
- end
- end
+ project = nil
+
+ Sidekiq::Worker.skipping_transaction_check do
+ project = ::Projects::CreateService.new(User.first, params).execute
+
+ # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
+ # hook won't run until after the fixture is loaded. That is too late
+ # since the Sidekiq::Testing block has already exited. Force clearing
+ # the `after_commit` queue to ensure the job is run now.
+ project.send(:_run_after_commit_queue)
+ project.import_state.send(:_run_after_commit_queue)
+ end
+
+ if project.valid? && project.valid_repo?
+ print '.'
+ else
+ puts project.errors.full_messages
+ print 'F'
end
end
+
+ def create_mass_projects!
+ projects_per_user_count = MASS_PROJECTS_COUNT_PER_USER.values.sum
+ visibility_per_user = ['private'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:private) +
+ ['internal'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:internal) +
+ ['public'] * MASS_PROJECTS_COUNT_PER_USER.fetch(:public)
+ visibility_level_per_user = visibility_per_user.map { |visibility| Gitlab::VisibilityLevel.level_value(visibility) }
+
+ visibility_per_user = visibility_per_user.join(',')
+ visibility_level_per_user = visibility_level_per_user.join(',')
+
+ Gitlab::Seeder.with_mass_insert(User.count * projects_per_user_count, "Projects and relations") do
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO projects (name, path, creator_id, namespace_id, visibility_level, created_at, updated_at)
+ SELECT
+ 'Seed project ' || seq || ' ' || ('{#{visibility_per_user}}'::text[])[seq] AS project_name,
+ 'mass_insert_project_' || ('{#{visibility_per_user}}'::text[])[seq] || '_' || seq AS project_path,
+ u.id AS user_id,
+ n.id AS namespace_id,
+ ('{#{visibility_level_per_user}}'::int[])[seq] AS visibility_level,
+ NOW() AS created_at,
+ NOW() AS updated_at
+ FROM users u
+ CROSS JOIN generate_series(1, #{projects_per_user_count}) AS seq
+ JOIN namespaces n ON n.owner_id=u.id
+ SQL
+
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO project_features (project_id, merge_requests_access_level, issues_access_level, wiki_access_level,
+ pages_access_level)
+ SELECT
+ id,
+ #{ProjectFeature::ENABLED} AS merge_requests_access_level,
+ #{ProjectFeature::ENABLED} AS issues_access_level,
+ #{ProjectFeature::ENABLED} AS wiki_access_level,
+ #{ProjectFeature::ENABLED} AS pages_access_level
+ FROM projects ON CONFLICT (project_id) DO NOTHING;
+ SQL
+
+ ActiveRecord::Base.connection.execute <<~SQL
+ INSERT INTO routes (source_id, source_type, name, path)
+ SELECT
+ p.id,
+ 'Project',
+ u.name || ' / ' || p.name,
+ u.username || '/' || p.path
+ FROM projects p JOIN users u ON u.id=p.creator_id
+ ON CONFLICT (source_type, source_id) DO NOTHING;
+ SQL
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ projects = Gitlab::Seeder::Projects.new
+ projects.seed!
end
diff --git a/db/fixtures/development/04_labels.rb b/db/fixtures/development/04_labels.rb
index b9ae4098d76..21d552c89f5 100644
--- a/db/fixtures/development/04_labels.rb
+++ b/db/fixtures/development/04_labels.rb
@@ -43,7 +43,7 @@ Gitlab::Seeder.quiet do
end
puts "\nGenerating project labels"
- Project.all.find_each do |project|
+ Project.not_mass_generated.find_each do |project|
Gitlab::Seeder::ProjectLabels.new(project).seed!
end
end
diff --git a/db/fixtures/development/05_users.rb b/db/fixtures/development/05_users.rb
deleted file mode 100644
index 101ff3a1209..00000000000
--- a/db/fixtures/development/05_users.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require './spec/support/sidekiq'
-
-Gitlab::Seeder.quiet do
- 20.times do |i|
- begin
- User.create!(
- username: FFaker::Internet.user_name,
- name: FFaker::Name.name,
- email: FFaker::Internet.email,
- confirmed_at: DateTime.now,
- password: '12345678'
- )
-
- print '.'
- rescue ActiveRecord::RecordInvalid
- print 'F'
- end
- end
-
- 5.times do |i|
- begin
- User.create!(
- username: "user#{i}",
- name: "User #{i}",
- email: "user#{i}@example.com",
- confirmed_at: DateTime.now,
- password: '12345678'
- )
- print '.'
- rescue ActiveRecord::RecordInvalid
- print 'F'
- end
- end
-end
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index b218f4e71fd..79ea96bf30e 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -3,7 +3,7 @@ require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
Group.all.each do |group|
- User.all.sample(4).each do |user|
+ User.not_mass_generated.sample(4).each do |user|
if group.add_user(user, Gitlab::Access.values.sample).persisted?
print '.'
else
@@ -12,8 +12,8 @@ Sidekiq::Testing.inline! do
end
end
- Project.all.each do |project|
- User.all.sample(4).each do |user|
+ Project.not_mass_generated.each do |project|
+ User.not_mass_generated.sample(4).each do |user|
if project.add_role(user, Gitlab::Access.sym_options.keys.sample)
print '.'
else
diff --git a/db/fixtures/development/07_milestones.rb b/db/fixtures/development/07_milestones.rb
index 271bfbc97e0..1194bb3fe6f 100644
--- a/db/fixtures/development/07_milestones.rb
+++ b/db/fixtures/development/07_milestones.rb
@@ -1,7 +1,7 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- Project.all.each do |project|
+ Project.not_mass_generated.each do |project|
5.times do |i|
milestone_params = {
title: "v#{i}.0",
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 4af545614f7..29f2fabbd5f 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -4,7 +4,13 @@ Gitlab::Seeder.quiet do
# Limit the number of merge requests per project to avoid long seeds
MAX_NUM_MERGE_REQUESTS = 10
- Project.non_archived.with_merge_requests_enabled.reject(&:empty_repo?).each do |project|
+ projects = Project
+ .non_archived
+ .with_merge_requests_enabled
+ .not_mass_generated
+ .reject(&:empty_repo?)
+
+ projects.each do |project|
branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2)
branches.each do |branch_name|
diff --git a/db/fixtures/development/11_keys.rb b/db/fixtures/development/11_keys.rb
index c405ecfdaf3..13eadc35e07 100644
--- a/db/fixtures/development/11_keys.rb
+++ b/db/fixtures/development/11_keys.rb
@@ -9,7 +9,7 @@ Sidekiq::Testing.disable! do
# that it falls under `Sidekiq::Testing.disable!`.
Key.skip_callback(:commit, :after, :add_to_shell)
- User.first(10).each do |user|
+ User.not_mass_generated.first(10).each do |user|
key = "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt#{user.id + 100}6k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
key = user.keys.create(
diff --git a/db/fixtures/development/12_snippets.rb b/db/fixtures/development/12_snippets.rb
index a9f4069a0f8..0ee9058a20b 100644
--- a/db/fixtures/development/12_snippets.rb
+++ b/db/fixtures/development/12_snippets.rb
@@ -25,7 +25,7 @@ end
eos
50.times do |i|
- user = User.all.sample
+ user = User.not_mass_generated.sample
PersonalSnippet.seed(:id, [{
id: i,
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 5c8b681fa92..468caac23f9 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -214,7 +214,7 @@ class Gitlab::Seeder::Pipelines
end
Gitlab::Seeder.quiet do
- Project.all.sample(5).each do |project|
+ Project.not_mass_generated.sample(5).each do |project|
project_builds = Gitlab::Seeder::Pipelines.new(project)
project_builds.seed!
end
diff --git a/db/fixtures/development/16_protected_branches.rb b/db/fixtures/development/16_protected_branches.rb
index 39d466fb43f..2b492ac1f61 100644
--- a/db/fixtures/development/16_protected_branches.rb
+++ b/db/fixtures/development/16_protected_branches.rb
@@ -3,7 +3,7 @@ require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
admin_user = User.find(1)
- Project.all.each do |project|
+ Project.not_mass_generated.each do |project|
params = {
name: 'master'
}
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index b7ddeef95b8..606a4cb1dde 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -217,7 +217,7 @@ Gitlab::Seeder.quiet do
flag = 'SEED_CYCLE_ANALYTICS'
if ENV[flag]
- Project.find_each do |project|
+ Project.not_mass_generated.find_each do |project|
# This seed naively assumes that every project has a repository, and every
# repository has a `master` branch, which may be the case for a pristine
# GDK seed, but is almost never true for a GDK that's actually had
diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb
index 3e227928a29..08363804216 100644
--- a/db/fixtures/development/19_environments.rb
+++ b/db/fixtures/development/19_environments.rb
@@ -67,7 +67,7 @@ class Gitlab::Seeder::Environments
end
Gitlab::Seeder.quiet do
- Project.all.sample(5).each do |project|
+ Project.not_mass_generated.sample(5).each do |project|
project_environments = Gitlab::Seeder::Environments.new(project)
project_environments.seed!
end
diff --git a/db/fixtures/development/23_spam_logs.rb b/db/fixtures/development/23_spam_logs.rb
index 81cc13e6b2d..4a839f5bc23 100644
--- a/db/fixtures/development/23_spam_logs.rb
+++ b/db/fixtures/development/23_spam_logs.rb
@@ -22,7 +22,7 @@ module Db
end
def self.random_user
- User.find(User.pluck(:id).sample)
+ User.find(User.not_mass_generated.pluck(:id).sample)
end
end
end
diff --git a/db/fixtures/development/24_forks.rb b/db/fixtures/development/24_forks.rb
index 971c6f0d0c8..fa16b2a1d93 100644
--- a/db/fixtures/development/24_forks.rb
+++ b/db/fixtures/development/24_forks.rb
@@ -2,8 +2,8 @@ require './spec/support/sidekiq'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
- User.all.sample(10).each do |user|
- source_project = Project.public_only.sample
+ User.not_mass_generated.sample(10).each do |user|
+ source_project = Project.not_mass_generated.public_only.sample
##
# 03_project.rb might not have created a public project because
diff --git a/db/migrate/20180215181245_users_name_lower_index.rb b/db/migrate/20180215181245_users_name_lower_index.rb
index 3b80601a727..fa1a115a78a 100644
--- a/db/migrate/20180215181245_users_name_lower_index.rb
+++ b/db/migrate/20180215181245_users_name_lower_index.rb
@@ -20,10 +20,6 @@ class UsersNameLowerIndex < ActiveRecord::Migration[4.2]
def down
return unless Gitlab::Database.postgresql?
- if supports_drop_index_concurrently?
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
- else
- execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
- end
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
end
diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb
index 3fe90c3fbb1..fa74330d5d9 100644
--- a/db/migrate/20180504195842_project_name_lower_index.rb
+++ b/db/migrate/20180504195842_project_name_lower_index.rb
@@ -22,11 +22,7 @@ class ProjectNameLowerIndex < ActiveRecord::Migration[4.2]
return unless Gitlab::Database.postgresql?
disable_statement_timeout do
- if supports_drop_index_concurrently?
- execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
- else
- execute "DROP INDEX IF EXISTS #{INDEX_NAME}"
- end
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}"
end
end
end
diff --git a/db/migrate/20180902070406_create_group_group_links.rb b/db/migrate/20180902070406_create_group_group_links.rb
new file mode 100644
index 00000000000..95fed0ebf96
--- /dev/null
+++ b/db/migrate/20180902070406_create_group_group_links.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class CreateGroupGroupLinks < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :group_group_links do |t|
+ t.timestamps_with_timezone null: false
+
+ t.references :shared_group, null: false,
+ index: false,
+ foreign_key: { on_delete: :cascade,
+ to_table: :namespaces }
+ t.references :shared_with_group, null: false,
+ foreign_key: { on_delete: :cascade,
+ to_table: :namespaces }
+ t.date :expires_at
+ t.index [:shared_group_id, :shared_with_group_id],
+ { unique: true,
+ name: 'index_group_group_links_on_shared_group_and_shared_with_group' }
+ t.integer :group_access, { limit: 2,
+ default: 30, # Gitlab::Access::DEVELOPER
+ null: false }
+ end
+ end
+
+ def down
+ drop_table :group_group_links
+ end
+end
diff --git a/db/migrate/20190703171157_add_sourcing_epic_dates.rb b/db/migrate/20190703171157_add_sourcing_epic_dates.rb
new file mode 100644
index 00000000000..202e2098d5b
--- /dev/null
+++ b/db/migrate/20190703171157_add_sourcing_epic_dates.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddSourcingEpicDates < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ add_column :epics, :start_date_sourcing_epic_id, :integer
+ add_column :epics, :due_date_sourcing_epic_id, :integer
+ end
+end
diff --git a/db/migrate/20190703171555_add_sourcing_epic_dates_fks.rb b/db/migrate/20190703171555_add_sourcing_epic_dates_fks.rb
new file mode 100644
index 00000000000..4995a3cd03f
--- /dev/null
+++ b/db/migrate/20190703171555_add_sourcing_epic_dates_fks.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddSourcingEpicDatesFks < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :epics, :start_date_sourcing_epic_id, where: 'start_date_sourcing_epic_id is not null'
+ add_concurrent_index :epics, :due_date_sourcing_epic_id, where: 'due_date_sourcing_epic_id is not null'
+
+ add_concurrent_foreign_key :epics, :epics, column: :start_date_sourcing_epic_id, on_delete: :nullify
+ add_concurrent_foreign_key :epics, :epics, column: :due_date_sourcing_epic_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key_if_exists :epics, column: :start_date_sourcing_epic_id
+ remove_foreign_key_if_exists :epics, column: :due_date_sourcing_epic_id
+
+ remove_concurrent_index :epics, :start_date_sourcing_epic_id
+ remove_concurrent_index :epics, :due_date_sourcing_epic_id
+ end
+end
diff --git a/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb b/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb
index fc4bc1a423b..477f8a850f8 100644
--- a/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb
+++ b/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb
@@ -12,10 +12,13 @@ class RemoveRendundantIndexFromReleases < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- remove_concurrent_index :releases, :project_id
+ remove_concurrent_index_by_name :releases, 'index_releases_on_project_id'
+
+ # This is an extra index that is not present in db/schema.rb but known to exist on some installs
+ remove_concurrent_index_by_name :releases, 'releases_project_id_idx' if index_exists_by_name?(:releases, 'releases_project_id_idx')
end
def down
- add_concurrent_index :releases, :project_id
+ add_concurrent_index :releases, :project_id, name: 'index_releases_on_project_id'
end
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
new file mode 100644
index 00000000000..e624642c2fc
--- /dev/null
+++ b/db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSourcegraphConfigurationToApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ 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
+
+ def down
+ remove_column(:application_settings, :sourcegraph_enabled)
+ remove_column(:application_settings, :sourcegraph_url)
+ end
+end
diff --git a/db/migrate/20190910211526_create_packages_conan_file_metadata.rb b/db/migrate/20190910211526_create_packages_conan_file_metadata.rb
new file mode 100644
index 00000000000..0f8dacb72de
--- /dev/null
+++ b/db/migrate/20190910211526_create_packages_conan_file_metadata.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreatePackagesConanFileMetadata < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ 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
+ t.timestamps_with_timezone
+ t.string "recipe_revision", null: false, default: "0", limit: 255
+ t.string "package_revision", limit: 255
+ t.string "conan_package_reference", limit: 255
+ t.integer "conan_file_type", limit: 2, null: false
+ end
+ end
+end
diff --git a/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb b/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb
new file mode 100644
index 00000000000..0ba9d8e6c89
--- /dev/null
+++ b/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddCleanupStatusToCluster < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:clusters, :cleanup_status,
+ :smallint,
+ default: 1,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:clusters, :cleanup_status)
+ end
+end
diff --git a/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb b/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb
new file mode 100644
index 00000000000..4e71905e3a3
--- /dev/null
+++ b/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddCleanupStatusReasonToCluster < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :clusters, :cleanup_status_reason, :text
+ end
+end
diff --git a/db/migrate/20190930153535_create_zoom_meetings.rb b/db/migrate/20190930153535_create_zoom_meetings.rb
new file mode 100644
index 00000000000..6b92c53da79
--- /dev/null
+++ b/db/migrate/20190930153535_create_zoom_meetings.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class CreateZoomMeetings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ ZOOM_MEETING_STATUS_ADDED = 1
+
+ def change
+ create_table :zoom_meetings do |t|
+ t.references :project, foreign_key: { on_delete: :cascade },
+ null: false
+ t.references :issue, foreign_key: { on_delete: :cascade },
+ null: false
+ t.timestamps_with_timezone null: false
+ t.integer :issue_status, limit: 2, default: 1, null: false
+ t.string :url, limit: 255
+
+ t.index [:issue_id, :issue_status], unique: true,
+ where: "issue_status = #{ZOOM_MEETING_STATUS_ADDED}"
+ end
+ end
+end
diff --git a/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb b/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb
new file mode 100644
index 00000000000..8910dc0d9fb
--- /dev/null
+++ b/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateClustersApplicationsElasticStack < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_elastic_stacks do |t|
+ t.timestamps_with_timezone null: false
+ t.references :cluster, null: false, index: false, foreign_key: { on_delete: :cascade }
+ t.integer :status, null: false
+ t.string :version, null: false, limit: 255
+ t.string :kibana_hostname, limit: 255
+ t.text :status_reason
+ t.index :cluster_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
index 94d16e921df..71d10153422 100644
--- a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
+++ b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class AddSelfManagedPrometheusAlerts < ActiveRecord::Migration[5.2]
- # Set this constant to true if this migration requires downtime.
DOWNTIME = false
def change
diff --git a/db/migrate/20191003161031_add_mark_for_deletion_to_projects.rb b/db/migrate/20191003161031_add_mark_for_deletion_to_projects.rb
new file mode 100644
index 00000000000..86d581a4383
--- /dev/null
+++ b/db/migrate/20191003161031_add_mark_for_deletion_to_projects.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddMarkForDeletionToProjects < ActiveRecord::Migration[5.2]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :marked_for_deletion_at, :date
+ add_column :projects, :marked_for_deletion_by_user_id, :integer
+ end
+end
diff --git a/db/migrate/20191003161032_add_mark_for_deletion_indexes_to_projects.rb b/db/migrate/20191003161032_add_mark_for_deletion_indexes_to_projects.rb
new file mode 100644
index 00000000000..d6ef6509fff
--- /dev/null
+++ b/db/migrate/20191003161032_add_mark_for_deletion_indexes_to_projects.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddMarkForDeletionIndexesToProjects < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :projects, :users, column: :marked_for_deletion_by_user_id, on_delete: :nullify
+ add_concurrent_index :projects, :marked_for_deletion_by_user_id, where: 'marked_for_deletion_by_user_id IS NOT NULL'
+ end
+
+ def down
+ remove_foreign_key_if_exists :projects, column: :marked_for_deletion_by_user_id
+ remove_concurrent_index :projects, :marked_for_deletion_by_user_id
+ end
+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
new file mode 100644
index 00000000000..c5f5a8cd70c
--- /dev/null
+++ b/db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb
@@ -0,0 +1,15 @@
+class AddPendoEnabledToApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :pendo_enabled
+ end
+end
diff --git a/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb b/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb
new file mode 100644
index 00000000000..cc0895f8bee
--- /dev/null
+++ b/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddPendoUrlToApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :pendo_url, :string, limit: 255
+ end
+end
diff --git a/db/migrate/20191004080818_add_productivity_analytics_start_date.rb b/db/migrate/20191004080818_add_productivity_analytics_start_date.rb
new file mode 100644
index 00000000000..287b0755bc1
--- /dev/null
+++ b/db/migrate/20191004080818_add_productivity_analytics_start_date.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :productivity_analytics_start_date, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb b/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb
new file mode 100644
index 00000000000..9432cd68708
--- /dev/null
+++ b/db/migrate/20191004081520_fill_productivity_analytics_start_date.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+# Expected migration duration: 1 minute
+class FillProductivityAnalyticsStartDate < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_request_metrics, :merged_at,
+ where: "merged_at > '2019-09-01' AND commits_count IS NOT NULL",
+ name: 'fill_productivity_analytics_start_date_tmp_index'
+
+ execute(
+ <<SQL
+ UPDATE application_settings
+ SET productivity_analytics_start_date = COALESCE((SELECT MIN(merged_at) FROM merge_request_metrics
+ WHERE merged_at > '2019-09-01' AND commits_count IS NOT NULL), NOW())
+SQL
+ )
+
+ remove_concurrent_index :merge_request_metrics, :merged_at,
+ name: 'fill_productivity_analytics_start_date_tmp_index'
+ end
+
+ def down
+ execute('UPDATE application_settings SET productivity_analytics_start_date = NULL')
+ end
+end
diff --git a/db/migrate/20191009100244_add_geo_design_repository_counters.rb b/db/migrate/20191009100244_add_geo_design_repository_counters.rb
new file mode 100644
index 00000000000..26387453f88
--- /dev/null
+++ b/db/migrate/20191009100244_add_geo_design_repository_counters.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddGeoDesignRepositoryCounters < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_table :geo_node_statuses do |t|
+ t.column :design_repositories_count, :integer
+ t.column :design_repositories_synced_count, :integer
+ t.column :design_repositories_failed_count, :integer
+ t.column :design_repositories_registry_count, :integer
+ end
+ end
+end
diff --git a/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb b/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb
new file mode 100644
index 00000000000..86c3c540e5e
--- /dev/null
+++ b/db/migrate/20191009110124_add_has_exposed_artifacts_to_ci_builds_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddHasExposedArtifactsToCiBuildsMetadata < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :ci_builds_metadata, :has_exposed_artifacts, :boolean
+ end
+
+ def down
+ remove_column :ci_builds_metadata, :has_exposed_artifacts
+ end
+end
diff --git a/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb b/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb
new file mode 100644
index 00000000000..6b8c452a62a
--- /dev/null
+++ b/db/migrate/20191009110757_add_index_to_ci_builds_metadata_has_exposed_artifacts.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToCiBuildsMetadataHasExposedArtifacts < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds_metadata, [:build_id], where: "has_exposed_artifacts IS TRUE", name: 'index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts'
+ end
+
+ def down
+ remove_concurrent_index_by_name :ci_builds_metadata, 'index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts'
+ end
+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
new file mode 100644
index 00000000000..a40ce8dbee5
--- /dev/null
+++ b/db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddSnowplowIgluRegistryUrlToApplicationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :snowplow_iglu_registry_url, :string, limit: 255
+ end
+end
diff --git a/db/migrate/20191011084019_add_project_deletion_adjourned_period_to_application_settings.rb b/db/migrate/20191011084019_add_project_deletion_adjourned_period_to_application_settings.rb
new file mode 100644
index 00000000000..79546e33253
--- /dev/null
+++ b/db/migrate/20191011084019_add_project_deletion_adjourned_period_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddProjectDeletionAdjournedPeriodToApplicationSettings < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL = 7
+
+ def change
+ add_column :application_settings, :deletion_adjourned_period, :integer, default: DEFAULT_NUMBER_OF_DAYS_BEFORE_REMOVAL, null: false
+ end
+end
diff --git a/db/migrate/20191013100213_add_squash_commit_sha_to_merge_requests.rb b/db/migrate/20191013100213_add_squash_commit_sha_to_merge_requests.rb
new file mode 100644
index 00000000000..0a58f0a89aa
--- /dev/null
+++ b/db/migrate/20191013100213_add_squash_commit_sha_to_merge_requests.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddSquashCommitShaToMergeRequests < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :merge_requests, :squash_commit_sha, :binary
+ end
+end
diff --git a/db/migrate/20191014025629_rename_design_management_version_user_to_author.rb b/db/migrate/20191014025629_rename_design_management_version_user_to_author.rb
new file mode 100644
index 00000000000..2359cc2e826
--- /dev/null
+++ b/db/migrate/20191014025629_rename_design_management_version_user_to_author.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RenameDesignManagementVersionUserToAuthor < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :design_management_versions, :user_id, :author_id
+ end
+
+ def down
+ undo_rename_column_concurrently :design_management_versions, :user_id, :author_id
+ end
+end
diff --git a/db/migrate/20191014030730_add_author_index_to_design_management_versions.rb b/db/migrate/20191014030730_add_author_index_to_design_management_versions.rb
new file mode 100644
index 00000000000..30e076f1fe6
--- /dev/null
+++ b/db/migrate/20191014030730_add_author_index_to_design_management_versions.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddAuthorIndexToDesignManagementVersions < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :design_management_versions, :author_id, where: 'author_id IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :design_management_versions, :author_id
+ end
+end
diff --git a/db/migrate/20191016133352_create_ci_subscriptions_projects.rb b/db/migrate/20191016133352_create_ci_subscriptions_projects.rb
new file mode 100644
index 00000000000..00ab2c19193
--- /dev/null
+++ b/db/migrate/20191016133352_create_ci_subscriptions_projects.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateCiSubscriptionsProjects < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :ci_subscriptions_projects do |t|
+ t.references :downstream_project, null: false, index: false, foreign_key: { to_table: :projects, on_delete: :cascade }
+ t.references :upstream_project, null: false, foreign_key: { to_table: :projects, on_delete: :cascade }
+ end
+
+ add_index :ci_subscriptions_projects, [:downstream_project_id, :upstream_project_id],
+ unique: true, name: 'index_ci_subscriptions_projects_unique_subscription'
+ end
+end
diff --git a/db/migrate/20191017001326_create_users_security_dashboard_projects.rb b/db/migrate/20191017001326_create_users_security_dashboard_projects.rb
new file mode 100644
index 00000000000..398401dbee6
--- /dev/null
+++ b/db/migrate/20191017001326_create_users_security_dashboard_projects.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateUsersSecurityDashboardProjects < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+ INDEX_NAME = 'users_security_dashboard_projects_unique_index'
+
+ def change
+ create_table :users_security_dashboard_projects, id: false do |t|
+ t.references :user, null: false, foreign_key: { on_delete: :cascade }
+ t.references :project, null: false, index: false, foreign_key: { on_delete: :cascade }
+ end
+
+ add_index :users_security_dashboard_projects, [:project_id, :user_id], name: INDEX_NAME, unique: true
+ end
+end
diff --git a/db/migrate/20191017094449_add_remove_source_branch_after_merge_to_projects.rb b/db/migrate/20191017094449_add_remove_source_branch_after_merge_to_projects.rb
new file mode 100644
index 00000000000..021bf7d9870
--- /dev/null
+++ b/db/migrate/20191017094449_add_remove_source_branch_after_merge_to_projects.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRemoveSourceBranchAfterMergeToProjects < ActiveRecord::Migration[5.1]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column :projects, :remove_source_branch_after_merge, :boolean
+ end
+
+ def down
+ remove_column :projects, :remove_source_branch_after_merge
+ end
+end
diff --git a/db/migrate/20191017134513_add_deployment_merge_requests.rb b/db/migrate/20191017134513_add_deployment_merge_requests.rb
new file mode 100644
index 00000000000..dbe09463d22
--- /dev/null
+++ b/db/migrate/20191017134513_add_deployment_merge_requests.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class AddDeploymentMergeRequests < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :deployment_merge_requests, id: false do |t|
+ t.references(
+ :deployment,
+ foreign_key: { on_delete: :cascade },
+ type: :integer,
+ index: false,
+ null: false
+ )
+
+ t.references(
+ :merge_request,
+ foreign_key: { on_delete: :cascade },
+ type: :integer,
+ index: true,
+ null: false
+ )
+
+ t.index(
+ [:deployment_id, :merge_request_id],
+ unique: true,
+ name: 'idx_deployment_merge_requests_unique_index'
+ )
+ end
+ end
+end
diff --git a/db/migrate/20191017191341_create_clusters_applications_crossplane.rb b/db/migrate/20191017191341_create_clusters_applications_crossplane.rb
new file mode 100644
index 00000000000..8dc25c56116
--- /dev/null
+++ b/db/migrate/20191017191341_create_clusters_applications_crossplane.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateClustersApplicationsCrossplane < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :clusters_applications_crossplane do |t|
+ t.timestamps_with_timezone null: false
+ t.references :cluster, null: false, index: false, foreign_key: { on_delete: :cascade }
+ t.integer :status, null: false
+ t.string :version, null: false, limit: 255
+ t.string :stack, null: false, limit: 255
+ t.text :status_reason
+ t.index :cluster_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20191023132005_add_merge_requests_index_on_target_project_and_branch.rb b/db/migrate/20191023132005_add_merge_requests_index_on_target_project_and_branch.rb
new file mode 100644
index 00000000000..a3de3f34c44
--- /dev/null
+++ b/db/migrate/20191023132005_add_merge_requests_index_on_target_project_and_branch.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddMergeRequestsIndexOnTargetProjectAndBranch < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, [:target_project_id, :target_branch],
+ where: "state_id = 1 AND merge_when_pipeline_succeeds = true"
+ end
+
+ def down
+ remove_concurrent_index :merge_requests, [:target_project_id, :target_branch]
+ end
+end
diff --git a/db/migrate/20191023152913_add_default_and_free_plans.rb b/db/migrate/20191023152913_add_default_and_free_plans.rb
new file mode 100644
index 00000000000..4f5f8000386
--- /dev/null
+++ b/db/migrate/20191023152913_add_default_and_free_plans.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class AddDefaultAndFreePlans < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class Plan < ApplicationRecord
+ end
+
+ def up
+ plan_names.each do |plan_name|
+ Plan.create_with(title: plan_name.titleize).find_or_create_by(name: plan_name)
+ end
+ end
+
+ def down
+ Plan.where(name: plan_names).delete_all
+ end
+
+ private
+
+ def plan_names
+ [
+ ('free' if Gitlab.com?),
+ 'default'
+ ].compact
+ end
+end
diff --git a/db/migrate/20191024134020_add_index_to_zoom_meetings.rb b/db/migrate/20191024134020_add_index_to_zoom_meetings.rb
new file mode 100644
index 00000000000..ef3657b6a5e
--- /dev/null
+++ b/db/migrate/20191024134020_add_index_to_zoom_meetings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToZoomMeetings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :zoom_meetings, :issue_status
+ end
+
+ def down
+ remove_concurrent_index :zoom_meetings, :issue_status if index_exists?(:zoom_meetings, :issue_status)
+ end
+end
diff --git a/db/migrate/20191026124116_set_application_settings_default_project_and_snippet_visibility.rb b/db/migrate/20191026124116_set_application_settings_default_project_and_snippet_visibility.rb
new file mode 100644
index 00000000000..9d19279510a
--- /dev/null
+++ b/db/migrate/20191026124116_set_application_settings_default_project_and_snippet_visibility.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class SetApplicationSettingsDefaultProjectAndSnippetVisibility < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_column_null :application_settings, :default_project_visibility, false, 0
+ change_column_default :application_settings, :default_project_visibility, from: nil, to: 0
+
+ change_column_null :application_settings, :default_snippet_visibility, false, 0
+ change_column_default :application_settings, :default_snippet_visibility, from: nil, to: 0
+ end
+end
diff --git a/db/migrate/20191028162543_add_setup_for_company_to_user_preferences.rb b/db/migrate/20191028162543_add_setup_for_company_to_user_preferences.rb
new file mode 100644
index 00000000000..18a8a2306e2
--- /dev/null
+++ b/db/migrate/20191028162543_add_setup_for_company_to_user_preferences.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddSetupForCompanyToUserPreferences < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :user_preferences, :setup_for_company, :boolean
+ end
+end
diff --git a/db/migrate/20191028184740_rename_snowplow_site_id_to_snowplow_app_id.rb b/db/migrate/20191028184740_rename_snowplow_site_id_to_snowplow_app_id.rb
new file mode 100644
index 00000000000..4e3b2da670e
--- /dev/null
+++ b/db/migrate/20191028184740_rename_snowplow_site_id_to_snowplow_app_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RenameSnowplowSiteIdToSnowplowAppId < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :application_settings, :snowplow_site_id, :snowplow_app_id
+ end
+
+ def down
+ undo_rename_column_concurrently :application_settings, :snowplow_site_id, :snowplow_app_id
+ end
+end
diff --git a/db/migrate/20191029125305_create_packages_conan_metadata.rb b/db/migrate/20191029125305_create_packages_conan_metadata.rb
new file mode 100644
index 00000000000..c6abc509e41
--- /dev/null
+++ b/db/migrate/20191029125305_create_packages_conan_metadata.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreatePackagesConanMetadata < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ 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
+ t.timestamps_with_timezone
+ t.string "package_username", null: false, limit: 255
+ t.string "package_channel", null: false, limit: 255
+ end
+ end
+end
diff --git a/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb b/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb
new file mode 100644
index 00000000000..8db11724874
--- /dev/null
+++ b/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class AddEnabledToGrafanaIntegrations < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(
+ :grafana_integrations,
+ :enabled,
+ :boolean,
+ allow_null: false,
+ default: false
+ )
+ end
+
+ def down
+ remove_column(:grafana_integrations, :enabled)
+ end
+end
diff --git a/db/migrate/20191030135044_create_plan_limits.rb b/db/migrate/20191030135044_create_plan_limits.rb
new file mode 100644
index 00000000000..291d9824f6d
--- /dev/null
+++ b/db/migrate/20191030135044_create_plan_limits.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreatePlanLimits < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :plan_limits, id: false do |t|
+ t.references :plan, foreign_key: { on_delete: :cascade }, null: false, index: { unique: true }
+ t.integer :ci_active_pipelines, null: false, default: 0
+ t.integer :ci_pipeline_size, null: false, default: 0
+ t.integer :ci_active_jobs, null: false, default: 0
+ end
+ end
+end
diff --git a/db/migrate/20191030152934_move_limits_from_plans.rb b/db/migrate/20191030152934_move_limits_from_plans.rb
new file mode 100644
index 00000000000..020a028f648
--- /dev/null
+++ b/db/migrate/20191030152934_move_limits_from_plans.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class MoveLimitsFromPlans < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ execute <<~SQL
+ INSERT INTO plan_limits (plan_id, ci_active_pipelines, ci_pipeline_size, ci_active_jobs)
+ SELECT id, COALESCE(active_pipelines_limit, 0), COALESCE(pipeline_size_limit, 0), COALESCE(active_jobs_limit, 0)
+ FROM plans
+ SQL
+ end
+
+ def down
+ execute 'DELETE FROM plan_limits'
+ end
+end
diff --git a/db/migrate/20191101092917_replace_index_on_metrics_merged_at.rb b/db/migrate/20191101092917_replace_index_on_metrics_merged_at.rb
new file mode 100644
index 00000000000..b2baaee2b76
--- /dev/null
+++ b/db/migrate/20191101092917_replace_index_on_metrics_merged_at.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ReplaceIndexOnMetricsMergedAt < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_request_metrics, :merged_at
+ remove_concurrent_index :merge_request_metrics, [:merged_at, :id]
+ end
+
+ def down
+ add_concurrent_index :merge_request_metrics, [:merged_at, :id]
+ remove_concurrent_index :merge_request_metrics, :merged_at
+ end
+end
diff --git a/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb
new file mode 100644
index 00000000000..3a167b4c67f
--- /dev/null
+++ b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddEksCredentialsToApplicationSettings < ActiveRecord::Migration[5.2]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :eks_integration_enabled, :boolean, null: false, default: false
+ add_column :application_settings, :eks_account_id, :string, limit: 128
+ add_column :application_settings, :eks_access_key_id, :string, limit: 128
+ add_column :application_settings, :encrypted_eks_secret_access_key_iv, :string, limit: 255
+ add_column :application_settings, :encrypted_eks_secret_access_key, :text
+ end
+end
diff --git a/db/migrate/20191104205020_add_license_details_to_application_settings.rb b/db/migrate/20191104205020_add_license_details_to_application_settings.rb
new file mode 100644
index 00000000000..f951ae6492d
--- /dev/null
+++ b/db/migrate/20191104205020_add_license_details_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddLicenseDetailsToApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :license_trial_ends_on, :date, null: true
+ end
+end
diff --git a/db/migrate/20191105094558_add_report_type_to_vulnerabilities.rb b/db/migrate/20191105094558_add_report_type_to_vulnerabilities.rb
new file mode 100644
index 00000000000..8fb657bf9e7
--- /dev/null
+++ b/db/migrate/20191105094558_add_report_type_to_vulnerabilities.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddReportTypeToVulnerabilities < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :vulnerabilities, :report_type, :integer, limit: 2
+ end
+end
diff --git a/db/migrate/20191105193652_add_index_on_deployments_updated_at.rb b/db/migrate/20191105193652_add_index_on_deployments_updated_at.rb
new file mode 100644
index 00000000000..10371c26dcc
--- /dev/null
+++ b/db/migrate/20191105193652_add_index_on_deployments_updated_at.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnDeploymentsUpdatedAt < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_COLUMNS = [:project_id, :updated_at]
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:deployments, INDEX_COLUMNS)
+ end
+
+ def down
+ remove_concurrent_index(:deployments, INDEX_COLUMNS)
+ end
+end
diff --git a/db/migrate/20191107173446_add_sourcegraph_admin_and_user_preferences.rb b/db/migrate/20191107173446_add_sourcegraph_admin_and_user_preferences.rb
new file mode 100644
index 00000000000..731ed82c999
--- /dev/null
+++ b/db/migrate/20191107173446_add_sourcegraph_admin_and_user_preferences.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddSourcegraphAdminAndUserPreferences < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column(:application_settings, :sourcegraph_public_only, :boolean, default: true, null: false)
+ add_column(:user_preferences, :sourcegraph_enabled, :boolean)
+ end
+
+ def down
+ remove_column(:application_settings, :sourcegraph_public_only)
+ remove_column(:user_preferences, :sourcegraph_enabled)
+ end
+end
diff --git a/db/migrate/20191107220314_add_index_to_projects_on_marked_for_deletion.rb b/db/migrate/20191107220314_add_index_to_projects_on_marked_for_deletion.rb
new file mode 100644
index 00000000000..06849cf9bfd
--- /dev/null
+++ b/db/migrate/20191107220314_add_index_to_projects_on_marked_for_deletion.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexToProjectsOnMarkedForDeletion < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, :marked_for_deletion_at, where: 'marked_for_deletion_at IS NOT NULL'
+ end
+
+ def down
+ remove_concurrent_index :projects, :marked_for_deletion_at
+ end
+end
diff --git a/db/migrate/20191111115229_add_group_id_to_import_export_uploads.rb b/db/migrate/20191111115229_add_group_id_to_import_export_uploads.rb
new file mode 100644
index 00000000000..74ef0f27b3e
--- /dev/null
+++ b/db/migrate/20191111115229_add_group_id_to_import_export_uploads.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddGroupIdToImportExportUploads < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :import_export_uploads, :group_id, :bigint
+ end
+end
diff --git a/db/migrate/20191111115431_add_group_fk_to_import_export_uploads.rb b/db/migrate/20191111115431_add_group_fk_to_import_export_uploads.rb
new file mode 100644
index 00000000000..403de3f33ed
--- /dev/null
+++ b/db/migrate/20191111115431_add_group_fk_to_import_export_uploads.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddGroupFkToImportExportUploads < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :import_export_uploads, :namespaces, column: :group_id, on_delete: :cascade
+ add_concurrent_index :import_export_uploads, :group_id, unique: true, where: 'group_id IS NOT NULL'
+ end
+
+ def down
+ remove_foreign_key_without_error(:import_export_uploads, column: :group_id)
+ remove_concurrent_index(:import_export_uploads, :group_id)
+ end
+end
diff --git a/db/migrate/20191111121500_default_ci_config_path.rb b/db/migrate/20191111121500_default_ci_config_path.rb
new file mode 100644
index 00000000000..f391f5ffe99
--- /dev/null
+++ b/db/migrate/20191111121500_default_ci_config_path.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class DefaultCiConfigPath < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :application_settings, :default_ci_config_path, :string, limit: 255
+ end
+
+ def down
+ remove_column :application_settings, :default_ci_config_path
+ end
+end
diff --git a/db/migrate/20191112115247_add_cached_markdown_version_to_vulnerabilities.rb b/db/migrate/20191112115247_add_cached_markdown_version_to_vulnerabilities.rb
new file mode 100644
index 00000000000..b0c513737e8
--- /dev/null
+++ b/db/migrate/20191112115247_add_cached_markdown_version_to_vulnerabilities.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddCachedMarkdownVersionToVulnerabilities < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :vulnerabilities, :cached_markdown_version, :integer
+ end
+end
diff --git a/db/migrate/20191112214305_add_indexes_for_projects_api_default_params.rb b/db/migrate/20191112214305_add_indexes_for_projects_api_default_params.rb
new file mode 100644
index 00000000000..3893c0422c7
--- /dev/null
+++ b/db/migrate/20191112214305_add_indexes_for_projects_api_default_params.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexesForProjectsApiDefaultParams < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, %i(visibility_level created_at id)
+ remove_concurrent_index_by_name :projects, 'index_projects_on_visibility_level'
+ end
+
+ def down
+ add_concurrent_index :projects, :visibility_level
+ remove_concurrent_index :projects, %i(visibility_level created_at id)
+ end
+end
diff --git a/db/migrate/20191112221821_add_indexes_for_projects_api_default_params_authenticated.rb b/db/migrate/20191112221821_add_indexes_for_projects_api_default_params_authenticated.rb
new file mode 100644
index 00000000000..6ebc6a72854
--- /dev/null
+++ b/db/migrate/20191112221821_add_indexes_for_projects_api_default_params_authenticated.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexesForProjectsApiDefaultParamsAuthenticated < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, %i(created_at id)
+ remove_concurrent_index_by_name :projects, 'index_projects_on_created_at'
+ end
+
+ def down
+ add_concurrent_index :projects, :created_at
+ remove_concurrent_index_by_name :projects, 'index_projects_on_created_at_and_id'
+ end
+end
diff --git a/db/migrate/20191112232338_ensure_no_empty_milestone_titles.rb b/db/migrate/20191112232338_ensure_no_empty_milestone_titles.rb
new file mode 100644
index 00000000000..76cb511424e
--- /dev/null
+++ b/db/migrate/20191112232338_ensure_no_empty_milestone_titles.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class EnsureNoEmptyMilestoneTitles < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ loop do
+ rows_updated = exec_update <<~SQL
+ UPDATE milestones SET title = '%BLANK' WHERE id IN (SELECT id FROM milestones WHERE title = '' LIMIT 500)
+ SQL
+ break if rows_updated < 500
+ end
+ end
+
+ def down; end
+end
diff --git a/db/migrate/20191114173508_add_resolved_attributes_to_vulnerabilities.rb b/db/migrate/20191114173508_add_resolved_attributes_to_vulnerabilities.rb
new file mode 100644
index 00000000000..ec45a729ebb
--- /dev/null
+++ b/db/migrate/20191114173508_add_resolved_attributes_to_vulnerabilities.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddResolvedAttributesToVulnerabilities < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :vulnerabilities, :resolved_by_id, :bigint
+ add_column :vulnerabilities, :resolved_at, :datetime_with_timezone
+ end
+
+ def down
+ remove_column :vulnerabilities, :resolved_at
+ remove_column :vulnerabilities, :resolved_by_id
+ end
+end
diff --git a/db/migrate/20191114173602_add_foreign_key_on_resolved_by_id_to_vulnerabilities.rb b/db/migrate/20191114173602_add_foreign_key_on_resolved_by_id_to_vulnerabilities.rb
new file mode 100644
index 00000000000..e0a125ca756
--- /dev/null
+++ b/db/migrate/20191114173602_add_foreign_key_on_resolved_by_id_to_vulnerabilities.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddForeignKeyOnResolvedByIdToVulnerabilities < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :vulnerabilities, :resolved_by_id
+ add_concurrent_foreign_key :vulnerabilities, :users, column: :resolved_by_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key :vulnerabilities, column: :resolved_by_id
+ remove_concurrent_index :vulnerabilities, :resolved_by_id
+ end
+end
diff --git a/db/migrate/20191115091425_create_vulnerability_issue_links.rb b/db/migrate/20191115091425_create_vulnerability_issue_links.rb
new file mode 100644
index 00000000000..8398b6357c4
--- /dev/null
+++ b/db/migrate/20191115091425_create_vulnerability_issue_links.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CreateVulnerabilityIssueLinks < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :vulnerability_issue_links do |t|
+ # index: false because idx_vulnerability_issue_links_on_vulnerability_id_and_issue_id refers the same column
+ t.references :vulnerability, null: false, index: false, foreign_key: { on_delete: :cascade }
+ # index: true is implied
+ t.references :issue, null: false, foreign_key: { on_delete: :cascade }
+ t.integer 'link_type', limit: 2, null: false, default: 1 # 'related'
+ t.index %i[vulnerability_id issue_id],
+ name: 'idx_vulnerability_issue_links_on_vulnerability_id_and_issue_id',
+ unique: true # only one link (and of only one type) is allowed
+ t.index %i[vulnerability_id link_type],
+ name: 'idx_vulnerability_issue_links_on_vulnerability_id_and_link_type',
+ where: 'link_type = 2',
+ unique: true # only one 'created' link per vulnerability is allowed
+ t.timestamps_with_timezone
+ end
+ end
+end
diff --git a/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
index 0c4faebc548..d10887fb5d5 100644
--- a/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
+++ b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb
@@ -3,71 +3,17 @@
class SetSelfMonitoringProjectAlertingToken < ActiveRecord::Migration[5.2]
DOWNTIME = false
- module Migratable
- module Alerting
- class ProjectAlertingSetting < ApplicationRecord
- self.table_name = 'project_alerting_settings'
-
- belongs_to :project
-
- validates :token, presence: true
-
- attr_encrypted :token,
- mode: :per_attribute_iv,
- key: Settings.attr_encrypted_db_key_base_truncated,
- algorithm: 'aes-256-gcm'
-
- before_validation :ensure_token
-
- private
-
- def ensure_token
- self.token ||= generate_token
- end
-
- def generate_token
- SecureRandom.hex
- end
- end
- end
-
- class Project < ApplicationRecord
- has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting'
- end
-
- class ApplicationSetting < ApplicationRecord
- self.table_name = 'application_settings'
-
- belongs_to :instance_administration_project, class_name: 'Project'
-
- def self.current_without_cache
- last
- end
- end
- end
-
- def setup_alertmanager_token(project)
- return unless License.feature_available?(:prometheus_alerts)
-
- project.create_alerting_setting!
- end
-
def up
- Gitlab.ee do
- project = Migratable::ApplicationSetting.current_without_cache&.instance_administration_project
+ # no-op
+ # Converted to no-op in https://gitlab.com/gitlab-org/gitlab/merge_requests/17049.
- if project
- setup_alertmanager_token(project)
- end
- end
+ # This migration has been made a no-op because the pre-requisite migration
+ # which creates the self-monitoring project has already been removed in
+ # https://gitlab.com/gitlab-org/gitlab/merge_requests/16864. As
+ # such, this migration would do nothing.
end
def down
- Gitlab.ee do
- Migratable::ApplicationSetting.current_without_cache
- &.instance_administration_project
- &.alerting_setting
- &.destroy!
- end
+ # no-op
end
end
diff --git a/db/post_migrate/20190918104222_schedule_productivity_analytics_backfill.rb b/db/post_migrate/20190918104222_schedule_productivity_analytics_backfill.rb
index 23d3bbbc395..cd759735f00 100644
--- a/db/post_migrate/20190918104222_schedule_productivity_analytics_backfill.rb
+++ b/db/post_migrate/20190918104222_schedule_productivity_analytics_backfill.rb
@@ -6,7 +6,7 @@ class ScheduleProductivityAnalyticsBackfill < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
- # no-op since the scheduling times out on GitLab.com
+ # no-op since the migration was removed
end
def down
diff --git a/db/post_migrate/20190926180443_schedule_epic_issues_after_epics_move.rb b/db/post_migrate/20190926180443_schedule_epic_issues_after_epics_move.rb
new file mode 100644
index 00000000000..86fe0f26681
--- /dev/null
+++ b/db/post_migrate/20190926180443_schedule_epic_issues_after_epics_move.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ScheduleEpicIssuesAfterEpicsMove < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INTERVAL = 5.minutes.to_i
+ BATCH_SIZE = 100
+ MIGRATION = 'MoveEpicIssuesAfterEpics'
+
+ disable_ddl_transaction!
+
+ class Epic < ActiveRecord::Base
+ self.table_name = 'epics'
+
+ include ::EachBatch
+ end
+
+ def up
+ return unless ::Gitlab.ee?
+
+ Epic.each_batch(of: BATCH_SIZE) do |batch, index|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+ delay = index * INTERVAL
+ BackgroundMigrationWorker.perform_in(delay, MIGRATION, *range)
+ end
+ end
+
+ def down
+ # no need
+ end
+end
diff --git a/db/post_migrate/20191008143850_fix_any_approver_rule_for_projects.rb b/db/post_migrate/20191008143850_fix_any_approver_rule_for_projects.rb
new file mode 100644
index 00000000000..c1f4b7e42ab
--- /dev/null
+++ b/db/post_migrate/20191008143850_fix_any_approver_rule_for_projects.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FixAnyApproverRuleForProjects < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+ BATCH_SIZE = 1000
+
+ disable_ddl_transaction!
+
+ class ApprovalProjectRule < ActiveRecord::Base
+ NON_EXISTENT_RULE_TYPE = 4
+ ANY_APPROVER_RULE_TYPE = 3
+
+ include EachBatch
+
+ self.table_name = 'approval_project_rules'
+
+ scope :any_approver, -> { where(rule_type: ANY_APPROVER_RULE_TYPE) }
+ scope :non_existent_rule_type, -> { where(rule_type: NON_EXISTENT_RULE_TYPE) }
+ end
+
+ def up
+ return unless Gitlab.ee?
+
+ # Remove approval project rule with rule type 4 if the project has a rule with rule_type 3
+ #
+ # Currently, there is no projects on gitlab.com which have both rules with 3 and 4 rule type
+ # There's a code-level validation for a rule, which doesn't allow to create rules with the same names
+ #
+ # But in order to avoid failing the update query due to uniqueness constraint
+ # Let's run the delete query to be sure
+ project_ids = FixAnyApproverRuleForProjects::ApprovalProjectRule.any_approver.select(:project_id)
+ FixAnyApproverRuleForProjects::ApprovalProjectRule
+ .non_existent_rule_type
+ .where(project_id: project_ids)
+ .delete_all
+
+ # Set approval project rule types to 3
+ # Currently there are 18_445 records to be updated
+ FixAnyApproverRuleForProjects::ApprovalProjectRule.non_existent_rule_type.each_batch(of: BATCH_SIZE) do |rules|
+ rules.update_all(rule_type: FixAnyApproverRuleForProjects::ApprovalProjectRule::ANY_APPROVER_RULE_TYPE)
+ end
+ end
+
+ def down
+ # The migration doesn't leave the database in an inconsistent state
+ # And can be run multiple times
+ end
+end
diff --git a/db/post_migrate/20191014030134_cleanup_design_management_version_user_to_author_rename.rb b/db/post_migrate/20191014030134_cleanup_design_management_version_user_to_author_rename.rb
new file mode 100644
index 00000000000..e7132cbeeb7
--- /dev/null
+++ b/db/post_migrate/20191014030134_cleanup_design_management_version_user_to_author_rename.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CleanupDesignManagementVersionUserToAuthorRename < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :design_management_versions, :user_id, :author_id
+ end
+
+ def down
+ undo_cleanup_concurrent_column_rename :design_management_versions, :user_id, :author_id
+ end
+end
diff --git a/db/post_migrate/20191017045817_schedule_fix_gitlab_com_pages_access_level.rb b/db/post_migrate/20191017045817_schedule_fix_gitlab_com_pages_access_level.rb
new file mode 100644
index 00000000000..fc44568ea17
--- /dev/null
+++ b/db/post_migrate/20191017045817_schedule_fix_gitlab_com_pages_access_level.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+# Code of this migration was removed after execution on gitlab.com
+# https://gitlab.com/gitlab-org/gitlab/issues/34018
+# Empty migration is left here to avoid any problems with rolling back
+class ScheduleFixGitlabComPagesAccessLevel < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb b/db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb
new file mode 100644
index 00000000000..0405e23b465
--- /dev/null
+++ b/db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class DropCiBuildTraceSectionsId < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ ##
+ # This column has already been ignored since 12.4
+ # See https://gitlab.com/gitlab-org/gitlab/issues/32569
+ remove_column :ci_build_trace_sections, :id
+ end
+
+ def down
+ ##
+ # We don't backfill serial ids as it's not used in application code
+ # and quite expensive process.
+ add_column :ci_build_trace_sections, :id, :bigint
+ end
+end
diff --git a/db/post_migrate/20191021101942_remove_empty_github_service_templates.rb b/db/post_migrate/20191021101942_remove_empty_github_service_templates.rb
new file mode 100644
index 00000000000..64abe93b3e8
--- /dev/null
+++ b/db/post_migrate/20191021101942_remove_empty_github_service_templates.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+## It's expected to delete one record on GitLab.com
+#
+class RemoveEmptyGithubServiceTemplates < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ class Service < ActiveRecord::Base
+ self.table_name = 'services'
+ self.inheritance_column = :_type_disabled
+
+ serialize :properties, JSON
+ end
+
+ def up
+ relationship.where(properties: {}).delete_all
+ end
+
+ def down
+ relationship.find_or_create_by!(properties: {})
+ end
+
+ private
+
+ def relationship
+ RemoveEmptyGithubServiceTemplates::Service.where(template: true, type: 'GithubService')
+ end
+end
diff --git a/db/post_migrate/20191022113635_nullify_feature_flag_plaintext_tokens.rb b/db/post_migrate/20191022113635_nullify_feature_flag_plaintext_tokens.rb
new file mode 100644
index 00000000000..9ade1454844
--- /dev/null
+++ b/db/post_migrate/20191022113635_nullify_feature_flag_plaintext_tokens.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+class NullifyFeatureFlagPlaintextTokens < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ class FeatureFlagsClient < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'operations_feature_flags_clients'
+
+ scope :with_encrypted_token, -> { where.not(token_encrypted: nil) }
+ scope :with_plaintext_token, -> { where.not(token: nil) }
+ scope :without_plaintext_token, -> { where(token: nil) }
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab.ee?
+
+ # 7357 records to be updated on GitLab.com
+ FeatureFlagsClient.with_encrypted_token.with_plaintext_token.each_batch do |relation|
+ relation.update_all(token: nil)
+ end
+ end
+
+ def down
+ return unless Gitlab.ee?
+
+ # There is no way to restore only the tokens that were NULLifyed in the `up`
+ # but we can do is to restore _all_ of them in case it is needed.
+ say_with_time('Decrypting tokens from operations_feature_flags_clients') do
+ FeatureFlagsClient.with_encrypted_token.without_plaintext_token.find_each do |feature_flags_client|
+ token = Gitlab::CryptoHelper.aes256_gcm_decrypt(feature_flags_client.token_encrypted)
+ feature_flags_client.update_column(:token, token)
+ end
+ end
+ end
+end
diff --git a/db/post_migrate/20191029095537_cleanup_application_settings_snowplow_site_id_rename.rb b/db/post_migrate/20191029095537_cleanup_application_settings_snowplow_site_id_rename.rb
new file mode 100644
index 00000000000..83b4a2af2b6
--- /dev/null
+++ b/db/post_migrate/20191029095537_cleanup_application_settings_snowplow_site_id_rename.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CleanupApplicationSettingsSnowplowSiteIdRename < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :application_settings, :snowplow_site_id, :snowplow_app_id
+ end
+
+ def down
+ undo_cleanup_concurrent_column_rename :application_settings, :snowplow_site_id, :snowplow_app_id
+ 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
new file mode 100644
index 00000000000..33bbe6f8ea7
--- /dev/null
+++ b/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemovePendoFromApplicationSettings < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ remove_column :application_settings, :pendo_enabled
+ remove_column :application_settings, :pendo_url
+ end
+
+ def down
+ add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false
+ add_column :application_settings, :pendo_url, :string, limit: 255
+ end
+end
diff --git a/db/post_migrate/20191031112603_remove_limits_from_plans.rb b/db/post_migrate/20191031112603_remove_limits_from_plans.rb
new file mode 100644
index 00000000000..30fb6a9d193
--- /dev/null
+++ b/db/post_migrate/20191031112603_remove_limits_from_plans.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveLimitsFromPlans < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ remove_column :plans, :active_pipelines_limit
+ remove_column :plans, :pipeline_size_limit
+ remove_column :plans, :active_jobs_limit
+ end
+
+ def down
+ add_column :plans, :active_pipelines_limit, :integer
+ add_column :plans, :pipeline_size_limit, :integer
+ add_column :plans, :active_jobs_limit, :integer
+ end
+end
diff --git a/db/post_migrate/20191105094625_set_report_type_for_vulnerabilities.rb b/db/post_migrate/20191105094625_set_report_type_for_vulnerabilities.rb
new file mode 100644
index 00000000000..6b7a158584d
--- /dev/null
+++ b/db/post_migrate/20191105094625_set_report_type_for_vulnerabilities.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class SetReportTypeForVulnerabilities < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ # set report_type based on associated vulnerability_occurrences
+ execute <<~SQL
+ UPDATE vulnerabilities
+ SET report_type = vulnerability_occurrences.report_type
+ FROM vulnerability_occurrences
+ WHERE vulnerabilities.id = vulnerability_occurrences.vulnerability_id
+ SQL
+
+ # set default report_type for orphan vulnerabilities (there should be none but...)
+ execute 'UPDATE vulnerabilities SET report_type = 0 WHERE report_type IS NULL'
+
+ change_column_null :vulnerabilities, :report_type, false
+ end
+
+ def down
+ change_column_null :vulnerabilities, :report_type, true
+
+ execute 'UPDATE vulnerabilities SET report_type = NULL'
+ end
+end
diff --git a/db/post_migrate/20191105140942_add_indices_to_abuse_reports.rb b/db/post_migrate/20191105140942_add_indices_to_abuse_reports.rb
new file mode 100644
index 00000000000..2b2d04e8ccc
--- /dev/null
+++ b/db/post_migrate/20191105140942_add_indices_to_abuse_reports.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndicesToAbuseReports < ActiveRecord::Migration[5.2]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :abuse_reports, :user_id
+ end
+
+ def down
+ remove_concurrent_index :abuse_reports, :user_id
+ end
+end
diff --git a/db/post_migrate/20191112115317_change_vulnerabilities_title_html_to_nullable.rb b/db/post_migrate/20191112115317_change_vulnerabilities_title_html_to_nullable.rb
new file mode 100644
index 00000000000..6e0f3247410
--- /dev/null
+++ b/db/post_migrate/20191112115317_change_vulnerabilities_title_html_to_nullable.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ChangeVulnerabilitiesTitleHtmlToNullable < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ change_column_null :vulnerabilities, :title_html, true
+ end
+end
diff --git a/db/post_migrate/20191114173624_set_resolved_state_on_vulnerabilities.rb b/db/post_migrate/20191114173624_set_resolved_state_on_vulnerabilities.rb
new file mode 100644
index 00000000000..b28aecdc0a3
--- /dev/null
+++ b/db/post_migrate/20191114173624_set_resolved_state_on_vulnerabilities.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class SetResolvedStateOnVulnerabilities < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ execute <<~SQL
+ -- selecting IDs for all non-orphan Findings that either have no feedback or it's a non-dismissal feedback
+ WITH resolved_vulnerability_ids AS (
+ SELECT DISTINCT vulnerability_id AS id
+ FROM vulnerability_occurrences
+ LEFT JOIN vulnerability_feedback ON vulnerability_feedback.project_fingerprint = ENCODE(vulnerability_occurrences.project_fingerprint::bytea, 'HEX')
+ WHERE vulnerability_id IS NOT NULL
+ AND (vulnerability_feedback.id IS NULL OR vulnerability_feedback.feedback_type <> 0)
+ )
+ UPDATE vulnerabilities
+ SET state = 3, resolved_by_id = closed_by_id, resolved_at = NOW()
+ FROM resolved_vulnerability_ids
+ WHERE vulnerabilities.id IN (resolved_vulnerability_ids.id)
+ AND state = 2 -- only 'closed' Vulnerabilities become 'resolved'
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ UPDATE vulnerabilities
+ SET state = 2, resolved_by_id = NULL, resolved_at = NULL -- state = 'closed'
+ WHERE state = 3 -- 'resolved'
+ SQL
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 95a4efb7e2a..e3413722991 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_10_26_041447) do
+ActiveRecord::Schema.define(version: 2019_11_15_091425) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.datetime "updated_at"
t.text "message_html"
t.integer "cached_markdown_version"
+ t.index ["user_id"], name: "index_abuse_reports_on_user_id"
end
create_table "alerts_service_data", force: :cascade do |t|
@@ -158,8 +159,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false
- t.integer "default_project_visibility"
- t.integer "default_snippet_visibility"
+ t.integer "default_project_visibility", default: 0, null: false
+ t.integer "default_snippet_visibility", default: 0, null: false
t.text "domain_whitelist"
t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path"
@@ -287,7 +288,6 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "snowplow_enabled", default: false, null: false
t.string "snowplow_collector_hostname"
- t.string "snowplow_site_id"
t.string "snowplow_cookie_domain"
t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
@@ -338,9 +338,23 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "throttle_incident_management_notification_enabled", default: false, null: false
t.integer "throttle_incident_management_notification_period_in_seconds", default: 3600
t.integer "throttle_incident_management_notification_per_period", default: 3600
+ t.string "snowplow_iglu_registry_url", limit: 255
t.integer "push_event_hooks_limit", default: 3, null: false
t.integer "push_event_activities_limit", default: 3, null: false
t.string "custom_http_clone_url_root", limit: 511
+ t.integer "deletion_adjourned_period", default: 7, null: false
+ t.date "license_trial_ends_on"
+ t.boolean "eks_integration_enabled", default: false, null: false
+ t.string "eks_account_id", limit: 128
+ t.string "eks_access_key_id", limit: 128
+ t.string "encrypted_eks_secret_access_key_iv", limit: 255
+ t.text "encrypted_eks_secret_access_key"
+ t.string "snowplow_app_id"
+ t.datetime_with_timezone "productivity_analytics_start_date"
+ t.string "default_ci_config_path", limit: 255
+ t.boolean "sourcegraph_enabled", default: false, null: false
+ t.string "sourcegraph_url", limit: 255
+ t.boolean "sourcegraph_public_only", default: true, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id"
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id"
t.index ["instance_administration_project_id"], name: "index_applicationsettings_on_instance_administration_project_id"
@@ -597,7 +611,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["project_id", "name"], name: "index_ci_build_trace_section_names_on_project_id_and_name", unique: true
end
- create_table "ci_build_trace_sections", id: :serial, force: :cascade do |t|
+ create_table "ci_build_trace_sections", id: false, force: :cascade do |t|
t.integer "project_id", null: false
t.datetime "date_start", null: false
t.datetime "date_end", null: false
@@ -691,7 +705,9 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "interruptible"
t.jsonb "config_options"
t.jsonb "config_variables"
+ t.boolean "has_exposed_artifacts"
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true
+ t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_has_exposed_artifacts", where: "(has_exposed_artifacts IS TRUE)"
t.index ["build_id"], name: "index_ci_builds_metadata_on_build_id_and_interruptible", where: "(interruptible = true)"
t.index ["project_id"], name: "index_ci_builds_metadata_on_project_id"
end
@@ -911,6 +927,13 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["project_id"], name: "index_ci_stages_on_project_id"
end
+ create_table "ci_subscriptions_projects", force: :cascade do |t|
+ t.bigint "downstream_project_id", null: false
+ t.bigint "upstream_project_id", null: false
+ t.index ["downstream_project_id", "upstream_project_id"], name: "index_ci_subscriptions_projects_unique_subscription", unique: true
+ t.index ["upstream_project_id"], name: "index_ci_subscriptions_projects_on_upstream_project_id"
+ end
+
create_table "ci_trigger_requests", id: :serial, force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
@@ -1037,6 +1060,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "managed", default: true, null: false
t.boolean "namespace_per_environment", default: true, null: false
t.integer "management_project_id"
+ t.integer "cleanup_status", limit: 2, default: 1, null: false
+ t.text "cleanup_status_reason"
t.index ["enabled"], name: "index_clusters_on_enabled"
t.index ["management_project_id"], name: "index_clusters_on_management_project_id", where: "(management_project_id IS NOT NULL)"
t.index ["user_id"], name: "index_clusters_on_user_id"
@@ -1053,6 +1078,28 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["cluster_id"], name: "index_clusters_applications_cert_managers_on_cluster_id", unique: true
end
+ create_table "clusters_applications_crossplane", id: :serial, force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "cluster_id", null: false
+ t.integer "status", null: false
+ t.string "version", limit: 255, null: false
+ t.string "stack", limit: 255, null: false
+ t.text "status_reason"
+ t.index ["cluster_id"], name: "index_clusters_applications_crossplane_on_cluster_id", unique: true
+ end
+
+ create_table "clusters_applications_elastic_stacks", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "cluster_id", null: false
+ t.integer "status", null: false
+ t.string "version", limit: 255, null: false
+ t.string "kibana_hostname", limit: 255
+ t.text "status_reason"
+ t.index ["cluster_id"], name: "index_clusters_applications_elastic_stacks_on_cluster_id", unique: true
+ end
+
create_table "clusters_applications_helm", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime "created_at", null: false
@@ -1238,6 +1285,13 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["token_encrypted"], name: "index_deploy_tokens_on_token_encrypted", unique: true
end
+ create_table "deployment_merge_requests", id: false, force: :cascade do |t|
+ t.integer "deployment_id", null: false
+ t.integer "merge_request_id", null: false
+ t.index ["deployment_id", "merge_request_id"], name: "idx_deployment_merge_requests_unique_index", unique: true
+ t.index ["merge_request_id"], name: "index_deployment_merge_requests_on_merge_request_id"
+ end
+
create_table "deployments", id: :serial, force: :cascade do |t|
t.integer "iid", null: false
t.integer "project_id", null: false
@@ -1264,6 +1318,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true
t.index ["project_id", "status", "created_at"], name: "index_deployments_on_project_id_and_status_and_created_at"
t.index ["project_id", "status"], name: "index_deployments_on_project_id_and_status"
+ t.index ["project_id", "updated_at"], name: "index_deployments_on_project_id_and_updated_at"
end
create_table "description_versions", force: :cascade do |t|
@@ -1299,11 +1354,11 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
create_table "design_management_versions", force: :cascade do |t|
t.binary "sha", null: false
t.bigint "issue_id"
- t.integer "user_id"
t.datetime_with_timezone "created_at"
+ t.integer "author_id"
+ t.index ["author_id"], name: "index_design_management_versions_on_author_id", where: "(author_id IS NOT NULL)"
t.index ["issue_id"], name: "index_design_management_versions_on_issue_id"
t.index ["sha", "issue_id"], name: "index_design_management_versions_on_sha_and_issue_id", unique: true
- t.index ["user_id"], name: "index_design_management_versions_on_user_id", where: "(user_id IS NOT NULL)"
end
create_table "draft_notes", force: :cascade do |t|
@@ -1408,15 +1463,19 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.integer "parent_id"
t.integer "relative_position"
t.integer "state_id", limit: 2, default: 1, null: false
+ t.integer "start_date_sourcing_epic_id"
+ t.integer "due_date_sourcing_epic_id"
t.index ["assignee_id"], name: "index_epics_on_assignee_id"
t.index ["author_id"], name: "index_epics_on_author_id"
t.index ["closed_by_id"], name: "index_epics_on_closed_by_id"
+ t.index ["due_date_sourcing_epic_id"], name: "index_epics_on_due_date_sourcing_epic_id", where: "(due_date_sourcing_epic_id IS NOT NULL)"
t.index ["end_date"], name: "index_epics_on_end_date"
t.index ["group_id"], name: "index_epics_on_group_id"
t.index ["iid"], name: "index_epics_on_iid"
t.index ["milestone_id"], name: "index_milestone"
t.index ["parent_id"], name: "index_epics_on_parent_id"
t.index ["start_date"], name: "index_epics_on_start_date"
+ t.index ["start_date_sourcing_epic_id"], name: "index_epics_on_start_date_sourcing_epic_id", where: "(start_date_sourcing_epic_id IS NOT NULL)"
end
create_table "events", id: :serial, force: :cascade do |t|
@@ -1631,6 +1690,10 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.integer "container_repositories_synced_count"
t.integer "container_repositories_failed_count"
t.integer "container_repositories_registry_count"
+ t.integer "design_repositories_count"
+ t.integer "design_repositories_synced_count"
+ t.integer "design_repositories_failed_count"
+ t.integer "design_repositories_registry_count"
t.index ["geo_node_id"], name: "index_geo_node_statuses_on_geo_node_id", unique: true
end
@@ -1782,6 +1845,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.string "encrypted_token", limit: 255, null: false
t.string "encrypted_token_iv", limit: 255, null: false
t.string "grafana_url", limit: 1024, null: false
+ t.boolean "enabled", default: false, null: false
t.index ["project_id"], name: "index_grafana_integrations_on_project_id"
end
@@ -1795,6 +1859,17 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["key", "value"], name: "index_group_custom_attributes_on_key_and_value"
end
+ create_table "group_group_links", force: :cascade do |t|
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "shared_group_id", null: false
+ t.bigint "shared_with_group_id", null: false
+ t.date "expires_at"
+ t.integer "group_access", limit: 2, default: 30, null: false
+ t.index ["shared_group_id", "shared_with_group_id"], name: "index_group_group_links_on_shared_group_and_shared_with_group", unique: true
+ t.index ["shared_with_group_id"], name: "index_group_group_links_on_shared_with_group_id"
+ end
+
create_table "historical_data", id: :serial, force: :cascade do |t|
t.date "date", null: false
t.integer "active_user_count"
@@ -1820,6 +1895,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.integer "project_id"
t.text "import_file"
t.text "export_file"
+ t.bigint "group_id"
+ t.index ["group_id"], name: "index_import_export_uploads_on_group_id", unique: true, where: "(group_id IS NOT NULL)"
t.index ["project_id"], name: "index_import_export_uploads_on_project_id"
t.index ["updated_at"], name: "index_import_export_uploads_on_updated_at"
end
@@ -2251,7 +2328,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["latest_closed_by_id"], name: "index_merge_request_metrics_on_latest_closed_by_id"
t.index ["merge_request_id", "merged_at"], name: "index_merge_request_metrics_on_merge_request_id_and_merged_at", where: "(merged_at IS NOT NULL)"
t.index ["merge_request_id"], name: "index_merge_request_metrics"
- t.index ["merged_at", "id"], name: "index_merge_request_metrics_on_merged_at_and_id"
+ t.index ["merged_at"], name: "index_merge_request_metrics_on_merged_at"
t.index ["merged_by_id"], name: "index_merge_request_metrics_on_merged_by_id"
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id"
end
@@ -2295,6 +2372,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "allow_maintainer_to_push"
t.integer "state_id", limit: 2, default: 1, null: false
t.string "rebase_jid"
+ t.binary "squash_commit_sha"
t.index ["assignee_id"], name: "index_merge_requests_on_assignee_id"
t.index ["author_id"], name: "index_merge_requests_on_author_id"
t.index ["created_at"], name: "index_merge_requests_on_created_at"
@@ -2317,6 +2395,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true
t.index ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)"
t.index ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id"
+ t.index ["target_project_id", "target_branch"], name: "index_merge_requests_on_target_project_id_and_target_branch", where: "((state_id = 1) AND (merge_when_pipeline_succeeds = true))"
t.index ["title"], name: "index_merge_requests_on_title"
t.index ["title"], name: "index_merge_requests_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)"
@@ -2613,6 +2692,26 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["project_id", "token_encrypted"], name: "index_feature_flags_clients_on_project_id_and_token_encrypted", unique: true
end
+ create_table "packages_conan_file_metadata", force: :cascade do |t|
+ t.bigint "package_file_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.string "recipe_revision", limit: 255, default: "0", null: false
+ t.string "package_revision", limit: 255
+ t.string "conan_package_reference", limit: 255
+ t.integer "conan_file_type", limit: 2, null: false
+ t.index ["package_file_id"], name: "index_packages_conan_file_metadata_on_package_file_id", unique: true
+ end
+
+ create_table "packages_conan_metadata", force: :cascade do |t|
+ t.bigint "package_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.string "package_username", limit: 255, null: false
+ t.string "package_channel", limit: 255, null: false
+ t.index ["package_id"], name: "index_packages_conan_metadata_on_package_id", unique: true
+ end
+
create_table "packages_maven_metadata", force: :cascade do |t|
t.bigint "package_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -2724,14 +2823,19 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["user_id"], name: "index_personal_access_tokens_on_user_id"
end
+ create_table "plan_limits", force: :cascade do |t|
+ t.bigint "plan_id", null: false
+ t.integer "ci_active_pipelines", default: 0, null: false
+ t.integer "ci_pipeline_size", default: 0, null: false
+ t.integer "ci_active_jobs", default: 0, null: false
+ t.index ["plan_id"], name: "index_plan_limits_on_plan_id", unique: true
+ end
+
create_table "plans", id: :serial, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "title"
- t.integer "active_pipelines_limit"
- t.integer "pipeline_size_limit"
- t.integer "active_jobs_limit", default: 0
t.index ["name"], name: "index_plans_on_name"
end
@@ -3035,9 +3139,12 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.integer "max_pages_size"
t.integer "max_artifacts_size"
t.string "pull_mirror_branch_prefix", limit: 50
+ t.boolean "remove_source_branch_after_merge"
+ t.date "marked_for_deletion_at"
+ t.integer "marked_for_deletion_by_user_id"
t.index "lower((name)::text)", name: "index_projects_on_lower_name"
t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))"
- t.index ["created_at"], name: "index_projects_on_created_at"
+ t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id"
t.index ["description"], name: "index_projects_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["id", "repository_storage", "last_repository_updated_at"], name: "idx_projects_on_repository_storage_last_repository_updated_at"
@@ -3047,6 +3154,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["last_repository_check_at"], name: "index_projects_on_last_repository_check_at", where: "(last_repository_check_at IS NOT NULL)"
t.index ["last_repository_check_failed"], name: "index_projects_on_last_repository_check_failed"
t.index ["last_repository_updated_at"], name: "index_projects_on_last_repository_updated_at"
+ t.index ["marked_for_deletion_at"], name: "index_projects_on_marked_for_deletion_at", where: "(marked_for_deletion_at IS NOT NULL)"
+ t.index ["marked_for_deletion_by_user_id"], name: "index_projects_on_marked_for_deletion_by_user_id", where: "(marked_for_deletion_by_user_id IS NOT NULL)"
t.index ["mirror_last_successful_update_at"], name: "index_projects_on_mirror_last_successful_update_at"
t.index ["mirror_user_id"], name: "index_projects_on_mirror_user_id"
t.index ["name"], name: "index_projects_on_name_trigram", opclass: :gin_trgm_ops, using: :gin
@@ -3060,7 +3169,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["runners_token"], name: "index_projects_on_runners_token"
t.index ["runners_token_encrypted"], name: "index_projects_on_runners_token_encrypted"
t.index ["star_count"], name: "index_projects_on_star_count"
- t.index ["visibility_level"], name: "index_projects_on_visibility_level"
+ t.index ["visibility_level", "created_at", "id"], name: "index_projects_on_visibility_level_and_created_at_and_id"
end
create_table "prometheus_alert_events", force: :cascade do |t|
@@ -3677,6 +3786,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "time_format_in_24h"
t.string "projects_sort", limit: 64
t.boolean "show_whitespace_in_diffs", default: true, null: false
+ t.boolean "sourcegraph_enabled"
+ t.boolean "setup_for_company"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end
@@ -3816,6 +3927,13 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["user_id", "project_id"], name: "index_users_ops_dashboard_projects_on_user_id_and_project_id", unique: true
end
+ create_table "users_security_dashboard_projects", id: false, force: :cascade do |t|
+ t.bigint "user_id", null: false
+ t.bigint "project_id", null: false
+ t.index ["project_id", "user_id"], name: "users_security_dashboard_projects_unique_index", unique: true
+ t.index ["user_id"], name: "index_users_security_dashboard_projects_on_user_id"
+ end
+
create_table "users_star_projects", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
@@ -3838,7 +3956,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "title", limit: 255, null: false
- t.text "title_html", null: false
+ t.text "title_html"
t.text "description"
t.text "description_html"
t.bigint "start_date_sourcing_milestone_id"
@@ -3850,6 +3968,10 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.boolean "severity_overridden", default: false
t.integer "confidence", limit: 2, null: false
t.boolean "confidence_overridden", default: false
+ t.bigint "resolved_by_id"
+ t.datetime_with_timezone "resolved_at"
+ t.integer "report_type", limit: 2, null: false
+ t.integer "cached_markdown_version"
t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"
@@ -3857,6 +3979,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["last_edited_by_id"], name: "index_vulnerabilities_on_last_edited_by_id"
t.index ["milestone_id"], name: "index_vulnerabilities_on_milestone_id"
t.index ["project_id"], name: "index_vulnerabilities_on_project_id"
+ t.index ["resolved_by_id"], name: "index_vulnerabilities_on_resolved_by_id"
t.index ["start_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_start_date_sourcing_milestone_id"
t.index ["updated_by_id"], name: "index_vulnerabilities_on_updated_by_id"
end
@@ -3895,6 +4018,17 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["project_id", "fingerprint"], name: "index_vulnerability_identifiers_on_project_id_and_fingerprint", unique: true
end
+ create_table "vulnerability_issue_links", force: :cascade do |t|
+ t.bigint "vulnerability_id", null: false
+ t.bigint "issue_id", null: false
+ t.integer "link_type", limit: 2, default: 1, null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.index ["issue_id"], name: "index_vulnerability_issue_links_on_issue_id"
+ t.index ["vulnerability_id", "issue_id"], name: "idx_vulnerability_issue_links_on_vulnerability_id_and_issue_id", unique: true
+ t.index ["vulnerability_id", "link_type"], name: "idx_vulnerability_issue_links_on_vulnerability_id_and_link_type", unique: true, where: "(link_type = 2)"
+ end
+
create_table "vulnerability_occurrence_identifiers", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -3990,6 +4124,19 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
t.index ["type"], name: "index_web_hooks_on_type"
end
+ create_table "zoom_meetings", force: :cascade do |t|
+ t.bigint "project_id", null: false
+ t.bigint "issue_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "issue_status", limit: 2, default: 1, null: false
+ t.string "url", limit: 255
+ t.index ["issue_id", "issue_status"], name: "index_zoom_meetings_on_issue_id_and_issue_status", unique: true, where: "(issue_status = 1)"
+ t.index ["issue_id"], name: "index_zoom_meetings_on_issue_id"
+ t.index ["issue_status"], name: "index_zoom_meetings_on_issue_status"
+ t.index ["project_id"], name: "index_zoom_meetings_on_project_id"
+ end
+
add_foreign_key "alerts_service_data", "services", on_delete: :cascade
add_foreign_key "allowed_email_domains", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade
@@ -4080,6 +4227,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "ci_sources_pipelines", "projects", name: "fk_1e53c97c0a", on_delete: :cascade
add_foreign_key "ci_stages", "ci_pipelines", column: "pipeline_id", name: "fk_fb57e6cc56", on_delete: :cascade
add_foreign_key "ci_stages", "projects", name: "fk_2360681d1d", on_delete: :cascade
+ add_foreign_key "ci_subscriptions_projects", "projects", column: "downstream_project_id", on_delete: :cascade
+ add_foreign_key "ci_subscriptions_projects", "projects", column: "upstream_project_id", on_delete: :cascade
add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade
add_foreign_key "ci_triggers", "projects", name: "fk_e3e63f966e", on_delete: :cascade
add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade
@@ -4095,6 +4244,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "clusters", "projects", column: "management_project_id", name: "fk_f05c5e5a42", on_delete: :nullify
add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "clusters_applications_cert_managers", "clusters", on_delete: :cascade
+ add_foreign_key "clusters_applications_crossplane", "clusters", on_delete: :cascade
+ add_foreign_key "clusters_applications_elastic_stacks", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_ingress", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
@@ -4111,6 +4262,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "dependency_proxy_blobs", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "dependency_proxy_group_settings", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
+ add_foreign_key "deployment_merge_requests", "deployments", on_delete: :cascade
+ add_foreign_key "deployment_merge_requests", "merge_requests", on_delete: :cascade
add_foreign_key "deployments", "clusters", name: "fk_289bba3222", on_delete: :nullify
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "description_versions", "epics", on_delete: :cascade
@@ -4121,7 +4274,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "design_management_designs_versions", "design_management_designs", column: "design_id", name: "fk_03c671965c", on_delete: :cascade
add_foreign_key "design_management_designs_versions", "design_management_versions", column: "version_id", name: "fk_f4d25ba00c", on_delete: :cascade
add_foreign_key "design_management_versions", "issues", on_delete: :cascade
- add_foreign_key "design_management_versions", "users", name: "fk_ee16b939e5", on_delete: :nullify
+ add_foreign_key "design_management_versions", "users", column: "author_id", name: "fk_c1440b4896", on_delete: :nullify
add_foreign_key "draft_notes", "merge_requests", on_delete: :cascade
add_foreign_key "draft_notes", "users", column: "author_id", on_delete: :cascade
add_foreign_key "elasticsearch_indexed_namespaces", "namespaces", on_delete: :cascade
@@ -4130,7 +4283,9 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "epic_issues", "epics", on_delete: :cascade
add_foreign_key "epic_issues", "issues", on_delete: :cascade
add_foreign_key "epic_metrics", "epics", on_delete: :cascade
+ add_foreign_key "epics", "epics", column: "due_date_sourcing_epic_id", name: "fk_013c9f36ca", on_delete: :nullify
add_foreign_key "epics", "epics", column: "parent_id", name: "fk_25b99c1be3", on_delete: :cascade
+ add_foreign_key "epics", "epics", column: "start_date_sourcing_epic_id", name: "fk_9d480c64b2", on_delete: :nullify
add_foreign_key "epics", "milestones", on_delete: :nullify
add_foreign_key "epics", "namespaces", column: "group_id", name: "fk_f081aa4489", on_delete: :cascade
add_foreign_key "epics", "users", column: "assignee_id", name: "fk_dccd3f98fc", on_delete: :nullify
@@ -4178,7 +4333,10 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "grafana_integrations", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "group_group_links", "namespaces", column: "shared_group_id", on_delete: :cascade
+ add_foreign_key "group_group_links", "namespaces", column: "shared_with_group_id", on_delete: :cascade
add_foreign_key "identities", "saml_providers", name: "fk_aade90f0fc", on_delete: :cascade
+ add_foreign_key "import_export_uploads", "namespaces", column: "group_id", name: "fk_83319d9721", on_delete: :cascade
add_foreign_key "import_export_uploads", "projects", on_delete: :cascade
add_foreign_key "index_statuses", "projects", name: "fk_74b2492545", on_delete: :cascade
add_foreign_key "insights", "namespaces", on_delete: :cascade
@@ -4264,6 +4422,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "operations_feature_flag_scopes", "operations_feature_flags", column: "feature_flag_id", on_delete: :cascade
add_foreign_key "operations_feature_flags", "projects", on_delete: :cascade
add_foreign_key "operations_feature_flags_clients", "projects", on_delete: :cascade
+ add_foreign_key "packages_conan_file_metadata", "packages_package_files", column: "package_file_id", on_delete: :cascade
+ add_foreign_key "packages_conan_metadata", "packages_packages", column: "package_id", on_delete: :cascade
add_foreign_key "packages_maven_metadata", "packages_packages", column: "package_id", name: "fk_be88aed360", on_delete: :cascade
add_foreign_key "packages_package_files", "packages_packages", column: "package_id", name: "fk_86f0f182f8", on_delete: :cascade
add_foreign_key "packages_package_metadata", "packages_packages", column: "package_id", on_delete: :cascade
@@ -4274,6 +4434,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "path_locks", "projects", name: "fk_5265c98f24", on_delete: :cascade
add_foreign_key "path_locks", "users"
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "plan_limits", "plans", on_delete: :cascade
add_foreign_key "pool_repositories", "projects", column: "source_project_id", on_delete: :nullify
add_foreign_key "pool_repositories", "shards", on_delete: :restrict
add_foreign_key "project_alerting_settings", "projects", on_delete: :cascade
@@ -4301,6 +4462,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "project_tracing_settings", "projects", on_delete: :cascade
add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify
+ add_foreign_key "projects", "users", column: "marked_for_deletion_by_user_id", name: "fk_25d8780d11", on_delete: :nullify
add_foreign_key "prometheus_alert_events", "projects", on_delete: :cascade
add_foreign_key "prometheus_alert_events", "prometheus_alerts", on_delete: :cascade
add_foreign_key "prometheus_alerts", "environments", on_delete: :cascade
@@ -4376,6 +4538,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "users", "namespaces", column: "managing_group_id", name: "fk_a4b8fefe3e", on_delete: :nullify
add_foreign_key "users_ops_dashboard_projects", "projects", on_delete: :cascade
add_foreign_key "users_ops_dashboard_projects", "users", on_delete: :cascade
+ add_foreign_key "users_security_dashboard_projects", "projects", on_delete: :cascade
+ add_foreign_key "users_security_dashboard_projects", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "vulnerabilities", "epics", name: "fk_1d37cddf91", on_delete: :nullify
add_foreign_key "vulnerabilities", "milestones", column: "due_date_sourcing_milestone_id", name: "fk_7c5bb22a22", on_delete: :nullify
@@ -4385,6 +4549,7 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "vulnerabilities", "users", column: "author_id", name: "fk_b1de915a15", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "closed_by_id", name: "fk_cf5c60acbf", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "last_edited_by_id", name: "fk_1302949740", on_delete: :nullify
+ add_foreign_key "vulnerabilities", "users", column: "resolved_by_id", name: "fk_76bc5f5455", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify
add_foreign_key "vulnerability_feedback", "ci_pipelines", column: "pipeline_id", on_delete: :nullify
add_foreign_key "vulnerability_feedback", "issues", on_delete: :nullify
@@ -4393,6 +4558,8 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "vulnerability_feedback", "users", column: "author_id", on_delete: :cascade
add_foreign_key "vulnerability_feedback", "users", column: "comment_author_id", name: "fk_94f7c8a81e", on_delete: :nullify
add_foreign_key "vulnerability_identifiers", "projects", on_delete: :cascade
+ add_foreign_key "vulnerability_issue_links", "issues", on_delete: :cascade
+ add_foreign_key "vulnerability_issue_links", "vulnerabilities", on_delete: :cascade
add_foreign_key "vulnerability_occurrence_identifiers", "vulnerability_identifiers", column: "identifier_id", on_delete: :cascade
add_foreign_key "vulnerability_occurrence_identifiers", "vulnerability_occurrences", column: "occurrence_id", on_delete: :cascade
add_foreign_key "vulnerability_occurrence_pipelines", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
@@ -4404,4 +4571,6 @@ ActiveRecord::Schema.define(version: 2019_10_26_041447) do
add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
+ add_foreign_key "zoom_meetings", "issues", on_delete: :cascade
+ add_foreign_key "zoom_meetings", "projects", on_delete: :cascade
end
diff --git a/doc/README.md b/doc/README.md
index 61265f94004..af573a3eb34 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -119,8 +119,8 @@ The following documentation relates to the DevOps **Plan** stage:
| [Related Issues](user/project/issues/related_issues.md) **(STARTER)** | Create a relationship between issues. |
| [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](workflow/time_tracking.md) | Track time spent on issues and merge requests. |
-| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. |
+| [Time Tracking](user/project/time_tracking.md) | Track time spent on issues and merge requests. |
+| [Todos](user/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -177,7 +177,7 @@ The following documentation relates to the DevOps **Create** stage:
| [Protected branches](user/project/protected_branches.md) | Use protected branches. |
| [Push rules](push_rules/push_rules.md) **(STARTER)** | Additional control over pushes to your projects. |
| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. |
-| [Repository mirroring](workflow/repository_mirroring.md) **(STARTER)** | Push to or pull from repositories outside of GitLab |
+| [Repository mirroring](user/project/repository/repository_mirroring.md) **(STARTER)** | Push to or pull from repositories outside of GitLab |
| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. |
<div align="right">
@@ -188,13 +188,13 @@ The following documentation relates to the DevOps **Create** stage:
#### Merge Requests
-| Create Topics - Merge Requests | Description |
-|:------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
-| [Checking out merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally) | Tips for working with merge requests locally. |
-| [Cherry-picking](user/project/merge_requests/cherry_pick_changes.md) | Use GitLab for cherry-picking changes. |
-| [Merge request thread resolution](user/discussions/index.md#moving-a-single-thread-to-a-new-issue) | Resolve threads, move threads in a merge request to an issue, and only allow merge requests to be merged if all threads are resolved. |
-| [Merge requests](user/project/merge_requests/index.md) | Merge request management. |
-| [Work In Progress "WIP" merge requests](user/project/merge_requests/work_in_progress_merge_requests.md) | Prevent merges of work-in-progress merge requests. |
+| Create Topics - Merge Requests | Description |
+|:--------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
+| [Checking out merge requests locally](user/project/merge_requests/reviewing_and_managing_merge_requests.md#checkout-merge-requests-locally) | Tips for working with merge requests locally. |
+| [Cherry-picking](user/project/merge_requests/cherry_pick_changes.md) | Use GitLab for cherry-picking changes. |
+| [Merge request thread resolution](user/discussions/index.md#moving-a-single-thread-to-a-new-issue) | Resolve threads, move threads in a merge request to an issue, and only allow merge requests to be merged if all threads are resolved. |
+| [Merge requests](user/project/merge_requests/index.md) | Merge request management. |
+| [Work In Progress "WIP" merge requests](user/project/merge_requests/work_in_progress_merge_requests.md) | Prevent merges of work-in-progress merge requests. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -305,7 +305,7 @@ The following documentation relates to the DevOps **Configure** stage:
| Configure Topics | Description |
|:-----------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
| [Auto DevOps](topics/autodevops/index.md) | Automatically employ a complete DevOps lifecycle. |
-| [Create Kubernetes clusters on GKE](user/project/clusters/index.md#add-new-gke-cluster) | Use Google Kubernetes Engine and GitLab. |
+| [Create Kubernetes clusters](user/project/clusters/add_remove_clusters.md#add-new-cluster) | Use Kubernetes and GitLab. |
| [Executable Runbooks](user/project/clusters/runbooks/index.md) | Documented procedures that explain how to carry out particular processes. |
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
@@ -336,7 +336,8 @@ The following documentation relates to the DevOps **Monitor** stage:
| [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. |
-| [Prometheus metrics](user/project/integrations/prometheus_library/index.md) | Let Prometheus collect metrics from various services, like Kubernetes, NGINX, NGINX ingress controller, HAProxy, and Amazon Cloud Watch. |
+| [Prometheus metrics](user/project/integrations/prometheus_library/index.md) | Let Prometheus collect metrics from various services, like Kubernetes, NGINX, NGINX Ingress controller, HAProxy, and Amazon Cloud Watch. |
+| [Incident management](user/incident_management/index.md) | Use GitLab to help you better respond to incidents that may occur in your systems. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -375,7 +376,7 @@ We have the following documentation to rapidly uplift your GitLab knowledge:
| Topic | Description |
|:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------|
| [GitLab basics guides](gitlab-basics/README.md) | Start working on the command line and with GitLab. |
-| [GitLab Workflow](workflow/README.md) and [overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Enhance your workflow with the best of GitLab Workflow. |
+| [GitLab workflow overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Enhance your workflow with the best of GitLab Workflow. |
| [Get started with GitLab CI/CD](ci/quick_start/README.md) | Quickly implement GitLab CI/CD. |
| [Auto DevOps](topics/autodevops/index.md) | Learn more about GitLab's Auto DevOps. |
| [GitLab Markdown](user/markdown.md) | GitLab's advanced formatting system (GitLab Flavored Markdown) |
@@ -393,7 +394,7 @@ Learn more about GitLab account management:
| Topic | Description |
|:-----------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------|
| [User account](user/profile/index.md) | Manage your account. |
-| [Authentication](topics/authentication/index.md) | Account security with two-factor authentication, set up your ssh keys, and deploy keys for secure access to your projects. |
+| [Authentication](topics/authentication/index.md) | Account security with two-factor authentication, set up your SSH keys, and deploy keys for secure access to your projects. |
| [Profile settings](user/profile/index.md#profile-settings) | Manage your profile settings, two factor authentication, and more. |
| [User permissions](user/permissions.md) | Learn what each role in a project can do. |
@@ -411,7 +412,7 @@ Learn more about using Git, and using Git with GitLab:
|:----------------------------------------------------------------------------|:---------------------------------------------------------------------------|
| [Git](topics/git/index.md) | Getting started with Git, branching strategies, Git LFS, and advanced use. |
| [Git cheatsheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF describing the most used Git operations. |
-| [GitLab Flow](workflow/gitlab_flow.md) | Explore the best of Git with the GitLab Flow strategy. |
+| [GitLab Flow](topics/gitlab_flow.md) | Explore the best of Git with the GitLab Flow strategy. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -426,7 +427,7 @@ If you are coming to GitLab from another platform, you'll find the following inf
| Topic | Description |
|:---------------------------------------------------------------|:---------------------------------------------------------------------------------------|
| [Importing to GitLab](user/project/import/index.md) | Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz, and SVN into GitLab. |
-| [Migrating from SVN](workflow/importing/migrating_from_svn.md) | Convert a SVN repository to Git and GitLab. |
+| [Migrating from SVN](user/project/import/svn.md) | Convert a SVN repository to Git and GitLab. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 61ea673071e..ccb4ccbd525 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -59,6 +59,8 @@ From there, you can see the following actions:
- 2FA enforcement/grace period changed
- Roles allowed to create project changed
+Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events-starter)
+
### Project events **(STARTER)**
NOTE: **Note:**
@@ -107,6 +109,8 @@ the filter drop-down. You can further filter by specific group, project or user
![audit log](img/audit_log.png)
+Instance events can also be accessed via the [Instance Audit Events API](../api/audit_events.md#instance-audit-events-premium-only)
+
### Missing events
Some events are not being tracked in Audit Events. Please see the following
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index e02ce1c0a21..d449a5a72af 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -118,6 +118,7 @@ LDAP users must have an email address set, regardless of whether it is used to l
```ruby
gitlab_rails['ldap_enabled'] = true
+gitlab_rails['prevent_ldap_sign_in'] = false
gitlab_rails['ldap_servers'] = YAML.load <<-EOS # remember to close this block with 'EOS' below
##
## 'main' is the GitLab 'provider ID' of this LDAP server
@@ -357,6 +358,7 @@ production:
# snip...
ldap:
enabled: false
+ prevent_ldap_sign_in: false
servers:
##
## 'main' is the GitLab 'provider ID' of this LDAP server
@@ -493,6 +495,38 @@ the configuration option `lowercase_usernames`. By default, this configuration o
1. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
+## Disable LDAP web sign in
+
+It can be be useful to prevent using LDAP credentials through the web UI when
+an alternative such as SAML is preferred. This allows LDAP to be used for group
+sync, while also allowing your SAML identity provider to handle additional
+checks like custom 2FA.
+
+When LDAP web sign in is disabled, users will not see a **LDAP** tab on the sign in page.
+This does not disable [using LDAP credentials for Git access](#git-password-authentication).
+
+**Omnibus configuration**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['prevent_ldap_sign_in'] = true
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+**Source configuration**
+
+1. Edit `config/gitlab.yaml`:
+
+ ```yaml
+ production:
+ ldap:
+ prevent_ldap_sign_in: true
+ ```
+
+1. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
+
## Encryption
### TLS Server Authentication
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index 34c668b5fb5..5caf1d53a2c 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -11,9 +11,8 @@ calculated checksum. If the checksum of the data on the **primary** node matches
data on the **secondary** node, the data transferred successfully. Following a planned failover,
any corrupted data may be **lost**, depending on the extent of the corruption.
-If verification fails on the **primary** node, this indicates that Geo is
-successfully replicating a corrupted object; restore it from backup or remove it
-it from the **primary** node to resolve the issue.
+If verification fails on the **primary** node, this indicates Geo is replicating a corrupted object.
+You can restore it from backup or remove it from the **primary** node to resolve the issue.
If verification succeeds on the **primary** node but fails on the **secondary** node,
this indicates that the object was corrupted during the replication process.
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index f09d9f20dab..44baab40153 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -187,14 +187,18 @@ keys must be manually replicated to the **secondary** node.
1. Visit the **primary** node's **Admin Area > Geo**
(`/admin/geo/nodes`) in your browser.
1. Click the **New node** button.
-1. Add the **secondary** node. Use the **exact** name you inputed for `gitlab_rails['geo_node_name']` as the Name and the full URL as the URL. **Do NOT** check the
- **This is a primary node** checkbox.
-
![Add secondary node](img/adding_a_secondary_node.png)
+1. Fill in **Name** with the `gitlab_rails['geo_node_name']` in
+ `/etc/gitlab/gitlab.rb`. These values must always match *exactly*, character
+ for character.
+1. Fill in **URL** with the `external_url` in `/etc/gitlab/gitlab.rb`. These
+ values must always match, but it doesn't matter if one ends with a `/` and
+ the other doesn't.
+1. **Do NOT** check the **This is a primary node** checkbox.
1. Optionally, choose which groups or storage shards should be replicated by the
**secondary** node. Leave blank to replicate all. Read more in
[selective synchronization](#selective-synchronization).
-1. Click the **Add node** button.
+1. Click the **Add node** button to add the **secondary** node.
1. SSH into your GitLab **secondary** server and restart the services:
```sh
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index fa1b0f0e1d7..f7da4e14e9d 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -425,6 +425,9 @@ data before running `pg_basebackup`.
--host=<primary_node_ip>
```
+ NOTE: **Note:**
+ Replication slot names must only contain lowercase letters, numbers, and the underscore character.
+
When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
user in the first step.
@@ -454,7 +457,7 @@ The replication process is now complete.
## PgBouncer support (optional)
-[PgBouncer](http://pgbouncer.github.io/) may be used with GitLab Geo to pool
+[PgBouncer](https://www.pgbouncer.org/) may be used with GitLab Geo to pool
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
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index a9087abcbd9..3251a673e4e 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -30,7 +30,7 @@ To enable GitLab replication, you must:
checkbox.
For LFS, follow the documentation to
-[set up LFS object storage](../../../workflow/lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
+[set up LFS object storage](../../lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
For CI job artifacts, there is similar documentation to configure
[jobs artifact object storage](../../job_artifacts.md#using-object-storage)
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 4d64941411a..d2fe02abbab 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -115,11 +115,19 @@ Any **secondary** nodes should point only to read-only instances.
#### Can Geo detect the current node correctly?
-Geo uses the defined node from the **Admin Area > Geo** screen, and tries to match
-it with the value defined in the `/etc/gitlab/gitlab.rb` configuration file.
-The relevant line looks like: `external_url "http://gitlab.example.com"`.
+Geo finds the current machine's name in `/etc/gitlab/gitlab.rb` by first looking
+for `gitlab_rails['geo_node_name']`. If it is not defined, then it defaults to
+the external URL defined in e.g. `external_url "http://gitlab.example.com"`. To
+get a machine's name, run:
-To check if the node on the current machine is correctly detected type:
+```sh
+sudo gitlab-rails runner "puts GeoNode.current_node_name"
+```
+
+This name is used to look up the node with the same **Name** in
+**Admin Area > Geo**.
+
+To check if current machine is correctly finding its node:
```sh
sudo gitlab-rails runner "puts Gitlab::Geo.current_node.inspect"
@@ -134,6 +142,106 @@ and expect something like:
By running the command above, `primary` should be `true` when executed in
the **primary** node, and `false` on any **secondary** node.
+## Fixing errors found when running the Geo check rake task
+
+When running this rake task, you may see errors if the nodes are not properly configured:
+
+```sh
+sudo gitlab-rake gitlab:geo:check
+```
+
+1. Rails did not provide a password when connecting to the database
+
+ ```text
+ Checking Geo ...
+
+ GitLab Geo is available ... Exception: fe_sendauth: no password supplied
+ GitLab Geo is enabled ... Exception: fe_sendauth: no password supplied
+ ...
+ Checking Geo ... Finished
+ ```
+
+ - Ensure that you have the `gitlab_rails['db_password']` set to the plain text-password used when creating the hash for `postgresql['sql_user_password']`.
+
+1. Rails is unable to connect to the database
+
+ ```text
+ Checking Geo ...
+
+ GitLab Geo is available ... Exception: FATAL: no pg_hba.conf entry for host "1.1.1.1", user "gitlab", database "gitlabhq_production", SSL on
+ FATAL: no pg_hba.conf entry for host "1.1.1.1", user "gitlab", database "gitlabhq_production", SSL off
+ GitLab Geo is enabled ... Exception: FATAL: no pg_hba.conf entry for host "1.1.1.1", user "gitlab", database "gitlabhq_production", SSL on
+ FATAL: no pg_hba.conf entry for host "1.1.1.1", user "gitlab", database "gitlabhq_production", SSL off
+ ...
+ Checking Geo ... Finished
+ ```
+
+ - Ensure that you have the IP address of the rails node included in `postgresql['md5_auth_cidr_addresses']`.
+ - Ensure that you have included the subnet mask on the IP address: `postgresql['md5_auth_cidr_addresses'] = ['1.1.1.1/32']`.
+
+1. Rails has supplied the incorrect password
+
+ ```text
+ Checking Geo ...
+ GitLab Geo is available ... Exception: FATAL: password authentication failed for user "gitlab"
+ FATAL: password authentication failed for user "gitlab"
+ GitLab Geo is enabled ... Exception: FATAL: password authentication failed for user "gitlab"
+ FATAL: password authentication failed for user "gitlab"
+ ...
+ Checking Geo ... Finished
+ ```
+
+ - Verify the correct password is set for `gitlab_rails['db_password']` that was used when creating the hash in `postgresql['sql_user_password']` by running `gitlab-ctl pg-password-md5 gitlab` and entering the password.
+
+1. Check returns not a secondary node
+
+ ```text
+ Checking Geo ...
+
+ GitLab Geo is available ... yes
+ GitLab Geo is enabled ... yes
+ GitLab Geo secondary database is correctly configured ... not a secondary node
+ Database replication enabled? ... not a secondary node
+ ...
+ Checking Geo ... Finished
+ ```
+
+ - Ensure that you have added the secondary node in the admin area of the primary node.
+ - Ensure that you entered the `external_url` or `gitlab_rails['geo_node_name']` when adding the secondary node in the admin are of the primary node.
+ - Prior to GitLab 12.4, edit the secondary node in the admin area of the primary node and ensure that there is a trailing `/` in the `Name` field.
+
+1. Check returns Exception: PG::UndefinedTable: ERROR: relation "geo_nodes" does not exist
+
+ ```text
+ Checking Geo ...
+
+ GitLab Geo is available ... no
+ Try fixing it:
+ Upload a new license that includes the GitLab Geo feature
+ For more information see:
+ https://about.gitlab.com/features/gitlab-geo/
+ GitLab Geo is enabled ... Exception: PG::UndefinedTable: ERROR: relation "geo_nodes" does not exist
+ LINE 8: WHERE a.attrelid = '"geo_nodes"'::regclass
+ ^
+ : SELECT a.attname, format_type(a.atttypid, a.atttypmod),
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
+ c.collname, col_description(a.attrelid, a.attnum) AS comment
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ LEFT JOIN pg_type t ON a.atttypid = t.oid
+ LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation
+ WHERE a.attrelid = '"geo_nodes"'::regclass
+ AND a.attnum > 0 AND NOT a.attisdropped
+ ORDER BY a.attnum
+ ...
+ Checking Geo ... Finished
+ ```
+
+ When performing a Postgres major version (9 > 10) update this is expected. Follow:
+
+ - [initiate-the-replication-process](https://docs.gitlab.com/ee/administration/geo/replication/database.html#step-3-initiate-the-replication-process)
+ - [Geo database has an outdated FDW remote schema](https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html#geo-database-has-an-outdated-fdw-remote-schema-error)
+
## Fixing replication errors
The following sections outline troubleshooting steps for fixing replication
@@ -258,7 +366,7 @@ to start again from scratch, there are a few steps that can help you:
gitlab-ctl tail sidekiq
```
-1. Rename repository storage folders and create new ones
+1. Rename repository storage folders and create new ones. If you are not concerned about possible orphaned directories and files, then you can simply skip this step.
```sh
mv /var/opt/gitlab/git-data/repositories /var/opt/gitlab/git-data/repositories.old
@@ -305,7 +413,9 @@ to start again from scratch, there are a few steps that can help you:
1. Reset the Tracking Database
```sh
- gitlab-rake geo:db:reset
+ gitlab-rake geo:db:drop
+ gitlab-ctl reconfigure
+ gitlab-rake geo:db:setup
```
1. Restart previously stopped services
@@ -511,6 +621,20 @@ to [cleanup orphan artifact files](../../../raketasks/cleanup.md#remove-orphan-a
On a Geo **secondary** node, this command will also clean up all Geo
registry record related to the orphan files on disk.
+## Fixing sign in errors
+
+### Message: The redirect URI included is not valid
+
+If you are able to log in to the **primary** node, but you receive this error
+when attempting to log into a **secondary**, you should check that the Geo
+node's URL matches its external URL.
+
+1. On the primary, visit **Admin Area > Geo**.
+1. Find the affected **secondary** and click **Edit**.
+1. Ensure the **URL** field matches the value found in `/etc/gitlab/gitlab.rb`
+ in `external_url "https://gitlab.example.com"` on the frontend server(s) of
+ the **secondary** node.
+
## Fixing common errors
This section documents common errors reported in the Admin UI and how to fix them.
@@ -531,13 +655,6 @@ Geo cannot reuse an existing tracking database.
It is safest to use a fresh secondary, or reset the whole secondary by following
[Resetting Geo secondary node replication](#resetting-geo-secondary-node-replication).
-If you are not concerned about possible orphaned directories and files, then you
-can simply reset the existing tracking database with:
-
-```sh
-sudo gitlab-rake geo:db:reset
-```
-
### Geo node has a database that is writable which is an indication it is not configured for replication with the primary node
This error refers to a problem with the database replica on a **secondary** node,
diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md
index 55c7e78da92..37982f2756c 100644
--- a/doc/administration/geo/replication/using_a_geo_server.md
+++ b/doc/administration/geo/replication/using_a_geo_server.md
@@ -10,8 +10,12 @@ Example of the output you will see when pushing to a **secondary** node:
```bash
$ git push
-> GitLab: You're pushing to a Geo secondary.
-> GitLab: We'll help you by proxying this request to the primary: ssh://git@primary.geo/user/repo.git
+remote:
+remote: You're pushing to a Geo secondary. We'll help you by proxying this
+remote: request to the primary:
+remote:
+remote: ssh://git@primary.geo/user/repo.git
+remote:
Everything up-to-date
```
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 6d550a49df4..5288cc6e186 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -4,6 +4,24 @@ Check this document if it includes instructions for the version you are updating
These steps go together with the [general steps](updating_the_geo_nodes.md#general-update-steps)
for updating Geo nodes.
+## Updating to GitLab 12.2
+
+GitLab 12.2 includes the following minor PostgreSQL updates:
+
+- To version `9.6.14` if you run PostgreSQL 9.6.
+- To version `10.9` if you run PostgreSQL 10.
+
+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),
+restart the Geo tracking database:
+
+```sh
+sudo gitlab-ctl restart geo-postgresql
+```
+
+The restart avoids a version mismatch when PostgreSQL tries to load the FDW extension.
+
## Updating to GitLab 12.1
By default, GitLab 12.1 will attempt to automatically update the
diff --git a/doc/administration/git_annex.md b/doc/administration/git_annex.md
new file mode 100644
index 00000000000..52d848efa27
--- /dev/null
+++ b/doc/administration/git_annex.md
@@ -0,0 +1,242 @@
+---
+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].
+
+The biggest limitation of Git, compared to some older centralized version
+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
+are over 50GB!), we subscribe to the advice to keep repositories as small as
+you can.
+
+Not being able to version control large binaries is a big problem for many
+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.
+
+This problem is solved in GitLab Enterprise Edition by integrating the
+[git-annex] application.
+
+`git-annex` allows managing large binaries with Git without checking the
+contents into Git.
+You check-in only a symlink that contains the SHA-1 of the large binary. If you
+need the large binary, you can sync it from the GitLab server over `rsync`, a
+very fast file copying tool.
+
+## GitLab git-annex Configuration
+
+`git-annex` is disabled by default in GitLab. Below you will find the
+configuration options required to enable it.
+
+### Requirements
+
+`git-annex` needs to be installed both on the server and the client side.
+
+For Debian-like systems (e.g., Debian, Ubuntu) this can be achieved by running:
+
+```
+sudo apt-get update && sudo apt-get install git-annex
+```
+
+For RedHat-like systems (e.g., CentOS, RHEL) this can be achieved by running:
+
+```
+sudo yum install epel-release && sudo yum install git-annex
+```
+
+### Configuration for Omnibus packages
+
+For Omnibus GitLab packages, only one configuration setting is needed.
+The Omnibus package will internally set the correct options in all locations.
+
+1. In `/etc/gitlab/gitlab.rb` add the following line:
+
+ ```ruby
+ gitlab_shell['git_annex_enabled'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+### Configuration for installations from source
+
+There are 2 settings to enable git-annex on your GitLab server.
+
+One is located in `config/gitlab.yml` of the GitLab repository and the other
+one is located in `config.yml` of GitLab Shell.
+
+1. In `config/gitlab.yml` add or edit the following lines:
+
+ ```yaml
+ gitlab_shell:
+ git_annex_enabled: true
+ ```
+
+1. In `config.yml` of GitLab Shell add or edit the following lines:
+
+ ```yaml
+ git_annex_enabled: true
+ ```
+
+1. Save the files and [restart GitLab][] for the changes to take effect.
+
+## Using GitLab git-annex
+
+> **Note:**
+> Your Git remotes must be using the SSH protocol, not HTTP(S).
+
+Here is an example workflow of uploading a very large file and then checking it
+into your Git repository:
+
+```bash
+git clone git@example.com:group/project.git
+
+git annex init 'My Laptop' # initialize the annex project and give an optional description
+cp ~/tmp/debian.iso ./ # copy a large file into the current directory
+git annex add debian.iso # add the large file to git annex
+git commit -am "Add Debian iso" # commit the file metadata
+git annex sync --content # sync the Git repo and large file to the GitLab server
+```
+
+The output should look like this:
+
+```
+commit
+On branch master
+Your branch is ahead of 'origin/master' by 1 commit.
+ (use "git push" to publish your local commits)
+nothing to commit, working tree clean
+ok
+pull origin
+remote: Counting objects: 5, done.
+remote: Compressing objects: 100% (4/4), done.
+remote: Total 5 (delta 2), reused 0 (delta 0)
+Unpacking objects: 100% (5/5), done.
+From example.com:group/project
+ 497842b..5162f80 git-annex -> origin/git-annex
+ok
+(merging origin/git-annex into git-annex...)
+(recording state in git...)
+copy debian.iso (checking origin...) (to origin...)
+SHA256E-s26214400--8092b3d482fb1b7a5cf28c43bc1425c8f2d380e86869c0686c49aa7b0f086ab2.iso
+ 26,214,400 100% 638.88kB/s 0:00:40 (xfr#1, to-chk=0/1)
+ok
+pull origin
+ok
+(recording state in git...)
+push origin
+Counting objects: 15, done.
+Delta compression using up to 4 threads.
+Compressing objects: 100% (13/13), done.
+Writing objects: 100% (15/15), 1.64 KiB | 0 bytes/s, done.
+Total 15 (delta 1), reused 0 (delta 0)
+To example.com:group/project.git
+ * [new branch] git-annex -> synced/git-annex
+ * [new branch] master -> synced/master
+ok
+```
+
+Your files can be found in the `master` branch, but you'll notice that there
+are more branches created by the `annex sync` command.
+
+Git Annex will also create a new directory at `.git/annex/` and will record the
+tracked files in the `.git/config` file. The files you assign to be tracked
+with `git-annex` will not affect the existing `.git/config` records. The files
+are turned into symbolic links that point to data in `.git/annex/objects/`.
+
+The `debian.iso` file in the example will contain the symbolic link:
+
+```
+.git/annex/objects/ZW/1k/SHA256E-s82701--6384039733b5035b559efd5a2e25a493ab6e09aabfd5162cc03f6f0ec238429d.png/SHA256E-s82701--6384039733b5035b559efd5a2e25a493ab6e09aabfd5162cc03f6f0ec238429d.iso
+```
+
+Use `git annex info` to retrieve the information about the local copy of your
+repository.
+
+---
+
+Downloading a single large file is also very simple:
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+
+git annex sync # sync Git branches but not the large file
+git annex get debian.iso # download the large file
+```
+
+To download all files:
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+
+git annex sync --content # sync Git branches and download all the large files
+```
+
+By using `git-annex` without GitLab, anyone that can access the server can also
+access the files of all projects, but GitLab Annex ensures that you can only
+access files of projects you have access to (developer, maintainer, or owner role).
+
+## How it works
+
+Internally GitLab uses [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
+can cause `git-annex` to raise unpredicted warnings and errors.
+
+Consult the [Annex upgrade page][annex-upgrade] 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.
+
+Although there is no general guide for `git-annex` errors, there are a few tips
+on how to go around the warnings.
+
+### `git-annex-shell: Not a git-annex or gcrypt repository`
+
+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].
+
+One important thing to note is that despite the warning, the `sync` succeeds
+and the files are pushed to the GitLab repository.
+
+If you get hit by this, you can run the following command inside the repository
+that the warning was raised:
+
+```
+git config remote.origin.annex-ignore false
+```
+
+Consecutive runs of `git annex sync --content` **should not** produce this
+warning and the output should look like this:
+
+```
+commit ok
+pull origin
+ok
+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]: lfs/migrate_from_git_annex_to_git_lfs.html
+[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/index.md b/doc/administration/gitaly/index.md
index d5749427f6e..82283650070 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -89,6 +89,10 @@ your GitLab installation has three repository storages: `default`,
`storage1` and `storage2`. You can use as little as just one server with one
repository storage if desired.
+Note: **Note:** The token referred to throughout the Gitaly documentation is
+just an arbitrary password selected by the administrator. It is unrelated to
+tokens created for the GitLab API or other similar web API tokens.
+
### 1. Installation
First install Gitaly on each Gitaly server using either
@@ -142,11 +146,6 @@ the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gi
from an existing GitLab server to the Gitaly server. Without this shared secret,
Git operations in GitLab will result in an API error.
-NOTE: **Note:**
-In most or all cases, the storage paths below end in `/repositories` which is
-not the case with `path` in `git_data_dirs` of Omnibus GitLab installations.
-Check the directory layout on your Gitaly server to be sure.
-
**For Omnibus GitLab**
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -193,24 +192,26 @@ Check the directory layout on your Gitaly server to be sure.
On `gitaly1.internal`:
```
- gitaly['storage'] = [
- { 'name' => 'default' },
- { 'name' => 'storage1' },
- ]
+ git_data_dirs({
+ 'default' => {
+ 'path' => '/var/opt/gitlab/git-data'
+ },
+ 'storage1' => {
+ 'path' => '/mnt/gitlab/git-data'
+ },
+ })
```
On `gitaly2.internal`:
```
- gitaly['storage'] = [
- { 'name' => 'storage2' },
- ]
+ git_data_dirs({
+ 'storage2' => {
+ 'path' => '/srv/gitlab/git-data'
+ },
+ })
```
- NOTE: **Note:**
- In some cases, you'll have to set `path` for `gitaly['storage']` in the
- format `'path' => '/mnt/gitlab/<storage name>/repositories'`.
-
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
**For installations from source**
@@ -222,6 +223,11 @@ Check the directory layout on your Gitaly server to be sure.
[auth]
token = 'abc123secret'
+
+ [logging]
+ format = 'json'
+ level = 'info'
+ dir = '/var/log/gitaly'
```
1. Append the following to `/home/git/gitaly/config.toml` for each respective server:
@@ -231,9 +237,11 @@ Check the directory layout on your Gitaly server to be sure.
```toml
[[storage]]
name = 'default'
+ path = '/var/opt/gitlab/git-data/repositories'
[[storage]]
name = 'storage1'
+ path = '/mnt/gitlab/git-data/repositories'
```
On `gitaly2.internal`:
@@ -241,12 +249,9 @@ Check the directory layout on your Gitaly server to be sure.
```toml
[[storage]]
name = 'storage2'
+ path = '/srv/gitlab/git-data/repositories'
```
- NOTE: **Note:**
- In some cases, you'll have to set `path` for each `[[storage]]` in the
- format `path = '/mnt/gitlab/<storage name>/repositories'`.
-
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
### 4. Converting clients to use the Gitaly server
@@ -327,6 +332,8 @@ When you tail the Gitaly logs on your Gitaly server you should see requests
coming in. One sure way to trigger a Gitaly request is to clone a repository
from your GitLab server over HTTP.
+DANGER: **Danger:** If you have [custom server-side Git hooks](../custom_hooks.md#custom-server-side-git-hooks) configured, either per repository or globally, you must move these to the Gitaly node. If you have multiple Gitaly nodes, copy your custom hook(s) to all nodes.
+
### Disabling the Gitaly service in a cluster environment
If you are running Gitaly [as a remote
@@ -404,11 +411,11 @@ To configure Gitaly with TLS:
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on client node(s).
-1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
+1. On the Gitaly server, create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
```sh
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/
```
@@ -550,8 +557,11 @@ a few things that you need to do:
to eliminate the need for a shared authorized_keys file.
1. Configure [object storage for job artifacts](../job_artifacts.md#using-object-storage)
including [incremental logging](../job_logs.md#new-incremental-logging-architecture).
-1. Configure [object storage for LFS objects](../../workflow/lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
+1. Configure [object storage for LFS objects](../lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage).
1. Configure [object storage for uploads](../uploads.md#using-object-storage-core-only).
+1. Configure [object storage for Merge Request Diffs](../merge_request_diffs.md#using-object-storage).
+1. Configure [object storage for Packages](../packages/index.md#using-object-storage) (Optional Feature).
+1. Configure [object storage for Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (Optional Feature).
NOTE: **Note:**
One current feature of GitLab that still requires a shared directory (NFS) is
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 9038675a28f..83c9aa3f013 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -26,34 +26,42 @@ For this document, the following network topology is assumed:
graph TB
GitLab --> Gitaly;
GitLab --> Praefect;
- Praefect --> Preafect-Git-1;
- Praefect --> Preafect-Git-2;
- Praefect --> Preafect-Git-3;
+ Praefect --> Praefect-Gitaly-1;
+ Praefect --> Praefect-Gitaly-2;
+ Praefect --> Praefect-Gitaly-3;
```
Where `GitLab` is the collection of clients that can request Git operations.
-`Gitaly` is a Gitaly server before using Praefect. The Praefect node has two
+`Gitaly` is a Gitaly server before using Praefect. The Praefect node has three
storage nodes attached. Praefect itself doesn't store data, but connects to
-three Gitaly nodes, `Praefect-Git-1`, `Praefect-Git-2`, and `Praefect-Git-3`.
-There should be no knowledge other than with Praefect about the existence of
-the `Praefect-Git-X` nodes.
+three Gitaly nodes, `Praefect-Gitaly-1`, `Praefect-Gitaly-2`, and `Praefect-Gitaly-3`.
-### Setup
+Praefect may be enabled on its own node or can be run on the GitLab server.
+In the example below we will use a separate server, but the optimal configuration
+for Praefect is still being determined.
-In this setup guide, the Gitaly node will be added first, then Praefect, and
-lastly we update the GitLab configuration.
+Praefect will handle all Gitaly RPC requests to its child nodes. However, the child nodes
+will still need to communicate with the GitLab server via its internal API for authentication
+purposes.
-#### Gitaly
+### Setup
-In their own machine, configure the Gitaly server as described in the
-[gitaly documentation](index.md#3-gitaly-server-configuration).
+In this setup guide we will start by configuring Praefect, then its child
+Gitaly nodes, and lastly the GitLab server configuration.
#### Praefect
-Next, Praefect has to be enabled on its own node. Disable all other services,
-and add each Gitaly node that will be connected to Praefect. In the example below,
-the Gitaly nodes are named `praefect-git-X`. Note that one node is designated as
-primary, by setting the primary to `true`:
+On the Praefect node we disable all other services, including Gitaly. We list each
+Gitaly node that will be connected to Praefect under `praefect['storage_nodes']`.
+
+In the example below, the Gitaly nodes are named `praefect-gitaly-N`. Note that one
+node is designated as primary by setting the primary to `true`.
+
+`praefect['auth_token']` is the token used to authenticate with the GitLab server,
+just like `gitaly['auth_token']` is used for a standard Gitaly server.
+
+The `token` field under each storage listed in `praefect['storage_nodes']` is used
+to authenticate each child Gitaly node with Praefect.
```ruby
# /etc/gitlab/gitlab.rb
@@ -67,38 +75,111 @@ unicorn['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitaly['enable'] = false
+```
+
+##### Set up Praefect and its Gitaly nodes
+
+In the example below, the Gitaly nodes are named `praefect-git-X`. Note that one node is designated as
+primary, by setting the primary to `true`:
+
+```ruby
+# /etc/gitlab/gitlab.rb
+
+# Prevent database connections during 'gitlab-ctl reconfigure'
+gitlab_rails['rake_cache_clear'] = false
+gitlab_rails['auto_migrate'] = false
+
+praefect['enable'] = true
+
+# Make Praefect accept connections on all network interfaces. You must use
+# firewalls to restrict access to this address/port.
+praefect['listen_addr'] = '0.0.0.0:2305'
# virtual_storage_name must match the same storage name given to praefect in git_data_dirs
praefect['virtual_storage_name'] = 'praefect'
-praefect['auth_token'] = 'super_secret_abc'
-praefect['enable'] = true
-praefect['storage_nodes'] = [
- {
- 'storage' => 'praefect-git-1',
- 'address' => 'tcp://praefect-git-1.internal',
- 'token' => 'token1',
+
+# Authentication token to ensure only authorized servers can communicate with
+# Praefect server
+praefect['auth_token'] = 'praefect-token'
+praefect['storage_nodes'] = {
+ 'praefect-gitaly-1' => {
+ 'address' => 'tcp://praefect-git-1.internal:8075',
+ 'token' => 'praefect-gitaly-token',
'primary' => true
},
- {
- 'storage' => 'praefect-git-2',
- 'address' => 'tcp://praefect-git-2.internal',
- 'token' => 'token2'
+ 'praefect-gitaly-2' => {
+ 'address' => 'tcp://praefect-git-2.internal:8075',
+ 'token' => 'praefect-gitaly-token'
},
- {
- 'storage' => 'praefect-git-3',
- 'address' => 'tcp://praefect-git-3.internal',
- 'token' => 'token3'
+ 'praefect-gitaly-3' => {
+ 'address' => 'tcp://praefect-git-3.internal:8075',
+ 'token' => 'praefect-gitaly-token'
}
-]
+}
```
Save the file and [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+#### Gitaly
+
+Next we will configure each Gitaly server assigned to Praefect. Configuration for these
+is the same as a normal standalone Gitaly server, except that we use storage names and
+auth tokens from Praefect instead of GitLab.
+
+Below is an example configuration for `praefect-gitaly-1`, the only difference for the
+other Gitaly nodes is the storage name under `git_data_dirs`.
+
+Note that `gitaly['auth_token']` matches the `token` value listed under `praefect['storage_nodes']`
+on the Praefect node.
+
+```ruby
+# /etc/gitlab/gitlab.rb
+
+# Avoid running unnecessary services on the Gitaly server
+postgresql['enable'] = false
+redis['enable'] = false
+nginx['enable'] = false
+prometheus['enable'] = false
+unicorn['enable'] = false
+sidekiq['enable'] = false
+gitlab_workhorse['enable'] = false
+
+# Prevent database connections during 'gitlab-ctl reconfigure'
+gitlab_rails['rake_cache_clear'] = false
+gitlab_rails['auto_migrate'] = false
+
+# Configure the gitlab-shell API callback URL. Without this, `git push` will
+# fail. This can be your 'front door' GitLab URL or an internal load
+# balancer.
+# 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'] = 'praefect-gitaly-token'
+
+# 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
+gitaly['listen_addr'] = "0.0.0.0:8075"
+
+git_data_dirs({
+ "praefect-gitaly-1" => {
+ "path" => "/var/opt/gitlab/git-data"
+ }
+})
+```
+
+Note that just as with a standard Gitaly server, `/etc/gitlab/gitlab-secrets.json` must
+be copied from the GitLab server to the Gitaly node for authentication purposes.
+
+For more information on Gitaly server configuration, see our [gitaly documentation](index.md#3-gitaly-server-configuration).
+
#### GitLab
When Praefect is running, it should be exposed as a storage to GitLab. This
is done through setting the `git_data_dirs`. Assuming the default storage
-configuration is used, there would be two storages available to GitLab:
+is present, there should be two storages available to GitLab:
```ruby
git_data_dirs({
@@ -109,6 +190,28 @@ git_data_dirs({
"gitaly_address" => "tcp://praefect.internal:2305"
}
})
+
+gitlab_rails['gitaly_token'] = 'praefect-token'
```
+Note that the storage name used is the same as the `praefect['virtual_storage_name']` set
+on the Praefect node.
+
+Also, the `gitlab_rails['gitaly_token']` matches the value of `praefect['auth_token']`
+on Praefect.
+
Restart GitLab using `gitlab-ctl restart` on the GitLab node.
+
+### Testing Praefect
+
+To test Praefect, first set it as the default storage node for new projects
+using **Admin Area > Settings > Repository > Repository storage**. Next,
+create a new project and check the "Initialize repository with a README" box.
+
+If you receive a 503 error, check `/var/log/gitlab/gitlab-rails/production.log`.
+A `GRPC::Unavailable (14:failed to connect to all addresses)` error indicates
+that GitLab was unable to connect to Praefect.
+
+If the project is created but the README is not, then ensure that the
+`/etc/gitlab/gitlab-secrets.json` file from the GitLab server has been copied
+to the Gitaly servers.
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 199944a160c..7f0b4056acc 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -38,14 +38,17 @@ The following components need to be considered for a scaled or highly-available
environment. In many cases, components can be combined on the same nodes to reduce
complexity.
-- Unicorn/Workhorse - Web-requests (UI, API, Git over HTTP)
+- GitLab application nodes (Unicorn / Puma, Workhorse) - Web-requests (UI, API, Git over HTTP)
- Sidekiq - Asynchronous/Background jobs
- PostgreSQL - Database
- Consul - Database service discovery and health checks/failover
- PgBouncer - Database pool manager
- Redis - Key/Value store (User sessions, cache, queue for Sidekiq)
- Sentinel - Redis health check/failover manager
-- Gitaly - Provides high-level RPC access to Git repositories
+- Gitaly - Provides high-level storage and RPC access to Git repositories
+- S3 Object Storage service[^3] and / or NFS storage servers[^4] for entities such as Uploads, Artifacts, LFS Objects, etc...
+- Load Balancer[^2] - Main entry point and handles load balancing for the GitLab application nodes.
+- Monitor - Prometheus and Grafana monitoring with auto discovery.
## Scalable Architecture Examples
@@ -67,8 +70,10 @@ larger one.
- 1 PostgreSQL node
- 1 Redis node
-- 1 NFS/Gitaly storage server
-- 2 or more GitLab application nodes (Unicorn, Workhorse, Sidekiq)
+- 1 Gitaly node
+- 1 or more Object Storage services[^3] and / or NFS storage server[^4]
+- 2 or more GitLab application nodes (Unicorn / Puma, Workhorse, Sidekiq)
+- 1 or more Load Balancer nodes[^2]
- 1 Monitoring node (Prometheus, Grafana)
#### Installation Instructions
@@ -77,10 +82,12 @@ Complete the following installation steps in order. A link at the end of each
section will bring you back to the Scalable Architecture Examples section so
you can continue with the next step.
-1. [PostgreSQL](database.md#postgresql-in-a-scaled-environment)
+1. [PostgreSQL](database.md#postgresql-in-a-scaled-environment) with [PGBouncer](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html)
1. [Redis](redis.md#redis-in-a-scaled-environment)
-1. [Gitaly](gitaly.md) (recommended) or [NFS](nfs.md)
+1. [Gitaly](gitaly.md) (recommended) and / or [NFS](nfs.md)[^4]
1. [GitLab application nodes](gitlab.md)
+ - With [Object Storage service enabled](../gitaly/index.md#eliminating-nfs-altogether)[^3]
+1. [Load Balancer(s)](load_balancer.md)[^2]
1. [Monitoring node (Prometheus and Grafana)](monitoring_node.md)
### Full Scaling
@@ -91,11 +98,13 @@ is split into separate Sidekiq and Unicorn/Workhorse nodes. One indication that
this architecture is required is if Sidekiq queues begin to periodically increase
in size, indicating that there is contention or there are not enough resources.
-- 1 PostgreSQL node
-- 1 Redis node
-- 2 or more NFS/Gitaly storage servers
+- 1 or more PostgreSQL nodes
+- 1 or more Redis nodes
+- 1 or more Gitaly storage servers
+- 1 or more Object Storage services[^3] and / or NFS storage server[^4]
- 2 or more Sidekiq nodes
-- 2 or more GitLab application nodes (Unicorn, Workhorse)
+- 2 or more GitLab application nodes (Unicorn / Puma, Workhorse, Sidekiq)
+- 1 or more Load Balancer nodes[^2]
- 1 Monitoring node (Prometheus, Grafana)
## High Availability Architecture Examples
@@ -114,10 +123,10 @@ This may lead to the other nodes believing a failure has occurred and initiating
automated failover. Isolating Redis and Consul from the services they monitor
reduces the chances of a false positive that a failure has occurred.
-The examples below do not really address high availability of NFS. Some enterprises
-have access to NFS appliances that manage availability. This is the best case
-scenario. In the future, GitLab may offer a more user-friendly solution to
-[GitLab HA Storage](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2472).
+The examples below do not address high availability of NFS for objects. We recommend a
+S3 Object Storage service[^3] is used where possible over NFS but it's still required in
+certain cases[^4]. Where NFS is to be used some enterprises have access to NFS appliances
+that manage availability and this would be best case scenario.
There are many options in between each of these examples. Work with GitLab Support
to understand the best starting point for your workload and adapt from there.
@@ -138,8 +147,10 @@ the contention.
- 3 PostgreSQL nodes
- 2 Redis nodes
- 3 Consul/Sentinel nodes
-- 2 or more GitLab application nodes (Unicorn, Workhorse, Sidekiq, PgBouncer)
-- 1 NFS/Gitaly server
+- 2 or more GitLab application nodes (Unicorn / Puma, Workhorse, Sidekiq)
+- 1 Gitaly storage servers
+- 1 Object Storage service[^3] and / or NFS storage server[^4]
+- 1 or more Load Balancer nodes[^2]
- 1 Monitoring node (Prometheus, Grafana)
![Horizontal architecture diagram](img/horizontal.png)
@@ -156,8 +167,10 @@ contention due to certain workloads.
- 2 Redis nodes
- 3 Consul/Sentinel nodes
- 2 or more Sidekiq nodes
-- 2 or more GitLab application nodes (Unicorn, Workhorse)
-- 1 or more NFS/Gitaly servers
+- 2 or more GitLab application nodes (Unicorn / Puma, Workhorse, Sidekiq)
+- 1 Gitaly storage servers
+- 1 Object Storage service[^3] and / or NFS storage server[^4]
+- 1 or more Load Balancer nodes[^2]
- 1 Monitoring node (Prometheus, Grafana)
![Hybrid architecture diagram](img/hybrid.png)
@@ -169,6 +182,7 @@ the basis of the GitLab.com architecture. While this scales well it also comes
with the added complexity of many more nodes to configure, manage, and monitor.
- 3 PostgreSQL nodes
+- 1 or more PgBouncer nodes (with associated internal load balancers)
- 4 or more Redis nodes (2 separate clusters for persistent and cache data)
- 3 Consul nodes
- 3 Sentinel nodes
@@ -177,120 +191,116 @@ with the added complexity of many more nodes to configure, manage, and monitor.
- 2 or more Git nodes (Git over SSH/Git over HTTP)
- 2 or more API nodes (All requests to `/api`)
- 2 or more Web nodes (All other web requests)
-- 2 or more NFS/Gitaly servers
+- 2 or more Gitaly storage servers
+- 1 or more Object Storage services[^3] and / or NFS storage servers[^4]
+- 1 or more Load Balancer nodes[^2]
- 1 Monitoring node (Prometheus, Grafana)
![Fully Distributed architecture diagram](img/fully-distributed.png)
-The following pages outline the steps necessary to configure each component
-separately:
+## Reference Architecture Examples
-1. [Configure the database](database.md)
-1. [Configure Redis](redis.md)
- 1. [Configure Redis for GitLab source installations](redis_source.md)
-1. [Configure NFS](nfs.md)
- 1. [NFS Client and Host setup](nfs_host_client_setup.md)
-1. [Configure the GitLab application servers](gitlab.md)
-1. [Configure the load balancers](load_balancer.md)
-1. [Monitoring node (Prometheus and Grafana)](monitoring_node.md)
+The Support and Quality teams build, performance test, and validate Reference
+Architectures that support set large numbers of users. The specifications below are a
+representation of this work so far and may be adjusted in the future based on
+additional testing and iteration.
-## Reference Architecture Examples
+The architectures have been tested with specific coded workloads. The throughputs
+used for testing are calculated based on sample customer data. We test each endpoint
+type with the following number of requests per second (RPS) per 1000 users:
-These reference architecture examples rely on the general rule that approximately 2 requests per second (RPS) of load is generated for every 100 users.
+- API: 20 RPS
+- Web: 2 RPS
+- Git: 2 RPS
-The specifications here were performance tested against a specific coded
-workload. Your exact needs may be more, depending on your workload. Your
+Note that your exact needs may be more, depending on your workload. 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.
### 10,000 User Configuration
- **Supported Users (approximate):** 10,000
-- **RPS:** 200 requests per second
+- **Test RPS Rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
- **Known Issues:** While validating the reference architecture, slow API endpoints
were discovered. For details, see the related issues list in
[this issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/64335).
-The Support and Quality teams built, performance tested, and validated an
-environment that supports about 10,000 users. The specifications below are a
-representation of the work so far. The specifications may be adjusted in the
-future based on additional testing and iteration.
-
-| Service | Configuration | GCP type |
-| ------------------------------|-------------------------|----------------|
-| 3 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
-| 3 PostgreSQL | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 16 vCPU, 60GB Memory | n1-standard-16 |
-| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
-| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
-| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Service | Nodes | Configuration | GCP type |
+| ----------------------------|-------|-----------------------|---------------|
+| GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Gitaly <br> - Gitaly Ruby workers on each node set to 20% of available CPUs | X[^1] . | 16 vCPU, 60GB Memory | n1-standard-16 |
+| Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Redis Persistent + Sentinel | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| NFS Server[^4] . | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| S3 Object Storage[^3] . | - | - | - |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| External load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Internal load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+
+NOTE: **Note:** Memory values are given directly by GCP machine sizes. On different cloud
+vendors a best effort like for like can be used.
### 25,000 User Configuration
- **Supported Users (approximate):** 25,000
-- **RPS:** 500 requests per second
+- **Test RPS Rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
- **Known Issues:** The slow API endpoints that were discovered during testing
the 10,000 user architecture also affect the 25,000 user architecture. For
details, see the related issues list in
[this issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/64335).
-The GitLab Support and Quality teams built, performance tested, and validated an
-environment that supports around 25,000 users. The specifications below are a
-representation of the work so far. The specifications may be adjusted in the
-future based on additional testing and iteration.
-
-NOTE: **Note:** The specifications here were performance tested against a
-specific coded workload. Your exact needs may be more, depending on your
-workload. 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.
-
-| Service | Configuration | GCP type |
-| ------------------------------|-------------------------|----------------|
-| 7 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
-| 3 PostgreSQL | 8 vCPU, 30GB Memory | n1-standard-8 |
-| 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 120GB Memory | n1-standard-32 |
-| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
-| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
-| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Service | Nodes | Configuration | GCP type |
+| ----------------------------|-------|-----------------------|---------------|
+| GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 7 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Gitaly <br> - Gitaly Ruby workers on each node set to 20% of available CPUs | X[^1] . | 32 vCPU, 120GB Memory | n1-standard-32 |
+| Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Redis Persistent + Sentinel | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| NFS Server[^4] . | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| S3 Object Storage[^3] . | - | - | - |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| External load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Internal load balancing node[^2] . | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+
+NOTE: **Note:** Memory values are given directly by GCP machine sizes. On different cloud
+vendors a best effort like for like can be used.
### 50,000 User Configuration
- **Supported Users (approximate):** 50,000
-- **RPS:** 1,000 requests per second
+- **Test RPS Rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
- **Status:** Work-in-progress
- **Related Issue:** See the [related issue](https://gitlab.com/gitlab-org/quality/performance/issues/66) for more information.
-The Support and Quality teams are in the process of building and performance
-testing an environment that will support around 50,000 users. The specifications
-below are a very rough work-in-progress representation of the work so far. The
-Quality team will be certifying this environment in late 2019. The
-specifications may be adjusted prior to certification based on performance
-testing.
-
-| Service | Configuration | GCP type |
-| ------------------------------|-------------------------|----------------|
-| 15 GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
-| 3 PostgreSQL | 8 vCPU, 30GB Memory | n1-standard-8 |
-| 1 PgBouncer | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| X Gitaly[^1] <br> - Gitaly Ruby workers on each node set to 90% of available CPUs with 16 threads | 64 vCPU, 240GB Memory | n1-standard-64 |
-| 3 Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Redis Persistent + Sentinel | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 4 Sidekiq | 4 vCPU, 15GB Memory | n1-standard-4 |
-| 3 Consul | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
-| 1 NFS Server | 16 vCPU, 14.4GB Memory | n1-highcpu-16 |
-| 1 Monitoring node | 4 CPU, 3.6GB Memory | n1-highcpu-4 |
-| 1 Load Balancing node[^2] . | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+NOTE: **Note:** This architecture is a work-in-progress of the work so far. The
+Quality team will be certifying this environment in late 2019. The specifications
+may be adjusted prior to certification based on performance testing.
+
+| Service | Nodes | Configuration | GCP type |
+| ----------------------------|-------|-----------------------|---------------|
+| GitLab Rails <br> - Puma workers on each node set to 90% of available CPUs with 16 threads | 15 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 |
+| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Gitaly <br> - Gitaly Ruby workers on each node set to 20% of available CPUs | X[^1] . | 64 vCPU, 240GB Memory | n1-standard-64 |
+| Redis Cache + Sentinel <br> - Cache maxmemory set to 90% of available memory | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Redis Persistent + Sentinel | 3 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| NFS Server[^4] . | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| S3 Object Storage[^3] . | - | - | - |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 |
+| External load balancing node[^2] . | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 |
+| Internal load balancing node[^2] . | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 |
+
+NOTE: **Note:** Memory values are given directly by GCP machine sizes. On different cloud
+vendors a best effort like for like can be used.
[^1]: Gitaly node requirements are dependent on customer data. We recommend 2
nodes as an absolute minimum for performance at the 10,000 and 25,000 user
@@ -298,5 +308,19 @@ testing.
additional nodes should be considered in conjunction with a review of
project counts and sizes.
-[^2]: HAProxy is the only tested and recommended load balancer. Additional
- options may be supported in the future.
+[^2]: 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 here but be aware these aren't validated.
+
+[^3]: For data objects such as LFS, Uploads, Artifacts, etc... We recommend a S3 Object Storage
+ where possible over NFS due to better performance and availability. Several types of objects
+ are supported for S3 storage - [Job artifacts](../job_artifacts.md#using-object-storage),
+ [LFS](../lfs/lfs_administration.md#storing-lfs-objects-in-remote-object-storage),
+ [Uploads](../uploads.md#using-object-storage-core-only),
+ [Merge Request Diffs](../merge_request_diffs.md#using-object-storage),
+ [Packages](../packages/index.md#using-object-storage) (Optional Feature),
+ [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (Optional Feature).
+
+[^4]: NFS storage server is still required for [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196)
+ and optionally for CI Job Incremental Logging
+ ([can be switched to use Redis instead](https://docs.gitlab.com/ee/administration/job_logs.html#new-incremental-logging-architecture)).
diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md
index b01419200cc..392b9b76c31 100644
--- a/doc/administration/high_availability/consul.md
+++ b/doc/administration/high_availability/consul.md
@@ -102,6 +102,23 @@ To be safe, we recommend you only restart one server agent at a time to ensure t
For larger clusters, it is possible to restart multiple agents at a time. See the [Consul consensus document](https://www.consul.io/docs/internals/consensus.html#deployment-table) for how many failures it can tolerate. This will be the number of simulateneous restarts it can sustain.
+## Upgrades for bundled Consul
+
+Nodes running GitLab-bundled Consul should be:
+
+- Members of a healthy cluster prior to upgrading the GitLab Omnibus package.
+- Upgraded one node at a time.
+
+NOTE: **NOTE:**
+Running `curl http://127.0.0.1:8500/v1/health/state/critical` from any Consul node will identify existing health issues in the cluster. The command will return an empty array if the cluster is healthy.
+
+Consul clusters communicate using the raft protocol. If the current leader goes offline, there needs to be a leader election. A leader node must exist to facilitate synchronization across the cluster. If too many nodes go offline at the same time, the cluster will lose quorum and not elect a leader due to [broken consensus](https://www.consul.io/docs/internals/consensus.html).
+
+Consult the [troubleshooting section](#troubleshooting) if the cluster is not able to recover after the upgrade. The [outage recovery](#outage-recovery) may be of particular interest.
+
+NOTE: **NOTE:**
+GitLab only uses Consul to store transient data that is easily regenerated. If the bundled Consul was not used by any process other than GitLab itself, then [rebuilding the cluster from scratch](#recreate-from-scratch) is fine.
+
## Troubleshooting
### Consul server agents unable to communicate
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index a50cc0cbd03..02684f575d4 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -135,7 +135,8 @@ The recommended configuration for a PostgreSQL HA requires:
- `repmgrd` - A service to monitor, and handle failover in case of a failure
- `Consul` agent - Used for service discovery, to alert other nodes when failover occurs
- A minimum of three `Consul` server nodes
-- A minimum of one `pgbouncer` service node
+- A minimum of one `pgbouncer` service node, but it's recommended to have one per database node
+ - An internal load balancer (TCP) is required when there is more than one `pgbouncer` service node
You also need to take into consideration the underlying network topology,
making sure you have redundant connectivity between all Database and GitLab instances,
@@ -155,13 +156,13 @@ Database nodes run two services with PostgreSQL:
On failure, the old master node is automatically evicted from the cluster, and should be rejoined manually once recovered.
- Consul. Monitors the status of each node in the database cluster and tracks its health in a service definition on the Consul cluster.
-Alongside PgBouncer, there is a Consul agent that watches the status of the PostgreSQL service. If that status changes, Consul runs a script which updates the configuration and reloads PgBouncer
+Alongside each PgBouncer, there is a Consul agent that watches the status of the PostgreSQL service. If that status changes, Consul runs a script which updates the configuration and reloads PgBouncer
##### Connection flow
Each service in the package comes with a set of [default ports](https://docs.gitlab.com/omnibus/package-information/defaults.html#ports). You may need to make specific firewall rules for the connections listed below:
-- Application servers connect to [PgBouncer default port](https://docs.gitlab.com/omnibus/package-information/defaults.html#pgbouncer)
+- Application servers connect to either PgBouncer directly via its [default port](https://docs.gitlab.com/omnibus/package-information/defaults.html#pgbouncer) or via a configured Internal Load Balancer (TCP) that serves multiple PgBouncers.
- PgBouncer connects to the primary database servers [PostgreSQL default port](https://docs.gitlab.com/omnibus/package-information/defaults.html#postgresql)
- Repmgr connects to the database servers [PostgreSQL default port](https://docs.gitlab.com/omnibus/package-information/defaults.html#postgresql)
- Postgres secondaries connect to the primary database servers [PostgreSQL default port](https://docs.gitlab.com/omnibus/package-information/defaults.html#postgresql)
@@ -499,7 +500,7 @@ attributes set, but the following need to be set.
# Disable PostgreSQL on the application node
postgresql['enable'] = false
- gitlab_rails['db_host'] = 'PGBOUNCER_NODE'
+ gitlab_rails['db_host'] = 'PGBOUNCER_NODE' or 'INTERNAL_LOAD_BALANCER'
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = 'POSTGRESQL_USER_PASSWORD'
gitlab_rails['auto_migrate'] = false
@@ -533,7 +534,8 @@ Here we'll show you some fully expanded example configurations.
##### Example recommended setup
-This example uses 3 Consul servers, 3 PostgreSQL servers, and 1 application node.
+This example uses 3 Consul servers, 3 PgBouncer servers (with associated internal load balancer),
+3 PostgreSQL servers, and 1 application node.
We start with all servers on the same 10.6.0.0/16 private network range, they
can connect to each freely other on those addresses.
@@ -543,14 +545,16 @@ Here is a list and description of each machine and the assigned IP:
- `10.6.0.11`: Consul 1
- `10.6.0.12`: Consul 2
- `10.6.0.13`: Consul 3
-- `10.6.0.21`: PostgreSQL master
-- `10.6.0.22`: PostgreSQL secondary
-- `10.6.0.23`: PostgreSQL secondary
-- `10.6.0.31`: GitLab application
-
-All passwords are set to `toomanysecrets`, please do not use this password or derived hashes.
+- `10.6.0.20`: Internal Load Balancer
+- `10.6.0.21`: PgBouncer 1
+- `10.6.0.22`: PgBouncer 2
+- `10.6.0.23`: PgBouncer 3
+- `10.6.0.31`: PostgreSQL master
+- `10.6.0.32`: PostgreSQL secondary
+- `10.6.0.33`: PostgreSQL secondary
+- `10.6.0.41`: GitLab application
-The external_url for GitLab is `http://gitlab.example.com`
+All passwords are set to `toomanysecrets`, please do not use this password or derived hashes and the external_url for GitLab is `http://gitlab.example.com`.
Please note that after the initial configuration, if a failover occurs, the PostgresSQL master will change to one of the available secondaries until it is failed back.
@@ -566,10 +570,45 @@ consul['configuration'] = {
server: true,
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
}
+consul['monitoring_service_discovery'] = true
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+##### Example recommended setup for PgBouncer servers
+
+On each server edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+# Disable all components except Pgbouncer and Consul agent
+roles ['pgbouncer_role']
+
+# Configure PgBouncer
+pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
+
+pgbouncer['users'] = {
+ 'gitlab-consul': {
+ password: '5e0e3263571e3704ad655076301d6ebe'
+ },
+ 'pgbouncer': {
+ password: '771a8625958a529132abe6f1a4acb19c'
+ }
+}
+
+consul['watchers'] = %w(postgresql)
+consul['enable'] = true
+consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
+}
+consul['monitoring_service_discovery'] = true
```
[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+##### Internal load balancer setup
+
+An internal load balancer (TCP) is then required to be setup to serve each PgBouncer node (in this example on the IP of `10.6.0.20`). An example of how to do this can be found in the [PgBouncer Configure Internal Load Balancer](pgbouncer.md#configure-the-internal-load-balancer) section.
+
##### Example recommended setup for PostgreSQL servers
###### Primary node
@@ -589,9 +628,6 @@ postgresql['shared_preload_libraries'] = 'repmgr_funcs'
# Disable automatic database migrations
gitlab_rails['auto_migrate'] = false
-# Configure the Consul agent
-consul['services'] = %w(postgresql)
-
postgresql['pgbouncer_user_password'] = '771a8625958a529132abe6f1a4acb19c'
postgresql['sql_user_password'] = '450409b85a0223a214b5fb1484f34d0f'
postgresql['max_wal_senders'] = 4
@@ -599,9 +635,13 @@ postgresql['max_wal_senders'] = 4
postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
repmgr['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
+# Configure the Consul agent
+consul['services'] = %w(postgresql)
+consul['enable'] = true
consul['configuration'] = {
retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
}
+consul['monitoring_service_discovery'] = true
```
[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
@@ -626,18 +666,15 @@ On the server edit `/etc/gitlab/gitlab.rb`:
```ruby
external_url 'http://gitlab.example.com'
-gitlab_rails['db_host'] = '127.0.0.1'
+gitlab_rails['db_host'] = '10.6.0.20' # Internal Load Balancer for PgBouncer nodes
gitlab_rails['db_port'] = 6432
gitlab_rails['db_password'] = 'toomanysecrets'
gitlab_rails['auto_migrate'] = false
postgresql['enable'] = false
-pgbouncer['enable'] = true
+pgbouncer['enable'] = false
consul['enable'] = true
-# Configure PgBouncer
-pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
-
# Configure Consul agent
consul['watchers'] = %w(postgresql)
@@ -661,7 +698,7 @@ consul['configuration'] = {
After deploying the configuration follow these steps:
-1. On `10.6.0.21`, our primary database
+1. On `10.6.0.31`, our primary database
Enable the `pg_trgm` extension
@@ -673,7 +710,7 @@ After deploying the configuration follow these steps:
CREATE EXTENSION pg_trgm;
```
-1. On `10.6.0.22`, our first standby database
+1. On `10.6.0.32`, our first standby database
Make this node a standby of the primary
@@ -681,7 +718,7 @@ After deploying the configuration follow these steps:
gitlab-ctl repmgr standby setup 10.6.0.21
```
-1. On `10.6.0.23`, our second standby database
+1. On `10.6.0.33`, our second standby database
Make this node a standby of the primary
@@ -689,7 +726,7 @@ After deploying the configuration follow these steps:
gitlab-ctl repmgr standby setup 10.6.0.21
```
-1. On `10.6.0.31`, our application server
+1. On `10.6.0.41`, our application server
Set `gitlab-consul` user's PgBouncer password to `toomanysecrets`
@@ -705,7 +742,7 @@ After deploying the configuration follow these steps:
#### Example minimal setup
-This example uses 3 PostgreSQL servers, and 1 application node.
+This example uses 3 PostgreSQL servers, and 1 application node (with PgBouncer setup alongside).
It differs from the [recommended setup](#example-recommended-setup) by moving the Consul servers into the same servers we use for PostgreSQL.
The trade-off is between reducing server counts, against the increased operational complexity of needing to deal with PostgreSQL [failover](#failover-procedure) and [restore](#restore-procedure) procedures in addition to [Consul outage recovery](consul.md#outage-recovery) on the same set of machines.
diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md
index e7479ad1ecb..09b33c3554a 100644
--- a/doc/administration/high_availability/pgbouncer.md
+++ b/doc/administration/high_availability/pgbouncer.md
@@ -4,13 +4,9 @@ type: reference
# Working with the bundle PgBouncer service
-As part of its High Availability stack, GitLab Premium includes a bundled version of [PgBouncer](https://pgbouncer.github.io/) that can be managed through `/etc/gitlab/gitlab.rb`.
+As part of its High Availability stack, GitLab Premium includes a bundled version of [PgBouncer](https://pgbouncer.github.io/) that can be managed through `/etc/gitlab/gitlab.rb`. PgBouncer is used to seamlessly migrate database connections between servers in a failover scenario. Additionally, it can be used in a non-HA setup to pool connections, speeding up response time while reducing resource usage.
-In a High Availability setup, PgBouncer is used to seamlessly migrate database connections between servers in a failover scenario.
-
-Additionally, it can be used in a non-HA setup to pool connections, speeding up response time while reducing resource usage.
-
-It is recommended to run PgBouncer alongside the `gitlab-rails` service, or on its own dedicated node in a cluster.
+In a HA setup, it's recommended to run a PgBouncer node separately for each database node with an internal load balancer (TCP) serving each accordingly.
## Operations
@@ -18,7 +14,7 @@ It is recommended to run PgBouncer alongside the `gitlab-rails` service, or on i
1. Make sure you collect [`CONSUL_SERVER_NODES`](database.md#consul-information), [`CONSUL_PASSWORD_HASH`](database.md#consul-information), and [`PGBOUNCER_PASSWORD_HASH`](database.md#pgbouncer-information) before executing the next step.
-1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+1. One each node, edit the `/etc/gitlab/gitlab.rb` config file and replace values noted in the `# START user configuration` section as below:
```ruby
# Disable all components except PgBouncer and Consul agent
@@ -67,7 +63,7 @@ It is recommended to run PgBouncer alongside the `gitlab-rails` service, or on i
#### PgBouncer Checkpoint
-1. Ensure the node is talking to the current master:
+1. Ensure each node is talking to the current master:
```sh
gitlab-ctl pgb-console # You will be prompted for PGBOUNCER_PASSWORD
@@ -100,6 +96,41 @@ It is recommended to run PgBouncer alongside the `gitlab-rails` service, or on i
(2 rows)
```
+#### Configure the internal load balancer
+
+If you're running more than one PgBouncer node as recommended, then at this time you'll need to set up a TCP internal load balancer to serve each correctly. This can be done with any reputable TCP load balancer.
+
+As an example here's how you could do it with [HAProxy](https://www.haproxy.org/):
+
+```
+global
+ log /dev/log local0
+ log localhost local1 notice
+ log stdout format raw local0
+
+defaults
+ log global
+ default-server inter 10s fall 3 rise 2
+ balance leastconn
+
+frontend internal-pgbouncer-tcp-in
+ bind *:6432
+ mode tcp
+ option tcplog
+
+ default_backend pgbouncer
+
+backend pgbouncer
+ mode tcp
+ option tcp-check
+
+ server pgbouncer1 <ip>:6432 check
+ server pgbouncer2 <ip>:6432 check
+ server pgbouncer3 <ip>:6432 check
+```
+
+Refer to your preferred Load Balancer's documentation for further guidance.
+
### Running PgBouncer as part of a non-HA GitLab installation
1. Generate PGBOUNCER_USER_PASSWORD_HASH with the command `gitlab-ctl pg-password-md5 pgbouncer`
@@ -177,7 +208,7 @@ If you enable Monitoring, it must be enabled on **all** PgBouncer servers.
#### Administrative console
-As part of Omnibus GitLab, we provide a command `gitlab-ctl pgb-console` to automatically connect to the PgBouncer administrative console. Please see the [PgBouncer documentation](https://pgbouncer.github.io/usage.html#admin-console) for detailed instructions on how to interact with the console.
+As part of Omnibus GitLab, we provide a command `gitlab-ctl pgb-console` to automatically connect to the PgBouncer administrative console. Please see the [PgBouncer documentation](https://www.pgbouncer.org/usage.html#admin-console) for detailed instructions on how to interact with the console.
To start a session, run
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index ba4599e5bcd..72968cfed56 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -491,7 +491,7 @@ multiple machines with the Sentinel daemon.
1. **You can omit this step if the Sentinels will be hosted in the same node as
the other Redis instances.**
- [Download/install](https://about.gitlab.com/downloads-ee) the
+ [Download/install](https://about.gitlab.com/install/) the
Omnibus GitLab Enterprise Edition package using **steps 1 and 2** from the
GitLab downloads page.
- Make sure you select the correct Omnibus package, with the same version
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 88cf702cf0e..a0360f1d252 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -7,7 +7,7 @@ GitLab has several features based on receiving incoming emails:
- [New issue by email](../user/project/issues/managing_issues.md#new-issue-via-email):
allow GitLab users to create a new issue by sending an email to a
user-specific email address.
-- [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email):
+- [New merge request by email](../user/project/merge_requests/creating_merge_requests.md#create-new-merge-requests-by-email):
allow GitLab users to create a new merge request by sending an email to a
user-specific email address.
- [Service Desk](../user/project/service_desk.md): provide e-mail support to
@@ -79,7 +79,7 @@ email address in order to sign up.
If you also host a public-facing GitLab instance at `hooli.com` and set your
incoming email domain to `hooli.com`, an attacker could abuse the "Create new
issue by email" or
-"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)"
+"[Create new merge request by email](../user/project/merge_requests/creating_merge_requests.md#create-new-merge-requests-by-email)"
features by using a project's unique address as the email when signing up for
Slack, which would send a confirmation email, which would create a new issue or
merge request on the project owned by the attacker, allowing them to click the
diff --git a/doc/administration/index.md b/doc/administration/index.md
index f90b9b2c7d5..bf21347fb99 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -43,7 +43,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
### Configuring GitLab
-- [Adjust your instance's timezone](../workflow/timezone.md): Customize the default time zone of GitLab.
+- [Adjust your instance's timezone](timezone.md): Customize the default time zone of GitLab.
- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
- [Security](../security/README.md): Learn what you can do to further secure your GitLab instance.
- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
@@ -51,7 +51,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Polling](polling.md): Configure how often the GitLab UI polls for updates.
- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
- [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on [source installations](../install/installation.md#installation-from-source).
-- [Uploads configuration](uploads.md): Configure GitLab uploads storage.
+- [Uploads administration](uploads.md): Configure GitLab uploads storage.
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
- [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code.
- [Enforcing Terms of Service](../user/admin_area/settings/terms.md)
@@ -68,11 +68,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
#### Customizing GitLab's appearance
-- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
-- [Favicon](../customization/favicon.md): Change the default favicon to your own logo.
-- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
-- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
-- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
+- [Header logo](../user/admin_area/appearance.md#navigation-bar): Change the logo on all pages and email headers.
+- [Favicon](../user/admin_area/appearance.md#favicon): Change the default favicon to your own logo.
+- [Branded login page](../user/admin_area/appearance.md#sign-in--sign-up-pages): Customize the login page with your own logo, title, and description.
+- ["New Project" page](../user/admin_area/appearance.md#new-project-pages): Customize the text to be displayed on the page that opens whenever your users create a new project.
- [Additional custom email text](../user/admin_area/settings/email.md#custom-additional-text-premium-only): Add additional custom text to emails sent from GitLab. **(PREMIUM ONLY)**
### Maintaining GitLab
@@ -105,7 +104,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
## User settings and permissions
- [Creating users](../user/profile/account/create_accounts.md): Create users manually or through authentication integrations.
-- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
+- [Libravatar](libravatar.md): Use Libravatar instead of Gravatar for user avatars.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
- [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers.
@@ -120,7 +119,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Auditor users](auditor_users.md): Users with read-only access to all projects, groups, and other resources on the GitLab instance. **(PREMIUM ONLY)**
- [Incoming email](incoming_email.md): Configure incoming emails to allow
users to [reply by email](reply_by_email.md), create [issues by email](../user/project/issues/managing_issues.md#new-issue-via-email) and
- [merge requests by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email), and to enable [Service Desk](../user/project/service_desk.md).
+ [merge requests by email](../user/project/merge_requests/creating_merge_requests.md#create-new-merge-requests-by-email), and to enable [Service Desk](../user/project/service_desk.md).
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
@@ -162,9 +161,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
## Git configuration options
- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
-- [Git LFS configuration](../workflow/lfs/lfs_administration.md): Learn how to configure LFS for GitLab.
+- [Git LFS configuration](lfs/lfs_administration.md): Learn how to configure LFS for GitLab.
- [Housekeeping](housekeeping.md): Keep your Git repositories tidy and fast.
- [Configuring Git Protocol v2](git_protocol.md): Git protocol version 2 support.
+- [Manage large files with `git-annex` (Deprecated)](git_annex.md)
## Monitoring GitLab
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index e595c640aac..23803b82543 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -96,7 +96,7 @@ To enable the redirection, add the following line in `/etc/gitlab/gitlab.rb`:
nginx['custom_gitlab_server_config'] = "location /-/plantuml/ { \n proxy_cache off; \n proxy_pass http://plantuml:8080/; \n}\n"
# Built from source
-nginx['custom_gitlab_server_config'] = "location /-/plantuml/ { \n proxy_cache off; \n proxy_pass http://127.0.0.1:8080/plantuml/; \n}\n"
+nginx['custom_gitlab_server_config'] = "location /-/plantuml { \n rewrite ^/-/(plantuml.*) /$1 break;\n proxy_cache off; \n proxy_pass http://localhost:8080/plantuml; \n}\n"
```
To activate the changes, run the following command:
diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md
index d6d56515ac6..6042786d101 100644
--- a/doc/administration/job_logs.md
+++ b/doc/administration/job_logs.md
@@ -1,8 +1,8 @@
# Job logs
-> [Renamed from Job Traces to Job logs](https://gitlab.com/gitlab-org/gitlab/issues/29121) in 12.4.
+> [Renamed from job traces to job logs](https://gitlab.com/gitlab-org/gitlab/issues/29121) in GitLab 12.5.
-Job logs (traces) are sent by GitLab Runner while it's processing a job. You can see
+Job logs are sent by GitLab Runner while it's processing a job. You can see
logs in job pages, pipelines, email notifications, etc.
## Data flow
@@ -33,9 +33,8 @@ To change the location where the job logs will be stored, follow the steps below
gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds'
```
-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:**
@@ -48,10 +47,8 @@ To change the location where the job logs will be stored, follow the steps below
builds_path: path/to/builds/
```
-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.
## Uploading logs to object storage
@@ -69,8 +66,8 @@ job output in the UI will be empty.
## New incremental logging architecture
-> [Introduced][ce-18169] in GitLab 10.4.
-> [Announced as General availability][ce-46097] in GitLab 11.0.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18169) in GitLab 10.4.
+> - [Announced as generally available](https://gitlab.com/gitlab-org/gitlab-foss/issues/46097) in GitLab 11.0.
NOTE: **Note:**
This feature is off by default. See below for how to [enable or disable](#enabling-incremental-logging) it.
@@ -83,7 +80,7 @@ The data flow is the same as described in the [data flow section](#data-flow)
with one change: _the stored path of the first two phases is different_. This incremental
log architecture stores chunks of logs in Redis and a persistent store (object storage or database) instead of
file storage. Redis is used as first-class storage, and it stores up-to 128KB
-of data. Once the full chunk is sent, it is flushed to a persistent store, either object storage(temporary directory) or database.
+of data. Once the full chunk is sent, it is flushed to a persistent store, either object storage (temporary directory) or database.
After a while, the data in Redis and a persitent store will be archived to [object storage](#uploading-logs-to-object-storage).
The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
@@ -163,7 +160,3 @@ instance. If the number of jobs is 1000, 128MB (128KB * 1000) is consumed.
Also, it could pressure the database replication lag. `INSERT`s are generated to
indicate that we have log chunk. `UPDATE`s with 128KB of data is issued once we
receive multiple chunks.
-
-[ce-18169]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18169
-[ce-21193]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/21193
-[ce-46097]: https://gitlab.com/gitlab-org/gitlab-foss/issues/46097
diff --git a/doc/workflow/lfs/images/git-annex-branches.png b/doc/administration/lfs/img/git-annex-branches.png
index 3d614f68177..3d614f68177 100644
--- a/doc/workflow/lfs/images/git-annex-branches.png
+++ b/doc/administration/lfs/img/git-annex-branches.png
Binary files differ
diff --git a/doc/workflow/lfs/img/lfs-icon.png b/doc/administration/lfs/img/lfs-icon.png
index eef9a14187a..eef9a14187a 100644
--- a/doc/workflow/lfs/img/lfs-icon.png
+++ b/doc/administration/lfs/img/lfs-icon.png
Binary files differ
diff --git a/doc/administration/lfs/lfs_administration.md b/doc/administration/lfs/lfs_administration.md
new file mode 100644
index 00000000000..f3b8029f487
--- /dev/null
+++ b/doc/administration/lfs/lfs_administration.md
@@ -0,0 +1,273 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/lfs/lfs_administration.html'
+---
+
+# GitLab Git LFS Administration
+
+Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](manage_large_binaries_with_git_lfs.md).
+
+## Requirements
+
+- Git LFS is supported in GitLab starting with version 8.2.
+- Support for object storage, such as AWS S3, was introduced in 10.0.
+- Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up.
+
+## Configuration
+
+Git LFS objects can be large in size. By default, they are stored on the server
+GitLab is installed on.
+
+There are various configuration options to help GitLab server administrators:
+
+- Enabling/disabling Git LFS support
+- Changing the location of LFS object storage
+- Setting up object storage supported by [Fog](http://fog.io/about/provider_documentation.html)
+
+### Configuration for Omnibus installations
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+# Change to true to enable lfs - enabled by default if not defined
+gitlab_rails['lfs_enabled'] = false
+
+# Optionally, change the storage path location. Defaults to
+# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
+# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
+gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
+```
+
+### Configuration for installations from source
+
+In `config/gitlab.yml`:
+
+```yaml
+# Change to true to enable lfs
+ lfs:
+ enabled: false
+ storage_path: /mnt/storage/lfs-objects
+```
+
+## Storing LFS objects in remote object storage
+
+> [Introduced][ee-2760] in [GitLab Premium][eep] 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.
+GitLab is tightly integrated with `Fog`, so you can refer to its [documentation](http://fog.io/about/provider_documentation.html)
+to check which storage services can be integrated with GitLab.
+You can also use external object storage in a private local network. For example,
+[MinIO](https://min.io/) is a standalone object storage service, is easy to set up, and works well with GitLab instances.
+
+GitLab provides two different options for the uploading mechanism: "Direct upload" and "Background upload".
+
+**Option 1. Direct upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-workhorse uploads the file directly to the external object storage
+1. GitLab-workhorse notifies GitLab-rails that the upload process is complete
+
+**Option 2. Background upload**
+
+1. User pushes an lfs file to the GitLab instance
+1. GitLab-rails stores the file in the local file storage
+1. GitLab-rails then uploads the file to the external object storage asynchronously
+
+The following general settings are supported.
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `enabled` | Enable/disable object storage | `false` |
+| `remote_directory` | The bucket name where LFS objects will be stored| |
+| `direct_upload` | Set to true to enable direct upload of LFS without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
+| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `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 | |
+
+The `connection` settings match those provided by [Fog](https://github.com/fog).
+
+Here is a configuration example with S3.
+
+| Setting | Description | example |
+|---------|-------------|---------|
+| `provider` | The provider name | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
+| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
+| `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 |
+| `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
+
+Here is a configuration example with GCS.
+
+| Setting | Description | example |
+|---------|-------------|---------|
+| `provider` | The provider name | `Google` |
+| `google_project` | GCP project name | `gcp-project-12345` |
+| `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` |
+| `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` |
+
+NOTE: **Note:**
+The service account must have permission to access the bucket.
+[See more](https://cloud.google.com/storage/docs/authentication)
+
+Here is a configuration example with Rackspace Cloud Files.
+
+| Setting | Description | example |
+|---------|-------------|---------|
+| `provider` | The provider name | `Rackspace` |
+| `rackspace_username` | The username of the Rackspace account with access to the container | `joe.smith` |
+| `rackspace_api_key` | The API key of the Rackspace account with access to the container | `ABC123DEF456ABC123DEF456ABC123DE` |
+| `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` |
+| `rackspace_temp_url_key` | The private key you have set in the Rackspace API for temporary URLs. Read more [here](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl) | `ABC123DEF456ABC123DEF456ABC123DE` |
+
+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 they 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.
+
+### Manual uploading to an object storage
+
+There are two ways to manually do the same thing as automatic uploading (described above).
+
+**Option 1: rake task**
+
+```sh
+rake gitlab:lfs:migrate
+```
+
+**Option 2: rails console**
+
+```sh
+$ sudo gitlab-rails console # Login to 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
+```
+
+### S3 for Omnibus installations
+
+On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['lfs_object_store_enabled'] = true
+ gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
+ gitlab_rails['lfs_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
+ 'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
+ # The below options configure an S3 compatible host instead of AWS
+ 'host' => 'localhost',
+ 'endpoint' => 'http://127.0.0.1:9000',
+ 'path_style' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab]s for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+ ```bash
+ gitlab-rake gitlab:lfs:migrate
+ ```
+
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless
+ `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+
+### S3 for installations from source
+
+For source installations the settings are nested under `lfs:` and then
+`object_store:`:
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
+ lines:
+
+ ```yaml
+ lfs:
+ enabled: true
+ object_store:
+ enabled: false
+ remote_directory: lfs-objects # Bucket name
+ connection:
+ provider: AWS
+ aws_access_key_id: 1ABCD2EFGHI34JKLM567N
+ aws_secret_access_key: abcdefhijklmnopQRSTUVwxyz0123456789ABCDE
+ region: eu-central-1
+ # Use the following options to configure an AWS compatible host such as Minio
+ host: 'localhost'
+ endpoint: 'http://127.0.0.1:9000'
+ path_style: true
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+ ```bash
+ sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
+ ```
+
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless `background_upload` is set to
+ false.
+
+### Migrating back to local storage
+
+In order to migrate back to local storage:
+
+1. Set both `direct_upload` and `background_upload` to false under the LFS object storage settings. Don't forget to restart GitLab.
+1. Run `rake gitlab:lfs:migrate_to_local` on your console.
+1. Disable `object_storage` for LFS objects in `gitlab.rb`. Remember to restart GitLab afterwards.
+
+## Storage statistics
+
+You can see the total storage used for LFS objects on groups and projects
+in the administration area, as well as through the [groups](../../api/groups.md)
+and [projects APIs](../../api/projects.md).
+
+## Troubleshooting: `Google::Apis::TransmissionError: execution expired`
+
+If LFS integration is configred with Google Cloud Storage and background uploads (`background_upload: true` and `direct_upload: false`),
+Sidekiq workers may encouter 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.
+
+```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
+```
+
+See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/19581)
+
+## Known limitations
+
+- Support for removing unreferenced LFS objects was added in 8.14 onwards.
+- LFS authentications via SSH was added with GitLab 8.12.
+- 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/lfs/manage_large_binaries_with_git_lfs.md b/doc/administration/lfs/manage_large_binaries_with_git_lfs.md
new file mode 100644
index 00000000000..1fd3077ecb9
--- /dev/null
+++ b/doc/administration/lfs/manage_large_binaries_with_git_lfs.md
@@ -0,0 +1,266 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html'
+---
+
+# Git 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
+larger than 1GB to preserve performance.
+
+![Git LFS tracking status](img/lfs-icon.png)
+
+An LFS icon is shown on files tracked by Git LFS to denote if a file is stored
+as a blob or as an LFS pointer.
+
+## How it works
+
+Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
+to authorize client requests. Once the request is authorized, Git LFS client receives
+instructions from where to fetch or where to push the large file.
+
+## GitLab server configuration
+
+Documentation for GitLab instance administrators is under [LFS administration doc](lfs_administration.md).
+
+## Requirements
+
+- Git LFS is supported in GitLab starting with version 8.2
+- Git LFS must be enabled under project settings
+- [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up
+
+## Known limitations
+
+- Git LFS v1 original API is not supported since it was deprecated early in LFS
+ development
+- When SSH is set as a remote, Git LFS objects still go through HTTPS
+- Any Git LFS request will ask for HTTPS credentials to be provided so a good Git
+ credentials store is recommended
+- Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
+ to add the URL to Git config manually (see [troubleshooting](#troubleshooting))
+
+NOTE: **Note:**
+With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+still goes over HTTP, but now the SSH client passes the correct credentials
+to the Git LFS client, so no action is required by the user.
+
+## Using Git LFS
+
+Lets take a look at the workflow when you need to check large files into your Git
+repository with Git LFS. For example, if you want to upload a very large file and
+check it into your Git repository:
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+git lfs install # initialize the Git LFS project
+git lfs track "*.iso" # select the file extensions that you want to treat as large files
+```
+
+Once a certain file extension is marked for tracking as a LFS object you can use
+Git as usual without having to redo the command to track a file with the same extension:
+
+```bash
+cp ~/tmp/debian.iso ./ # copy a large file into the current directory
+git add . # add the large file to the project
+git commit -am "Added Debian iso" # commit the file meta data
+git push origin master # sync the git repo and large file to the GitLab server
+```
+
+**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git
+LFS will not be working properly for people cloning the project:
+
+```bash
+git add .gitattributes
+```
+
+Cloning the repository works the same as before. Git automatically detects the
+LFS-tracked files and clones them via HTTP. If you performed the `git clone`
+command with a SSH URL, you have to enter your GitLab credentials for HTTP
+authentication.
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+```
+
+If you already cloned the repository and you want to get the latest LFS object
+that are on the remote repository, eg. for a branch from origin:
+
+```bash
+git lfs fetch origin master
+```
+
+### Migrate an existing repo to Git LFS
+
+Read the documentation on how to [migrate an existing Git repo with Git LFS](../../topics/git/migrate_to_git_lfs/index.md).
+
+## File Locking
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/35856) in GitLab 10.5.
+
+The first thing to do before using File Locking is to tell Git LFS which
+kind of files are lockable. The following command will store PNG files
+in LFS and flag them as lockable:
+
+```bash
+git lfs track "*.png" --lockable
+```
+
+After executing the above command a file named `.gitattributes` will be
+created or updated with the following content:
+
+```bash
+*.png filter=lfs diff=lfs merge=lfs -text lockable
+```
+
+You can also register a file type as lockable without using LFS
+(In order to be able to lock/unlock a file you need a remote server that implements the LFS File Locking API),
+in order to do that you can edit the `.gitattributes` file manually:
+
+```bash
+*.pdf lockable
+```
+
+After a file type has been registered as lockable, Git LFS will make
+them readonly on the file system automatically. This means you will
+need to lock the file before editing it.
+
+### Managing Locked Files
+
+Once you're ready to edit your file you need to lock it first:
+
+```bash
+git lfs lock images/banner.png
+Locked images/banner.png
+```
+
+This will register the file as locked in your name on the server:
+
+```bash
+git lfs locks
+images/banner.png joe ID:123
+```
+
+Once you have pushed your changes, you can unlock the file so others can
+also edit it:
+
+```bash
+git lfs unlock images/banner.png
+```
+
+You can also unlock by id:
+
+```bash
+git lfs unlock --id=123
+```
+
+If for some reason you need to unlock a file that was not locked by you,
+you can use the `--force` flag as long as you have a `maintainer` access on
+the project:
+
+```bash
+git lfs unlock --id=123 --force
+```
+
+## Troubleshooting
+
+### error: Repository or object not found
+
+There are a couple of reasons why this error can occur:
+
+- You don't have permissions to access certain LFS object
+
+Check if you have permissions to push to the project or fetch from the project.
+
+- Project is not allowed to access the LFS object
+
+LFS object you are trying to push to the project or fetch from the project is not
+available to the project anymore. Probably the object was removed from the server.
+
+- Local Git repository is using deprecated LFS API
+
+### Invalid status for `<url>` : 501
+
+Git LFS will log the failures into a log file.
+To view this log file, while in project directory:
+
+```bash
+git lfs logs last
+```
+
+If the status `error 501` is shown, it is because:
+
+- Git LFS is not enabled in project settings. Check your project settings and
+ enable Git LFS.
+
+- Git LFS support is not enabled on the GitLab server. Check with your GitLab
+ administrator why Git LFS is not enabled on the server. See
+ [LFS administration documentation](lfs_administration.md) for instructions
+ on how to enable LFS support.
+
+- Git LFS client version is not supported by GitLab server. Check your Git LFS
+ version with `git lfs version`. Check the Git config of the project for traces
+ of deprecated API with `git lfs -l`. If `batch = false` is set in the config,
+ remove the line and try to update your Git LFS client. Only version 1.0.1 and
+ newer are supported.
+
+### getsockopt: connection refused
+
+If you push a LFS object to a project and you receive an error similar to:
+`Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
+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
+`lfsurl` is not set in the Git config.
+
+To prevent this from happening, set the lfs url in project Git config:
+
+```bash
+git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
+```
+
+### Credentials are always required when pushing an object
+
+NOTE: **Note:**
+With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+still goes over HTTP, but now the SSH client passes the correct credentials
+to the Git LFS client, so no action is required by the user.
+
+Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
+the LFS object on every push for every object, user HTTPS credentials are required.
+
+By default, Git has support for remembering the credentials for each repository
+you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
+
+For example, you can tell Git to remember the password for a period of time in
+which you expect to push the objects:
+
+```bash
+git config --global credential.helper 'cache --timeout=3600'
+```
+
+This will remember the credentials for an hour after which Git operations will
+require re-authentication.
+
+If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
+For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
+
+More details about various methods of storing the user credentials can be found
+on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
+
+### LFS objects are missing on push
+
+GitLab checks files to detect LFS pointers on push. If LFS pointers are detected, GitLab tries to verify that those files already exist in LFS on GitLab.
+
+Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
+
+If you are storing LFS files outside of GitLab you can disable LFS on the project by setting `lfs_enabled: false` with the [projects API](../../api/projects.md#edit-project).
+
+### 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`.
+
+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.
+
+To stop push failure, LFS support can be disabled in the [Project settings](../../user/project/settings/index.md). This means you will lose GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS).
diff --git a/doc/administration/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/administration/lfs/migrate_from_git_annex_to_git_lfs.md
new file mode 100644
index 00000000000..cf798472d62
--- /dev/null
+++ b/doc/administration/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -0,0 +1,254 @@
+# Migration guide from Git Annex to Git LFS
+
+>**Note:**
+Git Annex support [has been removed][issue-remove-annex] in GitLab Enterprise
+Edition 9.0 (2017/03/22).
+
+Both [Git Annex][] and [Git LFS][] are tools to manage large files in Git.
+
+## History
+
+Git Annex [was introduced in GitLab Enterprise Edition 7.8][post-3], 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
+Enterprise editions.
+
+## Differences between Git Annex and Git LFS
+
+Some items below are general differences between the two protocols and some are
+ones that GitLab developed.
+
+- Git Annex works only through SSH, whereas Git LFS works both with SSH and HTTPS
+ (SSH support was added in GitLab 8.12).
+- Annex files are stored in a sub-directory of the normal repositories, whereas
+ LFS files are stored outside of the repositories in a place you can define.
+- Git Annex requires a more complex setup, but has much more options than Git
+ LFS. You can compare the commands each one offers by running `man git-annex`
+ and `man git-lfs`.
+- Annex files cannot be browsed directly in GitLab's interface, whereas LFS
+ files can.
+
+## Migration steps
+
+>**Note:**
+Since Git Annex files are stored in a sub-directory of the normal repositories
+(`.git/annex/objects`) and LFS files are stored outside of the repositories,
+they are not compatible as they are using a different scheme. Therefore, the
+migration has to be done manually per repository.
+
+There are basically two steps you need to take in order to migrate from Git
+Annex to Git LFS.
+
+### TL; DR
+
+If you know what you are doing and want to skip the reading, this is what you
+need to do (we assume you have [git-annex enabled](../git_annex.md#using-gitlab-git-annex) in your
+repository and that you have made backups in case something goes wrong).
+Fire up a terminal, navigate to your Git repository and:
+
+1. Disable `git-annex`:
+
+ ```bash
+ git annex sync --content
+ git annex direct
+ git annex uninit
+ git annex indirect
+ ```
+
+1. Enable `git-lfs`:
+
+ ```
+ git lfs install
+ git lfs track <files>
+ git add .
+ git commit -m "commit message"
+ git push
+ ```
+
+### Disabling Git Annex in your repo
+
+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].
+
+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
+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
+[Git Annex troubleshooting tips](../git_annex.md#troubleshooting-tips) section.
+
+1. Backup your repository
+
+ ```bash
+ cd repository
+ git annex sync --content
+ cd ..
+ git clone repository repository-backup
+ cd repository-backup
+ git annex get
+ cd ..
+ ```
+
+1. Use `annex direct`:
+
+ ```bash
+ cd repository
+ git annex direct
+ ```
+
+ The output should be similar to this:
+
+ ```bash
+ commit
+ On branch master
+ Your branch is up-to-date with 'origin/master'.
+ nothing to commit, working tree clean
+ ok
+ direct debian.iso ok
+ direct ok
+ ```
+
+1. Disable Git Annex with [`annex uninit`][uninit]:
+
+ ```bash
+ git annex uninit
+ ```
+
+ The output should be similar to this:
+
+ ```bash
+ unannex debian.iso ok
+ Deleted branch git-annex (was 2534d2c).
+ ```
+
+ This will `unannex` every file in the repository, leaving the original files.
+
+1. Switch back to `indirect` mode:
+
+ ```bash
+ git annex indirect
+ ```
+
+ The output should be similar to this:
+
+ ```bash
+ (merging origin/git-annex into git-annex...)
+ (recording state in git...)
+ commit (recording state in git...)
+
+ ok
+ (recording state in git...)
+ [master fac3194] commit before switching to indirect mode
+ 1 file changed, 1 deletion(-)
+ delete mode 120000 alpine-virt-3.4.4-x86_64.iso
+ ok
+ indirect ok
+ ok
+ ```
+
+---
+
+At this point, you have two options. Either add, commit and push the files
+directly back to GitLab or switch to Git LFS. We will tackle the LFS switch in
+the next section.
+
+### Enabling Git LFS in your repo
+
+Git LFS is enabled by default on all GitLab products (GitLab CE, GitLab EE,
+GitLab.com), therefore, you don't need to do anything server-side.
+
+1. First, make sure you have `git-lfs` installed locally:
+
+ ```bash
+ git lfs help
+ ```
+
+ If the terminal doesn't prompt you with a full response on `git-lfs` commands,
+ [install the Git LFS client][install-lfs] first.
+
+1. Inside the repo, run the following command to initiate LFS:
+
+ ```bash
+ git lfs install
+ ```
+
+1. Enable `git-lfs` for the group of files you want to track. You
+ can track specific files, all files containing the same extension, or an
+ entire directory:
+
+ ```bash
+ git lfs track images/01.png # per file
+ git lfs track **/*.png # per extension
+ git lfs track images/ # per directory
+ ```
+
+ Once you do that, run `git status` and you'll see `.gitattributes` added
+ to your repo. It collects all file patterns that you chose to track via
+ `git-lfs`.
+
+1. Add the files, commit and push them to GitLab:
+
+ ```bash
+ git add .
+ git commit -m "commit message"
+ git push
+ ```
+
+ If your remote is set up with HTTP, you will be asked to enter your login
+ credentials. If you have [2FA enabled](../../user/profile/account/two_factor_authentication.md), make sure to use a
+ [personal access token](../../user/profile/account/two_factor_authentication.md#personal-access-tokens)
+ instead of your password.
+
+## Removing the Git Annex branches
+
+After the migration finishes successfully, you can remove all `git-annex`
+related branches from your repository.
+
+On GitLab, navigate to your project's **Repository âž” Branches** and delete all
+branches created by Git Annex: `git-annex`, and all under `synced/`.
+
+![repository branches](img/git-annex-branches.png)
+
+You can also do this on the command line with:
+
+```bash
+git branch -d synced/master
+git branch -d synced/git-annex
+git push origin :synced/master
+git push origin :synced/git-annex
+git push origin :git-annex
+git remote prune origin
+```
+
+If there are still some Annex objects inside your repository (`.git/annex/`)
+or references inside `.git/config`, run `annex uninit` again:
+
+```bash
+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]
+- (GitLab Docs) [Git Annex](../git_annex.md)
+- (GitLab Docs) [Git LFS](manage_large_binaries_with_git_lfs.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
+[lfs-track]: https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/#tracking-files-with-lfs
+[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/administration/logs.md b/doc/administration/logs.md
index dae0dae8395..aa10cdd220c 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -42,6 +42,48 @@ User clone/fetch activity using http transport appears in this log as `action: g
In addition, the log contains the IP address from which the request originated
(`remote_ip`) as well as the user's ID (`user_id`), and username (`username`).
+NOTE: **Note:** Starting with GitLab 12.5, if an error occurs, an
+`exception` field is included with `class`, `message`, and
+`backtrace`. Previous versions included an `error` field instead of
+`exception.class` and `exception.message`. For example:
+
+```json
+{
+ "method": "GET",
+ "path": "/admin",
+ "format": "html",
+ "controller": "Admin::DashboardController",
+ "action": "index",
+ "status": 500,
+ "duration": 2584.11,
+ "view": 0,
+ "db": 9.21,
+ "time": "2019-11-14T13:12:46.156Z",
+ "params": [],
+ "remote_ip": "127.0.0.1",
+ "user_id": 1,
+ "username": "root",
+ "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0",
+ "queue_duration": 274.35,
+ "correlation_id": "KjDVUhNvvV3",
+ "cpu_s": 2.837645135999999,
+ "exception": {
+ "class": "NameError",
+ "message": "undefined local variable or method `adsf' for #<Admin::DashboardController:0x00007ff3c9648588>",
+ "backtrace": [
+ "app/controllers/admin/dashboard_controller.rb:11:in `index'",
+ "ee/app/controllers/ee/admin/dashboard_controller.rb:14:in `index'",
+ "ee/lib/gitlab/ip_address_state.rb:10:in `with'",
+ "ee/app/controllers/ee/application_controller.rb:43:in `set_current_ip_address'",
+ "lib/gitlab/session.rb:11:in `with_session'",
+ "app/controllers/application_controller.rb:450:in `set_session_storage'",
+ "app/controllers/application_controller.rb:444:in `set_locale'",
+ "ee/lib/gitlab/jira/middleware.rb:19:in `call'"
+ ]
+ }
+}
+```
+
## `production.log`
This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
index bb76ad59e3b..b07bbafaf7d 100644
--- a/doc/administration/monitoring/gitlab_instance_administration_project/index.md
+++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
@@ -1,6 +1,7 @@
# GitLab instance administration project
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/56883) in GitLab 12.2.
+NOTE: **Note:**
+This feature is not yet available and is [planned for 12.6](https://gitlab.com/gitlab-org/gitlab/issues/32351).
GitLab has been adding the ability for administrators to see insights into the health of
their GitLab instance. In order to surface this experience in a native way, similar to how
diff --git a/doc/administration/monitoring/performance/img/performance_bar.png b/doc/administration/monitoring/performance/img/performance_bar.png
index d1187fd879a..acad60f863e 100644
--- a/doc/administration/monitoring/performance/img/performance_bar.png
+++ b/doc/administration/monitoring/performance/img/performance_bar.png
Binary files differ
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index 53c08e32cb2..a52b6227e14 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -8,14 +8,17 @@ activated, it looks as follows:
It allows you to see (from left to right):
- the current host serving the page
-- time taken and number of DB queries, click through for details of these queries
+- time taken and number of DB queries; click through for details of these queries
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
-- time taken and number of [Gitaly] calls, click through for details of these calls
+- time taken and number of [Gitaly] calls; click through for details of these calls
![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png)
-- time taken and number of [Rugged] calls, click through for details of these calls
+- time taken and number of [Rugged] calls; click through for details of these calls
![Rugged profiling using the Performance Bar](img/performance_bar_rugged_calls.png)
-- time taken and number of Redis calls, click through for details of these calls
+- time taken and number of Redis calls; click through for details of these calls
![Redis profiling using the Performance Bar](img/performance_bar_redis_calls.png)
+- a link to add a request's details to the performance bar; the request can be
+ added by its full URL (authenticated as the current user), or by the value of
+ its `X-Request-Id` header
On the far right is a request selector that allows you to view the same metrics
(excluding the page timing and line profiler) for any requests made while the
@@ -51,7 +54,7 @@ Make sure _Enable the Performance Bar_ is checked and hit
**Save** to save the changes.
Once the Performance Bar is enabled, you will need to press the [<kbd>p</kbd> +
-<kbd>b</kbd> keyboard shortcut](../../../workflow/shortcuts.md) to actually
+<kbd>b</kbd> keyboard shortcut](../../../user/shortcuts.md) to actually
display it.
You can toggle the Bar using the same shortcut.
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index c35d6f505be..c0b563bd76e 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -78,6 +78,31 @@ To change the address/port that Prometheus listens on:
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
+### Adding custom scrape configs
+
+You can configure additional scrape targets for the GitLab Omnibus-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.
+
+Here is an example configuration to scrape `http://1.1.1.1:8060/probe?param_a=test&param_b=additional_test`:
+
+```ruby
+prometheus['scrape_configs'] = [
+ {
+ 'job_name': 'custom-scrape',
+ 'metrics_path': '/probe',
+ 'params' => {
+ 'param_a' => ['test'],
+ 'param_b' => ['additional_test']
+ },
+ 'static_configs' => [
+ 'targets' => ['1.1.1.1:8060'],
+ ],
+ },
+]
+```
+
### Using an external Prometheus server
NOTE: **Note:**
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
index df208b3f427..1d80e23eecf 100644
--- a/doc/administration/operations/index.md
+++ b/doc/administration/operations/index.md
@@ -21,3 +21,6 @@ Keep your GitLab instance up and running smoothly.
performance can have a big impact on GitLab performance, especially for actions
that read or write Git repositories. This information will help benchmark
filesystem performance against known good and bad real-world systems.
+- [ChatOps Scripts](https://gitlab.com/gitlab-com/chatops): The GitLab.com Infrastructure team uses this repository to house
+ common ChatOps scripts they use to troubleshoot and maintain the production instance of GitLab.com.
+ These scripts are likely useful to administrators of GitLab instances of all sizes.
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index 79e9fb778b6..6438dbb9dab 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -34,7 +34,7 @@ The MemoryKiller is controlled using environment variables.
In _daemon_ mode, the MemoryKiller checks the Sidekiq process RSS every 3 seconds
(defined by `SIDEKIQ_MEMORY_KILLER_CHECK_INTERVAL`).
-- `SIDEKIQ_MEMORY_KILLER_MAX_RSS`: if this variable is set, and its value is greater
+- `SIDEKIQ_MEMORY_KILLER_MAX_RSS` (KB): if this variable is set, and its value is greater
than 0, the MemoryKiller is enabled. Otherwise the MemoryKiller is disabled.
`SIDEKIQ_MEMORY_KILLER_MAX_RSS` defines the Sidekiq process allowed RSS.
@@ -52,7 +52,7 @@ The MemoryKiller is controlled using environment variables.
[in the Omnibus GitLab
repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/attributes/default.rb).
-- `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS`: is used by _daemon_ mode. If the Sidekiq
+- `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS` (KB): is used by _daemon_ mode. If the Sidekiq
process RSS (expressed in kilobytes) exceeds `SIDEKIQ_MEMORY_KILLER_HARD_LIMIT_RSS`,
an immediate graceful restart of Sidekiq is triggered.
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index bf86a549fda..a62e3ab603d 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -18,7 +18,9 @@ You can read more about the Docker Registry at
**Omnibus GitLab installations**
-All you have to do is configure the domain name under which the Container
+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 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.
@@ -353,7 +355,7 @@ configuration.
NOTE: **Note:** Enabling a storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
-In that case, you must use an address that resolves and is accessible outside GitLab server.
+In that case, you must use an address that resolves and is accessible outside GitLab server. The Docker client will continue to authenticate via GitLab but data transfer will be direct to and from the storage backend.
The different supported drivers are:
@@ -877,6 +879,6 @@ The above image shows:
- The HEAD request to the AWS bucket reported a 403 Unauthorized.
What does this mean? This strongly suggests that the S3 user does not have the right
-[permissions to perform a HEAD request](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+[permissions to perform a HEAD request](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
Once the right permissions were set, the error will go away.
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index cacfb73451c..f51c375860b 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -120,7 +120,7 @@ The Pages daemon doesn't listen to the outside world.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
pages_external_url 'http://example.io'
```
@@ -145,7 +145,7 @@ outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
- ```shell
+ ```ruby
pages_external_url 'https://example.io'
pages_nginx['redirect_http_to_https'] = true
@@ -167,7 +167,7 @@ behavior:
1. Edit `/etc/gitlab/gitlab.rb`.
1. Set the `inplace_chroot` to `true` for GitLab Pages:
- ```shell
+ ```ruby
gitlab_pages['inplace_chroot'] = true
```
@@ -202,7 +202,7 @@ world. Custom domains are supported, but no TLS.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
@@ -233,7 +233,7 @@ world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
@@ -271,7 +271,7 @@ sites served under a custom domain.
To enable it, you'll need to:
-1. Choose an email on which you will recieve notifications about expiring domains.
+1. Choose an email on which you will receive notifications about expiring domains.
1. Navigate to your instance's **Admin Area > Settings > Preferences** and expand **Pages** settings.
1. Enter the email for receiving notifications and accept Let's Encrypt's Terms of Service as shown below.
1. Click **Save changes**.
@@ -332,7 +332,7 @@ Follow the steps below to configure verbose logging of GitLab Pages daemon.
If you wish to make it log events with level `DEBUG` you must configure this in
`/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
gitlab_pages['log_verbose'] = true
```
@@ -347,7 +347,7 @@ are stored.
If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
@@ -363,14 +363,14 @@ Omnibus GitLab 11.1.
If you wish to disable it you must configure this in
`/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
gitlab_pages['listen_proxy'] = nil
```
If you wish to make it listen on a different port you must configure this also in
`/etc/gitlab/gitlab.rb`:
- ```shell
+ ```ruby
gitlab_pages['listen_proxy'] = "localhost:10080"
```
@@ -382,21 +382,26 @@ The maximum size of the unpacked archive per project can be configured in the
Admin area under the Application settings in the **Maximum size of pages (MB)**.
The default is 100MB.
-## Running GitLab Pages in a separate server
+## Running GitLab Pages on a separate server
-You may want to run GitLab Pages daemon on a separate server in order to decrease the load on your main application server.
-Follow the steps below to configure GitLab Pages in a separate server.
+You can run the GitLab Pages daemon on a separate server in order to decrease the load on your main application server.
-1. Suppose you have the main GitLab application server named `app1`. Prepare
- new Linux server (let's call it `app2`), create NFS share there and configure access to
- this share from `app1`. Let's use the default GitLab Pages folder `/var/opt/gitlab/gitlab-rails/shared/pages`
- as the shared folder on `app2` and mount it to `/mnt/pages` on `app1`.
+To configure GitLab Pages on a separate server:
-1. On `app2` install GitLab omnibus and modify `/etc/gitlab/gitlab.rb` this way:
+1. Set up a new server. This will become the **Pages server**.
- ```shell
+1. Create an NFS share 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`
+ on the **GitLab server**.
+
+1. On the **Pages server**, install Omnibus GitLab and modify `/etc/gitlab/gitlab.rb`
+ to include:
+
+ ```ruby
external_url 'http://<ip-address-of-the-server>'
- pages_external_url "http://<your-pages-domain>"
+ pages_external_url "http://<your-pages-server-URL>"
postgresql['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
@@ -409,20 +414,82 @@ Follow the steps below to configure GitLab Pages in a separate server.
gitlab_rails['auto_migrate'] = false
```
-1. Run `sudo gitlab-ctl reconfigure`.
-1. On `app1` apply the following changes to `/etc/gitlab/gitlab.rb`:
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
- ```shell
+1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
gitlab_pages['enable'] = false
- pages_external_url "http://<your-pages-domain>"
+ pages_external_url "http://<your-pages-server-URL>"
gitlab_rails['pages_path'] = "/mnt/pages"
```
-1. Run `sudo gitlab-ctl reconfigure`.
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+It is possible to run GitLab Pages on multiple servers if you wish to distribute
+the load. You can do this through standard load balancing practices such as
+configuring your DNS server to return multiple IPs for your Pages server,
+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>"
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
## Backup
-Pages are part of the [regular backup][backup] so there is nothing to configure.
+GitLab Pages are part of the [regular backup][backup], so there is no separate backup to configure.
## Security
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 7d3e36e9796..86998280b93 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -2,8 +2,8 @@
> [Introduced][ce-4578] in GitLab 8.10.
-GitLab allows you to define multiple repository storage paths to distribute the
-storage load between several mount points.
+GitLab allows you to define multiple repository storage paths (sometimes called
+storage shards) to distribute the storage load between several mount points.
> **Notes:**
>
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 227d6928baf..9c7b5bc6b87 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -5,8 +5,8 @@
Two different storage layouts can be used
to store the repositories on disk and their characteristics.
-GitLab can be configured to use one or multiple repository shard locations
-that can be:
+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
@@ -34,8 +34,8 @@ 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][storage-paths].
+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
@@ -183,7 +183,8 @@ CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in
##### LFS Objects
-LFS Objects implements a similar storage pattern using 2 chars, 2 level folders, following Git own implementation:
+[LFS Objects in GitLab](lfs/manage_large_binaries_with_git_lfs.md) implement a similar
+storage pattern using 2 chars, 2 level folders, following Git's own implementation:
```ruby
"shared/lfs-objects/#{oid[0..1}/#{oid[2..3]}/#{oid[4..-1]}"
@@ -192,10 +193,9 @@ LFS Objects implements a similar storage pattern using 2 chars, 2 level folders,
"shared/lfs-objects/89/09/029eb962194cfb326259411b22ae3f4a814b5be4f80651735aeef9f3229c"
```
-They are also S3 compatible since **10.0** (GitLab Premium), and available in GitLab Core since **10.7**.
+LFS objects are also [S3 compatible](lfs/lfs_administration.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
-[storage-paths]: repository_storage_types.md
[gitaly]: gitaly/index.md
diff --git a/doc/administration/timezone.md b/doc/administration/timezone.md
new file mode 100644
index 00000000000..3594ba19181
--- /dev/null
+++ b/doc/administration/timezone.md
@@ -0,0 +1,37 @@
+# Changing your time zone
+
+The global time zone configuration parameter can be changed in `config/gitlab.yml`:
+
+```text
+# time_zone: 'UTC'
+```
+
+Uncomment and customize if you want to change the default time zone of the GitLab application.
+
+## Viewing available timezones
+
+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).
+
+## Changing time zone in Omnibus installations
+
+GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`.
+
+To obtain a list of timezones, log in to your GitLab application server and run a command that generates a list of timezones in TZInfo format for the server. For example, install `timedatectl` and run `timedatectl list-timezones`.
+
+To update, add the timezone that best applies to your location. For example:
+
+```ruby
+gitlab_rails['time_zone'] = 'America/New_York'
+```
+
+After adding the configuration parameter, reconfigure and restart your GitLab instance:
+
+```sh
+gitlab-ctl reconfigure
+gitlab-ctl restart
+```
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 34a5acbe7b7..dd220d0871d 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -442,7 +442,7 @@ personal_access_token = User.find(123).personal_access_tokens.create(
scopes: [:api]
)
-personal_access_token.token
+puts personal_access_token.token
```
You might also want to manually set the token string:
@@ -715,7 +715,7 @@ For more information, see the [confidential issue](../../user/project/issues/con
```ruby
Ci::Pipeline.where(project_id: p.id).where(status: 'pending').count
-Ci::Pipeline.where(project_id: p.id).where(status: 'pending').each {|p| p.cancel}
+Ci::Pipeline.where(project_id: p.id).where(status: 'pending').each {|p| p.cancel if p.stuck?}
Ci::Pipeline.where(project_id: p.id).where(status: 'pending').count
```
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index 04cebe568f3..bccfe7c542f 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -63,8 +63,8 @@ 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 enable direct upload of Uploads without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
-| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
+| `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 [what the direct_upload setting means](https://docs.gitlab.com/ee/development/uploads.html#what-does-the-direct_upload-setting-mean). | `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 | |
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 232a9825691..c2713f54c47 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -24,7 +24,7 @@ The following API resources are available in the project context:
| [Commits](commits.md) | `/projects/:id/repository/commits`, `/projects/:id/statuses` |
| [Container Registry](container_registry.md) | `/projects/:id/registry/repositories` |
| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) |
-| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies`
+| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
| [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) |
@@ -67,7 +67,9 @@ 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` |
-| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities`
+| [Visual Review discussions](visual_review_discussions.md) **(STARTER**) | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
+| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
+| [Vulnerability Findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` |
| [Wikis](wikis.md) | `/projects/:id/wikis` |
## Group resources
diff --git a/doc/api/audit_events.md b/doc/api/audit_events.md
index aca221cf990..e451b975d42 100644
--- a/doc/api/audit_events.md
+++ b/doc/api/audit_events.md
@@ -1,10 +1,12 @@
-# Audit Events API **(PREMIUM ONLY)**
+# Audit Events API
+
+## Instance Audit Events **(PREMIUM ONLY)**
The Audit Events API allows you to retrieve [instance audit events](../administration/audit_events.md#instance-events-premium-only).
To retrieve audit events using the API, you must [authenticate yourself](README.html#authentication) as an Administrator.
-## Retrieve all instance audit events
+### Retrieve all instance audit events
```
GET /audit_events
@@ -15,7 +17,7 @@ GET /audit_events
| `created_after` | string | no | Return audit events created on or after the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
| `created_before` | string | no | Return audit events created on or before the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
| `entity_type` | string | no | Return audit events for the given entity type. Valid values are: `User`, `Group`, or `Project`. |
-| `entity_id` | boolean | no | Return audit events for the given entity ID. Requires `entity_type` attribute to be present. |
+| `entity_id` | integer | no | Return audit events for the given entity ID. Requires `entity_type` attribute to be present. |
By default, `GET` requests return 20 results at a time because the API results
are paginated.
@@ -83,7 +85,7 @@ Example response:
]
```
-## Retrieve single instance audit event
+### Retrieve single instance audit event
```
GET /audit_events/:id
@@ -113,3 +115,109 @@ Example response:
"created_at": "2019-08-30T07:00:41.885Z"
}
```
+
+## Group Audit Events **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34078) in GitLab 12.5.
+
+The Group Audit Events API allows you to retrieve [group audit events](../administration/audit_events.html#group-events-starter).
+
+To retrieve group audit events using the API, you must [authenticate yourself](README.html#authentication) as an Administrator or an owner of the group.
+
+### Retrieve all group audit events
+
+```
+GET /groups/:id/audit_events
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `created_after` | string | no | Return group audit events created on or after the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
+| `created_before` | string | no | Return group audit events created on or before the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/groups/60/audit_events
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 2,
+ "author_id": 1,
+ "entity_id": 60,
+ "entity_type": "Group",
+ "details": {
+ "custom_message": "Group marked for deletion",
+ "author_name": "Administrator",
+ "target_id": "flightjs",
+ "target_type": "Group",
+ "target_details": "flightjs",
+ "ip_address": "127.0.0.1",
+ "entity_path": "flightjs"
+ },
+ "created_at": "2019-08-28T19:36:44.162Z"
+ },
+ {
+ "id": 1,
+ "author_id": 1,
+ "entity_id": 60,
+ "entity_type": "Group",
+ "details": {
+ "add": "group",
+ "author_name": "Administrator",
+ "target_id": "flightjs",
+ "target_type": "Group",
+ "target_details": "flightjs",
+ "ip_address": "127.0.0.1",
+ "entity_path": "flightjs"
+ },
+ "created_at": "2019-08-27T18:36:44.162Z"
+ }
+]
+```
+
+### Retrieve a specific group audit event
+
+Only available to group owners and administrators.
+
+```
+GET /groups/:id/audit_events/:audit_event_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `audit_event_id` | integer | yes | ID of the audit event |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/groups/60/audit_events/2
+```
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "author_id": 1,
+ "entity_id": 60,
+ "entity_type": "Group",
+ "details": {
+ "custom_message": "Group marked for deletion",
+ "author_name": "Administrator",
+ "target_id": "flightjs",
+ "target_type": "Group",
+ "target_details": "flightjs",
+ "ip_address": "127.0.0.1",
+ "entity_path": "flightjs"
+ },
+ "created_at": "2019-08-28T19:36:44.162Z"
+}
+```
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 31c8add300d..bba8876163e 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -21,7 +21,7 @@ Parameters:
| 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.|
-| `search` | string | no | Return list of branches containing the search string. You can use `^term` and `term$` to find branches that begin and end with `term` respectively.|
+| `search` | string | no | Return list of branches containing the search string. You can use `^term` and `term$` to find branches that begin and end with `term` respectively. |
Example request:
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 3927a4bbc62..f4bb09843c8 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -317,6 +317,21 @@ Example response:
}
```
+In the event of a failed cherry-pick, the response will provide context about
+why:
+
+```json
+{
+ "message": "Sorry, we cannot cherry-pick this commit automatically. This commit may already have been cherry-picked, or a more recent commit may have updated some of its content.",
+ "error_code": "empty"
+}
+```
+
+In this case, the cherry-pick failed because the changeset was empty and likely
+indicates that the commit already exists in the target branch. The other
+possible error code is `conflict`, which indicates that there was a merge
+conflict.
+
## Revert a commit
> [Introduced][ce-22919] in GitLab 11.5.
@@ -358,6 +373,19 @@ Example response:
}
```
+In the event of a failed revert, the response will provide context about why:
+
+```json
+{
+ "message": "Sorry, we cannot revert this commit automatically. This commit may already have been reverted, or a more recent commit may have updated some of its content.",
+ "error_code": "conflict"
+}
+```
+
+In this case, the revert failed because the attempted revert generated a merge
+conflict. The other possible error code is `empty`, which indicates that the
+changeset was empty, likely due to the change having already been reverted.
+
## Get the diff of a commit
Get the diff of a commit in a project.
@@ -670,6 +698,7 @@ Example response:
"merge_status":"can_be_merged",
"sha":"af5b13261899fb2c0db30abdd0af8b07cb44fdc5",
"merge_commit_sha":null,
+ "squash_commit_sha":null,
"user_notes_count":0,
"discussion_locked":null,
"should_remove_source_branch":null,
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 27254c42e3a..6fc6599a47d 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -11,7 +11,7 @@ GET /projects/:id/deployments
| 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 |
-| `order_by`| string | no | Return deployments ordered by `id` or `iid` or `created_at` or `ref` fields. Default is `id` |
+| `order_by`| string | no | Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref` fields. Default is `id` |
| `sort` | string | no | Return deployments sorted in `asc` or `desc` order. Default is `asc` |
```bash
@@ -62,6 +62,15 @@ Example of response
"twitter": "",
"website_url": "",
"organization": ""
+ },
+ "pipeline": {
+ "created_at": "2016-08-11T02:12:10.222Z",
+ "id": 36,
+ "ref": "master",
+ "sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+ "status": "success",
+ "updated_at": "2016-08-11T02:12:10.222Z",
+ "web_url": "http://gitlab.dev/root/project/pipelines/12"
}
},
"environment": {
@@ -122,6 +131,15 @@ Example of response
"twitter": "",
"website_url": "",
"organization": ""
+ },
+ "pipeline": {
+ "created_at": "2016-08-11T07:43:52.143Z",
+ "id": 37,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "status": "success",
+ "updated_at": "2016-08-11T07:43:52.143Z",
+ "web_url": "http://gitlab.dev/root/project/pipelines/13"
}
},
"environment": {
@@ -219,6 +237,15 @@ Example of response
"created_at": "2016-08-11T13:28:26.000+02:00",
"message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
},
+ "pipeline": {
+ "created_at": "2016-08-11T07:43:52.143Z",
+ "id": 42,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "status": "success",
+ "updated_at": "2016-08-11T07:43:52.143Z",
+ "web_url": "http://gitlab.dev/root/project/pipelines/5"
+ }
"runner": null
}
}
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index 665c902355f..e81dc88da81 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -50,12 +50,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"labels": []
@@ -102,12 +104,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"labels": []
@@ -189,12 +193,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"labels": []
@@ -241,12 +247,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"labels": []
diff --git a/doc/api/epics.md b/doc/api/epics.md
index c7a050f1465..531c75fd8c5 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -14,9 +14,13 @@ The [epic issues API](epic_issues.md) allows you to interact with issues associa
> [Introduced][ee-6448] in GitLab 11.3.
-Since start date and due date can be dynamically sourced from related issue milestones, when user has edit permission, additional fields will be shown. These include two boolean fields `start_date_is_fixed` and `due_date_is_fixed`, and four date fields `start_date_fixed`, `start_date_from_milestones`, `due_date_fixed` and `due_date_from_milestones`.
+Since start date and due date can be dynamically sourced from related issue milestones, when user has edit permission,
+additional fields will be shown. These include two boolean fields `start_date_is_fixed` and `due_date_is_fixed`,
+and four date fields `start_date_fixed`, `start_date_from_inherited_source`, `due_date_fixed` and `due_date_from_inherited_source`.
-`end_date` has been deprecated in favor of `due_date`.
+- `end_date` has been deprecated in favor of `due_date`.
+- `start_date_from_milestones` has been deprecated in favor of `start_date_from_inherited_source`
+- `due_date_from_milestones` has been deprecated in favor of `due_date_from_inherited_source`
## Epics pagination
@@ -80,12 +84,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
@@ -136,12 +142,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
@@ -204,12 +212,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
@@ -272,12 +282,14 @@ Example response:
"start_date": null,
"start_date_is_fixed": false,
"start_date_fixed": null,
- "start_date_from_milestones": null,
- "end_date": "2018-07-31",
+ "start_date_from_milestones": null, //deprecated in favor of start_date_from_inherited_source
+ "start_date_from_inherited_source": null,
+ "end_date": "2018-07-31", //deprecated in favor of due_date
"due_date": "2018-07-31",
"due_date_is_fixed": false,
"due_date_fixed": null,
- "due_date_from_milestones": "2018-07-31",
+ "due_date_from_milestones": "2018-07-31", //deprecated in favor of start_date_from_inherited_source
+ "due_date_from_inherited_source": "2018-07-31",
"created_at": "2018-07-17T13:36:22.770Z",
"updated_at": "2018-07-18T12:22:05.239Z",
"closed_at": "2018-08-18T12:22:05.239Z",
diff --git a/doc/api/feature_flag_specs.md b/doc/api/feature_flag_specs.md
new file mode 100644
index 00000000000..6a2cd047f85
--- /dev/null
+++ b/doc/api/feature_flag_specs.md
@@ -0,0 +1,291 @@
+# Feature Flag Specs API **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
+
+The API for creating, updating, reading and deleting [Feature Flag Specs](../user/project/operations/feature_flags.md#define-environment-specs).
+Automation engineers benefit from this API by being able to modify Feature Flag Specs without accessing user interface.
+To manage the [Feature Flag](../user/project/operations/feature_flags.md) resources via public API, please refer to the [Feature Flags API](feature_flags.md) document.
+
+Users with Developer or higher [permissions](../user/permissions.md) can access Feature Flag Specs API.
+
+## List all effective feature flag specs under the specified environment
+
+Get all effective feature flag specs under the specified [environment](../ci/environments.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
+the `production` feature flag spec only.
+
+```
+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 |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flag_scopes?environment=production
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 88,
+ "active": true,
+ "environment_scope": "production",
+ "strategies": [
+ {
+ "name": "userWithId",
+ "parameters": {
+ "userIds": "1,2,3"
+ }
+ }
+ ],
+ "created_at": "2019-11-04T08:36:41.327Z",
+ "updated_at": "2019-11-04T08:36:41.327Z",
+ "name": "awesome_feature"
+ },
+ {
+ "id": 82,
+ "active": true,
+ "environment_scope": "*",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:51.425Z",
+ "updated_at": "2019-11-04T08:39:45.751Z",
+ "name": "merge_train"
+ },
+ {
+ "id": 81,
+ "active": false,
+ "environment_scope": "production",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.527Z",
+ "updated_at": "2019-11-04T08:13:10.527Z",
+ "name": "new_live_trace"
+ }
+]
+```
+
+## List all specs of a feature flag
+
+Get all specs of a feature flag.
+
+```
+GET /projects/:id/feature_flags/:name/scopes
+```
+
+| 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. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace/scopes
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 79,
+ "active": false,
+ "environment_scope": "*",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.516Z",
+ "updated_at": "2019-11-04T08:13:10.516Z"
+ },
+ {
+ "id": 80,
+ "active": true,
+ "environment_scope": "staging",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.525Z",
+ "updated_at": "2019-11-04T08:13:10.525Z"
+ },
+ {
+ "id": 81,
+ "active": false,
+ "environment_scope": "production",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.527Z",
+ "updated_at": "2019-11-04T08:13:10.527Z"
+ }
+]
+```
+
+## New feature flag spec
+
+Creates a new feature flag spec.
+
+```
+POST /projects/:id/feature_flags/:name/scopes
+```
+
+| 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. |
+| `environment_scope` | string | yes | The [environment spec](../ci/environments.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. |
+
+```bash
+curl https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace/scopes \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ --header "Content-type: application/json" \
+ --data @- << EOF
+{
+ "environment_scope": "*",
+ "active": false,
+ "strategies": [{ "name": "default", "parameters": {} }]
+}
+EOF
+```
+
+Example response:
+
+```json
+{
+ "id": 81,
+ "active": false,
+ "environment_scope": "*",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.527Z",
+ "updated_at": "2019-11-04T08:13:10.527Z"
+}
+```
+
+## Single feature flag spec
+
+Gets a single feature flag spec.
+
+```
+GET /projects/:id/feature_flags/:name/scopes/:environment_scope
+```
+
+| 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. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/feature_flags/new_live_trace/scopes/production
+```
+
+Example response:
+
+```json
+{
+ "id": 81,
+ "active": false,
+ "environment_scope": "production",
+ "strategies": [
+ {
+ "name": "default",
+ "parameters": {}
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.527Z",
+ "updated_at": "2019-11-04T08:13:10.527Z"
+}
+```
+
+## Edit feature flag spec
+
+Updates an existing feature flag spec.
+
+```
+PUT /projects/:id/feature_flags/:name/scopes/:environment_scope
+```
+
+| 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. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.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. |
+
+```bash
+curl https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace/scopes/production \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ --header "Content-type: application/json" \
+ --data @- << EOF
+{
+ "active": true,
+ "strategies": [{ "name": "userWithId", "parameters": { "userIds": "1,2,3" } }]
+}
+EOF
+```
+
+Example response:
+
+```json
+{
+ "id": 81,
+ "active": true,
+ "environment_scope": "production",
+ "strategies": [
+ {
+ "name": "userWithId",
+ "parameters": { "userIds": "1,2,3" }
+ }
+ ],
+ "created_at": "2019-11-04T08:13:10.527Z",
+ "updated_at": "2019-11-04T08:13:10.527Z"
+}
+```
+
+## Delete feature flag spec
+
+Deletes a feature flag spec.
+
+```
+DELETE /projects/:id/feature_flags/:name/scopes/:environment_scope
+```
+
+| 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. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+
+```bash
+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_flags.md b/doc/api/feature_flags.md
new file mode 100644
index 00000000000..def452d36fb
--- /dev/null
+++ b/doc/api/feature_flags.md
@@ -0,0 +1,308 @@
+# Feature Flags API **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9566) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
+
+API for accessing resources of [GitLab Feature Flags](../user/project/operations/feature_flags.md).
+
+Users with Developer or higher [permissions](../user/permissions.md) can access Feature Flag API.
+
+## Feature Flags pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are [paginated](README.md#pagination).
+
+## List feature flags for a project
+
+Gets all feature flags of the requested project.
+
+```
+GET /projects/:id/feature_flags
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `scope` | string | no | The condition of feature flags, one of: `enabled`, `disabled`. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags
+```
+
+Example response:
+
+```json
+[
+ {
+ "name":"merge_train",
+ "description":"This feature is about merge train",
+ "created_at":"2019-11-04T08:13:51.423Z",
+ "updated_at":"2019-11-04T08:13:51.423Z",
+ "scopes":[
+ {
+ "id":82,
+ "active":false,
+ "environment_scope":"*",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:51.425Z",
+ "updated_at":"2019-11-04T08:13:51.425Z"
+ },
+ {
+ "id":83,
+ "active":true,
+ "environment_scope":"review/*",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:51.427Z",
+ "updated_at":"2019-11-04T08:13:51.427Z"
+ },
+ {
+ "id":84,
+ "active":false,
+ "environment_scope":"production",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:51.428Z",
+ "updated_at":"2019-11-04T08:13:51.428Z"
+ }
+ ]
+ },
+ {
+ "name":"new_live_trace",
+ "description":"This is a new live trace feature",
+ "created_at":"2019-11-04T08:13:10.507Z",
+ "updated_at":"2019-11-04T08:13:10.507Z",
+ "scopes":[
+ {
+ "id":79,
+ "active":false,
+ "environment_scope":"*",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.516Z",
+ "updated_at":"2019-11-04T08:13:10.516Z"
+ },
+ {
+ "id":80,
+ "active":true,
+ "environment_scope":"staging",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.525Z",
+ "updated_at":"2019-11-04T08:13:10.525Z"
+ },
+ {
+ "id":81,
+ "active":false,
+ "environment_scope":"production",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.527Z",
+ "updated_at":"2019-11-04T08:13:10.527Z"
+ }
+ ]
+ }
+]
+```
+
+## New feature flag
+
+Creates a new feature flag.
+
+```
+POST /projects/:id/feature_flags
+```
+
+| 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. |
+| `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: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. |
+
+```bash
+curl https://gitlab.example.com/api/v4/projects/1/feature_flags \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ --header "Content-type: application/json" \
+ --data @- << EOF
+{
+ "name": "awesome_feature",
+ "scopes": [{ "environment_scope": "*", "active": false, "strategies": [{ "name": "default", "parameters": {} }] },
+ { "environment_scope": "production", "active": true, "strategies": [{ "name": "userWithId", "parameters": { "userIds": "1,2,3" } }] }]
+}
+EOF
+```
+
+Example response:
+
+```json
+{
+ "name":"awesome_feature",
+ "description":null,
+ "created_at":"2019-11-04T08:32:27.288Z",
+ "updated_at":"2019-11-04T08:32:27.288Z",
+ "scopes":[
+ {
+ "id":85,
+ "active":false,
+ "environment_scope":"*",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:32:29.324Z",
+ "updated_at":"2019-11-04T08:32:29.324Z"
+ },
+ {
+ "id":86,
+ "active":true,
+ "environment_scope":"production",
+ "strategies":[
+ {
+ "name":"userWithId",
+ "parameters":{
+ "userIds":"1,2,3"
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:32:29.328Z",
+ "updated_at":"2019-11-04T08:32:29.328Z"
+ }
+ ]
+}
+```
+
+## Single feature flag
+
+Gets a single feature flag.
+
+```
+GET /projects/:id/feature_flags/:name
+```
+
+| 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. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace
+```
+
+Example response:
+
+```json
+{
+ "name":"new_live_trace",
+ "description":"This is a new live trace feature",
+ "created_at":"2019-11-04T08:13:10.507Z",
+ "updated_at":"2019-11-04T08:13:10.507Z",
+ "scopes":[
+ {
+ "id":79,
+ "active":false,
+ "environment_scope":"*",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.516Z",
+ "updated_at":"2019-11-04T08:13:10.516Z"
+ },
+ {
+ "id":80,
+ "active":true,
+ "environment_scope":"staging",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.525Z",
+ "updated_at":"2019-11-04T08:13:10.525Z"
+ },
+ {
+ "id":81,
+ "active":false,
+ "environment_scope":"production",
+ "strategies":[
+ {
+ "name":"default",
+ "parameters":{
+
+ }
+ }
+ ],
+ "created_at":"2019-11-04T08:13:10.527Z",
+ "updated_at":"2019-11-04T08:13:10.527Z"
+ }
+ ]
+}
+```
+
+## Delete feature flag
+
+Deletes a feature flag.
+
+```
+DELETE /projects/:id/feature_flags/:name
+```
+
+| 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. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
+```
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index 5eba7f038ed..e44d69f1419 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -237,6 +237,10 @@ Example response:
"container_repositories_synced_count": nil,
"container_repositories_failed_count": nil,
"container_repositories_synced_in_percentage": "0.00%",
+ "design_repositories_count": 3,
+ "design_repositories_synced_count": nil,
+ "design_repositories_failed_count": nil,
+ "design_repositories_synced_in_percentage": "0.00%",
"projects_count": 41,
"repositories_failed_count": nil,
"repositories_synced_count": nil,
@@ -304,6 +308,10 @@ Example response:
"container_repositories_synced_count": nil,
"container_repositories_failed_count": nil,
"container_repositories_synced_in_percentage": "0.00%",
+ "design_repositories_count": 3,
+ "design_repositories_synced_count": nil,
+ "design_repositories_failed_count": nil,
+ "design_repositories_synced_in_percentage": "0.00%",
"projects_count": 41,
"repositories_failed_count": 1,
"repositories_synced_count": 40,
@@ -387,6 +395,10 @@ Example response:
"container_repositories_synced_count": nil,
"container_repositories_failed_count": nil,
"container_repositories_synced_in_percentage": "0.00%",
+ "design_repositories_count": 3,
+ "design_repositories_synced_count": nil,
+ "design_repositories_failed_count": nil,
+ "design_repositories_synced_in_percentage": "0.00%",
"projects_count": 41,
"repositories_failed_count": 1,
"repositories_synced_count": 40,
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index 9eb254b4677..510b36eba8f 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -53,6 +53,11 @@ GitLab's GraphQL reference [is available](reference/index.md).
It is automatically generated from GitLab's GraphQL schema and embedded in a Markdown file.
+Machine-readable versions are also available:
+
+- [JSON format](reference/gitlab_schema.json)
+- [IDL format](reference/gitlab_schema.graphql)
+
## GraphiQL
The API can be explored by using the GraphiQL IDE, it is available on your
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
new file mode 100644
index 00000000000..a357c93b020
--- /dev/null
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -0,0 +1,5422 @@
+"""
+Autogenerated input type of AddAwardEmoji
+"""
+input AddAwardEmojiInput {
+ """
+ The global id of the awardable resource
+ """
+ awardableId: ID!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The emoji name
+ """
+ name: String!
+}
+
+"""
+Autogenerated return type of AddAwardEmoji
+"""
+type AddAwardEmojiPayload {
+ """
+ The award emoji after mutation
+ """
+ awardEmoji: AwardEmoji
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+type AwardEmoji {
+ """
+ The emoji description
+ """
+ description: String!
+
+ """
+ The emoji as an icon
+ """
+ emoji: String!
+
+ """
+ The emoji name
+ """
+ name: String!
+
+ """
+ The emoji in unicode
+ """
+ unicode: String!
+
+ """
+ The unicode version for this emoji
+ """
+ unicodeVersion: String!
+
+ """
+ The user who awarded the emoji
+ """
+ user: User!
+}
+
+type Blob implements Entry {
+ flatPath: String!
+ id: ID!
+ lfsOid: String
+ name: String!
+ path: String!
+
+ """
+ Last commit sha for entry
+ """
+ sha: String!
+ type: EntryType!
+ webUrl: String
+}
+
+"""
+The connection type for Blob.
+"""
+type BlobConnection {
+ """
+ A list of edges.
+ """
+ edges: [BlobEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Blob]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type BlobEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Blob
+}
+
+type Commit {
+ """
+ Author of the commit
+ """
+ author: User
+
+ """
+ Commit authors name
+ """
+ authorName: String
+
+ """
+ Timestamp of when the commit was authored
+ """
+ authoredDate: Time
+
+ """
+ Description of the commit message
+ """
+ description: String
+
+ """
+ ID (global ID) of the commit
+ """
+ id: ID!
+
+ """
+ Latest pipeline of the commit
+ """
+ latestPipeline(
+ """
+ Filter pipelines by the ref they are run for
+ """
+ ref: String
+
+ """
+ Filter pipelines by the sha of the commit they are run for
+ """
+ sha: String
+
+ """
+ Filter pipelines by their status
+ """
+ status: PipelineStatusEnum
+ ): Pipeline @deprecated(reason: "use pipelines")
+
+ """
+ Raw commit message
+ """
+ message: String
+
+ """
+ Pipelines of the commit ordered latest first
+ """
+ pipelines(
+ """
+ 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
+
+ """
+ Filter pipelines by the ref they are run for
+ """
+ ref: String
+
+ """
+ Filter pipelines by the sha of the commit they are run for
+ """
+ sha: String
+
+ """
+ Filter pipelines by their status
+ """
+ status: PipelineStatusEnum
+ ): PipelineConnection
+
+ """
+ SHA1 ID of the commit
+ """
+ sha: String!
+
+ """
+ Rendered HTML of the commit signature
+ """
+ signatureHtml: String
+
+ """
+ Title of the commit message
+ """
+ title: String
+
+ """
+ Web URL of the commit
+ """
+ webUrl: String!
+}
+
+"""
+Autogenerated input type of CreateDiffNote
+"""
+input CreateDiffNoteInput {
+ """
+ The content note itself
+ """
+ body: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the resource to add a note to
+ """
+ noteableId: ID!
+
+ """
+ The position of this note on a diff
+ """
+ position: DiffPositionInput!
+}
+
+"""
+Autogenerated return type of CreateDiffNote
+"""
+type CreateDiffNotePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The note after mutation
+ """
+ note: Note
+}
+
+"""
+Autogenerated input type of CreateEpic
+"""
+input CreateEpicInput {
+ """
+ The IDs of labels to be added to the epic.
+ """
+ addLabelIds: [ID!]
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The description of the epic
+ """
+ description: String
+
+ """
+ The end date of the epic
+ """
+ dueDateFixed: String
+
+ """
+ Indicates end date should be sourced from due_date_fixed field not the issue milestones
+ """
+ dueDateIsFixed: Boolean
+
+ """
+ The group the epic to mutate is in
+ """
+ groupPath: ID!
+
+ """
+ The IDs of labels to be removed from the epic.
+ """
+ removeLabelIds: [ID!]
+
+ """
+ The start date of the epic
+ """
+ startDateFixed: String
+
+ """
+ Indicates start date should be sourced from start_date_fixed field not the issue milestones
+ """
+ startDateIsFixed: Boolean
+
+ """
+ The title of the epic
+ """
+ title: String
+}
+
+"""
+Autogenerated return type of CreateEpic
+"""
+type CreateEpicPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The created epic
+ """
+ epic: Epic
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+"""
+Autogenerated input type of CreateImageDiffNote
+"""
+input CreateImageDiffNoteInput {
+ """
+ The content note itself
+ """
+ body: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the resource to add a note to
+ """
+ noteableId: ID!
+
+ """
+ The position of this note on a diff
+ """
+ position: DiffImagePositionInput!
+}
+
+"""
+Autogenerated return type of CreateImageDiffNote
+"""
+type CreateImageDiffNotePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The note after mutation
+ """
+ note: Note
+}
+
+"""
+Autogenerated input type of CreateNote
+"""
+input CreateNoteInput {
+ """
+ The content note itself
+ """
+ body: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the discussion this note is in reply to
+ """
+ discussionId: ID
+
+ """
+ The global id of the resource to add a note to
+ """
+ noteableId: ID!
+}
+
+"""
+Autogenerated return type of CreateNote
+"""
+type CreateNotePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The note after mutation
+ """
+ note: Note
+}
+
+type Design implements Noteable {
+ diffRefs: DiffRefs!
+
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+
+ """
+ The change that happened to the design at this version
+ """
+ event: DesignVersionEvent!
+ filename: String!
+ fullPath: String!
+ id: ID!
+ image: String!
+ issue: Issue!
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+
+ """
+ The total count of user-created notes for this design
+ """
+ notesCount: Int!
+ project: Project!
+
+ """
+ All versions related to this design ordered newest first
+ """
+ versions(
+ """
+ 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
+ ): DesignVersionConnection!
+}
+
+type DesignCollection {
+ """
+ All designs for this collection
+ """
+ designs(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Filters designs to only those that existed at the version. If argument is
+ omitted or nil then all designs will reflect the latest version
+ """
+ atVersion: ID
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Filters designs by their filename
+ """
+ filenames: [String!]
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Filters designs by their ID
+ """
+ ids: [ID!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): DesignConnection!
+ issue: Issue!
+ project: Project!
+
+ """
+ All versions related to all designs ordered newest first
+ """
+ versions(
+ """
+ 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
+ ): DesignVersionConnection!
+}
+
+"""
+The connection type for Design.
+"""
+type DesignConnection {
+ """
+ A list of edges.
+ """
+ edges: [DesignEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Design]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type DesignEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Design
+}
+
+"""
+Autogenerated input type of DesignManagementDelete
+"""
+input DesignManagementDeleteInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The filenames of the designs to delete
+ """
+ filenames: [String!]!
+
+ """
+ The iid of the issue to modify designs for
+ """
+ iid: ID!
+
+ """
+ The project where the issue is to upload designs for
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of DesignManagementDelete
+"""
+type DesignManagementDeletePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The new version in which the designs are deleted
+ """
+ version: DesignVersion
+}
+
+"""
+Autogenerated input type of DesignManagementUpload
+"""
+input DesignManagementUploadInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The files to upload
+ """
+ files: [Upload!]!
+
+ """
+ The iid of the issue to modify designs for
+ """
+ iid: ID!
+
+ """
+ The project where the issue is to upload designs for
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of DesignManagementUpload
+"""
+type DesignManagementUploadPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The designs that were uploaded by the mutation
+ """
+ designs: [Design!]!
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ Any designs that were skipped from the upload due to there being no change to their content since their last version
+ """
+ skippedDesigns: [Design!]!
+}
+
+type DesignVersion {
+ """
+ All designs that were changed in this version
+ """
+ designs(
+ """
+ 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
+ ): DesignConnection!
+ id: ID!
+ sha: ID!
+}
+
+"""
+The connection type for DesignVersion.
+"""
+type DesignVersionConnection {
+ """
+ A list of edges.
+ """
+ edges: [DesignVersionEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [DesignVersion]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type DesignVersionEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: DesignVersion
+}
+
+"""
+Mutation event of a Design within a Version
+"""
+enum DesignVersionEvent {
+ """
+ A creation event
+ """
+ CREATION
+
+ """
+ A deletion event
+ """
+ DELETION
+
+ """
+ A modification event
+ """
+ MODIFICATION
+
+ """
+ No change
+ """
+ NONE
+}
+
+"""
+Autogenerated input type of DestroyNote
+"""
+input DestroyNoteInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the note to destroy
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of DestroyNote
+"""
+type DestroyNotePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The note after mutation
+ """
+ note: Note
+}
+
+type DetailedStatus {
+ detailsPath: String!
+ favicon: String!
+ group: String!
+ hasDetails: Boolean!
+ icon: String!
+ label: String!
+ text: String!
+ tooltip: String!
+}
+
+input DiffImagePositionInput {
+ """
+ The merge base of the branch the comment was made on
+ """
+ baseSha: String
+
+ """
+ The sha of the head at the time the comment was made
+ """
+ headSha: String!
+
+ """
+ The total height of the image
+ """
+ height: Int!
+
+ """
+ The paths of the file that was changed. Both of the properties of this input
+ are optional, but at least one of them is required
+ """
+ paths: DiffPathsInput!
+
+ """
+ The sha of the branch being compared against
+ """
+ startSha: String!
+
+ """
+ The total width of the image
+ """
+ width: Int!
+
+ """
+ The X postion on which the comment was made
+ """
+ x: Int!
+
+ """
+ The Y position on which the comment was made
+ """
+ y: Int!
+}
+
+input DiffPathsInput {
+ """
+ The path of the file on the head sha
+ """
+ newPath: String
+
+ """
+ The path of the file on the start sha
+ """
+ oldPath: String
+}
+
+type DiffPosition {
+ diffRefs: DiffRefs!
+
+ """
+ The path of the file that was changed
+ """
+ filePath: String!
+
+ """
+ The total height of the image
+ """
+ height: Int
+
+ """
+ The line on head sha that was changed
+ """
+ newLine: Int
+
+ """
+ The path of the file on the head sha.
+ """
+ newPath: String
+
+ """
+ The line on start sha that was changed
+ """
+ oldLine: Int
+
+ """
+ The path of the file on the start sha.
+ """
+ oldPath: String
+ positionType: DiffPositionType!
+
+ """
+ The total width of the image
+ """
+ width: Int
+
+ """
+ The X postion on which the comment was made
+ """
+ x: Int
+
+ """
+ The Y position on which the comment was made
+ """
+ y: Int
+}
+
+input DiffPositionInput {
+ """
+ The merge base of the branch the comment was made on
+ """
+ baseSha: String
+
+ """
+ The sha of the head at the time the comment was made
+ """
+ headSha: String!
+
+ """
+ The line on head sha that was changed
+ """
+ newLine: Int!
+
+ """
+ The line on start sha that was changed
+ """
+ oldLine: Int
+
+ """
+ The paths of the file that was changed. Both of the properties of this input
+ are optional, but at least one of them is required
+ """
+ paths: DiffPathsInput!
+
+ """
+ The sha of the branch being compared against
+ """
+ startSha: String!
+}
+
+"""
+Type of file the position refers to
+"""
+enum DiffPositionType {
+ image
+ text
+}
+
+type DiffRefs {
+ """
+ The merge base of the branch the comment was made on
+ """
+ baseSha: String!
+
+ """
+ The sha of the head at the time the comment was made
+ """
+ headSha: String!
+
+ """
+ The sha of the branch being compared against
+ """
+ startSha: String!
+}
+
+type Discussion {
+ createdAt: Time!
+ id: ID!
+
+ """
+ All notes in the discussion
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+
+ """
+ The ID used to reply to this discussion
+ """
+ replyId: ID!
+}
+
+"""
+The connection type for Discussion.
+"""
+type DiscussionConnection {
+ """
+ A list of edges.
+ """
+ edges: [DiscussionEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Discussion]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type DiscussionEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Discussion
+}
+
+interface Entry {
+ flatPath: String!
+ id: ID!
+ name: String!
+ path: String!
+
+ """
+ Last commit sha for entry
+ """
+ sha: String!
+ type: EntryType!
+}
+
+"""
+Type of a tree entry
+"""
+enum EntryType {
+ blob
+ commit
+ tree
+}
+
+type Epic implements Noteable {
+ author: User!
+ children(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Filter epics by author
+ """
+ authorUsername: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ List epics within a time frame where epics.end_date is between start_date
+ and end_date parameters (start_date parameter must be present)
+ """
+ endDate: Time
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ The IID of the epic, e.g., "1"
+ """
+ iid: ID
+
+ """
+ The list of IIDs of epics, e.g., [1, 2]
+ """
+ iids: [ID!]
+
+ """
+ Filter epics by labels
+ """
+ labelName: [String!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ Filter epics by title and description
+ """
+ search: String
+
+ """
+ List epics by sort order
+ """
+ sort: EpicSort
+
+ """
+ List epics within a time frame where epics.start_date is between start_date
+ and end_date parameters (end_date parameter must be present)
+ """
+ startDate: Time
+
+ """
+ Filter epics by state
+ """
+ state: EpicState
+ ): EpicConnection
+ closedAt: Time
+ createdAt: Time
+
+ """
+ Number of open and closed descendant epics and issues
+ """
+ descendantCounts: EpicDescendantCount
+ description: String
+
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+ dueDate: Time
+ dueDateFixed: Time
+ dueDateFromMilestones: Time
+ dueDateIsFixed: Boolean
+ group: Group!
+ hasChildren: Boolean!
+ hasIssues: Boolean!
+ id: ID!
+ iid: ID!
+
+ """
+ A list of issues associated with the epic
+ """
+ issues(
+ """
+ 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
+ ): EpicIssueConnection
+
+ """
+ Labels assigned to the epic
+ """
+ labels(
+ """
+ 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
+ ): LabelConnection
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+ parent: Epic
+
+ """
+ List of participants for the epic
+ """
+ participants(
+ """
+ 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
+ ): UserConnection
+ reference(full: Boolean = false): String!
+ relationPath: String
+
+ """
+ The relative position of the epic in the Epic tree
+ """
+ relativePosition: Int
+ startDate: Time
+ startDateFixed: Time
+ startDateFromMilestones: Time
+ startDateIsFixed: Boolean
+ state: EpicState!
+
+ """
+ Boolean flag for whether the currently logged in user is subscribed to this epic
+ """
+ subscribed: Boolean!
+ title: String
+ updatedAt: Time
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: EpicPermissions!
+ webPath: String!
+ webUrl: String!
+}
+
+"""
+The connection type for Epic.
+"""
+type EpicConnection {
+ """
+ A list of edges.
+ """
+ edges: [EpicEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Epic]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+type EpicDescendantCount {
+ """
+ Number of closed sub-epics
+ """
+ closedEpics: Int
+
+ """
+ Number of closed epic issues
+ """
+ closedIssues: Int
+
+ """
+ Number of opened sub-epics
+ """
+ openedEpics: Int
+
+ """
+ Number of opened epic issues
+ """
+ openedIssues: Int
+}
+
+"""
+An edge in a connection.
+"""
+type EpicEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Epic
+}
+
+type EpicIssue implements Noteable {
+ """
+ Assignees of the issue
+ """
+ assignees(
+ """
+ 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
+ ): UserConnection
+
+ """
+ User that created the issue
+ """
+ author: User!
+
+ """
+ Timestamp of when the issue was closed
+ """
+ closedAt: Time
+
+ """
+ Indicates the issue is confidential
+ """
+ confidential: Boolean!
+
+ """
+ Timestamp of when the issue was created
+ """
+ createdAt: Time!
+
+ """
+ Description of the issue
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+ designCollection: DesignCollection
+ designs: DesignCollection @deprecated(reason: "use design_collection")
+
+ """
+ Indicates discussion is locked on the issue
+ """
+ discussionLocked: Boolean!
+
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+
+ """
+ Number of downvotes the issue has received
+ """
+ downvotes: Int!
+
+ """
+ Due date of the issue
+ """
+ dueDate: Time
+
+ """
+ The epic to which issue belongs
+ """
+ epic: Epic
+ epicIssueId: ID!
+
+ """
+ The global id of the epic-issue relation
+ """
+ id: ID
+
+ """
+ Internal ID of the issue
+ """
+ iid: ID!
+
+ """
+ Labels of the issue
+ """
+ labels(
+ """
+ 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
+ ): LabelConnection
+
+ """
+ Milestone of the issue
+ """
+ milestone: Milestone
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+
+ """
+ List of participants in the issue
+ """
+ participants(
+ """
+ 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
+ ): UserConnection
+
+ """
+ Internal reference of the issue. Returned in shortened format by default
+ """
+ reference(
+ """
+ Boolean option specifying whether the reference should be returned in full
+ """
+ full: Boolean = false
+ ): String!
+ relationPath: String
+
+ """
+ Relative position of the issue (used for positioning in epic tree and issue boards)
+ """
+ relativePosition: Int
+
+ """
+ State of the issue
+ """
+ state: IssueState!
+
+ """
+ Boolean flag for whether the currently logged in user is subscribed to this issue
+ """
+ subscribed: Boolean!
+
+ """
+ Task completion status of the issue
+ """
+ taskCompletionStatus: TaskCompletionStatus!
+
+ """
+ Time estimate of the issue
+ """
+ timeEstimate: Int!
+
+ """
+ Title of the issue
+ """
+ title: String!
+
+ """
+ The GitLab Flavored Markdown rendering of `title`
+ """
+ titleHtml: String
+
+ """
+ Total time reported as spent on the issue
+ """
+ totalTimeSpent: Int!
+
+ """
+ Timestamp of when the issue was last updated
+ """
+ updatedAt: Time!
+
+ """
+ Number of upvotes the issue has received
+ """
+ upvotes: Int!
+
+ """
+ Number of user notes of the issue
+ """
+ userNotesCount: Int!
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: IssuePermissions!
+
+ """
+ Web path of the issue
+ """
+ webPath: String!
+
+ """
+ Web URL of the issue
+ """
+ webUrl: String!
+ weight: Int
+}
+
+"""
+The connection type for EpicIssue.
+"""
+type EpicIssueConnection {
+ """
+ A list of edges.
+ """
+ edges: [EpicIssueEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [EpicIssue]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type EpicIssueEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: EpicIssue
+}
+
+"""
+Check permissions for the current user on an epic
+"""
+type EpicPermissions {
+ """
+ Whether or not a user can perform `admin_epic` on this resource
+ """
+ adminEpic: Boolean!
+
+ """
+ Whether or not a user can perform `award_emoji` on this resource
+ """
+ awardEmoji: Boolean!
+
+ """
+ Whether or not a user can perform `create_epic` on this resource
+ """
+ createEpic: Boolean!
+
+ """
+ Whether or not a user can perform `create_note` on this resource
+ """
+ createNote: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_epic` on this resource
+ """
+ destroyEpic: Boolean!
+
+ """
+ Whether or not a user can perform `read_epic` on this resource
+ """
+ readEpic: Boolean!
+
+ """
+ Whether or not a user can perform `read_epic_iid` on this resource
+ """
+ readEpicIid: Boolean!
+
+ """
+ Whether or not a user can perform `update_epic` on this resource
+ """
+ updateEpic: Boolean!
+}
+
+"""
+Autogenerated input type of EpicSetSubscription
+"""
+input EpicSetSubscriptionInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The group the epic to (un)subscribe is in
+ """
+ groupPath: ID!
+
+ """
+ The iid of the epic to (un)subscribe
+ """
+ iid: ID!
+
+ """
+ The desired state of the subscription
+ """
+ subscribedState: Boolean!
+}
+
+"""
+Autogenerated return type of EpicSetSubscription
+"""
+type EpicSetSubscriptionPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The epic after mutation
+ """
+ epic: Epic
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+"""
+Roadmap sort values
+"""
+enum EpicSort {
+ """
+ End date at ascending order
+ """
+ end_date_asc
+
+ """
+ End date at descending order
+ """
+ end_date_desc
+
+ """
+ Start date at ascending order
+ """
+ start_date_asc
+
+ """
+ Start date at descending order
+ """
+ start_date_desc
+}
+
+"""
+State of a GitLab epic
+"""
+enum EpicState {
+ all
+ closed
+ opened
+}
+
+"""
+State event of a GitLab Epic
+"""
+enum EpicStateEvent {
+ """
+ Close the Epic
+ """
+ CLOSE
+
+ """
+ Reopen the Epic
+ """
+ REOPEN
+}
+
+input EpicTreeNodeFieldsInputType {
+ """
+ The id of the epic_issue or issue that the actual epic or issue is switched with
+ """
+ adjacentReferenceId: ID!
+
+ """
+ The id of the epic_issue or epic that is being moved
+ """
+ id: ID!
+
+ """
+ The type of the switch, after or before allowed
+ """
+ relativePosition: MoveType!
+}
+
+"""
+Autogenerated input type of EpicTreeReorder
+"""
+input EpicTreeReorderInput {
+ """
+ The id of the base epic of the tree
+ """
+ baseEpicId: ID!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Parameters for updating the tree positions
+ """
+ moved: EpicTreeNodeFieldsInputType!
+}
+
+"""
+Autogenerated return type of EpicTreeReorder
+"""
+type EpicTreeReorderPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+type Group {
+ """
+ Avatar URL of the group
+ """
+ avatarUrl: String
+
+ """
+ Description of the namespace
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+ epic(
+ """
+ Filter epics by author
+ """
+ authorUsername: String
+
+ """
+ List epics within a time frame where epics.end_date is between start_date
+ and end_date parameters (start_date parameter must be present)
+ """
+ endDate: Time
+
+ """
+ The IID of the epic, e.g., "1"
+ """
+ iid: ID
+
+ """
+ The list of IIDs of epics, e.g., [1, 2]
+ """
+ iids: [ID!]
+
+ """
+ Filter epics by labels
+ """
+ labelName: [String!]
+
+ """
+ Filter epics by title and description
+ """
+ search: String
+
+ """
+ List epics by sort order
+ """
+ sort: EpicSort
+
+ """
+ List epics within a time frame where epics.start_date is between start_date
+ and end_date parameters (end_date parameter must be present)
+ """
+ startDate: Time
+
+ """
+ Filter epics by state
+ """
+ state: EpicState
+ ): Epic
+ epics(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Filter epics by author
+ """
+ authorUsername: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ List epics within a time frame where epics.end_date is between start_date
+ and end_date parameters (start_date parameter must be present)
+ """
+ endDate: Time
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ The IID of the epic, e.g., "1"
+ """
+ iid: ID
+
+ """
+ The list of IIDs of epics, e.g., [1, 2]
+ """
+ iids: [ID!]
+
+ """
+ Filter epics by labels
+ """
+ labelName: [String!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ Filter epics by title and description
+ """
+ search: String
+
+ """
+ List epics by sort order
+ """
+ sort: EpicSort
+
+ """
+ List epics within a time frame where epics.start_date is between start_date
+ and end_date parameters (end_date parameter must be present)
+ """
+ startDate: Time
+
+ """
+ Filter epics by state
+ """
+ state: EpicState
+ ): EpicConnection
+ epicsEnabled: Boolean
+
+ """
+ Full name of the namespace
+ """
+ fullName: String!
+
+ """
+ Full path of the namespace
+ """
+ fullPath: ID!
+
+ """
+ ID of the namespace
+ """
+ id: ID!
+
+ """
+ Indicates if Large File Storage (LFS) is enabled for namespace
+ """
+ lfsEnabled: Boolean
+
+ """
+ Name of the namespace
+ """
+ name: String!
+
+ """
+ Parent group
+ """
+ parent: Group
+
+ """
+ Path of the namespace
+ """
+ path: String!
+
+ """
+ Projects within this namespace
+ """
+ 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
+
+ """
+ Include also subgroup projects
+ """
+ includeSubgroups: Boolean = false
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): ProjectConnection!
+
+ """
+ Indicates if users can request access to namespace
+ """
+ requestAccessEnabled: Boolean
+
+ """
+ Aggregated storage statistics of the namespace. Only available for root namespaces
+ """
+ rootStorageStatistics: RootStorageStatistics
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: GroupPermissions!
+
+ """
+ Visibility of the namespace
+ """
+ visibility: String
+
+ """
+ Web URL of the group
+ """
+ webUrl: String!
+}
+
+type GroupPermissions {
+ """
+ Whether or not a user can perform `read_group` on this resource
+ """
+ readGroup: Boolean!
+}
+
+"""
+State of a GitLab issue or merge request
+"""
+enum IssuableState {
+ closed
+ locked
+ opened
+}
+
+type Issue implements Noteable {
+ """
+ Assignees of the issue
+ """
+ assignees(
+ """
+ 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
+ ): UserConnection
+
+ """
+ User that created the issue
+ """
+ author: User!
+
+ """
+ Timestamp of when the issue was closed
+ """
+ closedAt: Time
+
+ """
+ Indicates the issue is confidential
+ """
+ confidential: Boolean!
+
+ """
+ Timestamp of when the issue was created
+ """
+ createdAt: Time!
+
+ """
+ Description of the issue
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+ designCollection: DesignCollection
+ designs: DesignCollection @deprecated(reason: "use design_collection")
+
+ """
+ Indicates discussion is locked on the issue
+ """
+ discussionLocked: Boolean!
+
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+
+ """
+ Number of downvotes the issue has received
+ """
+ downvotes: Int!
+
+ """
+ Due date of the issue
+ """
+ dueDate: Time
+
+ """
+ The epic to which issue belongs
+ """
+ epic: Epic
+
+ """
+ Internal ID of the issue
+ """
+ iid: ID!
+
+ """
+ Labels of the issue
+ """
+ labels(
+ """
+ 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
+ ): LabelConnection
+
+ """
+ Milestone of the issue
+ """
+ milestone: Milestone
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+
+ """
+ List of participants in the issue
+ """
+ participants(
+ """
+ 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
+ ): UserConnection
+
+ """
+ Internal reference of the issue. Returned in shortened format by default
+ """
+ reference(
+ """
+ Boolean option specifying whether the reference should be returned in full
+ """
+ full: Boolean = false
+ ): String!
+
+ """
+ Relative position of the issue (used for positioning in epic tree and issue boards)
+ """
+ relativePosition: Int
+
+ """
+ State of the issue
+ """
+ state: IssueState!
+
+ """
+ Boolean flag for whether the currently logged in user is subscribed to this issue
+ """
+ subscribed: Boolean!
+
+ """
+ Task completion status of the issue
+ """
+ taskCompletionStatus: TaskCompletionStatus!
+
+ """
+ Time estimate of the issue
+ """
+ timeEstimate: Int!
+
+ """
+ Title of the issue
+ """
+ title: String!
+
+ """
+ The GitLab Flavored Markdown rendering of `title`
+ """
+ titleHtml: String
+
+ """
+ Total time reported as spent on the issue
+ """
+ totalTimeSpent: Int!
+
+ """
+ Timestamp of when the issue was last updated
+ """
+ updatedAt: Time!
+
+ """
+ Number of upvotes the issue has received
+ """
+ upvotes: Int!
+
+ """
+ Number of user notes of the issue
+ """
+ userNotesCount: Int!
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: IssuePermissions!
+
+ """
+ Web path of the issue
+ """
+ webPath: String!
+
+ """
+ Web URL of the issue
+ """
+ webUrl: String!
+ weight: Int
+}
+
+"""
+The connection type for Issue.
+"""
+type IssueConnection {
+ """
+ A list of edges.
+ """
+ edges: [IssueEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Issue]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type IssueEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Issue
+}
+
+"""
+Check permissions for the current user on a issue
+"""
+type IssuePermissions {
+ """
+ Whether or not a user can perform `admin_issue` on this resource
+ """
+ adminIssue: Boolean!
+
+ """
+ Whether or not a user can perform `create_design` on this resource
+ """
+ createDesign: Boolean!
+
+ """
+ Whether or not a user can perform `create_note` on this resource
+ """
+ createNote: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_design` on this resource
+ """
+ destroyDesign: Boolean!
+
+ """
+ Whether or not a user can perform `read_design` on this resource
+ """
+ readDesign: Boolean!
+
+ """
+ Whether or not a user can perform `read_issue` on this resource
+ """
+ readIssue: Boolean!
+
+ """
+ Whether or not a user can perform `reopen_issue` on this resource
+ """
+ reopenIssue: Boolean!
+
+ """
+ Whether or not a user can perform `update_issue` on this resource
+ """
+ updateIssue: Boolean!
+}
+
+"""
+Values for sorting issues
+"""
+enum IssueSort {
+ """
+ Due date by ascending order
+ """
+ DUE_DATE_ASC
+
+ """
+ Due date by descending order
+ """
+ DUE_DATE_DESC
+
+ """
+ Relative position by ascending order
+ """
+ RELATIVE_POSITION_ASC
+
+ """
+ Created at ascending order
+ """
+ created_asc
+
+ """
+ Created at descending order
+ """
+ created_desc
+
+ """
+ Updated at ascending order
+ """
+ updated_asc
+
+ """
+ Updated at descending order
+ """
+ updated_desc
+}
+
+"""
+State of a GitLab issue
+"""
+enum IssueState {
+ closed
+ locked
+ opened
+}
+
+type Label {
+ """
+ Background color of the label
+ """
+ color: String!
+
+ """
+ Description of the label (markdown rendered as HTML for caching)
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+
+ """
+ Label ID
+ """
+ id: ID!
+
+ """
+ Text color of the label
+ """
+ textColor: String!
+
+ """
+ Content of the label
+ """
+ title: String!
+}
+
+"""
+The connection type for Label.
+"""
+type LabelConnection {
+ """
+ A list of edges.
+ """
+ edges: [LabelEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Label]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type LabelEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Label
+}
+
+type MergeRequest implements Noteable {
+ """
+ Indicates if members of the target project can push to the fork
+ """
+ allowCollaboration: Boolean
+
+ """
+ Assignees of the merge request
+ """
+ assignees(
+ """
+ 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
+ ): UserConnection
+
+ """
+ Timestamp of when the merge request was created
+ """
+ createdAt: Time!
+
+ """
+ Default merge commit message of the merge request
+ """
+ defaultMergeCommitMessage: String
+
+ """
+ Description of the merge request (markdown rendered as HTML for caching)
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+
+ """
+ Diff head SHA of the merge request
+ """
+ diffHeadSha: String
+
+ """
+ References of the base SHA, the head SHA, and the start SHA for this merge request
+ """
+ diffRefs: DiffRefs
+
+ """
+ Indicates if comments on the merge request are locked to members only
+ """
+ discussionLocked: Boolean!
+
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+
+ """
+ Number of downvotes for the merge request
+ """
+ downvotes: Int!
+
+ """
+ Indicates if the project settings will lead to source branch deletion after merge
+ """
+ forceRemoveSourceBranch: Boolean
+
+ """
+ The pipeline running on the branch HEAD of the merge request
+ """
+ headPipeline: Pipeline
+
+ """
+ ID of the merge request
+ """
+ id: ID!
+
+ """
+ Internal ID of the merge request
+ """
+ iid: String!
+
+ """
+ Commit SHA of the merge request if merge is in progress
+ """
+ inProgressMergeCommitSha: String
+
+ """
+ Labels of the merge request
+ """
+ labels(
+ """
+ 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
+ ): LabelConnection
+
+ """
+ Deprecated - renamed to defaultMergeCommitMessage
+ """
+ mergeCommitMessage: String @deprecated(reason: "Renamed to defaultMergeCommitMessage")
+
+ """
+ SHA of the merge request commit (set once merged)
+ """
+ mergeCommitSha: String
+
+ """
+ Error message due to a merge error
+ """
+ mergeError: String
+
+ """
+ Indicates if a merge is currently occurring
+ """
+ mergeOngoing: Boolean!
+
+ """
+ Status of the merge request
+ """
+ mergeStatus: String
+
+ """
+ Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)
+ """
+ mergeWhenPipelineSucceeds: Boolean
+
+ """
+ Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged
+ """
+ mergeableDiscussionsState: Boolean
+
+ """
+ The milestone of the merge request
+ """
+ milestone: Milestone
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+
+ """
+ Participants in the merge request
+ """
+ participants(
+ """
+ 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
+ ): UserConnection
+
+ """
+ Pipelines for the merge request
+ """
+ pipelines(
+ """
+ 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
+
+ """
+ Filter pipelines by the ref they are run for
+ """
+ ref: String
+
+ """
+ Filter pipelines by the sha of the commit they are run for
+ """
+ sha: String
+
+ """
+ Filter pipelines by their status
+ """
+ status: PipelineStatusEnum
+ ): PipelineConnection!
+
+ """
+ Alias for target_project
+ """
+ project: Project!
+
+ """
+ ID of the merge request project
+ """
+ projectId: Int!
+
+ """
+ Rebase commit SHA of the merge request
+ """
+ rebaseCommitSha: String
+
+ """
+ Indicates if there is a rebase currently in progress for the merge request
+ """
+ rebaseInProgress: Boolean!
+
+ """
+ Internal reference of the merge request. Returned in shortened format by default
+ """
+ reference(
+ """
+ Boolean option specifying whether the reference should be returned in full
+ """
+ full: Boolean = false
+ ): String!
+
+ """
+ Indicates if the merge request will be rebased
+ """
+ shouldBeRebased: Boolean!
+
+ """
+ Indicates if the source branch of the merge request will be deleted after merge
+ """
+ shouldRemoveSourceBranch: Boolean
+
+ """
+ Source branch of the merge request
+ """
+ sourceBranch: String!
+
+ """
+ Indicates if the source branch of the merge request exists
+ """
+ sourceBranchExists: Boolean!
+
+ """
+ Source project of the merge request
+ """
+ sourceProject: Project
+
+ """
+ ID of the merge request source project
+ """
+ sourceProjectId: Int
+
+ """
+ State of the merge request
+ """
+ state: MergeRequestState!
+
+ """
+ Indicates if the currently logged in user is subscribed to this merge request
+ """
+ subscribed: Boolean!
+
+ """
+ Target branch of the merge request
+ """
+ targetBranch: String!
+
+ """
+ Target project of the merge request
+ """
+ targetProject: Project!
+
+ """
+ ID of the merge request target project
+ """
+ targetProjectId: Int!
+
+ """
+ Completion status of tasks
+ """
+ taskCompletionStatus: TaskCompletionStatus!
+
+ """
+ Time estimate of the merge request
+ """
+ timeEstimate: Int!
+
+ """
+ Title of the merge request
+ """
+ title: String!
+
+ """
+ The GitLab Flavored Markdown rendering of `title`
+ """
+ titleHtml: String
+
+ """
+ Total time reported as spent on the merge request
+ """
+ totalTimeSpent: Int!
+
+ """
+ Timestamp of when the merge request was last updated
+ """
+ updatedAt: Time!
+
+ """
+ Number of upvotes for the merge request
+ """
+ upvotes: Int!
+
+ """
+ User notes count of the merge request
+ """
+ userNotesCount: Int
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: MergeRequestPermissions!
+
+ """
+ Web URL of the merge request
+ """
+ webUrl: String
+
+ """
+ Indicates if the merge request is a work in progress (WIP)
+ """
+ workInProgress: Boolean!
+}
+
+"""
+The connection type for MergeRequest.
+"""
+type MergeRequestConnection {
+ """
+ A list of edges.
+ """
+ edges: [MergeRequestEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [MergeRequest]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type MergeRequestEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: MergeRequest
+}
+
+"""
+Check permissions for the current user on a merge request
+"""
+type MergeRequestPermissions {
+ """
+ Whether or not a user can perform `admin_merge_request` on this resource
+ """
+ adminMergeRequest: Boolean!
+
+ """
+ Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource
+ """
+ cherryPickOnCurrentMergeRequest: Boolean!
+
+ """
+ Whether or not a user can perform `create_note` on this resource
+ """
+ createNote: Boolean!
+
+ """
+ Whether or not a user can perform `push_to_source_branch` on this resource
+ """
+ pushToSourceBranch: Boolean!
+
+ """
+ Whether or not a user can perform `read_merge_request` on this resource
+ """
+ readMergeRequest: Boolean!
+
+ """
+ Whether or not a user can perform `remove_source_branch` on this resource
+ """
+ removeSourceBranch: Boolean!
+
+ """
+ Whether or not a user can perform `revert_on_current_merge_request` on this resource
+ """
+ revertOnCurrentMergeRequest: Boolean!
+
+ """
+ Whether or not a user can perform `update_merge_request` on this resource
+ """
+ updateMergeRequest: Boolean!
+}
+
+"""
+Autogenerated input type of MergeRequestSetAssignees
+"""
+input MergeRequestSetAssigneesInput {
+ """
+ The usernames to assign to the merge request. Replaces existing assignees by default.
+ """
+ assigneeUsernames: [String!]!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The operation to perform. Defaults to REPLACE.
+ """
+ operationMode: MutationOperationMode
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of MergeRequestSetAssignees
+"""
+type MergeRequestSetAssigneesPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+Autogenerated input type of MergeRequestSetLabels
+"""
+input MergeRequestSetLabelsInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The Label IDs to set. Replaces existing labels by default.
+ """
+ labelIds: [ID!]!
+
+ """
+ Changes the operation mode. Defaults to REPLACE.
+ """
+ operationMode: MutationOperationMode
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of MergeRequestSetLabels
+"""
+type MergeRequestSetLabelsPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+Autogenerated input type of MergeRequestSetLocked
+"""
+input MergeRequestSetLockedInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ Whether or not to lock the merge request.
+ """
+ locked: Boolean!
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of MergeRequestSetLocked
+"""
+type MergeRequestSetLockedPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+Autogenerated input type of MergeRequestSetMilestone
+"""
+input MergeRequestSetMilestoneInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The milestone to assign to the merge request.
+ """
+ milestoneId: ID
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of MergeRequestSetMilestone
+"""
+type MergeRequestSetMilestonePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+Autogenerated input type of MergeRequestSetSubscription
+"""
+input MergeRequestSetSubscriptionInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+
+ """
+ The desired state of the subscription
+ """
+ subscribedState: Boolean!
+}
+
+"""
+Autogenerated return type of MergeRequestSetSubscription
+"""
+type MergeRequestSetSubscriptionPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+Autogenerated input type of MergeRequestSetWip
+"""
+input MergeRequestSetWipInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the merge request to mutate
+ """
+ iid: String!
+
+ """
+ The project the merge request to mutate is in
+ """
+ projectPath: ID!
+
+ """
+ Whether or not to set the merge request as a WIP.
+ """
+ wip: Boolean!
+}
+
+"""
+Autogenerated return type of MergeRequestSetWip
+"""
+type MergeRequestSetWipPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The merge request after mutation
+ """
+ mergeRequest: MergeRequest
+}
+
+"""
+State of a GitLab merge request
+"""
+enum MergeRequestState {
+ closed
+ locked
+ merged
+ opened
+}
+
+type Metadata {
+ """
+ Revision
+ """
+ revision: String!
+
+ """
+ Version
+ """
+ version: String!
+}
+
+type Milestone {
+ """
+ Timestamp of milestone creation
+ """
+ createdAt: Time!
+
+ """
+ Description of the milestone
+ """
+ description: String
+
+ """
+ Timestamp of the milestone due date
+ """
+ dueDate: Time
+
+ """
+ ID of the milestone
+ """
+ id: ID!
+
+ """
+ Timestamp of the milestone start date
+ """
+ startDate: Time
+
+ """
+ State of the milestone
+ """
+ state: String!
+
+ """
+ Title of the milestone
+ """
+ title: String!
+
+ """
+ Timestamp of last milestone update
+ """
+ updatedAt: Time!
+}
+
+"""
+The position the adjacent object should be moved.
+"""
+enum MoveType {
+ """
+ The adjacent object will be moved after the object that is being moved.
+ """
+ after
+
+ """
+ The adjacent object will be moved before the object that is being moved.
+ """
+ before
+}
+
+type Mutation {
+ addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
+ createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
+ createEpic(input: CreateEpicInput!): CreateEpicPayload
+ createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
+ createNote(input: CreateNoteInput!): CreateNotePayload
+ designManagementDelete(input: DesignManagementDeleteInput!): DesignManagementDeletePayload
+ designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
+ destroyNote(input: DestroyNoteInput!): DestroyNotePayload
+ epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
+ epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
+ mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
+ mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
+ mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload
+ mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
+ mergeRequestSetSubscription(input: MergeRequestSetSubscriptionInput!): MergeRequestSetSubscriptionPayload
+ mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
+ removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
+ todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
+ toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
+ updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
+ updateNote(input: UpdateNoteInput!): UpdateNotePayload
+}
+
+"""
+Different toggles for changing mutator behavior.
+"""
+enum MutationOperationMode {
+ """
+ Performs an append operation
+ """
+ APPEND
+
+ """
+ Performs a removal operation
+ """
+ REMOVE
+
+ """
+ Performs a replace operation
+ """
+ REPLACE
+}
+
+type Namespace {
+ """
+ Description of the namespace
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+
+ """
+ Full name of the namespace
+ """
+ fullName: String!
+
+ """
+ Full path of the namespace
+ """
+ fullPath: ID!
+
+ """
+ ID of the namespace
+ """
+ id: ID!
+
+ """
+ Indicates if Large File Storage (LFS) is enabled for namespace
+ """
+ lfsEnabled: Boolean
+
+ """
+ Name of the namespace
+ """
+ name: String!
+
+ """
+ Path of the namespace
+ """
+ path: String!
+
+ """
+ Projects within this namespace
+ """
+ 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
+
+ """
+ Include also subgroup projects
+ """
+ includeSubgroups: Boolean = false
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): ProjectConnection!
+
+ """
+ Indicates if users can request access to namespace
+ """
+ requestAccessEnabled: Boolean
+
+ """
+ Aggregated storage statistics of the namespace. Only available for root namespaces
+ """
+ rootStorageStatistics: RootStorageStatistics
+
+ """
+ Visibility of the namespace
+ """
+ visibility: String
+}
+
+type Note {
+ """
+ The user who wrote this note
+ """
+ author: User!
+
+ """
+ The content note itself
+ """
+ body: String!
+
+ """
+ The GitLab Flavored Markdown rendering of `note`
+ """
+ bodyHtml: String
+ createdAt: Time!
+
+ """
+ The discussion this note is a part of
+ """
+ discussion: Discussion
+ id: ID!
+
+ """
+ The position of this note on a diff
+ """
+ position: DiffPosition
+
+ """
+ The project this note is associated to
+ """
+ project: Project
+ resolvable: Boolean!
+
+ """
+ The time the discussion was resolved
+ """
+ resolvedAt: Time
+
+ """
+ The user that resolved the discussion
+ """
+ resolvedBy: User
+
+ """
+ Whether or not this note was created by the system or by a user
+ """
+ system: Boolean!
+ updatedAt: Time!
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: NotePermissions!
+}
+
+"""
+The connection type for Note.
+"""
+type NoteConnection {
+ """
+ A list of edges.
+ """
+ edges: [NoteEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Note]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type NoteEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Note
+}
+
+type NotePermissions {
+ """
+ Whether or not a user can perform `admin_note` on this resource
+ """
+ adminNote: Boolean!
+
+ """
+ Whether or not a user can perform `award_emoji` on this resource
+ """
+ awardEmoji: Boolean!
+
+ """
+ Whether or not a user can perform `create_note` on this resource
+ """
+ createNote: Boolean!
+
+ """
+ Whether or not a user can perform `read_note` on this resource
+ """
+ readNote: Boolean!
+
+ """
+ Whether or not a user can perform `resolve_note` on this resource
+ """
+ resolveNote: Boolean!
+}
+
+interface Noteable {
+ """
+ All discussions on this noteable
+ """
+ discussions(
+ """
+ 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
+ ): DiscussionConnection!
+
+ """
+ All notes on this noteable
+ """
+ notes(
+ """
+ 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
+ ): NoteConnection!
+}
+
+"""
+Information about pagination in a connection.
+"""
+type PageInfo {
+ """
+ When paginating forwards, the cursor to continue.
+ """
+ endCursor: String
+
+ """
+ When paginating forwards, are there more items?
+ """
+ hasNextPage: Boolean!
+
+ """
+ When paginating backwards, are there more items?
+ """
+ hasPreviousPage: Boolean!
+
+ """
+ When paginating backwards, the cursor to continue.
+ """
+ startCursor: String
+}
+
+type Pipeline {
+ beforeSha: String
+ committedAt: Time
+
+ """
+ Coverage percentage
+ """
+ coverage: Float
+ createdAt: Time!
+ detailedStatus: DetailedStatus!
+
+ """
+ Duration of the pipeline in seconds
+ """
+ duration: Int
+ finishedAt: Time
+ id: ID!
+ iid: String!
+ sha: String!
+ startedAt: Time
+ status: PipelineStatusEnum!
+ updatedAt: Time!
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: PipelinePermissions!
+}
+
+"""
+The connection type for Pipeline.
+"""
+type PipelineConnection {
+ """
+ A list of edges.
+ """
+ edges: [PipelineEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Pipeline]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type PipelineEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Pipeline
+}
+
+type PipelinePermissions {
+ """
+ Whether or not a user can perform `admin_pipeline` on this resource
+ """
+ adminPipeline: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_pipeline` on this resource
+ """
+ destroyPipeline: Boolean!
+
+ """
+ Whether or not a user can perform `update_pipeline` on this resource
+ """
+ updatePipeline: Boolean!
+}
+
+enum PipelineStatusEnum {
+ CANCELED
+ CREATED
+ FAILED
+ MANUAL
+ PENDING
+ PREPARING
+ RUNNING
+ SCHEDULED
+ SKIPPED
+ SUCCESS
+}
+
+type Project {
+ """
+ Archived status of the project
+ """
+ archived: Boolean
+
+ """
+ URL to avatar image file of the project
+ """
+ avatarUrl: String
+
+ """
+ Indicates if the project stores Docker container images in a container registry
+ """
+ containerRegistryEnabled: Boolean
+
+ """
+ Timestamp of the project creation
+ """
+ createdAt: Time
+
+ """
+ Short description of the project
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+
+ """
+ Number of times the project has been forked
+ """
+ forksCount: Int!
+
+ """
+ Full path of the project
+ """
+ fullPath: ID!
+
+ """
+ Group of the project
+ """
+ group: Group
+
+ """
+ URL to connect to the project via HTTPS
+ """
+ httpUrlToRepo: String
+
+ """
+ ID of the project
+ """
+ id: ID!
+
+ """
+ Status of project import background job of the project
+ """
+ importStatus: String
+
+ """
+ A single issue of the project
+ """
+ issue(
+ """
+ Issues closed after this date
+ """
+ closedAfter: Time
+
+ """
+ Issues closed before this date
+ """
+ closedBefore: Time
+
+ """
+ Issues created after this date
+ """
+ createdAfter: Time
+
+ """
+ Issues created before this date
+ """
+ createdBefore: Time
+
+ """
+ The IID of the issue, e.g., "1"
+ """
+ iid: String
+
+ """
+ The list of IIDs of issues, e.g., [1, 2]
+ """
+ iids: [String!]
+
+ """
+ Labels applied to the Issue
+ """
+ labelName: [String]
+ search: String
+
+ """
+ Sort issues by this criteria
+ """
+ sort: IssueSort = created_desc
+
+ """
+ Current state of Issue
+ """
+ state: IssuableState
+
+ """
+ Issues updated after this date
+ """
+ updatedAfter: Time
+
+ """
+ Issues updated before this date
+ """
+ updatedBefore: Time
+ ): Issue
+
+ """
+ Issues of the project
+ """
+ issues(
+ """
+ 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
+
+ """
+ Issues closed after this date
+ """
+ closedAfter: Time
+
+ """
+ Issues closed before this date
+ """
+ closedBefore: Time
+
+ """
+ Issues created after this date
+ """
+ createdAfter: Time
+
+ """
+ Issues created before this date
+ """
+ createdBefore: Time
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ The IID of the issue, e.g., "1"
+ """
+ iid: String
+
+ """
+ The list of IIDs of issues, e.g., [1, 2]
+ """
+ iids: [String!]
+
+ """
+ Labels applied to the Issue
+ """
+ labelName: [String]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ search: String
+
+ """
+ Sort issues by this criteria
+ """
+ sort: IssueSort = created_desc
+
+ """
+ Current state of Issue
+ """
+ state: IssuableState
+
+ """
+ Issues updated after this date
+ """
+ updatedAfter: Time
+
+ """
+ Issues updated before this date
+ """
+ updatedBefore: Time
+ ): IssueConnection
+
+ """
+ (deprecated) Does this project have issues enabled?. Use `issues_access_level` instead
+ """
+ issuesEnabled: Boolean
+
+ """
+ (deprecated) Enable jobs for this project. Use `builds_access_level` instead
+ """
+ jobsEnabled: Boolean
+
+ """
+ Timestamp of the project last activity
+ """
+ lastActivityAt: Time
+
+ """
+ Indicates if the project has Large File Storage (LFS) enabled
+ """
+ lfsEnabled: Boolean
+
+ """
+ A single merge request of the project
+ """
+ mergeRequest(
+ """
+ The IID of the merge request, e.g., "1"
+ """
+ iid: String
+
+ """
+ The list of IIDs of issues, e.g., [1, 2]
+ """
+ iids: [String!]
+ ): MergeRequest
+
+ """
+ Merge requests of the project
+ """
+ mergeRequests(
+ """
+ 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
+
+ """
+ The IID of the merge request, e.g., "1"
+ """
+ iid: String
+
+ """
+ The list of IIDs of issues, e.g., [1, 2]
+ """
+ iids: [String!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): MergeRequestConnection
+
+ """
+ (deprecated) Does this project have merge_requests enabled?. Use `merge_requests_access_level` instead
+ """
+ mergeRequestsEnabled: Boolean
+
+ """
+ Indicates if no merge commits should be created and all merges should instead
+ be fast-forwarded, which means that merging is only allowed if the branch
+ could be fast-forwarded.
+ """
+ mergeRequestsFfOnlyEnabled: Boolean
+
+ """
+ Name of the project (without namespace)
+ """
+ name: String!
+
+ """
+ Full name of the project with its namespace
+ """
+ nameWithNamespace: String!
+
+ """
+ Namespace of the project
+ """
+ namespace: Namespace
+
+ """
+ Indicates if merge requests of the project can only be merged when all the discussions are resolved
+ """
+ onlyAllowMergeIfAllDiscussionsAreResolved: Boolean
+
+ """
+ Indicates if merge requests of the project can only be merged with successful jobs
+ """
+ onlyAllowMergeIfPipelineSucceeds: Boolean
+
+ """
+ Number of open issues for the project
+ """
+ openIssuesCount: Int
+
+ """
+ Path of the project
+ """
+ path: String!
+
+ """
+ Build pipelines of the project
+ """
+ pipelines(
+ """
+ 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
+
+ """
+ Filter pipelines by the ref they are run for
+ """
+ ref: String
+
+ """
+ Filter pipelines by the sha of the commit they are run for
+ """
+ sha: String
+
+ """
+ Filter pipelines by their status
+ """
+ status: PipelineStatusEnum
+ ): PipelineConnection
+
+ """
+ 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
+ """
+ printingMergeRequestLinkEnabled: Boolean
+
+ """
+ Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts
+ """
+ publicJobs: Boolean
+
+ """
+ Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project
+ """
+ removeSourceBranchAfterMerge: Boolean
+
+ """
+ Git repository of the project
+ """
+ repository: Repository
+
+ """
+ Indicates if users can request member access to the project
+ """
+ requestAccessEnabled: Boolean
+
+ """
+ Indicates if shared runners are enabled on the project
+ """
+ sharedRunnersEnabled: Boolean
+
+ """
+ (deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead
+ """
+ snippetsEnabled: Boolean
+
+ """
+ URL to connect to the project via SSH
+ """
+ sshUrlToRepo: String
+
+ """
+ Number of times the project has been starred
+ """
+ starCount: Int!
+
+ """
+ Statistics of the project
+ """
+ statistics: ProjectStatistics
+
+ """
+ List of project tags
+ """
+ tagList: String
+
+ """
+ Permissions for the current user on the resource
+ """
+ userPermissions: ProjectPermissions!
+
+ """
+ Visibility of the project
+ """
+ visibility: String
+
+ """
+ Web URL of the project
+ """
+ webUrl: String
+
+ """
+ (deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead
+ """
+ wikiEnabled: Boolean
+}
+
+"""
+The connection type for Project.
+"""
+type ProjectConnection {
+ """
+ A list of edges.
+ """
+ edges: [ProjectEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Project]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type ProjectEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Project
+}
+
+type ProjectPermissions {
+ """
+ Whether or not a user can perform `admin_operations` on this resource
+ """
+ adminOperations: Boolean!
+
+ """
+ Whether or not a user can perform `admin_project` on this resource
+ """
+ adminProject: Boolean!
+
+ """
+ Whether or not a user can perform `admin_remote_mirror` on this resource
+ """
+ adminRemoteMirror: Boolean!
+
+ """
+ Whether or not a user can perform `admin_wiki` on this resource
+ """
+ adminWiki: Boolean!
+
+ """
+ Whether or not a user can perform `archive_project` on this resource
+ """
+ archiveProject: Boolean!
+
+ """
+ Whether or not a user can perform `change_namespace` on this resource
+ """
+ changeNamespace: Boolean!
+
+ """
+ Whether or not a user can perform `change_visibility_level` on this resource
+ """
+ changeVisibilityLevel: Boolean!
+
+ """
+ Whether or not a user can perform `create_deployment` on this resource
+ """
+ createDeployment: Boolean!
+
+ """
+ Whether or not a user can perform `create_design` on this resource
+ """
+ createDesign: Boolean!
+
+ """
+ Whether or not a user can perform `create_issue` on this resource
+ """
+ createIssue: Boolean!
+
+ """
+ Whether or not a user can perform `create_label` on this resource
+ """
+ createLabel: Boolean!
+
+ """
+ Whether or not a user can perform `create_merge_request_from` on this resource
+ """
+ createMergeRequestFrom: Boolean!
+
+ """
+ Whether or not a user can perform `create_merge_request_in` on this resource
+ """
+ createMergeRequestIn: Boolean!
+
+ """
+ Whether or not a user can perform `create_pages` on this resource
+ """
+ createPages: Boolean!
+
+ """
+ Whether or not a user can perform `create_pipeline` on this resource
+ """
+ createPipeline: Boolean!
+
+ """
+ Whether or not a user can perform `create_pipeline_schedule` on this resource
+ """
+ createPipelineSchedule: Boolean!
+
+ """
+ Whether or not a user can perform `create_project_snippet` on this resource
+ """
+ createProjectSnippet: Boolean!
+
+ """
+ Whether or not a user can perform `create_wiki` on this resource
+ """
+ createWiki: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_design` on this resource
+ """
+ destroyDesign: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_pages` on this resource
+ """
+ destroyPages: Boolean!
+
+ """
+ Whether or not a user can perform `destroy_wiki` on this resource
+ """
+ destroyWiki: Boolean!
+
+ """
+ Whether or not a user can perform `download_code` on this resource
+ """
+ downloadCode: Boolean!
+
+ """
+ Whether or not a user can perform `download_wiki_code` on this resource
+ """
+ downloadWikiCode: Boolean!
+
+ """
+ Whether or not a user can perform `fork_project` on this resource
+ """
+ forkProject: Boolean!
+
+ """
+ Whether or not a user can perform `push_code` on this resource
+ """
+ pushCode: Boolean!
+
+ """
+ Whether or not a user can perform `push_to_delete_protected_branch` on this resource
+ """
+ pushToDeleteProtectedBranch: Boolean!
+
+ """
+ Whether or not a user can perform `read_commit_status` on this resource
+ """
+ readCommitStatus: Boolean!
+
+ """
+ Whether or not a user can perform `read_cycle_analytics` on this resource
+ """
+ readCycleAnalytics: Boolean!
+
+ """
+ Whether or not a user can perform `read_design` on this resource
+ """
+ readDesign: Boolean!
+
+ """
+ Whether or not a user can perform `read_pages_content` on this resource
+ """
+ readPagesContent: Boolean!
+
+ """
+ Whether or not a user can perform `read_project` on this resource
+ """
+ readProject: Boolean!
+
+ """
+ Whether or not a user can perform `read_project_member` on this resource
+ """
+ readProjectMember: Boolean!
+
+ """
+ Whether or not a user can perform `read_wiki` on this resource
+ """
+ readWiki: Boolean!
+
+ """
+ Whether or not a user can perform `remove_fork_project` on this resource
+ """
+ removeForkProject: Boolean!
+
+ """
+ Whether or not a user can perform `remove_pages` on this resource
+ """
+ removePages: Boolean!
+
+ """
+ Whether or not a user can perform `remove_project` on this resource
+ """
+ removeProject: Boolean!
+
+ """
+ Whether or not a user can perform `rename_project` on this resource
+ """
+ renameProject: Boolean!
+
+ """
+ Whether or not a user can perform `request_access` on this resource
+ """
+ requestAccess: Boolean!
+
+ """
+ Whether or not a user can perform `update_pages` on this resource
+ """
+ updatePages: Boolean!
+
+ """
+ Whether or not a user can perform `update_wiki` on this resource
+ """
+ updateWiki: Boolean!
+
+ """
+ Whether or not a user can perform `upload_file` on this resource
+ """
+ uploadFile: Boolean!
+}
+
+type ProjectStatistics {
+ """
+ Build artifacts size of the project
+ """
+ buildArtifactsSize: Int!
+
+ """
+ Commit count of the project
+ """
+ commitCount: Int!
+
+ """
+ Large File Storage (LFS) object size of the project
+ """
+ lfsObjectsSize: Int!
+
+ """
+ Packages size of the project
+ """
+ packagesSize: Int!
+
+ """
+ Repository size of the project
+ """
+ repositorySize: Int!
+
+ """
+ Storage size of the project
+ """
+ storageSize: Int!
+
+ """
+ Wiki size of the project
+ """
+ wikiSize: Int
+}
+
+type Query {
+ """
+ Get information about current user
+ """
+ currentUser: User
+
+ """
+ Testing endpoint to validate the API with
+ """
+ echo(text: String!): String!
+
+ """
+ Find a group
+ """
+ group(
+ """
+ The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-foss"
+ """
+ fullPath: ID!
+ ): Group
+
+ """
+ Metadata about GitLab
+ """
+ metadata: Metadata
+
+ """
+ Find a namespace
+ """
+ namespace(
+ """
+ The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-foss"
+ """
+ fullPath: ID!
+ ): Namespace
+
+ """
+ Find a project
+ """
+ project(
+ """
+ The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-foss"
+ """
+ fullPath: ID!
+ ): Project
+}
+
+"""
+Autogenerated input type of RemoveAwardEmoji
+"""
+input RemoveAwardEmojiInput {
+ """
+ The global id of the awardable resource
+ """
+ awardableId: ID!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The emoji name
+ """
+ name: String!
+}
+
+"""
+Autogenerated return type of RemoveAwardEmoji
+"""
+type RemoveAwardEmojiPayload {
+ """
+ The award emoji after mutation
+ """
+ awardEmoji: AwardEmoji
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+type Repository {
+ """
+ Indicates repository has no visible content
+ """
+ empty: Boolean!
+
+ """
+ Indicates a corresponding Git repository exists on disk
+ """
+ exists: Boolean!
+
+ """
+ Default branch of the repository
+ """
+ rootRef: String
+
+ """
+ Tree of the repository
+ """
+ tree(
+ """
+ The path to get the tree for. Default value is the root of the repository
+ """
+ path: String = ""
+
+ """
+ Used to get a recursive tree. Default is false
+ """
+ recursive: Boolean = false
+
+ """
+ The commit ref to get the tree for. Default value is HEAD
+ """
+ ref: String = "head"
+ ): Tree
+}
+
+type RootStorageStatistics {
+ """
+ The CI artifacts size in bytes
+ """
+ buildArtifactsSize: Int!
+
+ """
+ The LFS objects size in bytes
+ """
+ lfsObjectsSize: Int!
+
+ """
+ The packages size in bytes
+ """
+ packagesSize: Int!
+
+ """
+ The git repository size in bytes
+ """
+ repositorySize: Int!
+
+ """
+ The total storage in bytes
+ """
+ storageSize: Int!
+
+ """
+ The wiki size in bytes
+ """
+ wikiSize: Int!
+}
+
+type Submodule implements Entry {
+ flatPath: String!
+ id: ID!
+ name: String!
+ path: String!
+
+ """
+ Last commit sha for entry
+ """
+ sha: String!
+ treeUrl: String
+ type: EntryType!
+ webUrl: String
+}
+
+"""
+The connection type for Submodule.
+"""
+type SubmoduleConnection {
+ """
+ A list of edges.
+ """
+ edges: [SubmoduleEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Submodule]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type SubmoduleEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Submodule
+}
+
+"""
+Completion status of tasks
+"""
+type TaskCompletionStatus {
+ """
+ Number of completed tasks
+ """
+ completedCount: Int!
+
+ """
+ Number of total tasks
+ """
+ count: Int!
+}
+
+"""
+Time represented in ISO 8601
+"""
+scalar Time
+
+"""
+Representing a todo entry
+"""
+type Todo {
+ """
+ Action of the todo
+ """
+ action: TodoActionEnum!
+
+ """
+ The owner of this todo
+ """
+ author: User!
+
+ """
+ Body of the todo
+ """
+ body: String!
+
+ """
+ Timestamp this todo was created
+ """
+ createdAt: Time!
+
+ """
+ Group this todo is associated with
+ """
+ group: Group
+
+ """
+ Id of the todo
+ """
+ id: ID!
+
+ """
+ The project this todo is associated with
+ """
+ project: Project
+
+ """
+ State of the todo
+ """
+ state: TodoStateEnum!
+
+ """
+ Target type of the todo
+ """
+ targetType: TodoTargetEnum!
+}
+
+enum TodoActionEnum {
+ approval_required
+ assigned
+ build_failed
+ directly_addressed
+ marked
+ mentioned
+ unmergeable
+}
+
+"""
+The connection type for Todo.
+"""
+type TodoConnection {
+ """
+ A list of edges.
+ """
+ edges: [TodoEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Todo]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type TodoEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Todo
+}
+
+"""
+Autogenerated input type of TodoMarkDone
+"""
+input TodoMarkDoneInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the todo to mark as done
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of TodoMarkDone
+"""
+type TodoMarkDonePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The requested todo
+ """
+ todo: Todo!
+}
+
+enum TodoStateEnum {
+ done
+ pending
+}
+
+enum TodoTargetEnum {
+ """
+ A Commit
+ """
+ COMMIT
+
+ """
+ A Design
+ """
+ DESIGN
+
+ """
+ An Epic
+ """
+ EPIC
+
+ """
+ An Issue
+ """
+ ISSUE
+
+ """
+ A MergeRequest
+ """
+ MERGEREQUEST
+}
+
+"""
+Autogenerated input type of ToggleAwardEmoji
+"""
+input ToggleAwardEmojiInput {
+ """
+ The global id of the awardable resource
+ """
+ awardableId: ID!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The emoji name
+ """
+ name: String!
+}
+
+"""
+Autogenerated return type of ToggleAwardEmoji
+"""
+type ToggleAwardEmojiPayload {
+ """
+ The award emoji after mutation
+ """
+ awardEmoji: AwardEmoji
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ True when the emoji was awarded, false when it was removed
+ """
+ toggledOn: Boolean!
+}
+
+type Tree {
+ blobs(
+ """
+ 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
+ ): BlobConnection!
+
+ """
+ Last commit for the tree
+ """
+ lastCommit: Commit
+ submodules(
+ """
+ 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
+ ): SubmoduleConnection!
+ trees(
+ """
+ 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
+ ): TreeEntryConnection!
+}
+
+"""
+Represents a directory
+"""
+type TreeEntry implements Entry {
+ flatPath: String!
+ id: ID!
+ name: String!
+ path: String!
+
+ """
+ Last commit sha for entry
+ """
+ sha: String!
+ type: EntryType!
+ webUrl: String
+}
+
+"""
+The connection type for TreeEntry.
+"""
+type TreeEntryConnection {
+ """
+ A list of edges.
+ """
+ edges: [TreeEntryEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [TreeEntry]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type TreeEntryEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: TreeEntry
+}
+
+"""
+Autogenerated input type of UpdateEpic
+"""
+input UpdateEpicInput {
+ """
+ The IDs of labels to be added to the epic.
+ """
+ addLabelIds: [ID!]
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The description of the epic
+ """
+ description: String
+
+ """
+ The end date of the epic
+ """
+ dueDateFixed: String
+
+ """
+ Indicates end date should be sourced from due_date_fixed field not the issue milestones
+ """
+ dueDateIsFixed: Boolean
+
+ """
+ The group the epic to mutate is in
+ """
+ groupPath: ID!
+
+ """
+ The iid of the epic to mutate
+ """
+ iid: String!
+
+ """
+ The IDs of labels to be removed from the epic.
+ """
+ removeLabelIds: [ID!]
+
+ """
+ The start date of the epic
+ """
+ startDateFixed: String
+
+ """
+ Indicates start date should be sourced from start_date_fixed field not the issue milestones
+ """
+ startDateIsFixed: Boolean
+
+ """
+ State event for the epic
+ """
+ stateEvent: EpicStateEvent
+
+ """
+ The title of the epic
+ """
+ title: String
+}
+
+"""
+Autogenerated return type of UpdateEpic
+"""
+type UpdateEpicPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The epic after mutation
+ """
+ epic: Epic
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+}
+
+"""
+Autogenerated input type of UpdateNote
+"""
+input UpdateNoteInput {
+ """
+ The content note itself
+ """
+ body: String!
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the note to update
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of UpdateNote
+"""
+type UpdateNotePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The note after mutation
+ """
+ note: Note
+}
+
+scalar Upload
+
+type User {
+ """
+ URL of the user's avatar
+ """
+ avatarUrl: String!
+
+ """
+ Human-readable name of the user
+ """
+ name: String!
+
+ """
+ Todos of the user
+ """
+ todos(
+ """
+ The action to be filtered
+ """
+ action: [TodoActionEnum!]
+
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ The ID of an author
+ """
+ authorId: [ID!]
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ The ID of a group
+ """
+ groupId: [ID!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ The ID of a project
+ """
+ projectId: [ID!]
+
+ """
+ The state of the todo
+ """
+ state: [TodoStateEnum!]
+
+ """
+ The type of the todo
+ """
+ type: [TodoTargetEnum!]
+ ): TodoConnection!
+
+ """
+ Username of the user. Unique within this instance of GitLab
+ """
+ username: String!
+
+ """
+ Web URL of the user
+ """
+ webUrl: String!
+}
+
+"""
+The connection type for User.
+"""
+type UserConnection {
+ """
+ A list of edges.
+ """
+ edges: [UserEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [User]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type UserEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: User
+} \ No newline at end of file
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
new file mode 100644
index 00000000000..fea67f28d69
--- /dev/null
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -0,0 +1,18749 @@
+{
+ "data": {
+ "__schema": {
+ "queryType": {
+ "name": "Query"
+ },
+ "mutationType": {
+ "name": "Mutation"
+ },
+ "subscriptionType": null,
+ "types": [
+ {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "description": "Represents `true` or `false` values.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
+ "name": "String",
+ "description": "Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Query",
+ "description": null,
+ "fields": [
+ {
+ "name": "currentUser",
+ "description": "Get information about current user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "echo",
+ "description": "Testing endpoint to validate the API with",
+ "args": [
+ {
+ "name": "text",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "group",
+ "description": "Find a group",
+ "args": [
+ {
+ "name": "fullPath",
+ "description": "The full path of the project, group or namespace, e.g., \"gitlab-org/gitlab-foss\"",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Group",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "metadata",
+ "description": "Metadata about GitLab",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Metadata",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "namespace",
+ "description": "Find a namespace",
+ "args": [
+ {
+ "name": "fullPath",
+ "description": "The full path of the project, group or namespace, e.g., \"gitlab-org/gitlab-foss\"",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Namespace",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "Find a project",
+ "args": [
+ {
+ "name": "fullPath",
+ "description": "The full path of the project, group or namespace, e.g., \"gitlab-org/gitlab-foss\"",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Project",
+ "description": null,
+ "fields": [
+ {
+ "name": "archived",
+ "description": "Archived status of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "avatarUrl",
+ "description": "URL to avatar image file of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "containerRegistryEnabled",
+ "description": "Indicates if the project stores Docker container images in a container registry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp of the project creation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Short description of the project",
+ "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": "forksCount",
+ "description": "Number of times the project has been forked",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullPath",
+ "description": "Full path of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "group",
+ "description": "Group of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Group",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "httpUrlToRepo",
+ "description": "URL to connect to the project via HTTPS",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "importStatus",
+ "description": "Status of project import background job of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "A single issue of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the issue, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of issues, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Current state of Issue",
+ "type": {
+ "kind": "ENUM",
+ "name": "IssuableState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelName",
+ "description": "Labels applied to the Issue",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "createdBefore",
+ "description": "Issues created before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "createdAfter",
+ "description": "Issues created after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "updatedBefore",
+ "description": "Issues updated before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "updatedAfter",
+ "description": "Issues updated after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "closedBefore",
+ "description": "Issues closed before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "closedAfter",
+ "description": "Issues closed after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": null,
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "Sort issues by this criteria",
+ "type": {
+ "kind": "ENUM",
+ "name": "IssueSort",
+ "ofType": null
+ },
+ "defaultValue": "created_desc"
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issues",
+ "description": "Issues of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the issue, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of issues, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Current state of Issue",
+ "type": {
+ "kind": "ENUM",
+ "name": "IssuableState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelName",
+ "description": "Labels applied to the Issue",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "createdBefore",
+ "description": "Issues created before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "createdAfter",
+ "description": "Issues created after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "updatedBefore",
+ "description": "Issues updated before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "updatedAfter",
+ "description": "Issues updated after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "closedBefore",
+ "description": "Issues closed before this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "closedAfter",
+ "description": "Issues closed after this date",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": null,
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "Sort issues by this criteria",
+ "type": {
+ "kind": "ENUM",
+ "name": "IssueSort",
+ "ofType": null
+ },
+ "defaultValue": "created_desc"
+ },
+ {
+ "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": "IssueConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issuesEnabled",
+ "description": "(deprecated) Does this project have issues enabled?. Use `issues_access_level` instead",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "jobsEnabled",
+ "description": "(deprecated) Enable jobs for this project. Use `builds_access_level` instead",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastActivityAt",
+ "description": "Timestamp of the project last activity",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsEnabled",
+ "description": "Indicates if the project has Large File Storage (LFS) enabled",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequest",
+ "description": "A single merge request of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the merge request, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of issues, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequests",
+ "description": "Merge requests of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the merge request, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of issues, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "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": "MergeRequestConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestsEnabled",
+ "description": "(deprecated) Does this project have merge_requests enabled?. Use `merge_requests_access_level` instead",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestsFfOnlyEnabled",
+ "description": "Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the project (without namespace)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nameWithNamespace",
+ "description": "Full name of the project with its namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "namespace",
+ "description": "Namespace of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Namespace",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "onlyAllowMergeIfAllDiscussionsAreResolved",
+ "description": "Indicates if merge requests of the project can only be merged when all the discussions are resolved",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "onlyAllowMergeIfPipelineSucceeds",
+ "description": "Indicates if merge requests of the project can only be merged with successful jobs",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "openIssuesCount",
+ "description": "Number of open issues for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": "Path of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pipelines",
+ "description": "Build pipelines of the project",
+ "args": [
+ {
+ "name": "status",
+ "description": "Filter pipelines by their status",
+ "type": {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "ref",
+ "description": "Filter pipelines by the ref they are run for",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sha",
+ "description": "Filter pipelines by the sha of the commit they are run for",
+ "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": "PipelineConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "printingMergeRequestLinkEnabled",
+ "description": "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",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "publicJobs",
+ "description": "Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "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": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "repository",
+ "description": "Git repository of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Repository",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "requestAccessEnabled",
+ "description": "Indicates if users can request member access to the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sharedRunnersEnabled",
+ "description": "Indicates if shared runners are enabled on the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "snippetsEnabled",
+ "description": "(deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sshUrlToRepo",
+ "description": "URL to connect to the project via SSH",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "starCount",
+ "description": "Number of times the project has been starred",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "statistics",
+ "description": "Statistics of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ProjectStatistics",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tagList",
+ "description": "List of project tags",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ProjectPermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "visibility",
+ "description": "Visibility of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": "Web URL of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "wikiEnabled",
+ "description": "(deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ProjectPermissions",
+ "description": null,
+ "fields": [
+ {
+ "name": "adminOperations",
+ "description": "Whether or not a user can perform `admin_operations` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "adminProject",
+ "description": "Whether or not a user can perform `admin_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "adminRemoteMirror",
+ "description": "Whether or not a user can perform `admin_remote_mirror` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "adminWiki",
+ "description": "Whether or not a user can perform `admin_wiki` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "archiveProject",
+ "description": "Whether or not a user can perform `archive_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "changeNamespace",
+ "description": "Whether or not a user can perform `change_namespace` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "changeVisibilityLevel",
+ "description": "Whether or not a user can perform `change_visibility_level` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createDeployment",
+ "description": "Whether or not a user can perform `create_deployment` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createDesign",
+ "description": "Whether or not a user can perform `create_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createIssue",
+ "description": "Whether or not a user can perform `create_issue` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createLabel",
+ "description": "Whether or not a user can perform `create_label` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createMergeRequestFrom",
+ "description": "Whether or not a user can perform `create_merge_request_from` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createMergeRequestIn",
+ "description": "Whether or not a user can perform `create_merge_request_in` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createPages",
+ "description": "Whether or not a user can perform `create_pages` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createPipeline",
+ "description": "Whether or not a user can perform `create_pipeline` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createPipelineSchedule",
+ "description": "Whether or not a user can perform `create_pipeline_schedule` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createProjectSnippet",
+ "description": "Whether or not a user can perform `create_project_snippet` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createWiki",
+ "description": "Whether or not a user can perform `create_wiki` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyDesign",
+ "description": "Whether or not a user can perform `destroy_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyPages",
+ "description": "Whether or not a user can perform `destroy_pages` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyWiki",
+ "description": "Whether or not a user can perform `destroy_wiki` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "downloadCode",
+ "description": "Whether or not a user can perform `download_code` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "downloadWikiCode",
+ "description": "Whether or not a user can perform `download_wiki_code` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "forkProject",
+ "description": "Whether or not a user can perform `fork_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pushCode",
+ "description": "Whether or not a user can perform `push_code` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pushToDeleteProtectedBranch",
+ "description": "Whether or not a user can perform `push_to_delete_protected_branch` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readCommitStatus",
+ "description": "Whether or not a user can perform `read_commit_status` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readCycleAnalytics",
+ "description": "Whether or not a user can perform `read_cycle_analytics` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readDesign",
+ "description": "Whether or not a user can perform `read_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readPagesContent",
+ "description": "Whether or not a user can perform `read_pages_content` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readProject",
+ "description": "Whether or not a user can perform `read_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readProjectMember",
+ "description": "Whether or not a user can perform `read_project_member` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readWiki",
+ "description": "Whether or not a user can perform `read_wiki` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "removeForkProject",
+ "description": "Whether or not a user can perform `remove_fork_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "removePages",
+ "description": "Whether or not a user can perform `remove_pages` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "removeProject",
+ "description": "Whether or not a user can perform `remove_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "renameProject",
+ "description": "Whether or not a user can perform `rename_project` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "requestAccess",
+ "description": "Whether or not a user can perform `request_access` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatePages",
+ "description": "Whether or not a user can perform `update_pages` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateWiki",
+ "description": "Whether or not a user can perform `update_wiki` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "uploadFile",
+ "description": "Whether or not a user can perform `upload_file` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
+ "name": "ID",
+ "description": "Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `\"VXNlci0xMA==\"`) or integer (such as `4`) input value will be accepted as an ID.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "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,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
+ "name": "Time",
+ "description": "Time represented in ISO 8601",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Namespace",
+ "description": null,
+ "fields": [
+ {
+ "name": "description",
+ "description": "Description of the namespace",
+ "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": "fullName",
+ "description": "Full name of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullPath",
+ "description": "Full path of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsEnabled",
+ "description": "Indicates if Large File Storage (LFS) is enabled for namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": "Path of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "projects",
+ "description": "Projects within this namespace",
+ "args": [
+ {
+ "name": "includeSubgroups",
+ "description": "Include also subgroup projects",
+ "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": {
+ "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
+ },
+ {
+ "name": "requestAccessEnabled",
+ "description": "Indicates if users can request access to namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "rootStorageStatistics",
+ "description": "Aggregated storage statistics of the namespace. Only available for root namespaces",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RootStorageStatistics",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "visibility",
+ "description": "Visibility of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "RootStorageStatistics",
+ "description": null,
+ "fields": [
+ {
+ "name": "buildArtifactsSize",
+ "description": "The CI artifacts size in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsObjectsSize",
+ "description": "The LFS objects size in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "packagesSize",
+ "description": "The packages size in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "repositorySize",
+ "description": "The git repository size in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "storageSize",
+ "description": "The total storage in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "wikiSize",
+ "description": "The wiki size in bytes",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ProjectConnection",
+ "description": "The connection type for Project.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ProjectEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "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": "PageInfo",
+ "description": "Information about pagination in a connection.",
+ "fields": [
+ {
+ "name": "endCursor",
+ "description": "When paginating forwards, the cursor to continue.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasNextPage",
+ "description": "When paginating forwards, are there more items?",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasPreviousPage",
+ "description": "When paginating backwards, are there more items?",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startCursor",
+ "description": "When paginating backwards, the cursor to continue.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ProjectEdge",
+ "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": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Group",
+ "description": null,
+ "fields": [
+ {
+ "name": "avatarUrl",
+ "description": "Avatar URL of the group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the namespace",
+ "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": "epic",
+ "description": null,
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the epic, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of epics, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Filter epics by state",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Filter epics by title and description",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "List epics by sort order",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicSort",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "authorUsername",
+ "description": "Filter epics by author",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelName",
+ "description": "Filter epics by labels",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDate",
+ "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epics",
+ "description": null,
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the epic, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of epics, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Filter epics by state",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Filter epics by title and description",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "List epics by sort order",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicSort",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "authorUsername",
+ "description": "Filter epics by author",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelName",
+ "description": "Filter epics by labels",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDate",
+ "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "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": "EpicConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epicsEnabled",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullName",
+ "description": "Full name of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullPath",
+ "description": "Full path of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsEnabled",
+ "description": "Indicates if Large File Storage (LFS) is enabled for namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "parent",
+ "description": "Parent group",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Group",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": "Path of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "projects",
+ "description": "Projects within this namespace",
+ "args": [
+ {
+ "name": "includeSubgroups",
+ "description": "Include also subgroup projects",
+ "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": {
+ "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
+ },
+ {
+ "name": "requestAccessEnabled",
+ "description": "Indicates if users can request access to namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "rootStorageStatistics",
+ "description": "Aggregated storage statistics of the namespace. Only available for root namespaces",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RootStorageStatistics",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "GroupPermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "visibility",
+ "description": "Visibility of the namespace",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": "Web URL of the group",
+ "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": "GroupPermissions",
+ "description": null,
+ "fields": [
+ {
+ "name": "readGroup",
+ "description": "Whether or not a user can perform `read_group` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "description": null,
+ "fields": [
+ {
+ "name": "author",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "children",
+ "description": null,
+ "args": [
+ {
+ "name": "iid",
+ "description": "The IID of the epic, e.g., \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iids",
+ "description": "The list of IIDs of epics, e.g., [1, 2]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Filter epics by state",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Filter epics by title and description",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "List epics by sort order",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicSort",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "authorUsername",
+ "description": "Filter epics by author",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelName",
+ "description": "Filter epics by labels",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDate",
+ "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "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": "EpicConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "descendantCounts",
+ "description": "Number of open and closed descendant epics and issues",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicDescendantCount",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDate",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDateFixed",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDateFromMilestones",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDateIsFixed",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "group",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Group",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasChildren",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasIssues",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issues",
+ "description": "A list of issues associated with the epic",
+ "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": "EpicIssueConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "labels",
+ "description": "Labels assigned to the epic",
+ "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": "LabelConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "parent",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "participants",
+ "description": "List of participants for the epic",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reference",
+ "description": null,
+ "args": [
+ {
+ "name": "full",
+ "description": null,
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "relationPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "relativePosition",
+ "description": "The relative position of the epic in the Epic tree",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDate",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDateFixed",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDateFromMilestones",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDateIsFixed",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EpicState",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subscribed",
+ "description": "Boolean flag for whether the currently logged in user is subscribed to this epic",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicPermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "description": null,
+ "fields": [
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": [
+ {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicIssue",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ }
+ ]
+ },
+ {
+ "kind": "OBJECT",
+ "name": "NoteConnection",
+ "description": "The connection type for Note.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "NoteEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "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": "NoteEdge",
+ "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": "Note",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Note",
+ "description": null,
+ "fields": [
+ {
+ "name": "author",
+ "description": "The user who wrote this note",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "body",
+ "description": "The content note itself",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "bodyHtml",
+ "description": "The GitLab Flavored Markdown rendering of `note`",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussion",
+ "description": "The discussion this note is a part of",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Discussion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "position",
+ "description": "The position of this note on a diff",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DiffPosition",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "The project this note is associated to",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolvable",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolvedAt",
+ "description": "The time the discussion was resolved",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolvedBy",
+ "description": "The user that resolved the discussion",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "system",
+ "description": "Whether or not this note was created by the system or by a user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "NotePermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "NotePermissions",
+ "description": null,
+ "fields": [
+ {
+ "name": "adminNote",
+ "description": "Whether or not a user can perform `admin_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "awardEmoji",
+ "description": "Whether or not a user can perform `award_emoji` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createNote",
+ "description": "Whether or not a user can perform `create_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readNote",
+ "description": "Whether or not a user can perform `read_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolveNote",
+ "description": "Whether or not a user can perform `resolve_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "User",
+ "description": null,
+ "fields": [
+ {
+ "name": "avatarUrl",
+ "description": "URL of the user's avatar",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Human-readable name of the user",
+ "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": [
+ {
+ "name": "action",
+ "description": "The action to be filtered",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoActionEnum",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "authorId",
+ "description": "The ID of an author",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "projectId",
+ "description": "The ID of a project",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "groupId",
+ "description": "The ID of a group",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "The state of the todo",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "type",
+ "description": "The type of the todo",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoTargetEnum",
+ "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": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "username",
+ "description": "Username of the user. Unique within this instance of GitLab",
+ "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 user",
+ "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": "TodoConnection",
+ "description": "The connection type for Todo.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TodoEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Todo",
+ "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": "TodoEdge",
+ "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": "Todo",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Todo",
+ "description": "Representing a todo entry",
+ "fields": [
+ {
+ "name": "action",
+ "description": "Action of the todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoActionEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "author",
+ "description": "The owner of this todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "body",
+ "description": "Body of the todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp this todo was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "group",
+ "description": "Group this todo is associated with",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Group",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "Id of the todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "The project this todo is associated with",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "targetType",
+ "description": "Target type of the todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "TodoTargetEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "TodoActionEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "assigned",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mentioned",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "build_failed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "marked",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "approval_required",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "unmergeable",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "directly_addressed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "TodoTargetEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "COMMIT",
+ "description": "A Commit",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ISSUE",
+ "description": "An Issue",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MERGEREQUEST",
+ "description": "A MergeRequest",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "DESIGN",
+ "description": "A Design",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "EPIC",
+ "description": "An Epic",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "TodoStateEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "pending",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "done",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Discussion",
+ "description": null,
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes in the discussion",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "replyId",
+ "description": "The ID used to reply to this discussion",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DiffPosition",
+ "description": null,
+ "fields": [
+ {
+ "name": "diffRefs",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DiffRefs",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "filePath",
+ "description": "The path of the file that was changed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "height",
+ "description": "The total height of the image",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "newLine",
+ "description": "The line on head sha that was changed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "newPath",
+ "description": "The path of the file on the head sha.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "oldLine",
+ "description": "The line on start sha that was changed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "oldPath",
+ "description": "The path of the file on the start sha.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "positionType",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "DiffPositionType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "width",
+ "description": "The total width of the image",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "x",
+ "description": "The X postion on which the comment was made",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "y",
+ "description": "The Y position on which the comment was made",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DiffRefs",
+ "description": null,
+ "fields": [
+ {
+ "name": "baseSha",
+ "description": "The merge base of the branch the comment was made on",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "headSha",
+ "description": "The sha of the head at the time the comment was made",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startSha",
+ "description": "The sha of the branch being compared against",
+ "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": "ENUM",
+ "name": "DiffPositionType",
+ "description": "Type of file the position refers to",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "text",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "image",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DiscussionConnection",
+ "description": "The connection type for Discussion.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DiscussionEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Discussion",
+ "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": "DiscussionEdge",
+ "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": "Discussion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicPermissions",
+ "description": "Check permissions for the current user on an epic",
+ "fields": [
+ {
+ "name": "adminEpic",
+ "description": "Whether or not a user can perform `admin_epic` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "awardEmoji",
+ "description": "Whether or not a user can perform `award_emoji` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createEpic",
+ "description": "Whether or not a user can perform `create_epic` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createNote",
+ "description": "Whether or not a user can perform `create_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyEpic",
+ "description": "Whether or not a user can perform `destroy_epic` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readEpic",
+ "description": "Whether or not a user can perform `read_epic` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readEpicIid",
+ "description": "Whether or not a user can perform `read_epic_iid` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateEpic",
+ "description": "Whether or not a user can perform `update_epic` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "EpicState",
+ "description": "State of a GitLab epic",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "all",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "opened",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicConnection",
+ "description": "The connection type for Epic.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "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": "EpicEdge",
+ "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": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "EpicSort",
+ "description": "Roadmap sort values",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "start_date_desc",
+ "description": "Start date at descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "start_date_asc",
+ "description": "Start date at ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "end_date_desc",
+ "description": "End date at descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "end_date_asc",
+ "description": "End date at ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "LabelConnection",
+ "description": "The connection type for Label.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "LabelEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Label",
+ "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": "LabelEdge",
+ "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": "Label",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Label",
+ "description": null,
+ "fields": [
+ {
+ "name": "color",
+ "description": "Background color of the label",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the label (markdown rendered as HTML for caching)",
+ "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": "id",
+ "description": "Label ID",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "textColor",
+ "description": "Text color of the label",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Content of the label",
+ "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": "UserConnection",
+ "description": "The connection type for User.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "UserEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "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": "UserEdge",
+ "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": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicIssueConnection",
+ "description": "The connection type for EpicIssue.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicIssueEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "EpicIssue",
+ "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": "EpicIssueEdge",
+ "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": "EpicIssue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicIssue",
+ "description": null,
+ "fields": [
+ {
+ "name": "assignees",
+ "description": "Assignees of the issue",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "author",
+ "description": "User that created the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closedAt",
+ "description": "Timestamp of when the issue was closed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "confidential",
+ "description": "Indicates the issue is confidential",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp of when the issue was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the issue",
+ "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": "designCollection",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignCollection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "designs",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignCollection",
+ "ofType": null
+ },
+ "isDeprecated": true,
+ "deprecationReason": "use design_collection"
+ },
+ {
+ "name": "discussionLocked",
+ "description": "Indicates discussion is locked on the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "downvotes",
+ "description": "Number of downvotes the issue has received",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDate",
+ "description": "Due date of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epic",
+ "description": "The epic to which issue belongs",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epicIssueId",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "The global id of the epic-issue relation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": "Internal ID of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "labels",
+ "description": "Labels of the issue",
+ "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": "LabelConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "milestone",
+ "description": "Milestone of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Milestone",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "participants",
+ "description": "List of participants in the issue",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reference",
+ "description": "Internal reference of the issue. Returned in shortened format by default",
+ "args": [
+ {
+ "name": "full",
+ "description": "Boolean option specifying whether the reference should be returned in full",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "relationPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "relativePosition",
+ "description": "Relative position of the issue (used for positioning in epic tree and issue boards)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "IssueState",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subscribed",
+ "description": "Boolean flag for whether the currently logged in user is subscribed to this issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "taskCompletionStatus",
+ "description": "Task completion status of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TaskCompletionStatus",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "timeEstimate",
+ "description": "Time estimate of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "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": "totalTimeSpent",
+ "description": "Total time reported as spent on the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp of when the issue was last updated",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "upvotes",
+ "description": "Number of upvotes the issue has received",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userNotesCount",
+ "description": "Number of user notes of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "IssuePermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webPath",
+ "description": "Web path of the issue",
+ "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 issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "weight",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IssuePermissions",
+ "description": "Check permissions for the current user on a issue",
+ "fields": [
+ {
+ "name": "adminIssue",
+ "description": "Whether or not a user can perform `admin_issue` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createDesign",
+ "description": "Whether or not a user can perform `create_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createNote",
+ "description": "Whether or not a user can perform `create_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyDesign",
+ "description": "Whether or not a user can perform `destroy_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readDesign",
+ "description": "Whether or not a user can perform `read_design` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readIssue",
+ "description": "Whether or not a user can perform `read_issue` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reopenIssue",
+ "description": "Whether or not a user can perform `reopen_issue` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateIssue",
+ "description": "Whether or not a user can perform `update_issue` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "IssueState",
+ "description": "State of a GitLab issue",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "opened",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "locked",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Milestone",
+ "description": null,
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "Timestamp of milestone 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 milestone",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDate",
+ "description": "Timestamp of the milestone due date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the milestone",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDate",
+ "description": "Timestamp of the milestone start date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the milestone",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the milestone",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp of last milestone update",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TaskCompletionStatus",
+ "description": "Completion status of tasks",
+ "fields": [
+ {
+ "name": "completedCount",
+ "description": "Number of completed tasks",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "count",
+ "description": "Number of total tasks",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignCollection",
+ "description": null,
+ "fields": [
+ {
+ "name": "designs",
+ "description": "All designs for this collection",
+ "args": [
+ {
+ "name": "ids",
+ "description": "Filters designs by their ID",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filenames",
+ "description": "Filters designs by their filename",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "atVersion",
+ "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",
+ "type": {
+ "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": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "versions",
+ "description": "All versions related to all designs ordered newest first",
+ "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": "DesignVersionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "description": null,
+ "fields": [
+ {
+ "name": "assignees",
+ "description": "Assignees of the issue",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "author",
+ "description": "User that created the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closedAt",
+ "description": "Timestamp of when the issue was closed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "confidential",
+ "description": "Indicates the issue is confidential",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp of when the issue was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the issue",
+ "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": "designCollection",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignCollection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "designs",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignCollection",
+ "ofType": null
+ },
+ "isDeprecated": true,
+ "deprecationReason": "use design_collection"
+ },
+ {
+ "name": "discussionLocked",
+ "description": "Indicates discussion is locked on the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "downvotes",
+ "description": "Number of downvotes the issue has received",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDate",
+ "description": "Due date of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epic",
+ "description": "The epic to which issue belongs",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": "Internal ID of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "labels",
+ "description": "Labels of the issue",
+ "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": "LabelConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "milestone",
+ "description": "Milestone of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Milestone",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "participants",
+ "description": "List of participants in the issue",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reference",
+ "description": "Internal reference of the issue. Returned in shortened format by default",
+ "args": [
+ {
+ "name": "full",
+ "description": "Boolean option specifying whether the reference should be returned in full",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "relativePosition",
+ "description": "Relative position of the issue (used for positioning in epic tree and issue boards)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "IssueState",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subscribed",
+ "description": "Boolean flag for whether the currently logged in user is subscribed to this issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "taskCompletionStatus",
+ "description": "Task completion status of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TaskCompletionStatus",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "timeEstimate",
+ "description": "Time estimate of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "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": "totalTimeSpent",
+ "description": "Total time reported as spent on the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp of when the issue was last updated",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "upvotes",
+ "description": "Number of upvotes the issue has received",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userNotesCount",
+ "description": "Number of user notes of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "IssuePermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webPath",
+ "description": "Web path of the issue",
+ "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 issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "weight",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignConnection",
+ "description": "The connection type for Design.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Design",
+ "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": "DesignEdge",
+ "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": "Design",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Design",
+ "description": null,
+ "fields": [
+ {
+ "name": "diffRefs",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DiffRefs",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "event",
+ "description": "The change that happened to the design at this version",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "DesignVersionEvent",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "filename",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "image",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notesCount",
+ "description": "The total count of user-created notes for this design",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "versions",
+ "description": "All versions related to this design ordered newest first",
+ "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": "DesignVersionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "DesignVersionEvent",
+ "description": "Mutation event of a Design within a Version",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "NONE",
+ "description": "No change",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CREATION",
+ "description": "A creation event",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MODIFICATION",
+ "description": "A modification event",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "DELETION",
+ "description": "A deletion event",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignVersionConnection",
+ "description": "The connection type for DesignVersion.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignVersionEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "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": "DesignVersionEdge",
+ "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": "DesignVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "description": null,
+ "fields": [
+ {
+ "name": "designs",
+ "description": "All designs that were changed in this version",
+ "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": "DesignConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicDescendantCount",
+ "description": null,
+ "fields": [
+ {
+ "name": "closedEpics",
+ "description": "Number of closed sub-epics",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closedIssues",
+ "description": "Number of closed epic issues",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "openedEpics",
+ "description": "Number of opened sub-epics",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "openedIssues",
+ "description": "Number of opened epic issues",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ProjectStatistics",
+ "description": null,
+ "fields": [
+ {
+ "name": "buildArtifactsSize",
+ "description": "Build artifacts size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "commitCount",
+ "description": "Commit count of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsObjectsSize",
+ "description": "Large File Storage (LFS) object size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "packagesSize",
+ "description": "Packages size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "repositorySize",
+ "description": "Repository size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "storageSize",
+ "description": "Storage size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "wikiSize",
+ "description": "Wiki size of the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Repository",
+ "description": null,
+ "fields": [
+ {
+ "name": "empty",
+ "description": "Indicates repository has no visible content",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "exists",
+ "description": "Indicates a corresponding Git repository exists on disk",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "rootRef",
+ "description": "Default branch of the repository",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tree",
+ "description": "Tree of the repository",
+ "args": [
+ {
+ "name": "path",
+ "description": "The path to get the tree for. Default value is the root of the repository",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": "\"\""
+ },
+ {
+ "name": "ref",
+ "description": "The commit ref to get the tree for. Default value is HEAD",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": "\"head\""
+ },
+ {
+ "name": "recursive",
+ "description": "Used to get a recursive tree. Default is false",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Tree",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Tree",
+ "description": null,
+ "fields": [
+ {
+ "name": "blobs",
+ "description": null,
+ "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": "BlobConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastCommit",
+ "description": "Last commit for the tree",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Commit",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "submodules",
+ "description": null,
+ "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": "SubmoduleConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "trees",
+ "description": null,
+ "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": "TreeEntryConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Commit",
+ "description": null,
+ "fields": [
+ {
+ "name": "author",
+ "description": "Author of the commit",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "authorName",
+ "description": "Commit authors name",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "authoredDate",
+ "description": "Timestamp of when the commit was authored",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the commit message",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID (global ID) of the commit",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "latestPipeline",
+ "description": "Latest pipeline of the commit",
+ "args": [
+ {
+ "name": "status",
+ "description": "Filter pipelines by their status",
+ "type": {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "ref",
+ "description": "Filter pipelines by the ref they are run for",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sha",
+ "description": "Filter pipelines by the sha of the commit they are run for",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Pipeline",
+ "ofType": null
+ },
+ "isDeprecated": true,
+ "deprecationReason": "use pipelines"
+ },
+ {
+ "name": "message",
+ "description": "Raw commit message",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pipelines",
+ "description": "Pipelines of the commit ordered latest first",
+ "args": [
+ {
+ "name": "status",
+ "description": "Filter pipelines by their status",
+ "type": {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "ref",
+ "description": "Filter pipelines by the ref they are run for",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sha",
+ "description": "Filter pipelines by the sha of the commit they are run for",
+ "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": "PipelineConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": "SHA1 ID of the commit",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "signatureHtml",
+ "description": "Rendered HTML of the commit signature",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the commit message",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": "Web URL of the commit",
+ "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": "PipelineConnection",
+ "description": "The connection type for Pipeline.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PipelineEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Pipeline",
+ "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": "PipelineEdge",
+ "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": "Pipeline",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Pipeline",
+ "description": null,
+ "fields": [
+ {
+ "name": "beforeSha",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "committedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "coverage",
+ "description": "Coverage percentage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Float",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "detailedStatus",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DetailedStatus",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "duration",
+ "description": "Duration of the pipeline in seconds",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "finishedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "status",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PipelinePermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PipelinePermissions",
+ "description": null,
+ "fields": [
+ {
+ "name": "adminPipeline",
+ "description": "Whether or not a user can perform `admin_pipeline` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyPipeline",
+ "description": "Whether or not a user can perform `destroy_pipeline` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatePipeline",
+ "description": "Whether or not a user can perform `update_pipeline` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "CREATED",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "PREPARING",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "PENDING",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "RUNNING",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FAILED",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SUCCESS",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CANCELED",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SKIPPED",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MANUAL",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SCHEDULED",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DetailedStatus",
+ "description": null,
+ "fields": [
+ {
+ "name": "detailsPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "favicon",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "group",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasDetails",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "icon",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "label",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "text",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tooltip",
+ "description": null,
+ "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": "SCALAR",
+ "name": "Float",
+ "description": "Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TreeEntryConnection",
+ "description": "The connection type for TreeEntry.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TreeEntryEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TreeEntry",
+ "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": "TreeEntryEdge",
+ "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": "TreeEntry",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TreeEntry",
+ "description": "Represents a directory",
+ "fields": [
+ {
+ "name": "flatPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": "Last commit sha for entry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EntryType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Entry",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INTERFACE",
+ "name": "Entry",
+ "description": null,
+ "fields": [
+ {
+ "name": "flatPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": "Last commit sha for entry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EntryType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": [
+ {
+ "kind": "OBJECT",
+ "name": "Blob",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Submodule",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TreeEntry",
+ "ofType": null
+ }
+ ]
+ },
+ {
+ "kind": "ENUM",
+ "name": "EntryType",
+ "description": "Type of a tree entry",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "tree",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "blob",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "commit",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "SubmoduleConnection",
+ "description": "The connection type for Submodule.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "SubmoduleEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Submodule",
+ "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": "SubmoduleEdge",
+ "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": "Submodule",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Submodule",
+ "description": null,
+ "fields": [
+ {
+ "name": "flatPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": "Last commit sha for entry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "treeUrl",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EntryType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Entry",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "BlobConnection",
+ "description": "The connection type for Blob.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "BlobEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Blob",
+ "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": "BlobEdge",
+ "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": "Blob",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Blob",
+ "description": null,
+ "fields": [
+ {
+ "name": "flatPath",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lfsOid",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sha",
+ "description": "Last commit sha for entry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "EntryType",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Entry",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequestConnection",
+ "description": "The connection type for MergeRequest.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MergeRequestEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "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": "MergeRequestEdge",
+ "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": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "description": null,
+ "fields": [
+ {
+ "name": "allowCollaboration",
+ "description": "Indicates if members of the target project can push to the fork",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "assignees",
+ "description": "Assignees of the merge request",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp of when the merge request was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "defaultMergeCommitMessage",
+ "description": "Default merge commit message of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the merge request (markdown rendered as HTML for caching)",
+ "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": "diffHeadSha",
+ "description": "Diff head SHA of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "diffRefs",
+ "description": "References of the base SHA, the head SHA, and the start SHA for this merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DiffRefs",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussionLocked",
+ "description": "Indicates if comments on the merge request are locked to members only",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "discussions",
+ "description": "All discussions on this noteable",
+ "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": "DiscussionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "downvotes",
+ "description": "Number of downvotes for the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "forceRemoveSourceBranch",
+ "description": "Indicates if the project settings will lead to source branch deletion after merge",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "headPipeline",
+ "description": "The pipeline running on the branch HEAD of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Pipeline",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": "Internal ID of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "inProgressMergeCommitSha",
+ "description": "Commit SHA of the merge request if merge is in progress",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "labels",
+ "description": "Labels of the merge request",
+ "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": "LabelConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeCommitMessage",
+ "description": "Deprecated - renamed to defaultMergeCommitMessage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Renamed to defaultMergeCommitMessage"
+ },
+ {
+ "name": "mergeCommitSha",
+ "description": "SHA of the merge request commit (set once merged)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeError",
+ "description": "Error message due to a merge error",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeOngoing",
+ "description": "Indicates if a merge is currently occurring",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeStatus",
+ "description": "Status of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeWhenPipelineSucceeds",
+ "description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeableDiscussionsState",
+ "description": "Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "milestone",
+ "description": "The milestone of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Milestone",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notes",
+ "description": "All notes on this noteable",
+ "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": "NoteConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "participants",
+ "description": "Participants in the merge request",
+ "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": "UserConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pipelines",
+ "description": "Pipelines for the merge request",
+ "args": [
+ {
+ "name": "status",
+ "description": "Filter pipelines by their status",
+ "type": {
+ "kind": "ENUM",
+ "name": "PipelineStatusEnum",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "ref",
+ "description": "Filter pipelines by the ref they are run for",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sha",
+ "description": "Filter pipelines by the sha of the commit they are run for",
+ "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": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PipelineConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "Alias for target_project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "projectId",
+ "description": "ID of the merge request project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "rebaseCommitSha",
+ "description": "Rebase commit SHA of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "rebaseInProgress",
+ "description": "Indicates if there is a rebase currently in progress for the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "reference",
+ "description": "Internal reference of the merge request. Returned in shortened format by default",
+ "args": [
+ {
+ "name": "full",
+ "description": "Boolean option specifying whether the reference should be returned in full",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "shouldBeRebased",
+ "description": "Indicates if the merge request will be rebased",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "shouldRemoveSourceBranch",
+ "description": "Indicates if the source branch of the merge request will be deleted after merge",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sourceBranch",
+ "description": "Source branch of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sourceBranchExists",
+ "description": "Indicates if the source branch of the merge request exists",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sourceProject",
+ "description": "Source project of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "sourceProjectId",
+ "description": "ID of the merge request source project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "MergeRequestState",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subscribed",
+ "description": "Indicates if the currently logged in user is subscribed to this merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "targetBranch",
+ "description": "Target branch of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "targetProject",
+ "description": "Target project of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "targetProjectId",
+ "description": "ID of the merge request target project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "taskCompletionStatus",
+ "description": "Completion status of tasks",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "TaskCompletionStatus",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "timeEstimate",
+ "description": "Time estimate of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "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": "totalTimeSpent",
+ "description": "Total time reported as spent on the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp of when the merge request was last updated",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "upvotes",
+ "description": "Number of upvotes for the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userNotesCount",
+ "description": "User notes count of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "userPermissions",
+ "description": "Permissions for the current user on the resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "MergeRequestPermissions",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": "Web URL of the merge request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "workInProgress",
+ "description": "Indicates if the merge request is a work in progress (WIP)",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "Noteable",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequestPermissions",
+ "description": "Check permissions for the current user on a merge request",
+ "fields": [
+ {
+ "name": "adminMergeRequest",
+ "description": "Whether or not a user can perform `admin_merge_request` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "cherryPickOnCurrentMergeRequest",
+ "description": "Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createNote",
+ "description": "Whether or not a user can perform `create_note` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pushToSourceBranch",
+ "description": "Whether or not a user can perform `push_to_source_branch` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "readMergeRequest",
+ "description": "Whether or not a user can perform `read_merge_request` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "removeSourceBranch",
+ "description": "Whether or not a user can perform `remove_source_branch` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "revertOnCurrentMergeRequest",
+ "description": "Whether or not a user can perform `revert_on_current_merge_request` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateMergeRequest",
+ "description": "Whether or not a user can perform `update_merge_request` on this resource",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "MergeRequestState",
+ "description": "State of a GitLab merge request",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "opened",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "locked",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "merged",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IssueConnection",
+ "description": "The connection type for Issue.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "IssueEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "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": "IssueEdge",
+ "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": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "IssuableState",
+ "description": "State of a GitLab issue or merge request",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "opened",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "locked",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "IssueSort",
+ "description": "Values for sorting issues",
+ "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": "DUE_DATE_ASC",
+ "description": "Due date by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "DUE_DATE_DESC",
+ "description": "Due date by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "RELATIVE_POSITION_ASC",
+ "description": "Relative position by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Metadata",
+ "description": null,
+ "fields": [
+ {
+ "name": "revision",
+ "description": "Revision",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "Version",
+ "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": "Mutation",
+ "description": null,
+ "fields": [
+ {
+ "name": "addAwardEmoji",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "AddAwardEmojiInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AddAwardEmojiPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createDiffNote",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateDiffNoteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateDiffNotePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createEpic",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateEpicInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateEpicPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createImageDiffNote",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateImageDiffNoteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateImageDiffNotePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createNote",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateNoteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateNotePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "designManagementDelete",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DesignManagementDeleteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignManagementDeletePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "designManagementUpload",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DesignManagementUploadInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignManagementUploadPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "destroyNote",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DestroyNoteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DestroyNotePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epicSetSubscription",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "EpicSetSubscriptionInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicSetSubscriptionPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "epicTreeReorder",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "EpicTreeReorderInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "EpicTreeReorderPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetAssignees",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetAssigneesInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetAssigneesPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetLabels",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetLabelsInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetLabelsPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetLocked",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetLockedInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetLockedPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetMilestone",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetMilestoneInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetMilestonePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetSubscription",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetSubscriptionInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetSubscriptionPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mergeRequestSetWip",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetWipInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetWipPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "removeAwardEmoji",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "RemoveAwardEmojiInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RemoveAwardEmojiPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "todoMarkDone",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "TodoMarkDoneInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "TodoMarkDonePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "toggleAwardEmoji",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "ToggleAwardEmojiInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ToggleAwardEmojiPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateEpic",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateEpicInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UpdateEpicPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updateNote",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateNoteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UpdateNotePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "AddAwardEmojiPayload",
+ "description": "Autogenerated return type of AddAwardEmoji",
+ "fields": [
+ {
+ "name": "awardEmoji",
+ "description": "The award emoji after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AwardEmoji",
+ "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": "Reasons why the mutation failed.",
+ "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": "OBJECT",
+ "name": "AwardEmoji",
+ "description": null,
+ "fields": [
+ {
+ "name": "description",
+ "description": "The emoji description",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "emoji",
+ "description": "The emoji as an icon",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The emoji name",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "unicode",
+ "description": "The emoji in unicode",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "unicodeVersion",
+ "description": "The unicode version for this emoji",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "user",
+ "description": "The user who awarded the emoji",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "AddAwardEmojiInput",
+ "description": "Autogenerated input type of AddAwardEmoji",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "awardableId",
+ "description": "The global id of the awardable resource",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "name",
+ "description": "The emoji name",
+ "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": "RemoveAwardEmojiPayload",
+ "description": "Autogenerated return type of RemoveAwardEmoji",
+ "fields": [
+ {
+ "name": "awardEmoji",
+ "description": "The award emoji after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AwardEmoji",
+ "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": "Reasons why the mutation failed.",
+ "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": "RemoveAwardEmojiInput",
+ "description": "Autogenerated input type of RemoveAwardEmoji",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "awardableId",
+ "description": "The global id of the awardable resource",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "name",
+ "description": "The emoji name",
+ "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": "ToggleAwardEmojiPayload",
+ "description": "Autogenerated return type of ToggleAwardEmoji",
+ "fields": [
+ {
+ "name": "awardEmoji",
+ "description": "The award emoji after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AwardEmoji",
+ "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": "Reasons why the mutation failed.",
+ "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": "toggledOn",
+ "description": "True when the emoji was awarded, false when it was removed",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "ToggleAwardEmojiInput",
+ "description": "Autogenerated input type of ToggleAwardEmoji",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "awardableId",
+ "description": "The global id of the awardable resource",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "name",
+ "description": "The emoji name",
+ "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": "MergeRequestSetLabelsPayload",
+ "description": "Autogenerated return type of MergeRequestSetLabels",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetLabelsInput",
+ "description": "Autogenerated input type of MergeRequestSetLabels",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "labelIds",
+ "description": "The Label IDs to set. Replaces existing labels by default.\n",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "operationMode",
+ "description": "Changes the operation mode. Defaults to REPLACE.\n",
+ "type": {
+ "kind": "ENUM",
+ "name": "MutationOperationMode",
+ "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": "ENUM",
+ "name": "MutationOperationMode",
+ "description": "Different toggles for changing mutator behavior.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "REPLACE",
+ "description": "Performs a replace operation",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "APPEND",
+ "description": "Performs an append operation",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "REMOVE",
+ "description": "Performs a removal operation",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "MergeRequestSetLockedPayload",
+ "description": "Autogenerated return type of MergeRequestSetLocked",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetLockedInput",
+ "description": "Autogenerated input type of MergeRequestSetLocked",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "locked",
+ "description": "Whether or not to lock the merge request.\n",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "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": "MergeRequestSetMilestonePayload",
+ "description": "Autogenerated return type of MergeRequestSetMilestone",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetMilestoneInput",
+ "description": "Autogenerated input type of MergeRequestSetMilestone",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "milestoneId",
+ "description": "The milestone to assign to the merge request.\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": "MergeRequestSetSubscriptionPayload",
+ "description": "Autogenerated return type of MergeRequestSetSubscription",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetSubscriptionInput",
+ "description": "Autogenerated input type of MergeRequestSetSubscription",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "subscribedState",
+ "description": "The desired state of the subscription",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "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": "MergeRequestSetWipPayload",
+ "description": "Autogenerated return type of MergeRequestSetWip",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetWipInput",
+ "description": "Autogenerated input type of MergeRequestSetWip",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "wip",
+ "description": "Whether or not to set the merge request as a WIP.\n",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "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": "MergeRequestSetAssigneesPayload",
+ "description": "Autogenerated return type of MergeRequestSetAssignees",
+ "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": "Reasons why the mutation failed.",
+ "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": "mergeRequest",
+ "description": "The merge request after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MergeRequest",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "MergeRequestSetAssigneesInput",
+ "description": "Autogenerated input type of MergeRequestSetAssignees",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the merge request 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 merge request to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "assigneeUsernames",
+ "description": "The usernames to assign to the merge request. Replaces existing assignees by default.\n",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "operationMode",
+ "description": "The operation to perform. Defaults to REPLACE.\n",
+ "type": {
+ "kind": "ENUM",
+ "name": "MutationOperationMode",
+ "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": "CreateNotePayload",
+ "description": "Autogenerated return type of CreateNote",
+ "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": "Reasons why the mutation failed.",
+ "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": "note",
+ "description": "The note after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "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,
+ "inputFields": [
+ {
+ "name": "noteableId",
+ "description": "The global id of the resource to add a note to",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "body",
+ "description": "The content note itself",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "discussionId",
+ "description": "The global id of the discussion this note is in reply to",
+ "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": "CreateDiffNotePayload",
+ "description": "Autogenerated return type of CreateDiffNote",
+ "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": "Reasons why the mutation failed.",
+ "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": "note",
+ "description": "The note after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "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,
+ "inputFields": [
+ {
+ "name": "noteableId",
+ "description": "The global id of the resource to add a note to",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "body",
+ "description": "The content note itself",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "position",
+ "description": "The position of this note on a diff",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DiffPositionInput",
+ "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": "INPUT_OBJECT",
+ "name": "DiffPositionInput",
+ "description": null,
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "headSha",
+ "description": "The sha of the head at the time the comment was made",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "baseSha",
+ "description": "The merge base of the branch the comment was made on",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startSha",
+ "description": "The sha of the branch being compared against",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "paths",
+ "description": "The paths of the file that was changed. Both of the properties of this input are optional, but at least one of them is required",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DiffPathsInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "oldLine",
+ "description": "The line on start sha that was changed",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "newLine",
+ "description": "The line on head sha that was changed",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "DiffPathsInput",
+ "description": null,
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "oldPath",
+ "description": "The path of the file on the start sha",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "newPath",
+ "description": "The path of the file on the head sha",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateImageDiffNotePayload",
+ "description": "Autogenerated return type of CreateImageDiffNote",
+ "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": "Reasons why the mutation failed.",
+ "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": "note",
+ "description": "The note after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateImageDiffNoteInput",
+ "description": "Autogenerated input type of CreateImageDiffNote",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "noteableId",
+ "description": "The global id of the resource to add a note to",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "body",
+ "description": "The content note itself",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "position",
+ "description": "The position of this note on a diff",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DiffImagePositionInput",
+ "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": "INPUT_OBJECT",
+ "name": "DiffImagePositionInput",
+ "description": null,
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "headSha",
+ "description": "The sha of the head at the time the comment was made",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "baseSha",
+ "description": "The merge base of the branch the comment was made on",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startSha",
+ "description": "The sha of the branch being compared against",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "paths",
+ "description": "The paths of the file that was changed. Both of the properties of this input are optional, but at least one of them is required",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "DiffPathsInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "x",
+ "description": "The X postion on which the comment was made",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "y",
+ "description": "The Y position on which the comment was made",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "width",
+ "description": "The total width of the image",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "height",
+ "description": "The total height of the image",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "UpdateNotePayload",
+ "description": "Autogenerated return type of UpdateNote",
+ "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": "Reasons why the mutation failed.",
+ "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": "note",
+ "description": "The note after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateNoteInput",
+ "description": "Autogenerated input type of UpdateNote",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The global id of the note to update",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "body",
+ "description": "The content note itself",
+ "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": "DestroyNotePayload",
+ "description": "Autogenerated return type of DestroyNote",
+ "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": "Reasons why the mutation failed.",
+ "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": "note",
+ "description": "The note after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Note",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "DestroyNoteInput",
+ "description": "Autogenerated input type of DestroyNote",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The global id of the note to destroy",
+ "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": "TodoMarkDonePayload",
+ "description": "Autogenerated return type of TodoMarkDone",
+ "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": "Reasons why the mutation failed.",
+ "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": "todo",
+ "description": "The requested todo",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Todo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "TodoMarkDoneInput",
+ "description": "Autogenerated input type of TodoMarkDone",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The global id of the todo to mark as done",
+ "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": "DesignManagementUploadPayload",
+ "description": "Autogenerated return type of DesignManagementUpload",
+ "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": "designs",
+ "description": "The designs that were uploaded by the mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "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": "skippedDesigns",
+ "description": "Any designs that were skipped from the upload due to there being no change to their content since their last version",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "DesignManagementUploadInput",
+ "description": "Autogenerated input type of DesignManagementUpload",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project where the issue is to upload designs for",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the issue to modify designs for",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "files",
+ "description": "The files to upload",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Upload",
+ "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": "SCALAR",
+ "name": "Upload",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignManagementDeletePayload",
+ "description": "Autogenerated return type of DesignManagementDelete",
+ "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": "Reasons why the mutation failed.",
+ "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": "version",
+ "description": "The new version in which the designs are deleted",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "DesignManagementDeleteInput",
+ "description": "Autogenerated input type of DesignManagementDelete",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project where the issue is to upload designs for",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the issue to modify designs for",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filenames",
+ "description": "The filenames of the designs to delete",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "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": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "EpicTreeReorderPayload",
+ "description": "Autogenerated return type of EpicTreeReorder",
+ "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": "Reasons why the mutation failed.",
+ "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": "EpicTreeReorderInput",
+ "description": "Autogenerated input type of EpicTreeReorder",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "baseEpicId",
+ "description": "The id of the base epic of the tree",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "moved",
+ "description": "Parameters for updating the tree positions",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "EpicTreeNodeFieldsInputType",
+ "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": "INPUT_OBJECT",
+ "name": "EpicTreeNodeFieldsInputType",
+ "description": null,
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "The id of the epic_issue or epic that is being moved",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "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
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "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
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "MoveType",
+ "description": "The position the adjacent object should be moved.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "before",
+ "description": "The adjacent object will be moved before the object that is being moved.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "after",
+ "description": "The adjacent object will be moved after the object that is being moved.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "UpdateEpicPayload",
+ "description": "Autogenerated return type of UpdateEpic",
+ "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": "epic",
+ "description": "The epic after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "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": "UpdateEpicInput",
+ "description": "Autogenerated input type of UpdateEpic",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "groupPath",
+ "description": "The group the epic to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "title",
+ "description": "The title of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "The description of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDateFixed",
+ "description": "The start date of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dueDateFixed",
+ "description": "The end date of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDateIsFixed",
+ "description": "Indicates start date should be sourced from start_date_fixed field not the issue milestones",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dueDateIsFixed",
+ "description": "Indicates end date should be sourced from due_date_fixed field not the issue milestones",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "addLabelIds",
+ "description": "The IDs of labels to be added to the epic.",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "removeLabelIds",
+ "description": "The IDs of labels to be removed from the epic.",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the epic to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "stateEvent",
+ "description": "State event for the epic",
+ "type": {
+ "kind": "ENUM",
+ "name": "EpicStateEvent",
+ "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": "ENUM",
+ "name": "EpicStateEvent",
+ "description": "State event of a GitLab Epic",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "REOPEN",
+ "description": "Reopen the Epic",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CLOSE",
+ "description": "Close the Epic",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateEpicPayload",
+ "description": "Autogenerated return type of CreateEpic",
+ "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": "epic",
+ "description": "The created epic",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "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": "CreateEpicInput",
+ "description": "Autogenerated input type of CreateEpic",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "groupPath",
+ "description": "The group the epic to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "title",
+ "description": "The title of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "The description of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDateFixed",
+ "description": "The start date of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dueDateFixed",
+ "description": "The end date of the epic",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDateIsFixed",
+ "description": "Indicates start date should be sourced from start_date_fixed field not the issue milestones",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dueDateIsFixed",
+ "description": "Indicates end date should be sourced from due_date_fixed field not the issue milestones",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "addLabelIds",
+ "description": "The IDs of labels to be added to the epic.",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "removeLabelIds",
+ "description": "The IDs of labels to be removed from the epic.",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "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": "EpicSetSubscriptionPayload",
+ "description": "Autogenerated return type of EpicSetSubscription",
+ "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": "epic",
+ "description": "The epic after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Epic",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "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": "EpicSetSubscriptionInput",
+ "description": "Autogenerated input type of EpicSetSubscription",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "groupPath",
+ "description": "The group the epic to (un)subscribe is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the epic to (un)subscribe",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "subscribedState",
+ "description": "The desired state of the subscription",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "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": "__Schema",
+ "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",
+ "fields": [
+ {
+ "name": "directives",
+ "description": "A list of all directives supported by this server.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Directive",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "mutationType",
+ "description": "If this server supports mutation, the type that mutation operations will be rooted at.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "queryType",
+ "description": "The type that query operations will be rooted at.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "subscriptionType",
+ "description": "If this server support subscription, the type that subscription operations will be rooted at.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "types",
+ "description": "A list of all types supported by this server.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",
+ "fields": [
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "enumValues",
+ "description": null,
+ "args": [
+ {
+ "name": "includeDeprecated",
+ "description": null,
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__EnumValue",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fields",
+ "description": null,
+ "args": [
+ {
+ "name": "includeDeprecated",
+ "description": null,
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ }
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Field",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "inputFields",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__InputValue",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "interfaces",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "kind",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "__TypeKind",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ofType",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "possibleTypes",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "__Field",
+ "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.",
+ "fields": [
+ {
+ "name": "args",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__InputValue",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "deprecationReason",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "isDeprecated",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "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": [
+ {
+ "name": "args",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__InputValue",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "locations",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "__DirectiveLocation",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "onField",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Use `locations`."
+ },
+ {
+ "name": "onFragment",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Use `locations`."
+ },
+ {
+ "name": "onOperation",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": true,
+ "deprecationReason": "Use `locations`."
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "__EnumValue",
+ "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.",
+ "fields": [
+ {
+ "name": "deprecationReason",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "isDeprecated",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "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": "__InputValue",
+ "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.",
+ "fields": [
+ {
+ "name": "defaultValue",
+ "description": "A GraphQL-formatted string representing the default value for this input value.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "type",
+ "description": null,
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "__Type",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "__TypeKind",
+ "description": "An enum describing what kind of type a given `__Type` is.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "SCALAR",
+ "description": "Indicates this type is a scalar.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "OBJECT",
+ "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INTERFACE",
+ "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "UNION",
+ "description": "Indicates this type is a union. `possibleTypes` is a valid field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ENUM",
+ "description": "Indicates this type is an enum. `enumValues` is a valid field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INPUT_OBJECT",
+ "description": "Indicates this type is an input object. `inputFields` is a valid field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "LIST",
+ "description": "Indicates this type is a list. `ofType` is a valid field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "NON_NULL",
+ "description": "Indicates this type is a non-null. `ofType` is a valid field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "__DirectiveLocation",
+ "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "QUERY",
+ "description": "Location adjacent to a query operation.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MUTATION",
+ "description": "Location adjacent to a mutation operation.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SUBSCRIPTION",
+ "description": "Location adjacent to a subscription operation.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FIELD",
+ "description": "Location adjacent to a field.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FRAGMENT_DEFINITION",
+ "description": "Location adjacent to a fragment definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FRAGMENT_SPREAD",
+ "description": "Location adjacent to a fragment spread.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INLINE_FRAGMENT",
+ "description": "Location adjacent to an inline fragment.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SCHEMA",
+ "description": "Location adjacent to a schema definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SCALAR",
+ "description": "Location adjacent to a scalar definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "OBJECT",
+ "description": "Location adjacent to an object type definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FIELD_DEFINITION",
+ "description": "Location adjacent to a field definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ARGUMENT_DEFINITION",
+ "description": "Location adjacent to an argument definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INTERFACE",
+ "description": "Location adjacent to an interface definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "UNION",
+ "description": "Location adjacent to a union definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ENUM",
+ "description": "Location adjacent to an enum definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ENUM_VALUE",
+ "description": "Location adjacent to an enum value definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INPUT_OBJECT",
+ "description": "Location adjacent to an input object type definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INPUT_FIELD_DEFINITION",
+ "description": "Location adjacent to an input object field definition.",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ }
+ ],
+ "directives": [
+ {
+ "name": "include",
+ "description": "Directs the executor to include this field or fragment only when the `if` argument is true.",
+ "locations": [
+ "FIELD",
+ "FRAGMENT_SPREAD",
+ "INLINE_FRAGMENT"
+ ],
+ "args": [
+ {
+ "name": "if",
+ "description": "Included when true.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ]
+ },
+ {
+ "name": "skip",
+ "description": "Directs the executor to skip this field or fragment when the `if` argument is true.",
+ "locations": [
+ "FIELD",
+ "FRAGMENT_SPREAD",
+ "INLINE_FRAGMENT"
+ ],
+ "args": [
+ {
+ "name": "if",
+ "description": "Skipped when true.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ]
+ },
+ {
+ "name": "deprecated",
+ "description": "Marks an element of a GraphQL schema as no longer supported.",
+ "locations": [
+ "FIELD_DEFINITION",
+ "ENUM_VALUE"
+ ],
+ "args": [
+ {
+ "name": "reason",
+ "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/).",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": "\"No longer supported\""
+ }
+ ]
+ }
+ ]
+ }
+ }
+} \ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 839289cf677..151e43f4cff 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -36,6 +36,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | |
+| `sha` | String! | Last commit sha for entry |
| `name` | String! | |
| `type` | EntryType! | |
| `path` | String! | |
@@ -47,16 +48,17 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `id` | ID! | |
-| `sha` | String! | |
-| `title` | String | |
-| `description` | String | |
-| `message` | String | |
-| `authoredDate` | Time | |
-| `webUrl` | String! | |
-| `signatureHtml` | String | Rendered html for the commit signature |
-| `author` | User | |
-| `latestPipeline` | Pipeline | Latest pipeline for this commit |
+| `id` | ID! | ID (global ID) of the commit |
+| `sha` | String! | SHA1 ID of the commit |
+| `title` | String | Title of the commit message |
+| `description` | String | Description of the commit message |
+| `message` | String | Raw commit message |
+| `authoredDate` | Time | Timestamp of when the commit was authored |
+| `webUrl` | String! | Web URL of the commit |
+| `signatureHtml` | String | Rendered HTML of the commit signature |
+| `authorName` | String | Commit authors name |
+| `author` | User | Author of the commit |
+| `latestPipeline` | Pipeline | Latest pipeline of the commit |
### CreateDiffNotePayload
@@ -66,6 +68,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. |
| `note` | Note | The note after mutation |
+### CreateEpicPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `epic` | Epic | The created epic |
+
### CreateImageDiffNotePayload
| Name | Type | Description |
@@ -212,36 +222,47 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `relationPath` | String | |
| `reference` | String! | |
| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this epic |
+| `descendantCounts` | EpicDescendantCount | Number of open and closed descendant epics and issues |
+
+### EpicDescendantCount
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `openedEpics` | Int | Number of opened sub-epics |
+| `closedEpics` | Int | Number of closed sub-epics |
+| `openedIssues` | Int | Number of opened epic issues |
+| `closedIssues` | Int | Number of closed epic issues |
### EpicIssue
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
-| `iid` | ID! | |
-| `title` | String! | |
+| `iid` | ID! | Internal ID of the issue |
+| `title` | String! | Title of the issue |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
-| `description` | String | |
+| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `state` | IssueState! | |
-| `reference` | String! | |
-| `author` | User! | |
-| `milestone` | Milestone | |
-| `dueDate` | Time | |
-| `confidential` | Boolean! | |
-| `discussionLocked` | Boolean! | |
-| `upvotes` | Int! | |
-| `downvotes` | Int! | |
-| `userNotesCount` | Int! | |
-| `webPath` | String! | |
-| `webUrl` | String! | |
-| `relativePosition` | Int | |
-| `timeEstimate` | Int! | The time estimate on the issue |
+| `state` | IssueState! | State of the issue |
+| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
+| `author` | User! | User that created the issue |
+| `milestone` | Milestone | Milestone of the issue |
+| `dueDate` | Time | Due date of the issue |
+| `confidential` | Boolean! | Indicates the issue is confidential |
+| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
+| `upvotes` | Int! | Number of upvotes the issue has received |
+| `downvotes` | Int! | Number of downvotes the issue has received |
+| `userNotesCount` | Int! | Number of user notes of the issue |
+| `webPath` | String! | Web path of the issue |
+| `webUrl` | String! | Web URL of the issue |
+| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
+| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this issue |
+| `timeEstimate` | Int! | Time estimate of the issue |
| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
-| `closedAt` | Time | |
-| `createdAt` | Time! | |
-| `updatedAt` | Time! | |
-| `taskCompletionStatus` | TaskCompletionStatus! | |
+| `closedAt` | Time | Timestamp of when the issue was closed |
+| `createdAt` | Time! | Timestamp of when the issue was created |
+| `updatedAt` | Time! | Timestamp of when the issue was last updated |
+| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
| `epic` | Epic | The epic to which issue belongs |
| `weight` | Int | |
| `designs` | DesignCollection | |
@@ -263,67 +284,40 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
| `awardEmoji` | Boolean! | Whether or not a user can perform `award_emoji` on this resource |
-### EpicTreeReorderPayload
+### EpicSetSubscriptionPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
+| `epic` | Epic | The epic after mutation |
-### ExtendedIssue
+### EpicTreeReorderPayload
| Name | Type | Description |
| --- | ---- | ---------- |
-| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
-| `iid` | ID! | |
-| `title` | String! | |
-| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
-| `description` | String | |
-| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `state` | IssueState! | |
-| `reference` | String! | |
-| `author` | User! | |
-| `milestone` | Milestone | |
-| `dueDate` | Time | |
-| `confidential` | Boolean! | |
-| `discussionLocked` | Boolean! | |
-| `upvotes` | Int! | |
-| `downvotes` | Int! | |
-| `userNotesCount` | Int! | |
-| `webPath` | String! | |
-| `webUrl` | String! | |
-| `relativePosition` | Int | |
-| `timeEstimate` | Int! | The time estimate on the issue |
-| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
-| `closedAt` | Time | |
-| `createdAt` | Time! | |
-| `updatedAt` | Time! | |
-| `taskCompletionStatus` | TaskCompletionStatus! | |
-| `epic` | Epic | The epic to which issue belongs |
-| `weight` | Int | |
-| `designs` | DesignCollection | |
-| `designCollection` | DesignCollection | |
-| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this issue |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
### Group
| Name | Type | Description |
| --- | ---- | ---------- |
-| `id` | ID! | |
-| `name` | String! | |
-| `path` | String! | |
-| `fullName` | String! | |
-| `fullPath` | ID! | |
-| `description` | String | |
+| `id` | ID! | ID of the namespace |
+| `name` | String! | Name of the namespace |
+| `path` | String! | Path of the namespace |
+| `fullName` | String! | Full name of the namespace |
+| `fullPath` | ID! | Full path of the namespace |
+| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `visibility` | String | |
-| `lfsEnabled` | Boolean | |
-| `requestAccessEnabled` | Boolean | |
-| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
+| `visibility` | String | Visibility of the namespace |
+| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
+| `requestAccessEnabled` | Boolean | Indicates if users can request access to namespace |
+| `rootStorageStatistics` | RootStorageStatistics | Aggregated storage statistics of the namespace. Only available for root namespaces |
| `userPermissions` | GroupPermissions! | Permissions for the current user on the resource |
-| `webUrl` | String! | |
-| `avatarUrl` | String | |
-| `parent` | Group | |
+| `webUrl` | String! | Web URL of the group |
+| `avatarUrl` | String | Avatar URL of the group |
+| `parent` | Group | Parent group |
| `epicsEnabled` | Boolean | |
| `epic` | Epic | |
@@ -338,30 +332,31 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
-| `iid` | ID! | |
-| `title` | String! | |
+| `iid` | ID! | Internal ID of the issue |
+| `title` | String! | Title of the issue |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
-| `description` | String | |
+| `description` | String | Description of the issue |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `state` | IssueState! | |
-| `reference` | String! | |
-| `author` | User! | |
-| `milestone` | Milestone | |
-| `dueDate` | Time | |
-| `confidential` | Boolean! | |
-| `discussionLocked` | Boolean! | |
-| `upvotes` | Int! | |
-| `downvotes` | Int! | |
-| `userNotesCount` | Int! | |
-| `webPath` | String! | |
-| `webUrl` | String! | |
-| `relativePosition` | Int | |
-| `timeEstimate` | Int! | The time estimate on the issue |
+| `state` | IssueState! | State of the issue |
+| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
+| `author` | User! | User that created the issue |
+| `milestone` | Milestone | Milestone of the issue |
+| `dueDate` | Time | Due date of the issue |
+| `confidential` | Boolean! | Indicates the issue is confidential |
+| `discussionLocked` | Boolean! | Indicates discussion is locked on the issue |
+| `upvotes` | Int! | Number of upvotes the issue has received |
+| `downvotes` | Int! | Number of downvotes the issue has received |
+| `userNotesCount` | Int! | Number of user notes of the issue |
+| `webPath` | String! | Web path of the issue |
+| `webUrl` | String! | Web URL of the issue |
+| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
+| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this issue |
+| `timeEstimate` | Int! | Time estimate of the issue |
| `totalTimeSpent` | Int! | Total time reported as spent on the issue |
-| `closedAt` | Time | |
-| `createdAt` | Time! | |
-| `updatedAt` | Time! | |
-| `taskCompletionStatus` | TaskCompletionStatus! | |
+| `closedAt` | Time | Timestamp of when the issue was closed |
+| `createdAt` | Time! | Timestamp of when the issue was created |
+| `updatedAt` | Time! | Timestamp of when the issue was last updated |
+| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
| `epic` | Epic | The epic to which issue belongs |
| `weight` | Int | |
| `designs` | DesignCollection | |
@@ -384,65 +379,66 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `description` | String | |
+| `id` | ID! | Label ID |
+| `description` | String | Description of the label (markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `title` | String! | |
-| `color` | String! | |
-| `textColor` | String! | |
+| `title` | String! | Content of the label |
+| `color` | String! | Background color of the label |
+| `textColor` | String! | Text color of the label |
### MergeRequest
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | MergeRequestPermissions! | Permissions for the current user on the resource |
-| `id` | ID! | |
-| `iid` | String! | |
-| `title` | String! | |
+| `id` | ID! | ID of the merge request |
+| `iid` | String! | Internal ID of the merge request |
+| `title` | String! | Title of the merge request |
| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
-| `description` | String | |
+| `description` | String | Description of the merge request (markdown rendered as HTML for caching) |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `state` | MergeRequestState! | |
-| `createdAt` | Time! | |
-| `updatedAt` | Time! | |
-| `sourceProject` | Project | |
-| `targetProject` | Project! | |
-| `diffRefs` | DiffRefs | |
-| `project` | Project! | |
-| `projectId` | Int! | |
-| `sourceProjectId` | Int | |
-| `targetProjectId` | Int! | |
-| `sourceBranch` | String! | |
-| `targetBranch` | String! | |
-| `workInProgress` | Boolean! | |
-| `mergeWhenPipelineSucceeds` | Boolean | |
-| `diffHeadSha` | String | |
-| `mergeCommitSha` | String | |
-| `userNotesCount` | Int | |
-| `shouldRemoveSourceBranch` | Boolean | |
-| `forceRemoveSourceBranch` | Boolean | |
-| `mergeStatus` | String | |
-| `inProgressMergeCommitSha` | String | |
-| `mergeError` | String | |
-| `allowCollaboration` | Boolean | |
-| `shouldBeRebased` | Boolean! | |
-| `rebaseCommitSha` | String | |
-| `rebaseInProgress` | Boolean! | |
-| `mergeCommitMessage` | String | |
-| `defaultMergeCommitMessage` | String | |
-| `mergeOngoing` | Boolean! | |
-| `sourceBranchExists` | Boolean! | |
-| `mergeableDiscussionsState` | Boolean | |
-| `webUrl` | String | |
-| `upvotes` | Int! | |
-| `downvotes` | Int! | |
-| `headPipeline` | Pipeline | |
-| `milestone` | Milestone | The milestone this merge request is linked to |
-| `subscribed` | Boolean! | Boolean flag for whether the currently logged in user is subscribed to this MR |
-| `discussionLocked` | Boolean! | Boolean flag determining if comments on the merge request are locked to members only |
-| `timeEstimate` | Int! | The time estimate for the merge request |
+| `state` | MergeRequestState! | State of the merge request |
+| `createdAt` | Time! | Timestamp of when the merge request was created |
+| `updatedAt` | Time! | Timestamp of when the merge request was last updated |
+| `sourceProject` | Project | Source project of the merge request |
+| `targetProject` | Project! | Target project of the merge request |
+| `diffRefs` | DiffRefs | References of the base SHA, the head SHA, and the start SHA for this merge request |
+| `project` | Project! | Alias for target_project |
+| `projectId` | Int! | ID of the merge request project |
+| `sourceProjectId` | Int | ID of the merge request source project |
+| `targetProjectId` | Int! | ID of the merge request target project |
+| `sourceBranch` | String! | Source branch of the merge request |
+| `targetBranch` | String! | Target branch of the merge request |
+| `workInProgress` | Boolean! | Indicates if the merge request is a work in progress (WIP) |
+| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
+| `diffHeadSha` | String | Diff head SHA of the merge request |
+| `mergeCommitSha` | String | SHA of the merge request commit (set once merged) |
+| `userNotesCount` | Int | User notes count of the merge request |
+| `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge |
+| `forceRemoveSourceBranch` | Boolean | Indicates if the project settings will lead to source branch deletion after merge |
+| `mergeStatus` | String | Status of the merge request |
+| `inProgressMergeCommitSha` | String | Commit SHA of the merge request if merge is in progress |
+| `mergeError` | String | Error message due to a merge error |
+| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
+| `shouldBeRebased` | Boolean! | Indicates if the merge request will be rebased |
+| `rebaseCommitSha` | String | Rebase commit SHA of the merge request |
+| `rebaseInProgress` | Boolean! | Indicates if there is a rebase currently in progress for the merge request |
+| `mergeCommitMessage` | String | Deprecated - renamed to defaultMergeCommitMessage |
+| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
+| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
+| `sourceBranchExists` | Boolean! | Indicates if the source branch of the merge request exists |
+| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
+| `webUrl` | String | Web URL of the merge request |
+| `upvotes` | Int! | Number of upvotes for the merge request |
+| `downvotes` | Int! | Number of downvotes for the merge request |
+| `headPipeline` | Pipeline | The pipeline running on the branch HEAD of the merge request |
+| `milestone` | Milestone | The milestone of the merge request |
+| `subscribed` | Boolean! | Indicates if the currently logged in user is subscribed to this merge request |
+| `discussionLocked` | Boolean! | Indicates if comments on the merge request are locked to members only |
+| `timeEstimate` | Int! | Time estimate of the merge request |
| `totalTimeSpent` | Int! | Total time reported as spent on the merge request |
-| `reference` | String! | Internal merge request reference. Returned in shortened format by default |
-| `taskCompletionStatus` | TaskCompletionStatus! | |
+| `reference` | String! | Internal reference of the merge request. Returned in shortened format by default |
+| `taskCompletionStatus` | TaskCompletionStatus! | Completion status of tasks |
### MergeRequestPermissions
@@ -457,6 +453,46 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `cherryPickOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource |
| `revertOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `revert_on_current_merge_request` on this resource |
+### MergeRequestSetAssigneesPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `mergeRequest` | MergeRequest | The merge request after mutation |
+
+### MergeRequestSetLabelsPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `mergeRequest` | MergeRequest | The merge request after mutation |
+
+### MergeRequestSetLockedPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `mergeRequest` | MergeRequest | The merge request after mutation |
+
+### MergeRequestSetMilestonePayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `mergeRequest` | MergeRequest | The merge request after mutation |
+
+### MergeRequestSetSubscriptionPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `mergeRequest` | MergeRequest | The merge request after mutation |
+
### MergeRequestSetWipPayload
| Name | Type | Description |
@@ -469,36 +505,37 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `version` | String! | |
-| `revision` | String! | |
+| `version` | String! | Version |
+| `revision` | String! | Revision |
### Milestone
| Name | Type | Description |
| --- | ---- | ---------- |
-| `description` | String | |
-| `title` | String! | |
-| `state` | String! | |
-| `dueDate` | Time | |
-| `startDate` | Time | |
-| `createdAt` | Time! | |
-| `updatedAt` | Time! | |
+| `id` | ID! | ID of the milestone |
+| `description` | String | Description of the milestone |
+| `title` | String! | Title of the milestone |
+| `state` | String! | State of the milestone |
+| `dueDate` | Time | Timestamp of the milestone due date |
+| `startDate` | Time | Timestamp of the milestone start date |
+| `createdAt` | Time! | Timestamp of milestone creation |
+| `updatedAt` | Time! | Timestamp of last milestone update |
### Namespace
| Name | Type | Description |
| --- | ---- | ---------- |
-| `id` | ID! | |
-| `name` | String! | |
-| `path` | String! | |
-| `fullName` | String! | |
-| `fullPath` | ID! | |
-| `description` | String | |
+| `id` | ID! | ID of the namespace |
+| `name` | String! | Name of the namespace |
+| `path` | String! | Path of the namespace |
+| `fullName` | String! | Full name of the namespace |
+| `fullPath` | ID! | Full path of the namespace |
+| `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `visibility` | String | |
-| `lfsEnabled` | Boolean | |
-| `requestAccessEnabled` | Boolean | |
-| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available for root namespaces |
+| `visibility` | String | Visibility of the namespace |
+| `lfsEnabled` | Boolean | Indicates if Large File Storage (LFS) is enabled for namespace |
+| `requestAccessEnabled` | Boolean | Indicates if users can request access to namespace |
+| `rootStorageStatistics` | RootStorageStatistics | Aggregated storage statistics of the namespace. Only available for root namespaces |
### Note
@@ -570,46 +607,47 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
-| `id` | ID! | |
-| `fullPath` | ID! | |
-| `path` | String! | |
-| `nameWithNamespace` | String! | |
-| `name` | String! | |
-| `description` | String | |
+| `id` | ID! | ID of the project |
+| `fullPath` | ID! | Full path of the project |
+| `path` | String! | Path of the project |
+| `nameWithNamespace` | String! | Full name of the project with its namespace |
+| `name` | String! | Name of the project (without namespace) |
+| `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
-| `tagList` | String | |
-| `sshUrlToRepo` | String | |
-| `httpUrlToRepo` | String | |
-| `webUrl` | String | |
-| `starCount` | Int! | |
-| `forksCount` | Int! | |
-| `createdAt` | Time | |
-| `lastActivityAt` | Time | |
-| `archived` | Boolean | |
-| `visibility` | String | |
-| `containerRegistryEnabled` | Boolean | |
-| `sharedRunnersEnabled` | Boolean | |
-| `lfsEnabled` | Boolean | |
-| `mergeRequestsFfOnlyEnabled` | Boolean | |
-| `avatarUrl` | String | |
-| `issuesEnabled` | Boolean | |
-| `mergeRequestsEnabled` | Boolean | |
-| `wikiEnabled` | Boolean | |
-| `snippetsEnabled` | Boolean | |
-| `jobsEnabled` | Boolean | |
-| `publicJobs` | Boolean | |
-| `openIssuesCount` | Int | |
-| `importStatus` | String | |
-| `onlyAllowMergeIfPipelineSucceeds` | Boolean | |
-| `requestAccessEnabled` | Boolean | |
-| `onlyAllowMergeIfAllDiscussionsAreResolved` | Boolean | |
-| `printingMergeRequestLinkEnabled` | Boolean | |
-| `namespace` | Namespace | |
-| `group` | Group | |
-| `statistics` | ProjectStatistics | |
-| `repository` | Repository | |
-| `mergeRequest` | MergeRequest | |
-| `issue` | ExtendedIssue | |
+| `tagList` | String | List of project tags |
+| `sshUrlToRepo` | String | URL to connect to the project via SSH |
+| `httpUrlToRepo` | String | URL to connect to the project via HTTPS |
+| `webUrl` | String | Web URL of the project |
+| `starCount` | Int! | Number of times the project has been starred |
+| `forksCount` | Int! | Number of times the project has been forked |
+| `createdAt` | Time | Timestamp of the project creation |
+| `lastActivityAt` | Time | Timestamp of the project last activity |
+| `archived` | Boolean | Archived status of the project |
+| `visibility` | String | Visibility of the project |
+| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
+| `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled on the project |
+| `lfsEnabled` | Boolean | Indicates if the project has Large File Storage (LFS) enabled |
+| `mergeRequestsFfOnlyEnabled` | Boolean | Indicates if no merge commits should be created and all merges should instead be fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. |
+| `avatarUrl` | String | URL to avatar image file of the project |
+| `issuesEnabled` | Boolean | (deprecated) Does this project have issues enabled?. Use `issues_access_level` instead |
+| `mergeRequestsEnabled` | Boolean | (deprecated) Does this project have merge_requests enabled?. Use `merge_requests_access_level` instead |
+| `wikiEnabled` | Boolean | (deprecated) Does this project have wiki enabled?. Use `wiki_access_level` instead |
+| `snippetsEnabled` | Boolean | (deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead |
+| `jobsEnabled` | Boolean | (deprecated) Enable jobs for this project. Use `builds_access_level` instead |
+| `publicJobs` | Boolean | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts |
+| `openIssuesCount` | Int | Number of open issues for the project |
+| `importStatus` | String | Status of project import background job of the project |
+| `onlyAllowMergeIfPipelineSucceeds` | Boolean | Indicates if merge requests of the project can only be merged with successful jobs |
+| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
+| `onlyAllowMergeIfAllDiscussionsAreResolved` | Boolean | Indicates if merge requests of the project can only be merged when all the discussions are resolved |
+| `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 |
+| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
+| `namespace` | Namespace | Namespace of the project |
+| `group` | Group | Group of the project |
+| `statistics` | ProjectStatistics | Statistics of the project |
+| `repository` | Repository | Git repository of the project |
+| `mergeRequest` | MergeRequest | A single merge request of the project |
+| `issue` | Issue | A single issue of the project |
### ProjectPermissions
@@ -661,13 +699,13 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `commitCount` | Int! | |
-| `storageSize` | Int! | |
-| `repositorySize` | Int! | |
-| `lfsObjectsSize` | Int! | |
-| `buildArtifactsSize` | Int! | |
-| `packagesSize` | Int! | |
-| `wikiSize` | Int | |
+| `commitCount` | Int! | Commit count of the project |
+| `storageSize` | Int! | Storage size of the project |
+| `repositorySize` | Int! | Repository size of the project |
+| `lfsObjectsSize` | Int! | Large File Storage (LFS) object size of the project |
+| `buildArtifactsSize` | Int! | Build artifacts size of the project |
+| `packagesSize` | Int! | Packages size of the project |
+| `wikiSize` | Int | Wiki size of the project |
### RemoveAwardEmojiPayload
@@ -681,10 +719,10 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `rootRef` | String | |
-| `empty` | Boolean! | |
-| `exists` | Boolean! | |
-| `tree` | Tree | |
+| `rootRef` | String | Default branch of the repository |
+| `empty` | Boolean! | Indicates repository has no visible content |
+| `exists` | Boolean! | Indicates a corresponding Git repository exists on disk |
+| `tree` | Tree | Tree of the repository |
### RootStorageStatistics
@@ -702,6 +740,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | |
+| `sha` | String! | Last commit sha for entry |
| `name` | String! | |
| `type` | EntryType! | |
| `path` | String! | |
@@ -713,8 +752,8 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `count` | Int! | |
-| `completedCount` | Int! | |
+| `count` | Int! | Number of total tasks |
+| `completedCount` | Int! | Number of completed tasks |
### Todo
@@ -730,6 +769,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `state` | TodoStateEnum! | State of the todo |
| `createdAt` | Time! | Timestamp this todo was created |
+### TodoMarkDonePayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `todo` | Todo! | The requested todo |
+
### ToggleAwardEmojiPayload
| Name | Type | Description |
@@ -750,6 +797,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | |
+| `sha` | String! | Last commit sha for entry |
| `name` | String! | |
| `type` | EntryType! | |
| `path` | String! | |
@@ -776,7 +824,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
-| `name` | String! | |
-| `username` | String! | |
-| `avatarUrl` | String! | |
-| `webUrl` | String! | |
+| `name` | String! | Human-readable name of the user |
+| `username` | String! | Username of the user. Unique within this instance of GitLab |
+| `avatarUrl` | String! | URL of the user's avatar |
+| `webUrl` | String! | Web URL of the user |
diff --git a/doc/api/group_clusters.md b/doc/api/group_clusters.md
index e878bb5fa4d..143f5762811 100644
--- a/doc/api/group_clusters.md
+++ b/doc/api/group_clusters.md
@@ -53,6 +53,16 @@ Example response:
"api_url":"https://104.197.68.152",
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+ },
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
}
},
{
@@ -111,6 +121,16 @@ Example response:
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
},
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
+ },
"group":
{
"id":26,
@@ -135,6 +155,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `name` | String | yes | The name of the cluster |
| `domain` | String | no | The [base domain](../user/group/clusters/index.md#base-domain) of the cluster |
+| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true |
| `managed` | Boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true |
| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API |
@@ -178,6 +199,7 @@ Example response:
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
},
+ "management_project":null,
"group":
{
"id":26,
@@ -210,7 +232,7 @@ Parameters:
NOTE: **Note:**
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
-through the ["Add existing Kubernetes cluster"](../user/project/clusters/index.md#add-existing-kubernetes-cluster) option or
+through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_remove_clusters.md#add-existing-cluster) option or
through the ["Add existing cluster to group"](#add-existing-cluster-to-group) endpoint.
Example request:
@@ -248,6 +270,16 @@ Example response:
"authorization_type":"rbac",
"ca_cert":null
},
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
+ },
"group":
{
"id":26,
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 312bd04e24c..94f46b11a0f 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -611,6 +611,10 @@ GET /groups?search=foobar
]
```
+## Group Audit Events **(STARTER)**
+
+Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events-starter)
+
## Sync group with LDAP **(CORE ONLY)**
Syncs the group with its linked LDAP group. Only available to group owners and administrators.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 0ddbb18ce92..54b27370741 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -577,14 +577,22 @@ the `weight` parameter:
```
Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) will additionally see
-the `epic_iid` property:
+the `epic` property:
-```json
+```javascript
{
"project_id" : 4,
"description" : "Omnis vero earum sunt corporis dolor et placeat.",
- "epic_iid" : 42,
- ...
+ "epic": {
+ "epic_iid" : 5, //deprecated, use `iid` of the `epic` attribute
+ "epic": {
+ "id" : 42,
+ "iid" : 5,
+ "title": "My epic epic",
+ "url" : "/groups/h5bp/-/epics/5",
+ "group_id": 8
+ },
+ // ...
}
```
@@ -592,6 +600,9 @@ the `epic_iid` property:
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-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).
+Please use `iid` of the `epic` attribute instead.
+
## New issue
Creates a new project issue.
@@ -1416,6 +1427,7 @@ Example response:
"merge_status": "cannot_be_merged",
"sha": "3b7b528e9353295c1c125dad281ac5b5deae5f12",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"discussion_locked": null,
"should_remove_source_branch": null,
"force_remove_source_branch": false,
@@ -1546,6 +1558,7 @@ Example response:
"merge_status": "unchecked",
"sha": "5a62481d563af92b8e32d735f2fa63b94e806835",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"should_remove_source_branch": null,
"force_remove_source_branch": false,
diff --git a/doc/api/license.md b/doc/api/license.md
index 12f1d03d576..c56a5fee95a 100644
--- a/doc/api/license.md
+++ b/doc/api/license.md
@@ -17,6 +17,7 @@ GET /license
"starts_at": "2018-01-27",
"expires_at": "2022-01-27",
"historical_max": 300,
+ "maximum_user_count": 300,
"expired": false,
"overage": 200,
"user_limit": 100,
@@ -46,6 +47,7 @@ GET /licenses
"starts_at": "2018-01-27",
"expires_at": "2022-01-27",
"historical_max": 300,
+ "maximum_user_count": 300,
"expired": false,
"overage": 200,
"user_limit": 100,
@@ -64,6 +66,7 @@ GET /licenses
"starts_at": "2018-01-27",
"expires_at": "2022-01-27",
"historical_max": 300,
+ "maximum_user_count": 300,
"expired": false,
"overage": 200,
"user_limit": 100,
@@ -112,6 +115,7 @@ Example response:
"starts_at": "2018-01-27",
"expires_at": "2022-01-27",
"historical_max": 300,
+ "maximum_user_count": 300,
"expired": false,
"overage": 200,
"user_limit": 100,
@@ -155,6 +159,7 @@ Example response:
"starts_at": "2018-01-27",
"expires_at": "2022-01-27",
"historical_max": 300,
+ "maximum_user_count": 300,
"expired": false,
"overage": 200,
"user_limit": 100,
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 4bc46c3030d..7074d0249ef 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -126,6 +126,7 @@ Parameters:
"merge_status": "can_be_merged",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -287,6 +288,7 @@ Parameters:
"merge_status": "can_be_merged",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -304,7 +306,9 @@ Parameters:
"task_completion_status":{
"count":0,
"completed_count":0
- }
+ },
+ "has_conflicts": false,
+ "blocking_discussions_resolved": true
}
]
```
@@ -438,6 +442,7 @@ Parameters:
"merge_status": "can_be_merged",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -453,7 +458,9 @@ Parameters:
"task_completion_status":{
"count":0,
"completed_count":0
- }
+ },
+ "has_conflicts": false,
+ "blocking_discussions_resolved": true
}
]
```
@@ -559,6 +566,7 @@ Parameters:
"merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -606,7 +614,9 @@ Parameters:
"task_completion_status":{
"count":0,
"completed_count":0
- }
+ },
+ "has_conflicts": false,
+ "blocking_discussions_resolved": true
}
```
@@ -763,6 +773,7 @@ Parameters:
"subscribed" : true,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"changes_count": "1",
"should_remove_source_branch": true,
@@ -970,6 +981,7 @@ order for it to take effect:
"merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -1123,6 +1135,7 @@ Must include at least one non-required attribute from above.
"merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -1292,6 +1305,7 @@ Parameters:
"merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -1464,6 +1478,7 @@ Parameters:
"merge_error": null,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -1749,6 +1764,7 @@ Example response:
"merge_status": "can_be_merged",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -1894,6 +1910,7 @@ Example response:
"merge_status": "can_be_merged",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
@@ -2055,6 +2072,7 @@ Example response:
"subscribed": true,
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 7,
"changes_count": "1",
"should_remove_source_branch": true,
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 13d773e4f99..bab3f91bc40 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -2,7 +2,9 @@
This is the API docs of [GitLab Packages](../administration/packages/index.md).
-## List project packages
+## List packages
+
+### Within a project
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9259) in GitLab 11.8.
@@ -42,6 +44,47 @@ Example response:
By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
+### Within a group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/18871) in GitLab 12.5.
+
+Get a list of project packages at the group level.
+When accessed without authentication, only packages of public projects are returned.
+
+```
+GET /groups/:id/packages
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the group](README.md#namespaced-path-encoding). |
+| `exclude_subgroups` | boolean | false | If the param is included as true, packages from projects from subgroups are not listed. Default is `false`. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=true
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "com/mycompany/my-app",
+ "version": "1.0-SNAPSHOT",
+ "package_type": "maven"
+ },
+ {
+ "id": 2,
+ "name": "@foo/bar",
+ "version": "1.0.3",
+ "package_type": "npm"
+ }
+]
+```
+
+By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
+
## Get a project package
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9667) in GitLab 11.9.
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 9678203eb40..9d482781cde 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -22,6 +22,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
"project_id": 1337,
+ "auto_ssl_enabled": false,
"certificate": {
"expired": false,
"expiration": "2020-04-12T14:32:00.000Z"
@@ -55,6 +56,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
+ "auto_ssl_enabled": false,
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
@@ -76,7 +78,7 @@ GET /projects/:id/pages/domains/:domain
| 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 |
-| `domain` | string | yes | The domain |
+| `domain` | string | yes | The custom domain indicated by the user |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/pages/domains/www.domain.example
@@ -97,6 +99,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
+ "auto_ssl_enabled": false,
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
@@ -114,12 +117,13 @@ Creates a new pages domain. The user must have permissions to create new pages d
POST /projects/:id/pages/domains
```
-| 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 |
-| `domain` | string | yes | The domain |
-| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
-| `key` | file/string | no | The certificate key in PEM format. |
+| 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 |
+| `domain` | string | yes | The custom domain indicated by the user |
+| `auto_ssl_enabled` | boolean | no | Enables [automatic generation](../user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md) of SSL certificates issued by Let's Encrypt for custom domains. |
+| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
+| `key` | file/string | no | The certificate key in PEM format. |
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains
@@ -129,10 +133,15 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "certificate=$CERT_PEM" --form "key=$KEY_PEM" https://gitlab.example.com/api/v4/projects/5/pages/domains
```
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain=ssl.domain.example" --form "auto_ssl_enabled=true" https://gitlab.example.com/api/v4/projects/5/pages/domains
+```
+
```json
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
+ "auto_ssl_enabled": true,
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
@@ -150,12 +159,15 @@ Updates an existing project pages domain. The user must have permissions to chan
PUT /projects/:id/pages/domains/:domain
```
-| 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 |
-| `domain` | string | yes | The domain |
-| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
-| `key` | file/string | no | The certificate key in PEM format. |
+| 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 |
+| `domain` | string | yes | The custom domain indicated by the user |
+| `auto_ssl_enabled` | boolean | no | Enables [automatic generation](../user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md) of SSL certificates issued by Let's Encrypt for custom domains. |
+| `certificate` | file/string | no | The certificate in PEM format with intermediates following in most specific to least specific order.|
+| `key` | file/string | no | The certificate key in PEM format. |
+
+### Adding certificate
```bash
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=@/path/to/cert.pem" --form "key=@/path/to/key.pem" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
@@ -169,6 +181,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi
{
"domain": "ssl.domain.example",
"url": "https://ssl.domain.example",
+ "auto_ssl_enabled": false,
"certificate": {
"subject": "/O=Example, Inc./OU=Example Origin CA/CN=Example Origin Certificate",
"expired": false,
@@ -178,6 +191,36 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi
}
```
+### Enabling Let's Encrypt integration for Pages custom domains
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "auto_ssl_enabled=true" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
+
+```json
+{
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "auto_ssl_enabled": true
+}
+```
+
+### Removing certificate
+
+To remove the SSL certificate attached to the Pages domain, run:
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certificate=" --form "key=" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
+```
+
+```json
+{
+ "domain": "ssl.domain.example",
+ "url": "https://ssl.domain.example",
+ "auto_ssl_enabled": false
+}
+```
+
## Delete pages domain
Deletes an existing project pages domain.
@@ -189,7 +232,7 @@ DELETE /projects/:id/pages/domains/:domain
| 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 |
-| `domain` | string | yes | The domain |
+| `domain` | string | yes | The custom domain indicated by the user |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/pages/domains/ssl.domain.example
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index 633ef20deb4..1aa225d30ab 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -1,7 +1,6 @@
# Project clusters API
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/23922)
-in GitLab 11.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/23922) in GitLab 11.7.
NOTE: **Note:**
User will need at least maintainer access to use these endpoints.
@@ -54,6 +53,16 @@ Example response:
"namespace":"cluster-1-namespace",
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
+ },
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
}
},
{
@@ -113,6 +122,16 @@ Example response:
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
},
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
+ },
"project":
{
"id":26,
@@ -205,6 +224,7 @@ Example response:
"authorization_type":"rbac",
"ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"
},
+ "management_project":null,
"project":
{
"id":26,
@@ -253,6 +273,7 @@ Parameters:
| `cluster_id` | integer | yes | The ID of the cluster |
| `name` | String | no | The name of the cluster |
| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster |
+| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes |
| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate |
@@ -261,7 +282,7 @@ Parameters:
NOTE: **Note:**
`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added
-through the ["Add existing Kubernetes cluster"](../user/project/clusters/index.md#add-existing-kubernetes-cluster) option or
+through the ["Add existing Kubernetes cluster"](../user/project/clusters/add_remove_clusters.md#add-existing-cluster) option or
through the ["Add existing cluster to project"](#add-existing-cluster-to-project) endpoint.
Example request:
@@ -300,6 +321,16 @@ Example response:
"authorization_type":"rbac",
"ca_cert":null
},
+ "management_project":
+ {
+ "id":2,
+ "description":null,
+ "name":"project2",
+ "name_with_namespace":"John Doe8 / project2",
+ "path":"project2",
+ "path_with_namespace":"namespace2/project2",
+ "created_at":"2019-10-11T02:55:54.138Z"
+ },
"project":
{
"id":26,
@@ -351,5 +382,5 @@ Parameters:
Example request:
```bash
-curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/23'
+curl --request DELETE --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/23
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index c352b972b17..222ab729810 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -58,6 +58,8 @@ GET /projects
| `wiki_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the wiki checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `repository_checksum_failed` | boolean | no | **(PREMIUM)** Limit projects where the repository checksum calculation has failed ([Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/6137) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2) |
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
+| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID |
+| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID |
When `simple=true` or the user is unauthenticated this returns something like:
@@ -148,6 +150,7 @@ When the user is authenticated and `simple` is not set this returns something li
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -232,6 +235,7 @@ When the user is authenticated and `simple` is not set this returns something li
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -302,6 +306,8 @@ GET /users/:user_id/projects
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
| `with_programming_language` | string | no | Limit by projects which use the given programming language |
| `min_access_level` | integer | no | Limit by current user minimal [access level](members.md) |
+| `id_after` | integer | no | Limit results to projects with IDs greater than the specified ID |
+| `id_before` | integer | no | Limit results to projects with IDs less than the specified ID |
```json
[
@@ -357,6 +363,7 @@ GET /users/:user_id/projects
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -441,6 +448,7 @@ GET /users/:user_id/projects
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -550,6 +558,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -631,6 +640,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"statistics": {
@@ -757,6 +767,7 @@ GET /projects/:id
"repository_storage": "default",
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"printing_merge_requests_link_enabled": true,
"request_access_enabled": false,
"merge_method": "merge",
@@ -917,6 +928,7 @@ POST /projects
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
+| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -936,6 +948,7 @@ POST /projects
| `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds |
| `initialize_with_readme` | boolean | no | `false` by default |
| `template_name` | string | no | When used without `use_custom_template`, name of a [built-in project template](../gitlab-basics/create-project.md#built-in-templates). When used with `use_custom_template`, name of a custom project template |
+| `template_project_id` | integer | no | **(PREMIUM)** When used with `use_custom_template`, project ID of a custom project template. This is preferable to using `template_name` since `template_name` may be ambiguous. |
| `use_custom_template` | boolean | no | **(PREMIUM)** Use either custom [instance](../user/admin_area/custom_project_templates.md) or [group](../user/group/custom_project_templates.md) (with `group_with_project_templates_id`) project template |
| `group_with_project_templates_id` | integer | no | **(PREMIUM)** For group-level custom templates, specifies ID of group from which all the custom project templates are sourced. Leave empty for instance-level templates. Requires `use_custom_template` to be true |
@@ -978,6 +991,7 @@ POST /projects/user/:user_id
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
+| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -1039,6 +1053,7 @@ PUT /projects/:id
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
+| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
| `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -1165,6 +1180,7 @@ Example responses:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"_links": {
@@ -1252,6 +1268,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"_links": {
@@ -1338,6 +1355,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"_links": {
@@ -1511,6 +1529,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"_links": {
@@ -1616,6 +1635,7 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
+ "remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"_links": {
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index ee2df3e4c5d..7f41e237401 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -303,7 +303,7 @@ POST /projects/:id/releases
| 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 release name. |
+| `name` | string | no | The release name. |
| `tag_name` | string | yes | The tag where the release will be created from. |
| `description` | string | yes | The description of the release. You can use [markdown](../../user/markdown.md). |
| `ref` | string | yes, if `tag_name` doesn't exist | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. |
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 8cbd6103e88..cf9d8ebbec2 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -5,8 +5,7 @@
The SCIM API implements the [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
NOTE: **Note:**
-[Group SSO](../user/group/saml_sso/index.md) and the feature
-flag `:group_scim` must be enabled for the group. For more information, see [SCIM setup documentation](../user/group/saml_sso/scim_setup.md#requirements).
+[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).
## Get a list of SAML users
@@ -22,7 +21,7 @@ Parameters:
| Attribute | Type | Required | Description |
|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------|
-| `filter` | string | yes | A [filter](#available-filters) expression. |
+| `filter` | string | no | A [filter](#available-filters) expression. |
| `group_path` | string | yes | Full path to the group. |
| `startIndex` | integer | no | The 1-based index indicating where to start returning results from. A value of less than one will be interpreted as 1. |
| `count` | integer | no | Desired maximum number of query results. |
diff --git a/doc/api/search.md b/doc/api/search.md
index ca08f5ca0d7..8e20722052e 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -181,6 +181,7 @@ Example response:
"merge_status": "can_be_merged",
"sha": "78765a2d5e0a43585945c58e61ba2f822e4d090b",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 0,
"discussion_locked": null,
"should_remove_source_branch": null,
@@ -299,6 +300,7 @@ Example response:
{
"basename": "home",
"data": "hello\n\nand bye\n\nend",
+ "path": "home.md",
"filename": "home.md",
"id": null,
"ref": "master",
@@ -308,6 +310,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: commits **(STARTER)**
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
@@ -367,6 +371,7 @@ Example response:
{
"basename": "README",
"data": "```\n\n## Installation\n\nQuick start using the [pre-built",
+ "path": "README.md",
"filename": "README.md",
"id": null,
"ref": "master",
@@ -376,6 +381,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: users
```bash
@@ -577,6 +584,7 @@ Example response:
"merge_status": "can_be_merged",
"sha": "78765a2d5e0a43585945c58e61ba2f822e4d090b",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 0,
"discussion_locked": null,
"should_remove_source_branch": null,
@@ -633,6 +641,7 @@ Example response:
{
"basename": "home",
"data": "hello\n\nand bye\n\nend",
+ "path": "home.md",
"filename": "home.md",
"id": null,
"ref": "master",
@@ -642,6 +651,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: commits **(STARTER)**
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
@@ -701,6 +712,7 @@ Example response:
{
"basename": "README",
"data": "```\n\n## Installation\n\nQuick start using the [pre-built",
+ "path": "README.md",
"filename": "README.md",
"id": null,
"ref": "master",
@@ -710,6 +722,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: users
```bash
@@ -878,6 +892,7 @@ Example response:
"merge_status": "can_be_merged",
"sha": "78765a2d5e0a43585945c58e61ba2f822e4d090b",
"merge_commit_sha": null,
+ "squash_commit_sha": null,
"user_notes_count": 0,
"discussion_locked": null,
"should_remove_source_branch": null,
@@ -981,6 +996,7 @@ Example response:
{
"basename": "home",
"data": "hello\n\nand bye\n\nend",
+ "path": "home.md",
"filename": "home.md",
"id": null,
"ref": "master",
@@ -990,6 +1006,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: commits
```bash
@@ -1051,6 +1069,7 @@ Example response:
{
"basename": "README",
"data": "```\n\n## Installation\n\nQuick start using the [pre-built",
+ "path": "README.md",
"filename": "README.md",
"id": null,
"ref": "master",
@@ -1060,6 +1079,8 @@ Example response:
]
```
+**Note:** `filename` is deprecated in favor of `path`. Both return the full path of the file inside the repository, but in the future `filename` will be only the file name and not the full path (see [this issue][gitlab-34521]).
+
### Scope: users
```bash
@@ -1082,3 +1103,4 @@ Example response:
```
[ce-41763]: https://gitlab.com/gitlab-org/gitlab-foss/issues/41763
+[gitlab-34521]: https://gitlab.com/gitlab-org/gitlab/issues/34521
diff --git a/doc/api/services.md b/doc/api/services.md
index 4abc02dec3c..609c7e62e36 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -605,7 +605,7 @@ Set Jira service for a project.
> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
> `project_url` are replaced by `url`. If you are using an
-> older version, [follow this documentation][old-jira-api].
+> older version, [follow this documentation](https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable-ee/doc/api/services.md#jira).
```
PUT /projects/:id/services/jira
@@ -1224,9 +1224,6 @@ Get Jenkins CI (Deprecated) service settings for a project.
GET /projects/:id/services/jenkins-deprecated
```
-[jira-doc]: ../user/project/integrations/jira.md
-[old-jira-api]: https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable/doc/api/services.md#jira
-
## MockCI
Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 2d9e435bbb6..51d5e5f35d7 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -40,6 +40,7 @@ Example response:
"domain_blacklist_enabled" : false,
"domain_blacklist" : [],
"created_at" : "2016-01-04T15:44:55.176Z",
+ "default_ci_config_path" : null,
"default_project_visibility" : "private",
"default_group_visibility" : "private",
"gravatar_enabled" : true,
@@ -113,6 +114,7 @@ Example response:
"restricted_visibility_levels": [],
"max_attachment_size": 10,
"session_expire_delay": 10080,
+ "default_ci_config_path" : null,
"default_project_visibility": "internal",
"default_snippet_visibility": "private",
"default_group_visibility": "private",
@@ -198,6 +200,7 @@ are listed in the descriptions of the relevant settings.
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. |
| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take: `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
+| `default_ci_config_path` | string | no | Default CI configuration path for new projects (`.gitlab-ci.yml` if not set). |
| `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `default_project_creation` | integer | no | Default project creation protection. Can take: `0` _(No one)_, `1` _(Maintainers)_ or `2` _(Developers + Maintainers)_|
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
@@ -212,6 +215,10 @@ are listed in the descriptions of the relevant settings.
| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys. |
| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys. |
| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys. |
+| `eks_integration_enabled` | boolean | no | Enable integration with Amazon EKS |
+| `eks_account_id` | string | no | Amazon account ID |
+| `eks_access_key_id` | string | no | AWS IAM access key ID |
+| `eks_secret_access_key` | string | no | AWS IAM secret access key |
| `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key |
| `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 |
@@ -257,7 +264,6 @@ are listed in the descriptions of the relevant settings.
| `housekeeping_incremental_repack_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which an incremental `git repack` is run. |
| `html_emails_enabled` | boolean | no | Enable HTML emails. |
| `import_sources` | array of strings | no | Sources to allow project import from, possible values: `github`, `bitbucket`, `bitbucket_server`, `gitlab`, `google_code`, `fogbugz`, `git`, `gitlab_project`, `gitea`, `manifest`, and `phabricator`. |
-
| `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins. |
| `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. |
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB |
@@ -271,7 +277,7 @@ are listed in the descriptions of the relevant settings.
| `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. |
-| `mirror_available` | boolean | no | Allow mirrors to be set up for projects. If disabled, only admins will be able to set up mirrors in projects. |
+| `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. |
| `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
@@ -316,7 +322,11 @@ are listed in the descriptions of the relevant settings.
| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) |
| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) |
| `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
-| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) |
+| `snowplow_app_id` | string | no | The Snowplow site name / application id. (e.g. `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. |
+| `sourcegraph_public_only` | boolean | no | Blocks Sourcegraph from being loaded on private and internal projects. Defaul is `true`. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to `0` for unlimited time. |
| `terms` | text | required by: `enforce_terms` | (**Required by:** `enforce_terms`) Markdown content for the ToS. |
| `throttle_authenticated_api_enabled` | boolean | no | (**If enabled, requires:** `throttle_authenticated_api_period_in_seconds` and `throttle_authenticated_api_requests_per_period`) Enable authenticated API request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index 5f2202fa51d..95449d1ff77 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -92,7 +92,8 @@ Example response:
"jobs": {
"processed": 2,
"failed": 0,
- "enqueued": 0
+ "enqueued": 0,
+ "dead": 0
}
}
```
@@ -145,7 +146,8 @@ Example response:
"jobs": {
"processed": 2,
"failed": 0,
- "enqueued": 0
+ "enqueued": 0,
+ "dead": 0
}
}
```
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 56143969e3c..13c4b83dda8 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -17,7 +17,7 @@ Parameters:
| `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user|
| `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` |
| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Return list of tags matching the search criteria |
+| `search` | string | no | Return list of tags matching the search criteria. You can use `^term` and `term$` to find tags that begin and end with `term` respectively. |
> Support for `search` was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/54401) in GitLab 11.8.
diff --git a/doc/api/users.md b/doc/api/users.md
index f95ad7b62ba..c82a5e23c8e 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1124,7 +1124,7 @@ Parameters:
## Block user
-Blocks the specified user. Available only for admin.
+Blocks the specified user. Available only for admin.
```
POST /users/:id/block
@@ -1139,7 +1139,7 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
## Unblock user
-Unblocks the specified user. Available only for admin.
+Unblocks the specified user. Available only for admin.
```
POST /users/:id/unblock
diff --git a/doc/api/visual_review_discussions.md b/doc/api/visual_review_discussions.md
new file mode 100644
index 00000000000..385c1bf201d
--- /dev/null
+++ b/doc/api/visual_review_discussions.md
@@ -0,0 +1,40 @@
+# Visual Review discussions API **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/18710) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.5.
+
+Visual Review discussions are notes on Merge Requests sent as
+feedback from [Visual Reviews](../ci/review_apps/index.md#visual-reviews-starter).
+
+## Create new merge request thread
+
+Creates a new thread to a single project merge request. This is similar to creating
+a note but other comments (replies) can be added to it later.
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/visual_review_discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
+| `body` | string | yes | The content of the thread |
+| `position` | hash | no | Position when creating a diff note |
+| `position[base_sha]` | string | yes | Base commit SHA in the source branch |
+| `position[start_sha]` | string | yes | SHA referencing commit in target branch |
+| `position[head_sha]` | string | yes | SHA referencing HEAD of this merge request |
+| `position[position_type]` | string | yes | Type of the position reference. Either `text` or `image`. |
+| `position[new_path]` | string | no | File path after change |
+| `position[new_line]` | integer | no | Line number after change (Only stored for `text` diff notes) |
+| `position[old_path]` | string | no | File path before change |
+| `position[old_line]` | integer | no | Line number before change (Only stored for `text` diff notes) |
+| `position[width]` | integer | no | Width of the image (Only stored for `image` diff notes) |
+| `position[height]` | integer | no | Height of the image (Only stored for `image` diff notes) |
+| `position[x]` | integer | no | X coordinate (Only stored for `image` diff notes) |
+| `position[y]` | integer | no | Y coordinate (Only stored for `image` diff notes) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/11/visual_review_discussions?body=comment
+```
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index eaa4c13de55..21b3a6f4c96 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -1,115 +1,3 @@
# Vulnerabilities API **(ULTIMATE)**
-Every API call to vulnerabilities must be authenticated.
-
-If a user is not a member of a project and the project is private, a `GET`
-request on that project will result in a `404` status code.
-
-CAUTION: **Caution:**
-This API is in an alpha stage and considered unstable.
-The response payload may be subject to change or breakage
-across GitLab releases.
-
-## Vulnerabilities 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 vulnerabilities
-
-List all of a project's vulnerabilities.
-
-```
-GET /projects/:id/vulnerabilities
-GET /projects/:id/vulnerabilities?report_type=sast
-GET /projects/:id/vulnerabilities?report_type=container_scanning
-GET /projects/:id/vulnerabilities?report_type=sast,dast
-GET /projects/:id/vulnerabilities?scope=all
-GET /projects/:id/vulnerabilities?scope=dismissed
-GET /projects/:id/vulnerabilities?severity=high
-GET /projects/:id/vulnerabilities?confidence=unknown,experimental
-GET /projects/:id/vulnerabilities?pipeline_id=42
-```
-
-| 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. |
-| `report_type` | string array | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. |
-| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` |
-| `severity` | string array | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' |
-| `confidence` | string array | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all |
-| `pipeline_id` | integer/string | no | Returns vulnerabilities belonging to specified pipeline. |
-
-```bash
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities
-```
-
-Example response:
-
-```json
-[
- {
- "id": null,
- "report_type": "dependency_scanning",
- "name": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
- "severity": "unknown",
- "confidence": "undefined",
- "scanner": {
- "external_id": "gemnasium",
- "name": "Gemnasium"
- },
- "identifiers": [
- {
- "external_type": "gemnasium",
- "external_id": "9952e574-7b5b-46fa-a270-aeb694198a98",
- "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
- "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
- },
- {
- "external_type": "cve",
- "external_id": "CVE-2017-11429",
- "name": "CVE-2017-11429",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
- }
- ],
- "project_fingerprint": "fa6f5b6c5d240b834ac5e901dc69f9484cef89ec",
- "create_vulnerability_feedback_issue_path": "/tests/yarn-remediation-test/vulnerability_feedback",
- "create_vulnerability_feedback_merge_request_path": "/tests/yarn-remediation-test/vulnerability_feedback",
- "create_vulnerability_feedback_dismissal_path": "/tests/yarn-remediation-test/vulnerability_feedback",
- "project": {
- "id": 31,
- "name": "yarn-remediation-test",
- "full_path": "/tests/yarn-remediation-test",
- "full_name": "tests / yarn-remediation-test"
- },
- "dismissal_feedback": null,
- "issue_feedback": null,
- "merge_request_feedback": null,
- "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
- "links": [
- {
- "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
- },
- {
- "url": "https://www.kb.cert.org/vuls/id/475445"
- },
- {
- "url": "https://github.com/Clever/saml2/issues/127"
- }
- ],
- "location": {
- "file": "yarn.lock",
- "dependency": {
- "package": {
- "name": "saml2-js"
- },
- "version": "1.5.0"
- }
- },
- "solution": "Upgrade to fixed version.\r\n",
- "blob_path": "/tests/yarn-remediation-test/blob/cc6c4a0778460455ae5d16ca7025ca9ca1ca75ac/yarn.lock"
- }
-]
-```
+This document was moved to [another location](vulnerability_findings.md).
diff --git a/doc/api/vulnerability_findings.md b/doc/api/vulnerability_findings.md
new file mode 100644
index 00000000000..3d3f12aeef5
--- /dev/null
+++ b/doc/api/vulnerability_findings.md
@@ -0,0 +1,128 @@
+# Vulnerability Findings API **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/19029) in GitLab Ultimate 12.5.
+
+NOTE: **Note:**
+This API resource is renamed from Vulnerabilities to Vulnerability Findings because the Vulnerabilities are reserved
+for serving the upcoming [Standalone Vulnerability objects](https://gitlab.com/gitlab-org/gitlab/issues/13561).
+To fix any broken integrations with the former Vulnerabilities API, change the `vulnerabilities` URL part to be
+`vulnerability_findings`.
+
+Every API call to vulnerability findings must be [authenticated](README.md#authentication).
+
+Vulnerability findings are project-bound entities. If a user is not
+a member of a project and the project is private, a request on
+that project will result in a `404` status code.
+
+If a user is able to access the project but does not have permission to
+[use the Project Security Dashboard](../user/permissions.md#project-members-permissions),
+any request for vulnerability findings of this project will result in a `403` status code.
+
+CAUTION: **Caution:**
+This API is in an alpha stage and considered unstable.
+The response payload may be subject to change or breakage
+across GitLab releases.
+
+## Vulnerability findings 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 vulnerability findings
+
+List all of a project's vulnerability findings.
+
+```
+GET /projects/:id/vulnerability_findings
+GET /projects/:id/vulnerability_findings?report_type=sast
+GET /projects/:id/vulnerability_findings?report_type=container_scanning
+GET /projects/:id/vulnerability_findings?report_type=sast,dast
+GET /projects/:id/vulnerability_findings?scope=all
+GET /projects/:id/vulnerability_findings?scope=dismissed
+GET /projects/:id/vulnerability_findings?severity=high
+GET /projects/:id/vulnerability_findings?confidence=unknown,experimental
+GET /projects/:id/vulnerability_findings?pipeline_id=42
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) which the authenticated user is a member of. |
+| `report_type` | string array | no | Returns vulnerability findings belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. Defaults to all. |
+| `scope` | string | no | Returns vulnerability findings for the given scope: `all` or `dismissed`. Defaults to `dismissed`. |
+| `severity` | string array | no | Returns vulnerability findings belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all. |
+| `confidence` | string array | no | Returns vulnerability findings belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all. |
+| `pipeline_id` | integer/string | no | Returns vulnerability findings belonging to specified pipeline. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerability_findings
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": null,
+ "report_type": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "severity": "unknown",
+ "confidence": "undefined",
+ "scanner": {
+ "external_id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "identifiers": [
+ {
+ "external_type": "gemnasium",
+ "external_id": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "external_type": "cve",
+ "external_id": "CVE-2017-11429",
+ "name": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "project_fingerprint": "fa6f5b6c5d240b834ac5e901dc69f9484cef89ec",
+ "create_vulnerability_feedback_issue_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "create_vulnerability_feedback_merge_request_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "create_vulnerability_feedback_dismissal_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "project": {
+ "id": 31,
+ "name": "yarn-remediation-test",
+ "full_path": "/tests/yarn-remediation-test",
+ "full_name": "tests / yarn-remediation-test"
+ },
+ "dismissal_feedback": null,
+ "issue_feedback": null,
+ "merge_request_feedback": null,
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ }
+ ],
+ "location": {
+ "file": "yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "solution": "Upgrade to fixed version.\r\n",
+ "blob_path": "/tests/yarn-remediation-test/blob/cc6c4a0778460455ae5d16ca7025ca9ca1ca75ac/yarn.lock"
+ }
+]
+```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 5286764d178..d1cf7e63c63 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -132,7 +132,7 @@ Its feature set is listed on the table below according to DevOps stages.
| [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities.|
| [Dependency Scanning](../user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [License Compliance](../user/application_security/license_compliance/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. |
-| [Security Test reports](../user/project/merge_requests/index.md#security-reports-ultimate) **(ULTIMATE)** | Check for app vulnerabilities. |
+| [Security Test reports](../user/application_security/index.md) **(ULTIMATE)** | Check for app vulnerabilities. |
## Examples
diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md
index 234e7f4ed80..d9236b47a9a 100644
--- a/doc/ci/chatops/README.md
+++ b/doc/ci/chatops/README.md
@@ -58,6 +58,11 @@ ls:
- echo -e "section_start:$( date +%s ):chat_reply\r\033[0K\n$( ls -la )\nsection_end:$( date +%s ):chat_reply\r\033[0K"
```
+## GitLab ChatOps Examples
+
+The GitLab.com team created a repository of [common ChatOps scripts they use to interact with our Production instance of GitLab](https://gitlab.com/gitlab-com/chatops). They are likely useful
+to other adminstrators of GitLab instances and can serve as inspiration for ChatOps scripts you can write to interact with your own applications.
+
## GitLab ChatOps icon
Say Hi to our ChatOps bot.
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 54b21939116..dd474b09a9c 100644
--- a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
@@ -151,7 +151,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
GitLab is now configured to mirror changes from Bitbucket, run CI/CD pipelines
configured in `.gitlab-ci.yml` and push the status to Bitbucket.
-[pull-mirroring]: ../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter
+[pull-mirroring]: ../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter
<!-- ## Troubleshooting
diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md
index 08660b014b0..3df47d4cd4f 100644
--- a/doc/ci/ci_cd_for_external_repos/github_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/github_integration.md
@@ -26,7 +26,7 @@ To perform a one-off authorization with GitHub to grant GitLab access your
repositories:
1. Open <https://github.com/settings/tokens/new> to create a **Personal Access
- Token**. This token with be used to access your repository and push commit
+ Token**. This token will be used to access your repository and push commit
statuses to GitHub.
The `repo` and `admin:repo_hook` should be enable to allow GitLab access to
@@ -46,7 +46,7 @@ repositories:
GitLab will:
1. Import the project.
-1. Enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter)
+1. Enable [Pull Mirroring](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter)
1. Enable [GitHub project integration](../../user/project/integrations/github.md)
1. Create a web hook on GitHub to notify GitLab of new commits.
diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md
index 35e2117c285..b5878e70c53 100644
--- a/doc/ci/ci_cd_for_external_repos/index.md
+++ b/doc/ci/ci_cd_for_external_repos/index.md
@@ -20,7 +20,7 @@ Instead of moving your entire project to GitLab, you can connect your
external repository to get the benefits of GitLab CI/CD.
Connecting an external repository will set up [repository mirroring][mirroring]
-and create a lightweight project where issues, merge requests, wiki, and
+and create a lightweight project with issues, merge requests, wiki, and
snippets disabled. These features
[can be re-enabled later][settings].
@@ -101,5 +101,5 @@ requests and not on branches you can add `except: [branches]` to the job specs.
[ee-4642]: https://gitlab.com/gitlab-org/gitlab/merge_requests/4642
[eep]: https://about.gitlab.com/pricing/
-[mirroring]: ../../workflow/repository_mirroring.md
+[mirroring]: ../../user/project/repository/repository_mirroring.md
[settings]: ../../user/project/settings/index.md#sharing-and-permissions
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index f4bb7cd7d9f..c892320327b 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -174,7 +174,7 @@ support this.
The above command will register a new Runner to use the special
`docker:19.03.1` image, which is provided by Docker. **Notice that it's
using the `privileged` mode to start the build and service
- containers.** If you want to use [docker-in-docker] mode, you always
+ containers.** If you want to use [docker-in-docker](https://www.docker.com/blog/docker-can-now-run-within-docker/) mode, you always
have to use `privileged = true` in your Docker containers.
This will also mount `/certs/client` for the service and build
@@ -723,19 +723,22 @@ or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) execut
make sure that [`pull_policy`](https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work)
is set to `always`.
-[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/
[docker-cap]: https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
[2fa]: ../../user/profile/account/two_factor_authentication.md
[pat]: ../../user/profile/personal_access_tokens.md
-<!-- ## Troubleshooting
+## 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.
+### docker: Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?
-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 is a common error when you are using
+[Docker in Docker](#use-docker-in-docker-workflow-with-docker-executor)
+v19.03 or higher.
+
+This occurs because Docker starts on TLS automatically, so you need to do some set up.
+If:
+
+- This is the first time setting it up, carefully read
+ [using Docker in Docker workflow](#use-docker-in-docker-workflow-with-docker-executor).
+- You are upgrading from v18.09 or earlier, read our
+ [upgrade guide](https://about.gitlab.com/blog/2019/07/31/docker-in-docker-with-docker-19-dot-03/).
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index cef95c8e22a..6d620722608 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -292,10 +292,10 @@ For the value of:
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](../workflow/gitlab_flow.md), collisions
+ 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_SLUG` in `environment:url` in
- the example above: `https://$CI_COMMIT_REF_SLUG.example.com`, which would give a URL
+ 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:**
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index afe02e0a7d8..7af797f1851 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -35,8 +35,8 @@ apt-get install ruby-dev
The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more.
To use it simply define provider and any additional parameters required by the provider.
-For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
-There's more and all possible parameters can be found here: <https://github.com/travis-ci/dpl#heroku>.
+For example if you want to use it to deploy your application to Heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
+All possible parameters can be found here: <https://github.com/travis-ci/dpl#heroku-api>.
```yaml
staging:
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 e1c59f3b025..ffcc8195395 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
@@ -38,7 +38,7 @@ that's tested and deployed on every push to the `master` branch of the [codebase
This will also provide
boilerplate code for starting a browser-based game with the following components:
-- Written in [Typescript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io)
+- Written in [TypeScript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io)
- Building, running, and testing with [Gulp](https://gulpjs.com)
- Unit tests with [Chai](https://www.chaijs.com) and [Mocha](https://mochajs.org/)
- CI/CD with GitLab
@@ -508,7 +508,7 @@ deploy:
## Conclusion
Within the [demo repository](https://gitlab.com/blitzgren/gitlab-game-demo) you can also find a handful of boilerplate code to get
-[Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](https://gulpjs.com/) and [Phaser](https://phaser.io) all playing
+[TypeScript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](https://gulpjs.com/) and [Phaser](https://phaser.io) all playing
together nicely with GitLab CI/CD, which is the result of lessons learned while making [Dark Nova](https://www.darknova.io).
Using a combination of free and open source software, we have a full CI/CD pipeline, a game foundation,
and unit tests, all running and deployed at every push to master - with shockingly little code.
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 a7ed4ca3514..5acdd273548 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -379,7 +379,7 @@ 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 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](../../../workflow/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
+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.
In a real world project, teams may use [Issue Tracker](../../../user/project/issues/index.md) and [Merge Requests](../../../user/project/merge_requests/index.md) to move their code across branches:
```bash
diff --git a/doc/ci/img/pipelines_junit_test_report_ui_v12_5.png b/doc/ci/img/pipelines_junit_test_report_ui_v12_5.png
new file mode 100644
index 00000000000..5b1e3254f8b
--- /dev/null
+++ b/doc/ci/img/pipelines_junit_test_report_ui_v12_5.png
Binary files differ
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 361e526ed96..f7e8a0e412c 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -59,7 +59,7 @@ the terminal and type commands like a normal shell.
If you have the terminal open and the job has finished with its tasks, the
terminal will block the job from finishing for the duration configured in
-[`[session_server].terminal_max_retention_time`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you
+[`[session_server].session_timeout`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section) until you
close the terminal window.
![finished job with terminal open](img/finished_job_with_terminal_open.png)
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index a644a89eee4..a07252f4803 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -5,7 +5,7 @@ type: concepts
# Introduction to CI/CD with GitLab
-In this document we'll present an overview of the concepts of Continuous Integration,
+In this document, we'll present an overview of the concepts of Continuous Integration,
Continuous Delivery, and Continuous Deployment, as well as an introduction to
GitLab CI/CD.
@@ -31,7 +31,7 @@ to be applied according to what best suits your strategy.
### Continuous Integration
-Consider an application which has its code stored in a Git
+Consider an application that has its code stored in a Git
repository in GitLab. Developers push code changes every day,
multiple times a day. For every push to the repository, you
can create a set of scripts to build and test your application
@@ -94,7 +94,7 @@ To add scripts to that file, you'll need to organize them in a
sequence that suits your application and are in accordance with
the tests you wish to perform. To visualize the process, imagine
that all the scripts you add to the configuration file are the
-same as the commands you run on a terminal in your computer.
+same as the commands you run on a terminal on your computer.
Once you've added your `.gitlab-ci.yml` configuration file to your
repository, GitLab will detect it and run your scripts with the
@@ -121,7 +121,7 @@ Both of them compose a **pipeline** triggered at every push
to any branch of the repository.
GitLab CI/CD not only executes the jobs you've
-set, but also shows you what's happening during execution, as you
+set but also shows you what's happening during execution, as you
would see in your terminal:
![job running](img/job_running.png)
@@ -164,7 +164,7 @@ Once you're happy with your implementation:
GitLab CI/CD is capable of doing a lot more, but this workflow
exemplifies GitLab's ability to track the entire process,
-without the need of any external tool to deliver your software.
+without the need for an external tool to deliver your software.
And, most usefully, you can visualize all the steps through
the GitLab UI.
@@ -172,7 +172,7 @@ the GitLab UI.
If we take a deeper look into the basic workflow, we can see
the features available in GitLab at each stage of the DevOps
-lifecycle, as shown on the illustration below.
+lifecycle, as shown in the illustration below.
![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_v12_3.png)
@@ -207,7 +207,7 @@ With GitLab CI/CD you can also:
- Deploy your app to different [environments](../environments.md).
- Install your own [GitLab Runner](https://docs.gitlab.com/runner/).
- [Schedule pipelines](../../user/project/pipelines/schedules.md).
-- Check for app vulnerabilities with [Security Test reports](../../user/project/merge_requests/index.md#security-reports-ultimate). **(ULTIMATE)**
+- Check for app vulnerabilities with [Security Test reports](../../user/application_security/index.md). **(ULTIMATE)**
To see all CI/CD features, navigate back to the [CI/CD index](../README.md).
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index bc30e007393..c03d1798ae1 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -178,3 +178,27 @@ Currently, the following tools might not work because their XML formats are unsu
|Case|Tool|Issue|
|---|---|---|
|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|<https://gitlab.com/gitlab-org/gitlab-foss/issues/50964>|
+
+## Viewing JUnit test reports on GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/24792) in GitLab 12.5.
+
+If JUnit XML files are generated and uploaded as part of a pipeline, these reports
+can be viewed inside the pipelines details page. The **Tests** tab on this page will
+display a list of test suites and cases reported from the XML file.
+
+![Test Reports Widget](img/pipelines_junit_test_report_ui_v12_5.png)
+
+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.
+
+### Enabling the feature
+
+This feature comes with the `:junit_pipeline_view` feature flag disabled by default.
+To enable this feature, ask a GitLab administrator with Rails console access to run the
+following command:
+
+```ruby
+Feature.enable(:junit_pipeline_view)
+```
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 f2a7902c9ca..b8976ffae7f 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
@@ -32,8 +32,8 @@ Merge trains have the following requirements and limitations:
- This feature requires that
[pipelines for merged results](../index.md#pipelines-for-merged-results-premium) are
**configured properly**.
-- Each merge train can run a maximum of **four** pipelines in parallel.
- If more than four merge requests are added to the merge train, the merge requests
+- 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.
- This feature does not support [squash and merge](../../../../user/project/merge_requests/squash_and_merge.md).
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 093d334e937..f13d05716f1 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -87,10 +87,9 @@ not be found, or a user does not have access rights to create pipeline there,
the `staging` job is going to be marked as _failed_.
CAUTION: **Caution:**
-`staging` will succeed as soon as a downstream pipeline gets created.
-GitLab does not support status attribution yet, however adding first-class
-`trigger` configuration syntax is ground work for implementing
-[status attribution](https://gitlab.com/gitlab-org/gitlab-foss/issues/39640).
+In the example, `staging` will be marked as succeeded as soon as a downstream pipeline
+gets created. If you want to display the downstream pipeline's status instead, see
+[Mirroring status from triggered pipeline](#mirroring-status-from-triggered-pipeline).
NOTE: **Note:**
Bridge jobs do not support every configuration entry that a user can use
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index e5f2701c6ae..590a02b306c 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -28,7 +28,7 @@ If all the jobs in a stage:
- Fail, the next stage is not (usually) executed and the pipeline ends early.
NOTE: **Note:**
-If you have a [mirrored repository that GitLab pulls from](../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository that GitLab pulls from](../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter),
you may need to enable pipeline triggering in your project's
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
@@ -269,6 +269,38 @@ To execute a pipeline manually:
The pipeline will execute the jobs as configured.
+#### Using a query string
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/24146) in GitLab 12.5.
+
+Variables on the **Run Pipeline** page can be pre-populated by passing variable keys and values
+in a query string appended to the `pipelines/new` URL. The format is:
+
+```plaintext
+.../pipelines/new?ref=<branch>&var[<variable_key>]=<value>&file_var[<file_key>]=<value>
+```
+
+The following parameters are supported:
+
+- `ref`: specify the branch to populate the **Run for** field with.
+- `var`: specify a `Variable` variable.
+- `file_var`: specify a `File` variable.
+
+For each `var` or `file_var`, a key and value are required.
+
+For example, the query string
+`.../pipelines/new?ref=my_branch&var[foo]=bar&file_var[file_foo]=file_bar` will pre-populate the
+**Run Pipeline** page as follows:
+
+- **Run for** field: `my_branch`.
+- **Variables** section:
+ - Variable:
+ - Key: `foo`
+ - Value: `bar`
+ - File:
+ - Key: `file_foo`
+ - Value: `file_bar`
+
### Accessing pipelines
You can find the current and historical pipeline runs under your project's
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 10a898be900..68e977c1c98 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -50,7 +50,7 @@ project's **Pipelines** page.
This guide assumes that you have:
-- A working GitLab instance of version 8.0+r or are using
+- A working GitLab instance of version 8.0+ or are using
[GitLab.com](https://gitlab.com).
- A project in GitLab that you would like to use CI for.
- Maintainer or owner access to the project
@@ -143,7 +143,7 @@ Now if you go to the **Pipelines** page you will see that the pipeline is
pending.
NOTE: **Note:**
-If you have a [mirrored repository where GitLab pulls from](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository where GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter),
you may need to enable pipeline triggering in your project's
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 5d86d382aa8..cff797549ba 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -54,25 +54,37 @@ 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.
-#### Variable types
+#### 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": the Runner will create an environment variable named same as the variable key and set its value to the variable value.
-- "File": 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 same as the variable key.
+- [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.
-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. Prior to the introduction of variable types, the common pattern was to use the value of a CI variable, save it in a file, and then use the newly created file in your script:
+##### 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:
```bash
-# Save the content of variable in a file
+# Read certificate stored in $KUBE_CA_PEM variable and save it in a new file
echo "$KUBE_CA_PEM" > "$(pwd)/kube.ca.pem"
- # Use the newly created file
+# Pass the newly created file to kubectl
kubectl config set-cluster e2e --server="$KUBE_URL" --certificate-authority="$(pwd)/kube.ca.pem"
```
-This can be simplified by creating a variable of type "File" and using it directly. For example, let's say we have the following variables.
+##### 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)
@@ -345,7 +357,12 @@ Group-level variables can be added by:
1. Inputing 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.
+Once you set them, they will be available for all subsequent pipelines. Any group-level user defined variables can be viewed in projects by:
+
+1. Navigating to the project's **Settings > CI/CD** page.
+1. Expanding the **Variables** section.
+
+![CI/CD settings - inherited variables](img/inherited_group_variables_v12_5.png)
## Priority of environment variables
diff --git a/doc/ci/variables/deprecated_variables.md b/doc/ci/variables/deprecated_variables.md
index cdca5bf27fc..543da481938 100644
--- a/doc/ci/variables/deprecated_variables.md
+++ b/doc/ci/variables/deprecated_variables.md
@@ -20,15 +20,15 @@ future GitLab releases.**
| 8.x name | 9.0+ name |
| --------------------- |------------------------ |
+| `CI_BUILD_BEFORE_SHA` | `CI_COMMIT_BEFORE_SHA` |
| `CI_BUILD_ID` | `CI_JOB_ID` |
+| `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` |
+| `CI_BUILD_NAME` | `CI_JOB_NAME` |
| `CI_BUILD_REF` | `CI_COMMIT_SHA` |
-| `CI_BUILD_TAG` | `CI_COMMIT_TAG` |
-| `CI_BUILD_BEFORE_SHA` | `CI_COMMIT_BEFORE_SHA` |
| `CI_BUILD_REF_NAME` | `CI_COMMIT_REF_NAME` |
| `CI_BUILD_REF_SLUG` | `CI_COMMIT_REF_SLUG` |
-| `CI_BUILD_NAME` | `CI_JOB_NAME` |
-| `CI_BUILD_STAGE` | `CI_JOB_STAGE` |
| `CI_BUILD_REPO` | `CI_REPOSITORY_URL` |
-| `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` |
-| `CI_BUILD_MANUAL` | `CI_JOB_MANUAL` |
+| `CI_BUILD_STAGE` | `CI_JOB_STAGE` |
+| `CI_BUILD_TAG` | `CI_COMMIT_TAG` |
| `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` |
+| `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` |
diff --git a/doc/ci/variables/img/inherited_group_variables_v12_5.png b/doc/ci/variables/img/inherited_group_variables_v12_5.png
new file mode 100644
index 00000000000..f9043df051c
--- /dev/null
+++ b/doc/ci/variables/img/inherited_group_variables_v12_5.png
Binary files differ
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 20e70d212b0..b93ff62cc21 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -21,111 +21,111 @@ future GitLab releases.**
## Variables reference
-| Variable | GitLab | Runner | Description |
-|-------------------------------------------|--------|--------|-------------|
-| `ARTIFACT_DOWNLOAD_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
-| `CHAT_INPUT` | 10.6 | all | Additional arguments passed in the [ChatOps](../chatops/README.md) command |
-| `CHAT_CHANNEL` | 10.6 | all | Source chat channel which triggered the [ChatOps](../chatops/README.md) command |
-| `CI` | all | 0.4 | Mark that job is executed in CI environment |
-| `CI_BUILDS_DIR` | all | 11.10 | Top-level directory where builds are executed. |
-| `CI_CONCURRENT_ID` | all | 11.10 | Unique ID of build execution within a single executor. |
-| `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | Unique ID of build execution within a single executor and project. |
-| `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch before a merge request. Only populated when there is a merge request associated with the pipeline. |
-| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
-| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
-| `CI_COMMIT_REF_NAME` | 9.0 | all | The branch or tag name for which project is built |
-| `CI_COMMIT_REF_SLUG` | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
-| `CI_COMMIT_SHA` | 9.0 | all | The commit revision for which project is built |
-| `CI_COMMIT_SHORT_SHA` | 11.7 | all | The first eight characters of `CI_COMMIT_SHA` |
-| `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
-| `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message |
-| `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
-| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug logging (tracing)](README.md#debug-logging) is enabled |
-| `CI_DEPLOY_PASSWORD` | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| `CI_DEPLOY_USER` | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.|
-| `CI_DISPOSABLE_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
-| `CI_ENVIRONMENT_NAME` | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
-| `CI_ENVIRONMENT_SLUG` | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
-| `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. |
-| `CI_DEFAULT_BRANCH` | 12.4 | all | The name of the default branch for the project. |
-| `CI_JOB_ID` | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
-| `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` |
-| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
-| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
-| `CI_MERGE_REQUEST_ID` | 11.6 | all | The ID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_IID` | 11.6 | all | The IID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_PROJECT_ID` | 11.6 | all | The ID of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_PROJECT_PATH` | 11.6 | all | The path of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_PROJECT_URL` | 11.6 | all | The URL of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_REF_PATH` | 11.6 | all | The ref path of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | 11.6 | all | The source branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used, the merge request is created, and the pipeline is a [merged result pipeline](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
-| `CI_MERGE_REQUEST_SOURCE_PROJECT_ID` | 11.6 | all | The ID of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH` | 11.6 | all | The path of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_SOURCE_PROJECT_URL` | 11.6 | all | The URL of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | 11.6 | all | The target branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used, the merge request is created, and the pipeline is a [merged result pipeline](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
-| `CI_MERGE_REQUEST_TITLE` | 11.9 | all | The title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `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]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
-| `CI_EXTERNAL_PULL_REQUEST_IID` | 12.3 | all | Pull Request ID from GitHub 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]` is used and the pull request is open. |
-| `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]` 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]` is used and the pull request is open. |
-| `CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME` | 12.3 | all | The source 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]` 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]` is used and the pull request is open. |
-| `CI_NODE_INDEX` | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
-| `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_API_V4_URL` | 11.7 | all | The GitLab API v4 root URL |
-| `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 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`, and `pipeline`. 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 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_TITLE` | 12.4 | all | The human-readable project name as displayed in the GitLab web interface. |
-| `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 |
-| `CI_PROJECT_PATH_SLUG` | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| `CI_PROJECT_URL` | 8.10 | 0.5 | The HTTP(S) address to access project |
-| `CI_PROJECT_VISIBILITY` | 10.3 | all | The project visibility (internal, private, public) |
-| `CI_PROJECT_REPOSITORY_LANGUAGES` | 12.3 | all | Comma-separated, lowercased list of the languages used in the repository (e.g. `ruby,javascript,html,css`) |
-| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | If the job is running on a protected branch |
-| `CI_REGISTRY` | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| `CI_REGISTRY_IMAGE` | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
-| `CI_REGISTRY_PASSWORD` | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
-| `CI_REGISTRY_USER` | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
-| `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_REVISION` | all | 10.6 | GitLab Runner revision that is executing the current job |
-| `CI_RUNNER_TAGS` | 8.10 | 0.5 | The defined runner tags |
-| `CI_RUNNER_VERSION` | all | 10.6 | GitLab Runner version 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_SERVER` | all | all | Mark that job is executed in CI environment |
-| `CI_SERVER_HOST` | 12.1 | all | Host component of the GitLab instance URL, without protocol and port (like `gitlab.example.com`) |
-| `CI_SERVER_NAME` | all | all | The name of CI server that is used to coordinate jobs |
-| `CI_SERVER_REVISION` | all | all | GitLab revision that is used to schedule jobs |
-| `CI_SERVER_VERSION` | all | all | GitLab version that is used to schedule jobs |
-| `CI_SERVER_VERSION_MAJOR` | 11.4 | all | GitLab version major component |
-| `CI_SERVER_VERSION_MINOR` | 11.4 | all | GitLab version minor component |
-| `CI_SERVER_VERSION_PATCH` | 11.4 | all | GitLab version patch component |
-| `CI_SHARED_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
-| `GET_SOURCES_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
-| `GITLAB_CI` | all | all | Mark that job is executed in GitLab CI environment |
-| `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_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 |
-| `GITLAB_FEATURES` | 10.6 | all | The comma separated list of licensed features available for your instance and plan |
+| Variable | GitLab | Runner | Description |
+|-----------------------------------------------|--------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `ARTIFACT_DOWNLOAD_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to download artifacts running a job |
+| `CHAT_CHANNEL` | 10.6 | all | Source chat channel which triggered the [ChatOps](../chatops/README.md) command |
+| `CHAT_INPUT` | 10.6 | all | Additional arguments passed in the [ChatOps](../chatops/README.md) command |
+| `CI` | all | 0.4 | Mark that job is executed in CI environment |
+| `CI_API_V4_URL` | 11.7 | all | The GitLab API v4 root URL |
+| `CI_BUILDS_DIR` | all | 11.10 | Top-level directory where builds are executed. |
+| `CI_COMMIT_BEFORE_SHA` | 11.2 | all | The previous latest commit present on a branch before a merge request. Only populated when there is a merge request associated with the pipeline. |
+| `CI_COMMIT_DESCRIPTION` | 10.8 | all | The description of the commit: the message without first line, if the title is shorter than 100 characters; full message in other case. |
+| `CI_COMMIT_MESSAGE` | 10.8 | all | The full commit message. |
+| `CI_COMMIT_REF_NAME` | 9.0 | all | The branch or tag name for which project is built |
+| `CI_COMMIT_REF_PROTECTED` | 11.11 | all | If the job is running on a protected branch |
+| `CI_COMMIT_REF_SLUG` | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. |
+| `CI_COMMIT_SHA` | 9.0 | all | The commit revision for which project is built |
+| `CI_COMMIT_SHORT_SHA` | 11.7 | all | The first eight characters of `CI_COMMIT_SHA` |
+| `CI_COMMIT_TAG` | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| `CI_COMMIT_TITLE` | 10.8 | all | The title of the commit - the full first line of the message |
+| `CI_CONCURRENT_ID` | all | 11.10 | Unique ID of build execution within a single executor. |
+| `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | Unique ID of build execution within a single executor and project. |
+| `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` |
+| `CI_DEBUG_TRACE` | all | 1.7 | Whether [debug logging (tracing)](README.md#debug-logging) is enabled |
+| `CI_DEFAULT_BRANCH` | 12.4 | all | The name of the default branch for the project. |
+| `CI_DEPLOY_PASSWORD` | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related. |
+| `CI_DEPLOY_USER` | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related. |
+| `CI_DISPOSABLE_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. |
+| `CI_ENVIRONMENT_NAME` | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
+| `CI_ENVIRONMENT_SLUG` | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. |
+| `CI_ENVIRONMENT_URL` | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. |
+| `CI_EXTERNAL_PULL_REQUEST_IID` | 12.3 | all | Pull Request ID from GitHub 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]` is used and the pull request is open. |
+| `CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME` | 12.3 | all | The source 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]` is used and the pull request is open. |
+| `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]` 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]` 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]` is used and the pull request is open. |
+| `CI_JOB_ID` | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
+| `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` |
+| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry][registry] and downloading [dependent repositories][dependent-repositories] |
+| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
+| `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]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_ID` | 11.6 | all | The ID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_IID` | 11.6 | all | The IID of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_LABELS` | 11.9 | all | Comma-separated label names of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_MILESTONE` | 11.9 | all | The milestone title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_PROJECT_ID` | 11.6 | all | The ID of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_PROJECT_PATH` | 11.6 | all | The path of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `namespace/awesome-project`). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_PROJECT_URL` | 11.6 | all | The URL of the project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md) (e.g. `http://192.168.10.15:3000/namespace/awesome-project`). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_REF_PATH` | 11.6 | all | The ref path of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). (e.g. `refs/merge-requests/1/head`). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | 11.6 | all | The source branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used, the merge request is created, and the pipeline is a [merged result pipeline](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
+| `CI_MERGE_REQUEST_SOURCE_PROJECT_ID` | 11.6 | all | The ID of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH` | 11.6 | all | The path of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_SOURCE_PROJECT_URL` | 11.6 | all | The URL of the source project of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | 11.6 | all | The target branch name of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used, the merge request is created, and the pipeline is a [merged result pipeline](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
+| `CI_MERGE_REQUEST_TITLE` | 11.9 | all | The title of the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` is used and the merge request is created. |
+| `CI_NODE_INDEX` | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. |
+| `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 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`, and `pipeline`. 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 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 |
+| `CI_PROJECT_PATH_SLUG` | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| `CI_PROJECT_REPOSITORY_LANGUAGES` | 12.3 | all | Comma-separated, lowercased list of the languages used in the repository (e.g. `ruby,javascript,html,css`) |
+| `CI_PROJECT_TITLE` | 12.4 | all | The human-readable project name as displayed in the GitLab web interface. |
+| `CI_PROJECT_URL` | 8.10 | 0.5 | The HTTP(S) address to access project |
+| `CI_PROJECT_VISIBILITY` | 10.3 | all | The project visibility (internal, private, public) |
+| `CI_REGISTRY` | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry. This variable will include a `:port` value if one has been specified in the registry configuration. |
+| `CI_REGISTRY_IMAGE` | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
+| `CI_REGISTRY_PASSWORD` | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
+| `CI_REGISTRY_USER` | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
+| `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_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 |
+| `CI_RUNNER_VERSION` | all | 10.6 | GitLab Runner version that is executing the current job |
+| `CI_SERVER` | all | all | Mark that job is executed in CI environment |
+| `CI_SERVER_HOST` | 12.1 | all | Host component of the GitLab instance URL, without protocol and port (like `gitlab.example.com`) |
+| `CI_SERVER_NAME` | all | all | The name of CI server that is used to coordinate jobs |
+| `CI_SERVER_REVISION` | all | all | GitLab revision that is used to schedule jobs |
+| `CI_SERVER_VERSION` | all | all | GitLab version that is used to schedule jobs |
+| `CI_SERVER_VERSION_MAJOR` | 11.4 | all | GitLab version major component |
+| `CI_SERVER_VERSION_MINOR` | 11.4 | all | GitLab version minor component |
+| `CI_SERVER_VERSION_PATCH` | 11.4 | all | GitLab version patch component |
+| `CI_SHARED_ENVIRONMENT` | all | 10.1 | Marks that the job is executed in a shared environment (something that is persisted across CI invocations like `shell` or `ssh` executor). If the environment is shared, it is set to true, otherwise it is not defined at all. |
+| `GET_SOURCES_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to fetch sources running a job |
+| `GITLAB_CI` | all | all | Mark that job is executed in GitLab CI 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_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 |
[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
[registry]: ../../user/packages/container_registry/index.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 4569e9ff9b6..62644e78872 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -23,7 +23,7 @@ We have complete examples of configuring pipelines:
- To see a large `.gitlab-ci.yml` file used in an enterprise, see the [`.gitlab-ci.yml` file for `gitlab`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml).
NOTE: **Note:**
-If you have a [mirrored repository where GitLab pulls from](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository where GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter),
you may need to enable pipeline triggering in your project's
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
@@ -106,7 +106,7 @@ The following table lists available parameters for jobs:
| [`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`, 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:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, and `artifacts:reports:junit`.<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`. |
+| [`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`, and `artifacts:reports:junit`.<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. |
@@ -135,6 +135,7 @@ The following job parameters can be defined inside a `default:` block:
- [`before_script`](#before_script-and-after_script)
- [`after_script`](#before_script-and-after_script)
- [`cache`](#cache)
+- [`interruptible`](#interruptible)
In the following example, the `ruby:2.5` image is set as the default for all
jobs except the `rspec 2.6` job, which uses the `ruby:2.6` image:
@@ -181,6 +182,25 @@ 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:
`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``.
+#### YAML anchors for `script`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/23005) in GitLab 12.5.
+
+You can use [YAML anchors](#anchors) with scripts, which makes it possible to
+include a predefined list of commands in multiple jobs.
+
+Example:
+
+```yaml
+.something: &something
+- echo 'something'
+
+job_name:
+ script:
+ - *something
+ - echo 'this is the script'
+```
+
### `image`
Used to specify [a Docker image](../docker/using_docker_images.md#what-is-an-image) to use for the job.
@@ -240,34 +260,26 @@ For more information, see see [Available settings for `services`](../docker/usin
> Introduced in GitLab 8.7 and requires GitLab Runner v1.2.
-`before_script` is used to define the command that should be run before all
-jobs, including deploy jobs, but after the restoration of [artifacts](#artifacts).
+`before_script` is used to define a command that should be run before each
+job, including deploy jobs, but after the restoration of any [artifacts](#artifacts).
This must be an an array.
-`after_script` is used to define the command that will be run after all
-jobs, including failed ones. This must be an an array.
-
-Scripts specified in `before_script` are:
+Scripts specified in `before_script` are concatenated with any scripts specified
+in the main [`script`](#script), and executed together in a single shell.
-- Concatenated with scripts specified in the main `script`. Job-level
- `before_script` definition override global-level `before_script` definition
- when concatenated with `script` definition.
-- Executed together with main `script` script as one script in a single shell
- context.
+`after_script` is used to define the command that will be run after each
+job, including failed ones. This must be an an array.
-Scripts specified in `after_script`:
+Scripts specified in `after_script` are executed in a new shell, separate from any
+`before_script` or `script` scripts. As a result, they:
- Have a current working directory set back to the default.
-- Are executed in a shell context separated from `before_script` and `script`
- scripts.
-- Because of separated context, cannot see changes done by scripts defined
- in `before_script` or `script` scripts, either:
- - In shell. For example, command aliases and variables exported in `script`
- scripts.
- - Outside of the working tree (depending on the Runner executor). For example,
- software installed by a `before_script` or `script` scripts.
-
-It's possible to overwrite the globally defined `before_script` and `after_script`
+- Have no access to changes done by scripts defined in `before_script` or `script`, including:
+ - Command aliases and variables exported in `script` scripts.
+ - Changes outside of the working tree (depending on the Runner executor), like
+ software installed by a `before_script` or `script` script.
+
+It's possible to overwrite a globally defined `before_script` or `after_script`
if you set it per-job:
```yaml
@@ -284,6 +296,33 @@ job:
- execute this after my script
```
+#### YAML anchors for `before_script` and `after_script`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/23005) in GitLab 12.5.
+
+You can use [YAML anchors](#anchors) with `before_script` and `after_script`,
+which makes it possible to include a predefined list of commands in multiple
+jobs.
+
+Example:
+
+```yaml
+.something_before: &something_before
+- echo 'something before'
+
+.something_after: &something_after
+- echo 'something after'
+
+
+job_name:
+ before_script:
+ - *something_before
+ script:
+ - echo 'this is the script'
+ after_script:
+ - *something_after
+```
+
### `stages`
`stages` is used to define stages that can be used by jobs and is defined
@@ -329,6 +368,37 @@ 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`.
+For example, the following are equivalent configuration:
+
+- Configured in order:
+
+ ```yml
+ stages:
+ - .pre
+ - a
+ - b
+ - .post
+ ```
+
+- Configured out of order:
+
+ ```yml
+ stages:
+ - a
+ - .pre
+ - b
+ - .post
+ ```
+
+- Not explicitly configured:
+
+ ```yml
+ stages:
+ - a
+ - b
+ ```
+
### `stage`
`stage` is defined per-job and relies on [`stages`](#stages) which is defined
@@ -379,6 +449,9 @@ Jobs will run on your own Runners in parallel only if:
### `only`/`except` (basic)
+NOTE: **Note:**
+These parameters will soon be [deprecated](https://gitlab.com/gitlab-org/gitlab/issues/27449) in favor of [`rules`](#rules) as it offers a more powerful syntax.
+
`only` and `except` are two parameters that set a job policy to limit when
jobs are created:
@@ -926,12 +999,11 @@ docker build:
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 isn't available, please
-[open an issue](https://www.gitlab.com/gitlab-org/gitlab/issues).
+[open an issue](https://gitlab.com/gitlab-org/gitlab/issues).
### `tags`
@@ -1463,6 +1535,50 @@ cache:
- binaries/
```
+##### `cache:key:files`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/18986) in GitLab v12.5.
+
+If `cache:key:files` is added, one or two files must be defined with it. The cache `key`
+will be a SHA computed from the most recent commits (one or two) that changed the
+given files. If neither file was changed in any commits, the key will be `default`.
+
+```yaml
+cache:
+ key:
+ files:
+ - Gemfile.lock
+ - package.json
+ paths:
+ - vendor/ruby
+ - node_modules
+```
+
+##### `cache:key:prefix`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/18986) in GitLab v12.5.
+
+The `prefix` parameter adds extra functionality to `key:files` by allowing the key to
+be composed of the given `prefix` combined with the SHA computed for `cache:key:files`.
+For example, adding a `prefix` of `rspec`, will
+cause keys to look like: `rspec-feef9576d21ee9b6a32e30c5c79d0a0ceb68d1e5`. If neither
+file was changed in any commits, the prefix is added to `default`, so the key in the
+example would be `rspec-default`.
+
+`prefix` follows the same restrictions as `key`, so it can use any of the
+[predefined variables](../variables/README.md). Similarly, the `/` character or the
+equivalent URI-encoded `%2F`, or a value made only of `.` or `%2E`, is not allowed.
+
+```yaml
+cache:
+ key:
+ files:
+ - Gemfile.lock
+ prefix: ${CI_JOB_NAME}
+ paths:
+ - vendor/ruby
+```
+
#### `cache:untracked`
Set `untracked: true` to cache all files that are untracked in your Git
@@ -1596,6 +1712,47 @@ release-job:
- tags
```
+#### `artifacts:expose_as`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15018) in GitLab 12.5.
+
+The `expose_as` keyword can be used to expose [job artifacts](../../user/project/pipelines/job_artifacts.md)
+in the [merge request](../../user/project/merge_requests/index.md) UI.
+
+For example, to match a single file:
+
+```yml
+test:
+ script: [ 'echo 1' ]
+ artifacts:
+ expose_as: 'artifact 1'
+ paths: ['path/to/file.txt']
+```
+
+With this configuration, GitLab will add a link **artifact 1** to the relevant merge request
+that points to `file1.txt`.
+
+An example that will match an entire directory:
+
+```yml
+test:
+ script: [ 'echo 1' ]
+ artifacts:
+ expose_as: 'artifact 1'
+ paths: ['path/to/directory/']
+```
+
+Note the following:
+
+- A maximum of 10 job artifacts per merge request can be exposed.
+- Glob patterns are unsupported.
+- If a directory is specified, the link will be to the job [artifacts browser](../../user/project/pipelines/job_artifacts.md#browsing-artifacts) if there is more than
+ one file in the directory.
+- For exposed single file artifacts with `.html`, `.htm`, `.txt`, `.json`, `.xml`,
+ and `.log` extensions, if [GitLab Pages](../../administration/pages/index.md) is:
+ - Enabled, GitLab will automatically render the artifact.
+ - Not enabled, you will see the file in the artifacts browser.
+
#### `artifacts:name`
> Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
@@ -1910,8 +2067,6 @@ 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.
----
-
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`
will be downloaded and extracted in the context of the build. The same happens
@@ -2249,6 +2404,24 @@ staging:
branch: stable
```
+It is possible to mirror the status from a triggered pipeline:
+
+```
+trigger_job:
+ trigger:
+ project: my/project
+ strategy: depend
+```
+
+It is possible to mirror the status from an upstream pipeline:
+
+```
+upstream_bridge:
+ stage: test
+ needs:
+ pipeline: other/project
+```
+
### `interruptible`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/23464) in GitLab 12.3.
diff --git a/doc/development/README.md b/doc/development/README.md
index 16b073045cc..66df6f46e86 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -99,7 +99,7 @@ description: 'Learn how to contribute to GitLab.'
- [Post deployment migrations](post_deployment_migrations.md)
- [Background migrations](background_migrations.md)
- [Swapping tables](swapping_tables.md)
-- [Deleting exiting migrations](deleting_migrations.md)
+- [Deleting migrations](deleting_migrations.md)
### Best practices
@@ -118,6 +118,7 @@ description: 'Learn how to contribute to GitLab.'
- [Query Count Limits](query_count_limits.md)
- [Database helper modules](database_helpers.md)
- [Code comments](code_comments.md)
+- [Creating enums](creating_enums.md)
### Case studies
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index cdd0e9b2a7b..b0d94511c6e 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -43,14 +43,14 @@ a new presenter specifically for GraphQL.
The presenter is initialized using the object resolved by a field, and
the context.
-### Exposing Global ids
+### Exposing Global IDs
-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
+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
-type for which you are exposing an id. Please make sure that when
+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.
@@ -146,6 +146,10 @@ query($project_path: ID!) {
}
```
+To ensure that we get consistent ordering, we will append an ordering on the primary
+key, in descending order. This is usually `id`, so basically we will add `order(id: :desc)`
+to the end of the relation. A primary key _must_ be available on the underlying table.
+
### Exposing permissions for a type
To expose permissions the current user has on a resource, you can call
@@ -236,6 +240,47 @@ end
```
+## Descriptions
+
+All fields and arguments
+[must have descriptions](https://gitlab.com/gitlab-org/gitlab/merge_requests/16438).
+
+A description of a field or argument is given using the `description:`
+keyword. For example:
+
+```ruby
+field :id, GraphQL::ID_TYPE, description: 'ID of the resource'
+```
+
+Descriptions of fields and arguments are viewable to users through:
+
+- The [GraphiQL explorer](../api/graphql/#graphiql).
+- The [static GraphQL API reference](../api/graphql/#reference).
+
+### Description styleguide
+
+To ensure consistency, the following should be followed whenever adding or updating
+descriptions:
+
+- Mention the name of the resource in the description. Example:
+ `'Labels of the issue'` (issue being the resource).
+- Use `"{x} of the {y}"` where possible. Example: `'Title of the issue'`.
+ Do not start descriptions with `The`.
+- Descriptions of `GraphQL::BOOLEAN_TYPE` fields should answer the question: "What does
+ this field do?". Example: `'Indicates project has a Git repository'`.
+- Always include the word `"timestamp"` when describing an argument or
+ field of type `Types::TimeType`. This lets the reader know that the
+ format of the property will be `Time`, rather than just `Date`.
+- No `.` at end of strings.
+
+Example:
+
+```ruby
+field :id, GraphQL::ID_TYPE, description: 'ID of the Issue'
+field :confidential, GraphQL::BOOLEAN_TYPE, description: 'Indicates the issue is confidential'
+field :closed_at, Types::TimeType, description: 'Timestamp of when the issue was closed'
+```
+
## Authorization
Authorizations can be applied to both types and fields using the same
@@ -350,7 +395,10 @@ To find objects to display in a field, we can add resolvers to
`app/graphql/resolvers`.
Arguments can be defined within the resolver, those arguments will be
-made available to the fields using the resolver.
+made available to the fields using the resolver. When exposing a model
+that had an internal ID (`iid`), prefer using that in combination with
+the namespace path as arguments in a resolver over a database
+ID. Othewise 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
@@ -365,6 +413,10 @@ actions. In the same way a GET-request should not modify data, we
cannot modify data in a regular GraphQL-query. We can however in a
mutation.
+To find objects for a mutation, arguments need to be specified. As with
+[resolvers](#resolvers), prefer using internal ID or, if needed, a
+global ID rather than the database ID.
+
### Fields
In the most common situations, a mutation would return 2 fields:
@@ -496,6 +548,32 @@ found, we should raise a
`Gitlab::Graphql::Errors::ResourceNotAvailable` error. Which will be
correctly rendered to the clients.
+## Gitlab's custom scalars
+
+### `Types::TimeType`
+
+[`Types::TimeType`](https://gitlab.com/gitlab-org/gitlab/blob/master/app%2Fgraphql%2Ftypes%2Ftime_type.rb)
+must be used as the type for all fields and arguments that deal with Ruby
+`Time` and `DateTime` objects.
+
+The type is
+[a custom scalar](https://github.com/rmosolgo/graphql-ruby/blob/master/guides/type_definitions/scalars.md#custom-scalars)
+that:
+
+- Converts Ruby's `Time` and `DateTime` objects into standardized
+ ISO-8601 formatted strings, when used as the type for our GraphQL fields.
+- Converts ISO-8601 formatted time strings into Ruby `Time` objects,
+ when used as the type for our GraphQL arguments.
+
+This allows our GraphQL API to have a standardized way that it presents time
+and handles time inputs.
+
+Example:
+
+```ruby
+field :created_at, Types::TimeType, null: false, description: 'Timestamp of when the issue was created'
+```
+
## Testing
_full stack_ tests for a graphql query or mutation live in
@@ -540,7 +618,7 @@ it 'returns a successful response' do
end
```
-## Documentation
+## Documentation and Schema
-For information on generating GraphQL documentation, see
-[Rake tasks related to GraphQL](rake_tasks.md#update-graphql-documentation).
+For information on generating GraphQL documentation and schema files, see
+[Rake tasks related to GraphQL](rake_tasks.md#update-graphql-documentation-and-schema-definitions).
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index ccedb96d27d..b579f812d99 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -59,10 +59,10 @@ graph TB
Unicorn --> Gitaly
Sidekiq --> Redis
Sidekiq --> PgBouncer
+ Sidekiq --> Gitaly
GitLabWorkhorse[GitLab Workhorse] --> Unicorn
GitLabWorkhorse --> Redis
GitLabWorkhorse --> Gitaly
- Gitaly --> Redis
NGINX --> GitLabWorkhorse
NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
NGINX --> Grafana[Grafana]
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 8ded3f393ee..af2c540cca5 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -41,7 +41,10 @@ the `author` field. GitLab team members **should not**.
a changelog entry regardless of these guidelines if the contributor wants one.
Example: "Fixed a typo on the search results page."
- Any docs-only changes **should not** have a changelog entry.
-- Any change behind a feature flag **should not** have a changelog entry. The entry should be added [in the merge request removing the feature flags](feature_flags/development.md).
+- Any change behind a feature flag **should not** have a changelog entry. The
+ entry should be added [in the merge request removing the feature flags](feature_flags/development.md).
+ If the change includes a database migration, there should be a changelog entry
+ for the migration change.
- A fix for a regression introduced and then fixed in the same release (i.e.,
fixing a bug introduced during a monthly release candidate) **should not**
have a changelog entry.
diff --git a/doc/development/chatops_on_gitlabcom.md b/doc/development/chatops_on_gitlabcom.md
index 8a313a120f1..456dd1d4b4b 100644
--- a/doc/development/chatops_on_gitlabcom.md
+++ b/doc/development/chatops_on_gitlabcom.md
@@ -14,7 +14,7 @@ tasks such as:
To request access to Chatops on GitLab.com:
1. Log into <https://ops.gitlab.net/users/sign_in> **using the same username** as for GitLab.com (you may have to rename it).
-1. Ask [an owner/maintainer in the `chatops` project](https://gitlab.com/gitlab-com/chatops/-/project_members?search=&sort=access_level_desc) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
+1. Ask [a project member in the `chatops` project](https://ops.gitlab.net/gitlab-com/chatops/-/project_members) to add you by running `/chatops run member add <username> gitlab-com/chatops --ops`.
## See also
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 421b70bd2db..77c57bb332d 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -170,8 +170,8 @@ Maintainers should check before merging if the merge request is approved by the
required approvers.
Maintainers must check before merging if the merge request is introducing new
-vulnerabilities, by inspecting the list in the Merge Request [Security
-Widget](../user/project/merge_requests/index.md#security-reports-ultimate).
+vulnerabilities, by inspecting the list in the Merge Request
+[Security Widget](../user/application_security/index.md).
When in doubt, a [Security Engineer](https://about.gitlab.com/company/team/) can be involved. The list of detected
vulnerabilities must be either empty or containing:
@@ -368,7 +368,7 @@ Enterprise Edition instance. This has some implications:
- [Background migrations](background_migrations.md) run in Sidekiq, and
should only be done for migrations that would take an extreme amount of
time at GitLab.com scale.
-1. **Sidekiq workers** [cannot change in a backwards-incompatible way](sidekiq_style_guide.md#removing-or-renaming-queues):
+1. **Sidekiq workers** [cannot change in a backwards-incompatible way](sidekiq_style_guide.md#sidekiq-compatibility-across-updates):
1. Sidekiq queues are not drained before a deploy happens, so there will be
workers in the queue from the previous version of GitLab.
1. If you need to change a method signature, try to do so across two releases,
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index 92dd040a2bd..481a18aac3d 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -118,6 +118,10 @@ This [documentation](merge_request_workflow.md) outlines the current merge reque
This [documentation](style_guides.md) outlines the current style guidelines.
+## Getting an Enterprise Edition License
+
+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).
+
---
[Return to Development documentation](../README.md)
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 349bb371835..f32400d44a2 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -268,7 +268,7 @@ Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
-#### Priority labels
+### Priority labels
Priority labels help us define the time a ~bug fix should be completed. Priority determines how quickly the defect turnaround time must be.
If there are multiple defects, the priority decides which defect has to be fixed immediately versus later.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 86f17f4ecdb..510e90524ed 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -36,7 +36,7 @@ include a regression test are merged quickly, while new features without proper
tests might be slower to receive feedback. The workflow to make a merge
request is as follows:
-1. [Fork](../../workflow/forking_workflow.md#creating-a-fork) the project into
+1. [Fork](../../user/project/repository/forking_workflow.md) the project into
your personal namespace (or group) on GitLab.com.
1. Create a feature branch in your fork (don't work off `master`).
1. Write [tests](../rake_tasks.md#run-tests) and code.
@@ -69,7 +69,7 @@ request is as follows:
the issue(s) once the merge request is merged.
1. If you're allowed to (Core team members, for example), set a relevant milestone
and [labels](issue_workflow.md).
-1. If the MR changes the UI, it should include *Before* and *After* screenshots.
+1. If the MR changes the UI, you'll need approval from a Product Designer (UX), based on the appropriate [product category](https://about.gitlab.com/handbook/product/categories/). UI changes should use available components from the GitLab Design System, [Pajamas](https://design.gitlab.com/). The MR must include *Before* and *After* screenshots.
1. If the MR changes CSS classes, please include the list of affected pages, which
can be found by running `grep css-class ./app -R`.
1. Be prepared to answer questions and incorporate feedback into your MR with new
@@ -222,7 +222,7 @@ requirements.
on the CI server.
1. Regressions and bugs are covered with tests that reduce the risk of the issue happening
again.
-1. Performance/scalability implications have been considered, addressed, and tested.
+1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed.
1. [Documented](../documentation/index.md) in the `/doc` directory.
1. [Changelog entry added](../changelog.md), if necessary.
1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed.
diff --git a/doc/development/creating_enums.md b/doc/development/creating_enums.md
new file mode 100644
index 00000000000..64385a2ea79
--- /dev/null
+++ b/doc/development/creating_enums.md
@@ -0,0 +1,15 @@
+# Creating enums
+
+When creating a new enum, it should use the database type `SMALLINT`.
+The `SMALLINT` type size is 2 bytes, which is sufficient for an enum.
+This would help to save space in the database.
+
+To use this type, add `limit: 2` to the migration that creates the column.
+
+Example:
+
+```rb
+def change
+ add_column :ci_job_artifacts, :file_format, :integer, limit: 2
+end
+```
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index 9947f9c16c0..65a3e518585 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -19,7 +19,7 @@ If you just want to delete everything and start over with an empty DB (~1 minute
- `bundle exec rake db:reset RAILS_ENV=development`
-If you just want to delete everything and start over with dummy data (~40 minutes). This also does `db:reset` and runs DB-specific migrations:
+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:
- `bundle exec rake dev:setup RAILS_ENV=development`
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index 39236ab1910..f3c19002417 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -47,6 +47,13 @@ A database **reviewer**'s role is to:
reassign MR to the database **maintainer** suggested by Reviewer
Roulette.
+#### When there are no database maintainers available
+
+Currently we have a [critical shortage of database maintainers](https://gitlab.com/gitlab-org/gitlab/issues/29717). Until we are able to increase the number of database maintainers to support the volume of reviews, we have implemented this temporary solution. If the database **reviewer** cannot find an available database **maintainer** then:
+
+1. Assign the MR for a second review by a **database trainee maintainer** for further review.
+1. Once satisfied with the review process, and if the database **maintainer** is still not available, skip the database maintainer approval step and assign the merge request to a backend maintainer for final review and approval.
+
A database **maintainer**'s role is to:
- Perform the final database review on the MR.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index fb0aa5130f8..7d575e9b0b1 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -16,7 +16,7 @@ In addition to this page, the following resources can help you craft and contrib
## Source files and rendered web locations
-Documentation for GitLab, GitLab Runner, Omnibus GitLab and Charts are published to <https://docs.gitlab.com>. Documentation for GitLab is also published within the application at `/help` on the domain of the GitLab instance.
+Documentation for GitLab, GitLab Runner, Omnibus GitLab and Charts is published to <https://docs.gitlab.com>. Documentation for GitLab is also published within the application at `/help` on the domain of the GitLab instance.
At `/help`, only help for your current edition and version is included. Help for other versions is available at <https://docs.gitlab.com/archives/>.
The source of the documentation exists within the codebase of each GitLab application in the following repository locations:
@@ -67,8 +67,6 @@ This document was moved to [another location](path/to/new_doc.md).
where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
----
-
For example, if you move `doc/workflow/lfs/lfs_administration.md` to
`doc/administration/lfs.md`, then the steps would be:
diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md
index f5a12e9c216..bf873995e54 100644
--- a/doc/development/documentation/site_architecture/index.md
+++ b/doc/development/documentation/site_architecture/index.md
@@ -4,14 +4,16 @@ description: "Learn how GitLab's documentation website is architectured."
# Documentation site architecture
-Learn how we build and architecture [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs)
-and deploy it to <https://docs.gitlab.com>.
+The [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) project hosts
+the repository which is used to generate the GitLab documentation website and
+is deployed to <https://docs.gitlab.com>. It uses the [Nanoc](http://nanoc.ws)
+static site generator.
-## Repository
+## Architecture
While the source of the documentation content is stored in GitLab's respective product
-repositories, the source that is used to build the documentation site _from that content_
-is located at <https://gitlab.com/gitlab-org/gitlab-docs>.
+repositories, the source that is used to build the documentation
+site _from that content_ is located at <https://gitlab.com/gitlab-org/gitlab-docs>.
The following diagram illustrates the relationship between the repositories
from where content is sourced, the `gitlab-docs` project, and the published output.
@@ -43,8 +45,23 @@ from where content is sourced, the `gitlab-docs` project, and the published outp
G --> L
```
-See the [README there](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/README.md)
-for detailed information.
+You will not find any GitLab docs content in the `gitlab-docs` repository.
+All documentation files are hosted in the respective repository of each
+product, and all together are pulled to generate the docs website:
+
+- [GitLab](https://gitlab.com/gitlab-org/gitlab/tree/master/doc)
+- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/doc)
+- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/tree/master/docs)
+- [GitLab Chart](https://gitlab.com/charts/gitlab/tree/master/doc)
+
+NOTE: **Note:**
+In September 2019, we [moved towards a single codebase](https://gitlab.com/gitlab-org/gitlab-ee/issues/2952),
+as such the docs for CE and EE are now identical. For historical reasons and
+in order not to break any existing links throughout the internet, we still
+maintain the CE docs (`https://docs.gitlab.com/ce/`), although it is hidden
+from the website, and is now a symlink to the EE docs. When
+[Pages supports redirects](https://gitlab.com/gitlab-org/gitlab-pages/issues/24),
+we will be able to remove this completely.
## Assets
@@ -73,28 +90,112 @@ Read through [the global navigation documentation](global_nav.md) to understand:
- How the global navigation is built.
- How to add new navigation items.
-## Deployment
+<!--
+## Helpers
-The docs site is deployed to production with GitLab Pages, and previewed in
-merge requests with Review Apps.
+TBA
+-->
-The deployment aspects will be soon transferred from the [original document](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/README.md)
-to this page.
+## Using YAML data files
-<!--
-## Repositories
+The easiest way to achieve something similar to
+[Jekyll's data files](https://jekyllrb.com/docs/datafiles/) in Nanoc is by
+using the [`@items`](https://nanoc.ws/doc/reference/variables/#items-and-layouts)
+variable.
-TBA
+The data file must be placed inside the `content/` directory and then it can
+be referenced in an ERB template.
-## Search engine
+Suppose we have the `content/_data/versions.yaml` file with the content:
-TBA
+```yaml
+versions:
+- 10.6
+- 10.5
+- 10.4
+```
-## Versions
+We can then loop over the `versions` array with something like:
-TBA
+```erb
+<% @items['/_data/versions.yaml'][:versions].each do | version | %>
-## Helpers
+<h3><%= version %></h3>
-TBA
--->
+<% end &>
+```
+
+Note that the data file must have the `yaml` extension (not `yml`) and that
+we reference the array with a symbol (`:versions`).
+
+## Bumping versions of CSS and Javascript
+
+Whenever the custom CSS and Javascript files under `content/assets/` change,
+make sure to bump their version in the frontmatter. This method guarantees that
+your changes will take effect by clearing the cache of previous files.
+
+Always use Nanoc's way of including those files, do not hardcode them in the
+layouts. For example use:
+
+```erb
+<script async type="application/javascript" src="<%= @items['/assets/javascripts/badges.*'].path %>"></script>
+
+<link rel="stylesheet" href="<%= @items['/assets/stylesheets/toc.*'].path %>">
+```
+
+The links pointing to the files should be similar to:
+
+```erb
+<%= @items['/path/to/assets/file.*'].path %>
+```
+
+Nanoc will then build and render those links correctly according with what's
+defined in [`Rules`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/Rules).
+
+## Linking to source files
+
+A helper called [`edit_on_gitlab`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/lib/helpers/edit_on_gitlab.rb) can be used
+to link to a page's source file. We can link to both the simple editor and the
+web IDE. Here's how you can use it in a Nanoc layout:
+
+- Default editor: `<a href="<%= edit_on_gitlab(@item, editor: :simple) %>">Simple editor</a>`
+- Web IDE: `<a href="<%= edit_on_gitlab(@item, editor: :webide) %>">Web IDE</a>`
+
+If you don't specify `editor:`, the simple one is used by default.
+
+## Algolia search engine
+
+The docs site uses [Algolia docsearch](https://community.algolia.com/docsearch/)
+for its search function. This is how it works:
+
+1. GitLab is a member of the [docsearch program](https://community.algolia.com/docsearch/#join-docsearch-program),
+ which is the free tier of [Algolia](https://www.algolia.com/).
+1. Algolia hosts a [doscsearch config](https://github.com/algolia/docsearch-configs/blob/master/configs/gitlab.json)
+ for the GitLab docs site, and we've worked together to refine it.
+1. That [config](https://community.algolia.com/docsearch/config-file.html) is
+ parsed by their [crawler](https://community.algolia.com/docsearch/crawler-overview.html)
+ every 24h and [stores](https://community.algolia.com/docsearch/inside-the-engine.html)
+ the [docsearch index](https://community.algolia.com/docsearch/how-do-we-build-an-index.html)
+ on [Algolia's servers](https://community.algolia.com/docsearch/faq.html#where-is-my-data-hosted%3F).
+1. On the docs side, we use a [docsearch layout](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/layouts/docsearch.html) which
+ is present on pretty much every page except <https://docs.gitlab.com/search/>,
+ which uses its [own layout](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/layouts/instantsearch.html). In those layouts,
+ there's a javascript snippet which initiates docsearch by using an API key
+ and an index name (`gitlab`) that are needed for Algolia to show the results.
+
+NOTE: **For GitLab employees:**
+The credentials to access the Algolia dashboard are stored in 1Password. If you
+want to receive weekly reports of the search usage, search the Google doc with
+title "Email, Slack, and GitLab Groups and Aliases", search for `docsearch`,
+and add a comment with your email to be added to the alias that gets the weekly
+reports.
+
+## Monthly release process (versions)
+
+The docs website supports versions and each month we add the latest one to the list.
+For more information, read about the [monthly release process](release_process.md).
+
+## Review Apps for documentation merge requests
+
+If you are contributing to GitLab docs read how to [create a Review App with each
+merge request](../index.md#previewing-the-changes-live).
diff --git a/doc/development/documentation/site_architecture/release_process.md b/doc/development/documentation/site_architecture/release_process.md
new file mode 100644
index 00000000000..6f723531f4c
--- /dev/null
+++ b/doc/development/documentation/site_architecture/release_process.md
@@ -0,0 +1,241 @@
+# GitLab Docs monthly release process
+
+The [`dockerfiles` directory](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/)
+contains all needed Dockerfiles to build and deploy the versioned website. It
+is heavily inspired by Docker's
+[Dockerfile](https://github.com/docker/docker.github.io/blob/06ed03db13895bfe867761b6fc2ad40acf6026dd/Dockerfile).
+
+The following Dockerfiles are used.
+
+| Dockerfile | Docker image | Description |
+| ---------- | ------------ | ----------- |
+| [`Dockerfile.bootstrap`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.bootstrap) | `gitlab-docs:bootstrap` | Contains all the dependencies that are needed to build the website. If the gems are updated and `Gemfile{,.lock}` changes, the image must be rebuilt. |
+| [`Dockerfile.builder.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.builder.onbuild) | `gitlab-docs:builder-onbuild` | Base image to build the docs website. It uses `ONBUILD` to perform all steps and depends on `gitlab-docs:bootstrap`. |
+| [`Dockerfile.nginx.onbuild`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.nginx.onbuild) | `gitlab-docs:nginx-onbuild` | Base image to use for building documentation archives. It uses `ONBUILD` to perform all required steps to copy the archive, and relies upon its parent `Dockerfile.builder.onbuild` that is invoked when building single documentation achives (see the `Dockerfile` of each branch. |
+| [`Dockerfile.archives`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/dockerfiles/Dockerfile.archives) | `gitlab-docs:archives` | Contains all the versions of the website in one archive. It copies all generated HTML files from every version in one location. |
+
+## How to build the images
+
+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. Build the images:
+
+ ```sh
+ docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:bootstrap -f Dockerfile.bootstrap ../
+ docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:builder-onbuild -f Dockerfile.builder.onbuild ../
+ docker build -t registry.gitlab.com/gitlab-org/gitlab-docs:nginx-onbuild -f Dockerfile.nginx.onbuild ../
+ ```
+
+For each image, there's a manual job under the `images` stage in
+[`.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/.gitlab-ci.yml) which can be invoked at will.
+
+## Monthly release process
+
+When a new GitLab version is released on the 22nd, we need to create the respective
+single Docker image, and update some files so that the dropdown works correctly.
+
+### 1. Add the chart version
+
+Since the charts use a different version number than all the other GitLab
+products, we need to add a
+[version mapping](https://docs.gitlab.com/charts/installation/version_mappings.html):
+
+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. 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.
+
+TIP: **Tip:**
+It can be handy to create the future mappings since they are pretty much known.
+In that case, when a new GitLab version is released, you don't have to repeat
+this first step.
+
+### 2. Create an image for a single version
+
+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. Run the raketask to create the single version:
+
+ ```sh
+ ./bin/rake "release:single[12.0]"
+ ```
+
+ A new `Dockerfile.12.0` should have been created and committed to a new branch.
+
+1. Push the newly created branch, but **don't create a merge request**.
+ Once you push, the `image:docker-singe` job will create a new Docker image
+ tagged with the branch name you created in the first step. In the end, the
+ image will be uploaded in the [Container Registry](https://gitlab.com/gitlab-org/gitlab-docs/container_registry)
+ and it will be listed under the
+ [`registry` environment folder](https://gitlab.com/gitlab-org/gitlab-docs/environments/folders/registry).
+
+Optionally, you can test locally by building the image and running it:
+
+```sh
+docker build -t docs:12.0 -f Dockerfile.12.0 .
+docker run -it --rm -p 4000:4000 docs:12.0
+```
+
+Visit `http://localhost:4000/12.0/` to see if everything works correctly.
+
+### 3. Create the release merge request
+
+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. Create a branch `release-X-Y`:
+
+ ```sh
+ git checkout -b release-12-0
+ ```
+
+1. **Rotate the online and offline versions:**
+
+ At any given time, there are 4 browsable online versions: one pulled from
+ the upstream master branches (docs for GitLab.com) and the three latest
+ stable versions.
+
+ Edit `content/_data/versions.yaml` and rotate the versions to reflect the
+ new changes:
+
+ - `online`: The 3 latest stable versions.
+ - `offline`: All the previous versions offered as an offline archive.
+
+1. **Add the new offline version in the 404 page redirect script:**
+
+ Since we're deprecating the oldest version each month, we need to redirect
+ those URLs in order not to create [404 entries](https://gitlab.com/gitlab-org/gitlab-docs/issues/221).
+ There's a temporary hack for now:
+
+ 1. Edit `content/404.html`, making sure all offline versions under
+ `content/_data/versions.yaml` are in the Javascript snippet at the end of
+ the document.
+
+1. **Update the `:latest` and `:archives` Docker images:**
+
+ The following two Dockerfiles need to be updated:
+
+ 1. `dockerfiles/Dockerfile.archives` - Add the latest version at the top of
+ the list.
+ 1. `Dockerfile.master` - Rotate the versions (oldest gets removed and latest
+ is added at the top of the list).
+
+1. In the end, there should be four files in total that have changed.
+ Commit and push to create the merge request using the "Release" template:
+
+ ```sh
+ git add content/ Dockerfile.master dockerfiles/Dockerfile.archives
+ git commit -m "Release 12.0"
+ git push origin release-12-0
+ ```
+
+### 4. Update the dropdown for all online versions
+
+The versions dropdown is in a way "hardcoded". When the site is built, it looks
+at the contents of `content/_data/versions.yaml` and based on that, the dropdown
+is populated. So, older branches will have different content, which means the
+dropdown will be one or more releases behind. Remember that the new changes of
+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. Before creating the merge request, [disable the scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules/228/edit)
+ by unchecking the "Active" option. Since all steps must run in sequence, we need
+ to do this to avoid race conditions in the event some previous versions are
+ updated before the release merge request is merged.
+1. Run the raketask 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:
+
+ ```sh
+ ./bin/rake release:dropdowns
+ ```
+
+Once all are merged, proceed to the following and final step.
+
+TIP: **Tip:**
+In case a pipeline fails, see [troubleshooting](#troubleshooting).
+
+### 5. Merge the release merge request
+
+The dropdown merge requests should have now been merged into their respective
+version (stable branch), which will trigger another pipeline. At this point,
+you need to only babysit the pipelines and make sure they don't fail:
+
+1. Check the [pipelines page](https://gitlab.com/gitlab-org/gitlab-docs/pipelines)
+ and make sure all stable branches have green pipelines.
+1. After all the pipelines of the online versions succeed, merge the release merge request.
+1. Finally, re-activate the [scheduled pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipeline_schedules/228/edit),
+ save it, and hit the play button to get it started.
+
+Once the scheduled pipeline succeeds, the docs site will be deployed with all
+new versions online.
+
+## Update an old Docker image with new upstream docs content
+
+If there are any changes to any of the stable branches of the products that are
+not included in the single Docker image, just
+[rerun the pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new)
+for the version in question.
+
+## Porting new website changes to old versions
+
+CAUTION: **Warning:**
+Porting changes to older branches can have unintended effects as we're constantly
+changing the backend of the website. Use only when you know what you're doing
+and make sure to test locally.
+
+The website will keep changing and being improved. In order to consolidate
+those changes to the stable branches, we'd need to pick certain changes
+from time to time.
+
+If this is not possible or there are many changes, merge master into them:
+
+```sh
+git branch 12.0
+git fetch origin master
+git merge origin/master
+```
+
+## Troubleshooting
+
+Releasing a new version is a long process that involves many moving parts.
+
+### `test_internal_links_and_anchors` failing on dropdown merge requests
+
+When [updating the dropdown for the stable versions](#4-update-the-dropdown-for-all-online-versions),
+there may be cases where some links might fail. The process of how the
+dropdown MRs are created have a caveat, and that is that the tests run by
+pulling the master branches of all products, instead of the respective stable
+ones.
+
+In a real world scenario, the [Update 12.2 dropdown to match that of 12.4](https://gitlab.com/gitlab-org/gitlab-docs/merge_requests/604)
+merge request failed because of the [`test_internal_links_and_anchors` test](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/328042431).
+
+This happened because there has been a rename of a product (`gitlab-monitor` to `gitlab-exporter`)
+and the old name was still referenced in the 12.2 docs. If the respective stable
+branches for 12.2 were used, this wouldn't have failed, but as we can see from
+the [`compile_dev` job](https://gitlab.com/gitlab-org/gitlab-docs/-/jobs/328042427),
+the `master` branches were pulled.
+
+To fix this, you need to [re-run the pipeline](https://gitlab.com/gitlab-org/gitlab-docs/pipelines/new)
+for the `update-12-2-for-release-12-4` branch, by including the following environment variables:
+
+- `BRANCH_CE` set to `12-2-stable`
+- `BRANCH_EE` set to `12-2-stable-ee`
+- `BRANCH_OMNIBUS` set to `12-2-stable`
+- `BRANCH_RUNNER` set to `12-2-stable`
+- `BRANCH_CHARTS` set to `2-2-stable`
+
+This should make the MR pass.
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index b6ec7a858fa..e6d666473c3 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -604,9 +604,6 @@ Inside the document:
- Always use a proper description for what the image is about. That way, when a
browser fails to show the image, this text will be used as an alternative
description.
-- If there are consecutive images with little text between them, always add
- three dashes (`---`) between the image and the text to create a horizontal
- line for better clarity.
- If a heading is placed right after an image, always add three dashes (`---`)
between the image and the heading.
@@ -1182,12 +1179,12 @@ Rendered example:
- Prefer to use examples using the personal access token and don't pass data of
username and password.
-| Methods | Description |
-|:-------------------------------------------|:------------------------------------------------------|
-| `-H "PRIVATE-TOKEN: <your_access_token>"` | Use this method as is, whenever authentication needed |
-| `-X POST` | Use this method when creating new objects |
-| `-X PUT` | Use this method when updating existing objects |
-| `-X DELETE` | Use this method when removing existing objects |
+| Methods | Description |
+|:------------------------------------------- |:------------------------------------------------------|
+| `--header "PRIVATE-TOKEN: <your_access_token>"` | Use this method as is, whenever authentication needed |
+| `--request POST` | Use this method when creating new objects |
+| `--request PUT` | Use this method when updating existing objects |
+| `--request DELETE` | Use this method when removing existing objects |
### cURL Examples
@@ -1209,9 +1206,9 @@ Create a new project under the authenticated user's namespace:
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects?name=foo"
```
-#### Post data using cURL's --data
+#### Post data using cURL's `--data`
-Instead of using `-X POST` and appending the parameters to the URI, you can use
+Instead of using `--request POST` and appending the parameters to the URI, you can use
cURL's `--data` option. The example below will create a new project `foo` under
the authenticated user's namespace.
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 24399391b1a..c373b976453 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -208,7 +208,7 @@ code reviewer have ensured:
Documentation [is required](../contributing/merge_request_workflow.html#definition-of-done) for a
milestone when:
-- A new or enhanced feature is shipped that impacts the user of administrator experience.
+- 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.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index cc9df479492..d716325f332 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -245,47 +245,29 @@ end
#### Use self-descriptive wrapper methods
-When it's not possible/logical to modify the implementation of a
-method. Wrap it in a self-descriptive method and use that method.
+When it's not possible/logical to modify the implementation of a method, then
+wrap it in a self-descriptive method and use that method.
-For example, in CE only an `admin` is allowed to access all private
-projects/groups, but in EE also an `auditor` has full private
-access. It would be incorrect to override the implementation of
-`User#admin?`, so instead add a method `full_private_access?` to
-`app/models/users.rb`. The implementation in CE will be:
+For example, in GitLab-FOSS, the only user created by the system is `User.ghost`
+but in EE there are several types of bot-users that aren't really users. It would
+be incorrect to override the implementation of `User#ghost?`, so instead we add
+a method `#internal?` to `app/models/user.rb`. The implementation will be:
```ruby
-def full_private_access?
- admin?
+def internal?
+ ghost?
end
```
In EE, the implementation `ee/app/models/ee/users.rb` would be:
```ruby
-override :full_private_access?
-def full_private_access?
- super || auditor?
+override :internal?
+def internal?
+ super || bot?
end
```
-In `lib/gitlab/visibility_level.rb` this method is used to return the
-allowed visibility levels:
-
-```ruby
-def levels_for_user(user = nil)
- if user.full_private_access?
- [PRIVATE, INTERNAL, PUBLIC]
- elsif # ...
-end
-```
-
-See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for
-full implementation details.
-
-[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12373
-[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab/merge_requests/2199
-
### Code in `config/routes`
When we add `draw :admin` in `config/routes.rb`, the application will try to
diff --git a/doc/development/event_tracking/index.md b/doc/development/event_tracking/index.md
index efc61d13cb0..ac19053320d 100644
--- a/doc/development/event_tracking/index.md
+++ b/doc/development/event_tracking/index.md
@@ -68,7 +68,3 @@ 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).
-
-## Additional libraries
-
-Session tracking is handled by [Pendo](https://www.pendo.io/), which is a purely client library and is a relatively minor development concern but is worth including in this documentation.
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index 3724bf60757..5b02098f020 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -73,7 +73,7 @@ With the purpose of being [respectful of others' time](https://about.gitlab.com/
- Before assigning to a maintainer, assign to a reviewer.
- If you assigned a merge request, or pinged someone directly, keep in mind that we work in different timezones and asynchronously, so be patient. Unless the merge request is urgent (like fixing a broken master), please don't DM or reassign the merge request before waiting for a 24-hour window.
- If you have a question regarding your merge request/issue, make it on the merge request/issue. When we DM each other, we no longer have a SSOT and [no one else is able to contribute](https://about.gitlab.com/handbook/values/#public-by-default).
-- When you have a big WIP merge request with many changes, you're adivsed to get the review started before adding/removing significant code. Make sure it is assigned well before the release cut-off, as the reviewer(s)/maintainer(s) would always prioritize reviewing finished MRs before WIP ones.
+- When you have a big WIP merge request with many changes, you're advised to get the review started before adding/removing significant code. Make sure it is assigned well before the release cut-off, as the reviewer(s)/maintainer(s) would always prioritize reviewing finished MRs before WIP ones.
- Make sure to remove the WIP title before the last round of review.
### Share your work early
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index fe4f6d7bec8..894a613ec2d 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -19,6 +19,14 @@ To save duplicated clients getting created in different apps, we have a
[default client][default-client] 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`.
+
+- `resolvers` parameter is created to accept an object of resolvers for [local state management](#local-state-with-apollo) queries and mutations
+- `config` parameter takes an object of configuration settings:
+ - `cacheConfig` field accepts an optional object of settings to [customize Apollo cache](https://github.com/apollographql/apollo-client/tree/master/packages/apollo-cache-inmemory#configuration)
+ - `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.`${gon.relative_url_root}/api/graphql`)
+ - `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, will assume that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`.
+
## GraphQL Queries
To save query compilation at runtime, webpack can directly import `.graphql`
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 306b19c6e5d..43cd8180b6e 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -581,6 +581,18 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
<component />
```
+#### Component usage within templates
+
+1. Prefer a component's kebab-cased name over other styles when using it in a template
+
+ ```javascript
+ // bad
+ <MyComponent />
+
+ // good
+ <my-component />
+ ```
+
#### Ordering
1. Tag order in `.vue` file
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 07c87920dab..54d41b42c77 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -27,7 +27,7 @@ New utility classes should be added to [`utilities.scss`](https://gitlab.com/git
| Font size | `.text-{size}` | `.text-2` |
- `{variant}` is one of 'primary', 'secondary', 'success', 'warning', 'error'
-- `{shade}` is on of the shades listed on [colors](https://design.gitlab.com/product-foundations/colors/)
+- `{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)
#### When should I create component classes?
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index f22c3bb1e37..cbbbe1cd5ea 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -29,6 +29,14 @@ Monitor stage, Health group.
For all production environment Chatops commands, use the `#production` channel.
+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
diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md
index 929c9b1c71c..c410c7eae41 100644
--- a/doc/development/feature_flags/development.md
+++ b/doc/development/feature_flags/development.md
@@ -25,7 +25,8 @@ end
Features that are developed and are intended to be merged behind a feature flag
should not include a changelog entry. The entry should be added in the merge
-request removing the feature flags.
+request removing the feature flag. If the feature contains any DB migration it
+should include a changelog entry for DB changes.
In the rare case that you need the feature flag to be on automatically, use
`default_enabled: true` when checking:
@@ -51,20 +52,16 @@ isn't gated by a License or Plan.
[namespace-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/ee/namespace.rb#L71-85
[license-fa]: https://gitlab.com/gitlab-org/gitlab/blob/4cc1c62918aa4c31750cb21dfb1a6c3492d71080/ee/app/models/license.rb#L293-300
-An important side-effect of the implicit feature flags mentioned above is that
+**An important side-effect of the implicit feature flags mentioned above is that
unless the feature is explicitly disabled or limited to a percentage of users,
-the feature flag check will default to `true`.
+the feature flag check will default to `true`.**
As an example, if you were to ship the backend half of a feature behind a flag,
you'd want to explicitly disable that flag until the frontend half is also ready
-to be shipped. [You can do this via Chatops](controls.md):
-
-```
-/chatops run feature set some_feature 0
-```
-
-Note that you can do this at any time, even before the merge request using the
-flag has been merged!
+to be shipped. To make sure this feature is disabled for both GitLab.com and
+self-managed instances you'd need to explicitly call `Feature.enabled?` method
+before the `feature_available` method. This ensures the feature_flag is defaulting
+to `false`.
## Feature groups
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 446d85fceed..5010e44e826 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -101,15 +101,16 @@ it's successful, we replace the main repo with the newly cloned one.
### Uploads replication
File uploads are also being replicated to the **secondary** node. To
-track the state of syncing, the `Geo::FileRegistry` model is used.
+track the state of syncing, the `Geo::UploadRegistry` model is used.
-#### File Registry
+#### Upload Registry
Similar to the [Project Registry](#project-registry), there is a
-`Geo::FileRegistry` model that tracks the synced uploads.
+`Geo::UploadRegistry` model that tracks the synced uploads.
-CI Job Artifacts are synced in a similar way as uploads or LFS
-objects, but they are tracked by `Geo::JobArtifactRegistry` model.
+CI Job Artifacts and LFS objects are synced in a similar way as uploads,
+but they are tracked by `Geo::JobArtifactRegistry`, and `Geo::LfsObjectRegistry`
+models respectively.
#### File Download Dispatch worker
@@ -490,6 +491,24 @@ When some write actions are not allowed because the node is a
The database itself will already be read-only in a replicated setup,
so we don't need to take any extra step for that.
+## Steps needed to replicate a new data type
+
+As GitLab evolves, we constantly need to add new resources to the Geo replication system.
+The implementation depends on resource specifics, but there are several things
+that need to be taken care of:
+
+- Event generation on the primary site. Whenever a new resource is changed/updated, we need to
+ create a task for the Log Cursor.
+- Event handling. The Log Cursor needs to have a handler for every event type generated by the primary site.
+- Dispatch worker (cron job). Make sure the backfill condition works well.
+- Sync worker.
+- Registry with all possible states.
+- Verification.
+- Cleaner. When sync settings are changed for the secondary site, some resources need to be cleaned up.
+- Geo Node Status. We need to provide API endpoints as well as some presentation in the GitLab Admin Area.
+- Health Check. If we can perform some pre-cheÑks and make node unhealthy if something is wrong, we should do that.
+ The `rake gitlab:geo:check` command has to be updated too.
+
## History of communication channel
The communication channel has changed since first iteration, you can
diff --git a/doc/development/git_object_deduplication.md b/doc/development/git_object_deduplication.md
index e8af6346524..6d9eb90d482 100644
--- a/doc/development/git_object_deduplication.md
+++ b/doc/development/git_object_deduplication.md
@@ -1,6 +1,6 @@
# How Git object deduplication works in GitLab
-When a GitLab user [forks a project](../workflow/forking_workflow.md),
+When a GitLab user [forks a project](../user/project/repository/forking_workflow.md),
GitLab creates a new Project with an associated Git repository that is a
copy of the original project at the time of the fork. If a large project
gets forked often, this can lead to a quick increase in Git repository
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index 64f283f69d9..7d3c2b8fdf8 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -166,12 +166,11 @@ end
Normally, GitLab CE/EE tests use a local clone of Gitaly in
`tmp/tests/gitaly` pinned at the version specified in
-`GITALY_SERVER_VERSION`. The `GITALY_SERVER_VERSION` file supports
-`=my-branch` syntax to use a custom branch in <https://gitlab.com/gitlab-org/gitaly>. If
+`GITALY_SERVER_VERSION`. The `GITALY_SERVER_VERSION` file supports also
+branches and SHA to use a custom commit in <https://gitlab.com/gitlab-org/gitaly>. If
you want to run tests locally against a modified version of Gitaly you
can replace `tmp/tests/gitaly` with a symlink. This is much faster
-because the `=my-branch` syntax forces a Gitaly re-install each time
-you run `rspec`.
+because if will avoid a Gitaly re-install each time you run `rspec`.
```shell
rm -rf tmp/tests/gitaly
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index c7eed880554..da27ae9110b 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -168,3 +168,73 @@ in an initializer._
### Further reading
- Stack Overflow: [Why you should not write inline JavaScript](https://softwareengineering.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
+
+## Auto loading
+
+Rails auto-loading on `development` differs from the load policy in the `production` environment.
+In development mode, `config.eager_load` is set to `false`, which means classes
+are loaded as needed. With the classic Rails autoloader, it is known that this can lead to
+[Rails resolving the wrong class](https://guides.rubyonrails.org/v5.2/autoloading_and_reloading_constants.html#when-constants-aren-t-missed-relative-references)
+if the class name is ambiguous. This can be fixed by specifying the complete namespace to the class.
+
+### Error prone example
+
+```ruby
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+ ...
+end
+
+# app/controllers/projects/application_controller.rb
+class Projects::ApplicationController < ApplicationController
+ ...
+ private
+
+ def project
+ ...
+ end
+end
+
+# app/controllers/projects/submodule/some_controller.rb
+module Projects
+ module Submodule
+ class SomeController < ApplicationController
+ def index
+ @some_id = project.id
+ end
+ end
+ end
+end
+```
+
+In this case, if for any reason the top level `ApplicationController`
+is loaded but `Projects::ApplicationController` is not, `ApplicationController`
+would be resolved to `::ApplicationController` and then the `project` method will
+be undefined and we will get an error.
+
+#### Solution
+
+```ruby
+# app/controllers/projects/submodule/some_controller.rb
+module Projects
+ module Submodule
+ class SomeController < Projects::ApplicationController
+ def index
+ @some_id = project.id
+ end
+ end
+ end
+end
+```
+
+By specifying `Projects::`, we tell Rails exactly what class we are referring
+to and we would avoid the issue.
+
+NOTE: **Note:**
+This problem will disappear as soon as we upgrade to Rails 6 and use the Zeitwerk autoloader.
+
+### Further reading
+
+- Rails Guides: [Autoloading and Reloading Constants (Classic Mode)](https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html)
+- Ruby Constant lookup: [Everything you ever wanted to know about constant lookup in Ruby](http://cirw.in/blog/constant-lookup)
+- Rails 6 and Zeitwerk autoloader: [Understanding Zeitwerk in Rails 6](https://medium.com/cedarcode/understanding-zeitwerk-in-rails-6-f168a9f09a1f)
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 2f067ede70f..c8960ac0f61 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -326,9 +326,8 @@ file in. Once the changes are on master, they will be picked up by
[Crowdin](https://translate.gitlab.com) and be presented for
translation.
-We don't need to check in any changes to the
-`locale/[language]/gitlab.po` files. Those will be updated in a [when
-translations from Crowdin are merged](merging_translations.md).
+We don't need to check in any changes to the `locale/[language]/gitlab.po` files.
+They are updated automatically when [translations from Crowdin are merged](merging_translations.md).
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command.
diff --git a/doc/development/i18n/merging_translations.md b/doc/development/i18n/merging_translations.md
index f5953cc5f6c..c773b187cc1 100644
--- a/doc/development/i18n/merging_translations.md
+++ b/doc/development/i18n/merging_translations.md
@@ -1,13 +1,12 @@
# Merging translations from Crowdin
-Crowdin automatically syncs the `gitlab.pot` file presenting newly
-added translations to the community of translators.
+Crowdin automatically syncs the `gitlab.pot` file with the Crowdin service, presenting
+newly added externalized strings to the community of translators.
-At the same time, it creates a merge request to merge all newly added
-& approved translations. Find the [merge request created by
-`gitlab-crowdin-bot`](https://gitlab.com/gitlab-org/gitlab/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&author_username=gitlab-crowdin-bot)
-to see new and merged merge requests. They are created in EE and need
-to be ported to CE manually.
+[GitLab Crowdin Bot](https://gitlab.com/gitlab-crowdin-bot) also creates merge requests
+to take newly approved translation submissions and merge them into the `locale/<language>/gitlab.po`
+files. Check the [merge requests created by `gitlab-crowdin-bot`](https://gitlab.com/gitlab-org/gitlab/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&author_username=gitlab-crowdin-bot)
+to see new and merged merge requests.
## Validation
@@ -21,7 +20,7 @@ doesn't do. Create a new pipeline at `https://gitlab.com/gitlab-org/gitlab/pipel
If there are validation errors, the easiest solution is to disapprove
the offending string in Crowdin, leaving a comment with what is
required to fix the offense. There is an
-[issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/49208)
+[issue](https://gitlab.com/gitlab-org/gitlab/issues/23256)
suggesting to automate this process. Disapproving will exclude the
invalid translation, the merge request will be updated within a few
minutes.
@@ -32,19 +31,16 @@ clicking `Pause sync` on the [Crowdin integration settings
page](https://translate.gitlab.com/project/gitlab-ee/settings#integration).
When all failures are resolved, the translations need to be double
-checked once more as discussed in [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/37850`.
+checked once more as discussed in [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab/issues/19485`.
## Merging translations
When all translations are found good and pipelines pass the
-translations can be merged into the master branch. After that is done,
-create a new merge request cherry-picking the translations from EE to
-CE. When merging the translations, make sure to check the `Remove
-source branch` checkbox, so Crowdin recreates the `master-i18n` from
-master after the new translation was merged.
-
-We are discussing automating this entire process
-[here](https://gitlab.com/gitlab-org/gitlab-foss/issues/39309).
+translations can be merged into the master branch. When merging the translations,
+make sure to check the **Remove source branch** checkbox, so Crowdin recreates the
+`master-i18n` from master after the new translation was merged.
+
+We are discussing [automating this entire process](https://gitlab.com/gitlab-org/gitlab/issues/19896).
## Recreate the merge request
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index c1a6fd8983c..50a417e9996 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -83,7 +83,7 @@ Therefore "create a new user" would translate into "Benutzer(in) anlegen".
### Updating the glossary
To propose additions to the glossary please
-[open an issue](https://gitlab.com/gitlab-org/gitlab-foss/issues).
+[open an issue](https://gitlab.com/gitlab-org/gitlab/issues?scope=all&utf8=✓&state=all&label_name[]=Category%3AInternationalization).
## French Translation Guidelines
diff --git a/doc/development/kubernetes.md b/doc/development/kubernetes.md
index 82aa02ac75d..a0dd97f2a1c 100644
--- a/doc/development/kubernetes.md
+++ b/doc/development/kubernetes.md
@@ -74,6 +74,50 @@ We have some Webmock stubs in
[`KubernetesHelpers`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/support/helpers/kubernetes_helpers.rb)
which can help with mocking out calls to Kubernetes API in your tests.
+### Amazon EKS integration
+
+This section outlines the process for allowing a GitLab instance to create EKS clusters.
+
+The following prerequisites are required:
+
+A `Customer` AWS account. This is the account in which the
+EKS cluster will be created. The following resources must be present:
+
+- A provisioning role that has permissions to create the cluster
+ and associated resources. It must list the `GitLab` AWS account
+ as a trusted entity.
+- A VPC, management role, security group, and subnets for use by the cluster.
+
+A `GitLab` AWS account. This is the account which performs
+the provisioning actions. The following resources must be present:
+
+- A service account with permissions to assume the provisioning
+ role in the `Customer` account above.
+- Credentials for this service account configured in GitLab via
+ the `kubernetes` section of `gitlab.yml`.
+
+The process for creating a cluster is as follows:
+
+1. Using the `:provision_role_external_id`, GitLab assumes the role provided
+ by `:provision_role_arn` and stores a set of temporary credentials on the
+ provider record. By default these credentials are valid for one hour.
+1. A CloudFormation stack is created, based on the
+ [`AWS CloudFormation EKS template`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/aws/cloudformation/eks_cluster.yaml).
+ This triggers creation of all resources required for an EKS cluster.
+1. GitLab polls the status of the stack until all resources are ready,
+ which takes somewhere between 10 and 15 minutes in most cases.
+1. When the stack is ready, GitLab stores the cluster details and generates
+ another set of temporary credentials, this time to allow connecting to
+ the cluster via Kubeclient. These credentials are valid for one minute.
+1. GitLab configures the worker nodes so that they are able to authenticate
+ to the cluster, and creates a service account for itself for future operations.
+1. Credentials that are no longer required are removed. This deletes the following
+ attributes:
+
+ - `access_key_id`
+ - `secret_access_key`
+ - `session_token`
+
## Security
### SSRF
@@ -123,3 +167,10 @@ they are written:
```bash
kubectl logs <pod_name> --follow -n gitlab-managed-apps
```
+
+## GitLab Managed Apps
+
+GitLab provides [GitLab Managed Apps](../user/clusters/applications.html), a one-click install for various applications which can be added directly to your configured cluster.
+
+**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview of how to add a new GitLab-mananged app, see [How to add GitLab-managed-apps to Kubernetes integration](https://youtu.be/mKm-jkranEk).**
diff --git a/doc/development/lfs.md b/doc/development/lfs.md
index cb4c2d8967b..9139bbaca0e 100644
--- a/doc/development/lfs.md
+++ b/doc/development/lfs.md
@@ -5,7 +5,7 @@
In April 2019, Francisco Javier López hosted a [Deep Dive] on GitLab's [Git LFS] 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], and the slides on [Google Slides] and in [PDF]. Everything covered in this deep dive was accurate as of GitLab 11.10, 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
-[Git LFS]: ../workflow/lfs/manage_large_binaries_with_git_lfs.html
+[Git LFS]: ../administration/lfs/manage_large_binaries_with_git_lfs.md
[recording on YouTube]: https://www.youtube.com/watch?v=Yyxwcksr0Qc
[Google Slides]: https://docs.google.com/presentation/d/1E-aw6-z0rYd0346YhIWE7E9A65zISL9iIMAOq2zaw9E/edit
[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 4456e5e6d18..2e80e813a4b 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -1,7 +1,9 @@
# Merge Request Performance Guidelines
+Each new introduced merge request **should be performant by default**.
+
To ensure a merge request does not negatively impact performance of GitLab
-_every_ merge request **must** adhere to the guidelines outlined in this
+_every_ merge request **should** adhere to the guidelines outlined in this
document. There are no exceptions to this rule unless specifically discussed
with and agreed upon by backend maintainers and performance specialists.
@@ -12,6 +14,19 @@ the following guides:
- [Performance Guidelines](performance.md)
- [What requires downtime?](what_requires_downtime.md)
+## Definition
+
+The term `SHOULD` per the [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) means:
+
+> This word, or the adjective "RECOMMENDED", mean that there
+> may exist valid reasons in particular circumstances to ignore a
+> particular item, but the full implications must be understood and
+> carefully weighed before choosing a different course.
+
+Ideally, each of these tradeoffs should be documented
+in the separate issues, labelled accordingly and linked
+to original issue and epic.
+
## Impact Analysis
**Summary:** think about the impact your merge request may have on performance
@@ -44,6 +59,64 @@ should ask one of the merge request reviewers to review your changes. You can
find a list of these reviewers at <https://about.gitlab.com/company/team/>. A reviewer
in turn can request a performance specialist to review the changes.
+## Think outside of the box
+
+Everyone has their own perception how the new feature is going to be used.
+Always consider how users might be using the feature instead. Usually,
+users test our features in a very unconventional way,
+like by brute forcing or abusing edge conditions that we have.
+
+## Data set
+
+The data set that will be processed by the merge request should be known
+and documented. The feature should clearly document what the expected
+data set is for this feature to process, and what problems it might cause.
+
+If you would think about the following example that puts
+a strong emphasis of data set being processed.
+The problem is simple: you want to filter a list of files from
+some git repository. Your feature requests a list of all files
+from the repository and perform search for the set of files.
+As an author you should in context of that problem consider
+the following:
+
+1. What repositories are going to be supported?
+1. How long it will take for big repositories like Linux kernel?
+1. Is there something that we can do differently to not process such a
+ big data set?
+1. Should we build some fail-safe mechanism to contain
+ computational complexity? Usually it is better to degrade
+ the service for a single user instead of all users.
+
+## Query plans and database structure
+
+The query plan can answer the questions whether we need additional
+indexes, or whether we perform expensive filtering (i.e. using sequential scans).
+
+Each query plan should be run against substantional size of data set.
+For example if you look for issues with specific conditions,
+you should consider validating the query against
+a small number (a few hundred) and a big number (100_000) of issues.
+See how the query will behave if the result will be a few
+and a few thousand.
+
+This is needed as we have users using GitLab for very big projects and
+in a very unconventional way. Even, if it seems that it is unlikely
+that such big data set will be used, it is still plausible that one
+of our customers will have the problem with the feature.
+
+Understanding ahead of time how it is going to behave at scale even if we accept it,
+is the desired outcome. We should always have a plan or understanding what it takes
+to optimise feature to the magnitude of higher usage patterns.
+
+Every database structure should be optimised and sometimes even over-described
+to be prepared to be easily extended. The hardest part after some point is
+data migration. Migrating millions of rows will always be troublesome and
+can have negative impact on application.
+
+To better understand how to get help with the query plan reviews
+read this section on [how to prepare the merge request for a database review](https://docs.gitlab.com/ee/development/database_review.html#how-to-prepare-the-merge-request-for-a-database-review).
+
## Query Counts
**Summary:** a merge request **should not** increase the number of executed SQL
@@ -172,3 +245,107 @@ Caching data per transaction can be done using
`Gitlab::SafeRequestStore` to avoid having to remember to check
`RequestStore.active?`). Caching data in Redis can be done using [Rails' caching
system](https://guides.rubyonrails.org/caching_with_rails.html).
+
+## Pagination
+
+Each feature that renders a list of items as a table needs to include pagination.
+
+The main styles of pagination are:
+
+1. Offset-based pagination: user goes to a specific page, like 1. User sees the next page number,
+ and the total number of pages. This style is well supported by all components of GitLab.
+1. Offset-based pagination, but without the count: user goes to a specific page, like 1.
+ User sees only the next page number, but does not see the total amount of pages.
+1. Next page using keyset-based pagination: user can only go to next page, as we do not know how many pages
+ are available.
+1. Infinite scrolling pagination: user scrolls the page and next items are loaded asynchronously. This is ideal,
+ as it has exact same benefits as the previous one.
+
+The ultimately scalable solution for pagination is to use Keyset-based pagination.
+However, we don't have support for that at GitLab at that moment. You
+can follow the progress looking at [API: Keyset Pagination
+](https://gitlab.com/groups/gitlab-org/-/epics/2039).
+
+Take into consideration the following when choosing a pagination strategy:
+
+1. It is very inefficient to calculate amount of objects that pass the filtering,
+ this operation usually can take seconds, and can time out,
+1. It is very inefficent to get entries for page at higher ordinals, like 1000.
+ The database has to sort and iterate all previous items, and this operation usually
+ can result in substantial load put on database.
+
+## Badge counters
+
+Counters should always be truncated. It means that we do not want to present
+the exact number over some threshold. The reason for that is for the cases where we want
+to calculate exact number of items, we effectively need to filter each of them for
+the purpose of knowing the exact number of items matching.
+
+From ~UX perspective it is often acceptable to see that you have over 1000+ pipelines,
+instead of that you have 40000+ pipelines, but at a tradeoff of loading page for 2s longer.
+
+An example of this pattern is the list of pipelines and jobs. We truncate numbers to `1000+`,
+but we show an accurate number of running pipelines, which is the most interesting information.
+
+There's a helper method that can be used for that purpose - `NumbersHelper.limited_counter_with_delimiter` -
+that accepts an upper limit of counting rows.
+
+In some cases it is desired that badge counters are loaded asynchronously.
+This can speed up the initial page load and give a better user experience overall.
+
+## Application/misuse limits
+
+Every new feature should have safe usage quotas introduced.
+The quota should be optimised to a level that we consider the feature to
+be performant and usable for the user, but **not limiting**.
+
+**We want the features to be fully usable for the users.**
+**However, we want to ensure that the feature will continue to perform well if used at its limit**
+**and it will not cause availability issues.**
+
+Consider that it is always better to start with some kind of limitation,
+instead of later introducing a breaking change that would result in some
+workflows breaking.
+
+The intent is to provide a safe usage pattern for the feature,
+as our implementation decisions are optimised for the given data set.
+Our feature limits should reflect the optimisations that we introduced.
+
+The intent of quotas could be different:
+
+1. We want to provide higher quotas for higher tiers of features:
+ we want to provide on GitLab.com more capabilities for different tiers,
+1. We want to prevent misuse of the feature: someone accidentially creates
+ 10000 deploy tokens, because of a broken API script,
+1. We want to prevent abuse of the feature: someone purposely creates
+ a 10000 pipelines to take advantage of the system.
+
+Examples:
+
+1. Pipeline Schedules: It is very unlikely that user will want to create
+ more than 50 schedules.
+ In such cases it is rather expected that this is either misuse
+ or abuse of the feature. Lack of the upper limit can result
+ in service degredation as the system will try to process all schedules
+ assigned the the project.
+
+1. GitLab CI includes: We started with the limit of maximum of 50 nested includes.
+ We understood that performance of the feature was acceptable at that level.
+ We received a request from the community that the limit is too small.
+ We had a time to understand the customer requirement, and implement an additional
+ fail-safe mechanism (time-based one) to increase the limit 100, and if needed increase it
+ further without negative impact on availability of the feature and GitLab.
+
+## Usage of feature flags
+
+Each feature that has performance critical elements or has a known performance deficiency
+needs to come with feature flag to disable it.
+
+The feature flag makes our team more happy, because they can monitor the system and
+quickly react without our users noticing the problem.
+
+Performance deficiencies should be addressed right away after we merge initial
+changes.
+
+Read more about when and how feature flags should be used in
+[Feature flags in GitLab development](https://docs.gitlab.com/ee/development/feature_flags/process.html#feature-flags-in-gitlab-development).
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 20d705136b2..32c4313a1ed 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -211,7 +211,7 @@ class MyMigration < ActiveRecord::Migration[4.2]
end
def down
- remove_index :table, :column if index_exists?(:table, :column)
+ remove_concurrent_index :table, :column
end
end
```
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 2474392db62..6d4a9ea9f41 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -4,12 +4,13 @@ This document will guide you through adding another [package management system](
See already supported package types in [Packages documentation](../administration/packages/index.md)
-Since GitLab packages' UI is pretty generic, it is possible to add new
+Since GitLab packages' UI is pretty generic, it is possible to add basic new
package system support by solely backend changes. This guide is superficial and does
not cover the way the code should be written. However, you can find a good example
by looking at existing merge requests with Maven and NPM support:
- [NPM registry support](https://gitlab.com/gitlab-org/gitlab/merge_requests/8673).
+- [Conan repository](https://gitlab.com/gitlab-org/gitlab/issues/8248).
- [Maven repository](https://gitlab.com/gitlab-org/gitlab/merge_requests/6607).
- [Instance level endpoint for Maven repository](https://gitlab.com/gitlab-org/gitlab/merge_requests/8757)
@@ -34,7 +35,7 @@ endpoints like:
- GET package file content.
- PUT upload package.
-Since the packages belong to a project, it's expected to have project-level endpoint
+Since the packages belong to a project, it's expected to have project-level endpoint (remote)
for uploading and downloading them. For example:
```
@@ -44,9 +45,48 @@ PUT https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/
Group-level and instance-level endpoints are good to have but are optional.
-NOTE: **Note:**
-To avoid name conflict for instance-level endpoints we use
-[the package naming convention](../user/packages/npm_registry/index.md#package-naming-convention)
+## Naming conventions
+
+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
+[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
+members to be certain that there is no conflict between two package names, however the system should prevent
+a user from reusing an existing name within a given scope.
+
+Otherwise, naming should follow the package manager's naming conventions and include a validation in the `package.md`
+model for that package type.
+
+## File uploads
+
+File uploads should be handled by GitLab workhorse using object accelerated uploads. What this means is that
+the workhorse proxy that checks all incoming requests to GitLab will intercept the upload request,
+upload the file, and forward a request to the main GitLab codebase only containing the metadata
+and file location rather than the file itself. An overview of this process can be found in the
+[development documentation](uploads.md#workhorse-object-storage-acceleration).
+
+In terms of code, this means a route will need to be added to the
+[gitlab-workhorse project](https://gitlab.com/gitlab-org/gitlab-workhorse) for each level of remote being added
+(instance, group, project). [This merge request](https://gitlab.com/gitlab-org/gitlab-workhorse/merge_requests/412/diffs)
+demonstrates adding an instance-level endpoint for Conan to workhorse. You can also see the Maven project level endpoint
+implemented in the same file.
+
+Once the route has been added, you will need to add an additional `/authorize` version of the upload endpoint to your API file.
+[Here is an example](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/maven_packages.rb#L164)
+of the additional endpoint added for Maven. The `/authorize` endpoint verifies and authorizes the request from workhorse,
+then the normal upload endpoint is implemented below, consuming the metadata that workhorse provides in order to
+create the package record. Workhorse provides a variety of file metadata such as type, size, and different checksum formats.
+
+For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md)
+in your local development environment.
+
+## Services and finders
+
+Logic for performing tasks such as creating package or package file records or finding packages should not live
+within the API file, but should live in services and finders. Existing services and finders should be used or
+extended when possible to keep the common package logic grouped as much as possible.
## Configuration
@@ -56,7 +96,7 @@ to add anything there.
Packages can be configured to use object storage, therefore your code must support it.
-## Database
+## Database and handling metadata
The current database model allows you to store a name and a version for each package.
Every time you upload a new package, you can either create a new record of `Package`
@@ -65,4 +105,58 @@ 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 example for package specific data.
+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.
+
+If there is package specific behavior for a given package manager, add those methods to the metadata models and
+delegate from the package model.
+
+Note that the existing package UI only displays information within the `packages_packages` and `packages_package_files`
+tables. If the data stored in the metadata tables need to be displayed, a ~frontend change will be required.
+
+## Authorization
+
+There are project and group level permissions for `read_package`, `create_package`, and `destroy_package`. Each
+endpoint should
+[authorize the requesting user](https://gitlab.com/gitlab-org/gitlab/blob/398fef1ca26ae2b2c3dc89750f6b20455a1e5507/ee/lib/api/conan_packages.rb#L84)
+against the project or group before continuing.
+
+## Keep iterations small
+
+When implementing a new package manager, it is easy to end up creating one large merge request containing all of the
+necessary endpoints and services necessary to support basic usage. If this is the case, consider putting the
+API endpoints behind a [feature flag](feature_flags/development.md) and
+submitting each endpoint or behavior (download, upload, etc) in different merge requests to shorten the review
+process.
+
+### Potential MRs for any given package system
+
+#### MVC MRs
+
+These changes represent all that is needed to deliver a minimally usable package management system.
+
+1. Empty file structure (api file, base service for this package)
+1. Authentication system for 'logging in' to the package manager
+1. Identify metadata and create applicable tables
+1. Workhorse route for [object storage accelerated uploads](uploads.md#workhorse-object-storage-acceleration)
+1. Endpoints required for upload/publish
+1. Endpoints required for install/download
+1. Endpoints required for remove/delete
+
+#### Possible post-MVC MRs
+
+These updates are not essential to be able to publish and consume packages, but may be desired as the system is
+released for general use.
+
+1. Endpoints required for search
+1. Front end updates to display additional package information and metadata
+1. Limits on file sizes
+1. Tracking for metrics
+
+## Exceptions
+
+This documentation is just guidelines on how to implement a package manager to match the existing structure and logic
+already present within GitLab. While the structure is intended to be extendable and flexible enough to allow for
+any given package manager, if there is good reason to stray due to the constraints or needs of a given package
+manager, then it should be raised and discussed within the implementation issue or merge request to work towards
+the most efficient outcome.
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 5954de03db4..764bd68000d 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -15,6 +15,8 @@ as much as possible.
The current stages are:
+- `sync`: This stage is used to synchronize changes from gitlab-org/gitlab to
+ gitlab-org/gitlab-foss.
- `prepare`: This stage includes jobs that prepare artifacts that are needed by
jobs in subsequent stages.
- `quick-test`: This stage includes test jobs that should run first and fail the
@@ -27,7 +29,6 @@ The current stages are:
- `review`: This stage includes jobs that deploy the GitLab and Docs Review Apps.
- `qa`: This stage includes jobs that perform QA tasks against the Review App
that is deployed in the previous stage.
-- `notification`: This stage includes jobs that sends notifications about pipeline status.
- `post-test`: This stage includes jobs that build reports or gather data from
the previous stages' jobs (e.g. coverage, Knapsack metadata etc.).
- `pages`: This stage includes a job that deploys the various reports as
@@ -38,7 +39,8 @@ The current stages are:
## Default image
The default image is currently
-`gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
+`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-9.6-graphicsmagick-1.3.33`.
+
It includes Ruby 2.6.3, Go 1.11, Git 2.22, Chrome 73, Node 12, Yarn 1.16,
PostgreSQL 9.6, and Graphics Magick 1.3.33.
@@ -48,24 +50,13 @@ project, which is push-mirrored to <https://dev.gitlab.org/gitlab/gitlab-build-i
for redundancy.
The current version of the build images can be found in the
-["Used by GitLab CE/EE section"](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/.gitlab-ci.yml).
+["Used by GitLab section"](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/.gitlab-ci.yml).
## Default variables
In addition to the [predefined variables](../ci/variables/predefined_variables.md),
-each pipeline includes the following [variables](../ci/variables/README.md):
-
-- `RAILS_ENV: "test"`
-- `NODE_ENV: "test"`
-- `SIMPLECOV: "true"`
-- `GIT_DEPTH: "50"`
-- `GIT_SUBMODULE_STRATEGY: "none"`
-- `GET_SOURCES_ATTEMPTS: "3"`
-- `KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json`
-- `FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json`
-- `BUILD_ASSETS_IMAGE: "false"`
-- `ES_JAVA_OPTS: "-Xms256m -Xmx256m"`
-- `ELASTIC_URL: "http://elastic:changeme@docker.elastic.co-elasticsearch-elasticsearch:9200"`
+each pipeline includes default variables defined in
+<https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml>.
## Common job definitions
@@ -85,22 +76,35 @@ These common definitions are:
Ruby/Rails and frontend tasks.
- `.default-only`: Restricts the cases where a job is created. This currently
includes `master`, `/^[\d-]+-stable(-ee)?$/` (stable branches),
- `/^\d+-\d+-auto-deploy-\d+$/` (security branches), `merge_requests`, `tags`.
+ `/^\d+-\d+-auto-deploy-\d+$/` (auto-deploy branches), `/^security\//` (security branches), `merge_requests`, `tags`.
Note that jobs won't be created for branches with this default configuration.
-- `.only-review`: Only creates a job for the `gitlab-org` namespace and if
- Kubernetes integration is available. Also, prevents a job from being created
- for `master` and auto-deploy branches.
-- `.only-review-schedules`: Same as `.only-review` but also restrict a job to
- only run for [schedules](../user/project/pipelines/schedules.md).
-- `.only-canonical-schedules`: Only creates a job for scheduled pipelines in
- the `gitlab-org/gitlab` and `gitlab-org/gitlab-foss` projects
+- `.only:variables-canonical-dot-com`: Only creates a job if the project is
+ located under <https://gitlab.com/gitlab-org>.
+- `.only:variables_refs-canonical-dot-com-schedules`: Same as
+ `.only:variables-canonical-dot-com` but add the condition that pipeline is scheduled.
+- `.except:refs-deploy`: Don't create a job if the `ref` is an auto-deploy branch.
+- `.except:refs-master-tags-stable-deploy`: Don't create a job if the `ref` is one of:
+ - `master`
+ - a tag
+ - a stable branch
+ - an auto-deploy branch
+- `.only:kubernetes`: Only creates a job if a Kubernetes integration is enabled
+ on the project.
+- `.only-review`: This extends from:
+ - `.only:variables-canonical-dot-com`
+ - `.only:kubernetes`
+ - `.except:refs-master-tags-stable-deploy`
+- `.only-review-schedules`: This extends from:
+ - `.only:variables_refs-canonical-dot-com-schedules`
+ - `.only:kubernetes`
+ - `.except:refs-deploy`
- `.use-pg9`: Allows a job to use the `postgres:9.6` and `redis:alpine` services.
- `.use-pg10`: Allows a job to use the `postgres:10.9` and `redis:alpine` services.
- `.use-pg9-ee`: Same as `.use-pg9` but also use the
`docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
- `.use-pg10-ee`: Same as `.use-pg10` but also use the
`docker.elastic.co/elasticsearch/elasticsearch:5.6.12` services.
-- `.only-ee`: Only creates a job for the `gitlab` project.
+- `.only-ee`: Only creates a job for the `gitlab` or `gitlab-ee` project.
- `.only-ee-as-if-foss`: Same as `.only-ee` but simulate the FOSS project by
setting the `FOSS_ONLY='1'` environment variable.
@@ -111,11 +115,13 @@ the cases where it should be created
[based on the changes](../ci/yaml/README.md#onlychangesexceptchanges)
from a commit or MR by extending from the following CI definitions:
-- `.only-code-changes`: Allows a job to only be created upon code-related changes.
-- `.only-qa-changes`: Allows a job to only be created upon QA-related changes.
-- `.only-docs-changes`: Allows a job to only be created upon docs-related changes.
-- `.only-code-qa-changes`: Allows a job to only be created upon code-related or QA-related changes.
-- `.only-graphql-changes`: Allows a job to only be created upon graphql-related changes.
+- `.only:changes-code`: Allows a job to only be created upon code-related changes.
+- `.only:changes-qa`: Allows a job to only be created upon QA-related changes.
+- `.only:changes-docs`: Allows a job to only be created upon docs-related changes.
+- `.only:changes-graphql`: Allows a job to only be created upon GraphQL-related changes.
+- `.only:changes-code-backstage`: Allows a job to only be created upon code-related or backstage-related (e.g. Danger, RuboCop, specs) changes.
+- `.only:changes-code-qa`: Allows a job to only be created upon code-related or QA-related changes.
+- `.only:changes-code-backstage-qa`: Allows a job to only be created upon code-related, backstage-related (e.g. Danger, RuboCop, specs) or QA-related changes.
**See <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/global.gitlab-ci.yml>
for the list of exact patterns.**
@@ -203,11 +209,6 @@ subgraph "`qa` stage"
dast -.-> |needs and depends on| G;
end
-subgraph "`notification` stage"
- NOTIFICATION1["schedule:package-and-qa:notify-success<br>(on_success)"] -.-> |needs| P;
- NOTIFICATION2["schedule:package-and-qa:notify-failure<br>(on_failure)"] -.-> |needs| P;
- end
-
subgraph "`post-test` stage"
M
end
diff --git a/doc/development/policies.md b/doc/development/policies.md
index 833b0acb13e..8e5ef6e57c0 100644
--- a/doc/development/policies.md
+++ b/doc/development/policies.md
@@ -157,3 +157,18 @@ end
```
will include all rules from `ProjectPolicy`. The delegated conditions will be evaluated with the correct delegated subject, and will be sorted along with the regular rules in the policy. Note that only the relevant rules for a particular ability will actually be considered.
+
+## Specifying Policy Class
+
+You can also override the Policy used for a given subject:
+
+```ruby
+class Foo
+
+ def self.declarative_policy_class
+ 'SomeOtherPolicy'
+ end
+end
+```
+
+This will use & check permissions on the `SomeOtherPolicy` class rather than the usual calculated `FooPolicy` class.
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 04897e770f8..18683fa10f8 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -42,6 +42,10 @@ Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send
ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source.
+```ruby
+Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first, logger: Logger.new(STDOUT))
+```
+
There is also a RubyProf printer available:
`Gitlab::Profiler::TotalTimeFlatPrinter`. This acts like
`RubyProf::FlatPrinter`, but its `min_percent` option works on the method's
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index a6d3c008686..369806d462b 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -12,6 +12,14 @@ 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
+
+**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.
+
### Seeding issues for all or a given project
You can seed issues for all or a given project with the `gitlab:seed:issues`
@@ -221,7 +229,7 @@ bundle exec rake db:obsolete_ignored_columns
Feel free to remove their definitions from their `ignored_columns` definitions.
-## Update GraphQL Documentation
+## Update GraphQL Documentation and Schema definitions
To generate GraphQL documentation based on the GitLab schema, run:
@@ -243,3 +251,13 @@ The actual renderer is at `Gitlab::Graphql::Docs::Renderer`.
`@parsed_schema` is an instance variable that the `graphql-docs` gem expects to have available.
`Gitlab::Graphql::Docs::Helper` defines the `object` method we currently use. This is also where you
should implement any new methods for new types you'd like to display.
+
+### Update machine-readable schema files
+
+To generate GraphQL schema files based on the GitLab schema, run:
+
+```shell
+bundle exec rake gitlab:graphql:schema:dump
+```
+
+This uses graphql-ruby's built-in rake tasks to generate files in both [IDL](https://www.prisma.io/blog/graphql-sdl-schema-definition-language-6755bcb9ce51) and JSON formats.
diff --git a/doc/development/repository_mirroring.md b/doc/development/repository_mirroring.md
index 8521d6fcd30..0a0c91821cf 100644
--- a/doc/development/repository_mirroring.md
+++ b/doc/development/repository_mirroring.md
@@ -5,6 +5,6 @@
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]: ../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter
+[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
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index d52a3e652e3..e433691c1ed 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -61,6 +61,168 @@ the extra jobs will take resources away from jobs from workers that were already
there, if the resources available to the Sidekiq process handling the namespace
are not adjusted appropriately.
+## Latency Sensitive Jobs
+
+If a large number of background jobs get scheduled at once, queueing of jobs may
+occur while jobs wait for a worker node to be become available. This is normal
+and gives the system resilience by allowing it to gracefully handle spikes in
+traffic. Some jobs, however, are more sensitive to latency than others. Examples
+of these jobs include:
+
+1. A job which updates a merge request following a push to a branch.
+1. A job which invalidates a cache of known branches for a project after a push
+ to the branch.
+1. A job which recalculates the groups and projects a user can see after a
+ change in permissions.
+1. A job which updates the status of a CI pipeline after a state change to a job
+ in the pipeline.
+
+When these jobs are delayed, the user may perceive the delay as a bug: for
+example, they may push a branch and then attempt to create a merge request for
+that branch, but be told in the UI that the branch does not exist. We deem these
+jobs to be `latency_sensitive`.
+
+Extra effort is made to ensure that these jobs are started within a very short
+period of time after being scheduled. However, in order to ensure throughput,
+these jobs also have very strict execution duration requirements:
+
+1. The median job execution time should be less than 1 second.
+1. 99% of jobs should complete within 10 seconds.
+
+If a worker cannot meet these expectations, then it cannot be treated as a
+`latency_sensitive` worker: consider redesigning the worker, or splitting the
+work between two different workers, one with `latency_sensitive` code that
+executes quickly, and the other with non-`latency_sensitive`, which has no
+execution latency requirements (but also has lower scheduling targets).
+
+This can be summed up in the following table:
+
+| **Latency Sensitivity** | **Queue Scheduling Target** | **Execution Latency Requirement** |
+|-------------------------|-----------------------------|-------------------------------------|
+| Not `latency_sensitive` | 1 minute | Maximum run time of 1 hour |
+| `latency_sensitive` | 100 milliseconds | p50 of 1 second, p99 of 10 seconds |
+
+To mark a worker as being `latency_sensitive`, use the
+`latency_sensitive_worker!` attribute, as shown in this example:
+
+```ruby
+class LatencySensitiveWorker
+ include ApplicationWorker
+
+ latency_sensitive_worker!
+
+ # ...
+end
+```
+
+## Jobs with External Dependencies
+
+Most background jobs in the GitLab application communicate with other GitLab
+services, eg Postgres, Redis, Gitaly and Object Storage. These are considered
+to be "internal" dependencies for a job.
+
+However, some jobs will be dependent on external services in order to complete
+successfully. Some examples include:
+
+1. Jobs which call web-hooks configured by a user.
+1. Jobs which deploy an application to a k8s cluster configured by a user.
+
+These jobs have "external dependencies". This is important for the operation of
+the background processing cluster in several ways:
+
+1. Most external dependencies (such as web-hooks) do not provide SLOs, and
+ therefore we cannot guarantee the execution latencies on these jobs. Since we
+ cannot guarantee execution latency, we cannot ensure throughput and
+ therefore, in high-traffic environments, we need to ensure that jobs with
+ external dependencies are separated from `latency_sensitive` jobs, to ensure
+ throughput on those queues.
+1. Errors in jobs with external dependencies have higher alerting thresholds as
+ there is a likelihood that the cause of the error is external.
+
+```ruby
+class ExternalDependencyWorker
+ include ApplicationWorker
+
+ # Declares that this worker depends on
+ # third-party, external services in order
+ # to complete successfully
+ worker_has_external_dependencies!
+
+ # ...
+end
+```
+
+NOTE: **Note:** Note that a job cannot be both latency sensitive and have
+external dependencies.
+
+## CPU-bound and Memory-bound Workers
+
+Workers that are constrained by CPU or memory resource limitations should be
+annotated with the `worker_resource_boundary` method.
+
+Most workers tend to spend most of their time blocked, wait on network responses
+from other services such as Redis, Postgres and Gitaly. Since Sidekiq is a
+multithreaded environment, these jobs can be scheduled with high concurrency.
+
+Some workers, however, spend large amounts of time _on-cpu_ running logic in
+Ruby. Ruby MRI does not support true multithreading - it relies on the
+[GIL](https://thoughtbot.com/blog/untangling-ruby-threads#the-global-interpreter-lock)
+to greatly simplify application development by only allowing one section of Ruby
+code in a process to run at a time, no matter how many cores the machine
+hosting the process has. For IO bound workers, this is not a problem, since most
+of the threads are blocked in underlying libraries (which are outside of the
+GIL).
+
+If many threads are attempting to run Ruby code simultaneously, this will lead
+to contention on the GIL which will have the affect of slowing down all
+processes.
+
+In high-traffic environments, knowing that a worker is CPU-bound allows us to
+run it on a different fleet with lower concurrency. This ensures optimal
+performance.
+
+Likewise, if a worker uses large amounts of memory, we can run these on a
+bespoke low concurrency, high memory fleet.
+
+Note that Memory-bound workers create heavy GC workloads, with pauses of
+10-50ms. This will have an impact on the latency requirements for the
+worker. For this reason, `memory` bound, `latency_sensitive` jobs are not
+permitted and will fail CI. In general, `memory` bound workers are
+discouraged, and alternative approaches to processing the work should be
+considered.
+
+## Declaring a Job as CPU-bound
+
+This example shows how to declare a job as being CPU-bound.
+
+```ruby
+class CPUIntensiveWorker
+ include ApplicationWorker
+
+ # Declares that this worker will perform a lot of
+ # calculations on-CPU.
+ worker_resource_boundary :cpu
+
+ # ...
+end
+```
+
+## Determining whether a worker is CPU-bound
+
+We use the following approach to determine whether a worker is CPU-bound:
+
+- In the sidekiq structured JSON logs, aggregate the worker `duration` and
+ `cpu_s` fields.
+- `duration` refers to the total job execution duration, in seconds
+- `cpu_s` is derived from the
+ [`Process::CLOCK_THREAD_CPUTIME_ID`](https://www.rubydoc.info/stdlib/core/Process:clock_gettime)
+ counter, and is a measure of time spent by the job on-CPU.
+- Divide `cpu_s` by `duration` to get the percentage time spend on-CPU.
+- If this ratio exceeds 33%, the worker is considered CPU-bound and should be
+ annotated as such.
+- Note that these values should not be used over small sample sizes, but
+ rather over fairly large aggregates.
+
## Feature Categorization
Each Sidekiq worker, or one of its ancestor classes, must declare a
@@ -74,7 +236,7 @@ The declaration uses the `feature_category` class method, as shown below.
class SomeScheduledTaskWorker
include ApplicationWorker
- # Declares that this feature is part of the
+ # Declares that this worker is part of the
# `continuous_integration` feature category
feature_category :continuous_integration
@@ -88,11 +250,11 @@ source](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml
### Updating `config/feature_categories.yml`
-Occassionally new features will be added to GitLab stages. When this occurs, you
+Occasionally new features will be added to GitLab stages. When this occurs, you
can automatically update `config/feature_categories.yml` by running
`scripts/update-feature-categories`. This script will fetch and parse
[`stages.yml`](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml)
-and generare a new version of the file, which needs to be checked into source control.
+and generate a new version of the file, which needs to be checked into source control.
### Excluding Sidekiq workers from feature categorization
@@ -116,9 +278,63 @@ end
Each Sidekiq worker must be tested using RSpec, just like any other class. These
tests should be placed in `spec/workers`.
-## Removing or renaming queues
+## Sidekiq Compatibility across Updates
+
+Keep in mind that the arguments for a Sidekiq job are stored in a queue while it
+is scheduled for execution. During a online update, this could lead to several
+possible situations:
+
+1. An older version of the application publishes a job, which is executed by an
+ upgraded Sidekiq node.
+1. A job is queued before an upgrade, but executed after an upgrade.
+1. A job is queued by a node running the newer version of the application, but
+ executed on a node running an older version of the application.
+
+### Changing the arguments for a worker
+
+Jobs need to be backwards- and forwards-compatible between consecutive versions
+of the application.
+
+This can be done by following this process:
+
+1. **Do not remove arguments from the `perform` function.**. Instead, use the
+ following approach
+ 1. Provide a default value (usually `nil`) and use a comment to mark the
+ argument as deprecated
+ 1. Stop using the argument in `perform_async`.
+ 1. Ignore the value in the worker class, but do not remove it until the next
+ major release.
+
+### Removing workers
+
+Try to avoid removing workers and their queues in minor and patch
+releases.
-Try to avoid renaming or removing workers and their queues in minor and patch releases.
During online update instance can have pending jobs and removing the queue can
lead to those jobs being stuck forever. If you can't write migration for those
-Sidekiq jobs, please consider doing rename or remove queue in major release only.
+Sidekiq jobs, please consider removing the worker in a major release only.
+
+### Renaming queues
+
+For the same reasons that removing workers is dangerous, care should be taken
+when renaming queues.
+
+When renaming queues, use the `sidekiq_queue_migrate` helper migration method,
+as show in this example:
+
+```ruby
+class MigrateTheRenamedSidekiqQueue < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ sidekiq_queue_migrate 'old_queue_name', to: 'new_queue_name'
+ end
+
+ def down
+ sidekiq_queue_migrate 'new_queue_name', to: 'old_queue_name'
+ end
+end
+
+```
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 32e079f915c..fe3989474e6 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -44,6 +44,14 @@ bundle exec rspec
bundle exec rspec spec/[path]/[to]/[spec].rb
```
+Use [guard](https://github.com/guard/guard) to continuously monitor for changes and only run matching tests:
+
+```sh
+bundle exec guard
+```
+
+When using spring and guard together, use `SPRING=1 bundle exec guard` instead to make use of spring.
+
### General guidelines
- Use a single, top-level `describe ClassName` block.
@@ -61,6 +69,7 @@ bundle exec rspec spec/[path]/[to]/[spec].rb
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists.
- Use `focus: true` to isolate parts of the specs you want to run.
+- Use [`:aggregate_failures`](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures) when there is more than one expectation in a test.
### System / Feature tests
@@ -356,10 +365,22 @@ However, if a spec makes direct Redis calls, it should mark itself with the
`:clean_gitlab_redis_cache`, `:clean_gitlab_redis_shared_state` or
`:clean_gitlab_redis_queues` traits as appropriate.
-Sidekiq jobs are typically not run in specs, but this behaviour can be altered
-in each spec through the use of `perform_enqueued_jobs` blocks. Any spec that
-causes Sidekiq jobs to be pushed to Redis should use the `:sidekiq` trait, to
-ensure that they are removed once the spec completes.
+#### Background jobs / Sidekiq
+
+By default, Sidekiq jobs are enqueued into a jobs array and aren't processed.
+If a test enqueues Sidekiq jobs and need them to be processed, the
+`:sidekiq_inline` trait can be used.
+
+The `:sidekiq_might_not_need_inline` trait was added when [Sidekiq inline mode was
+changed to fake mode](https://gitlab.com/gitlab-org/gitlab/merge_requests/15479)
+to all the tests that needed Sidekiq to actually process jobs. Tests with
+this trait should be either fixed to not rely on Sidekiq processing jobs, or their
+`:sidekiq_might_not_need_inline` trait should be updated to `:sidekiq_inline` if
+the processing of background jobs is needed/expected.
+
+NOTE: **Note:**
+The usage of `perform_enqueued_jobs` is currently useless since our
+workers aren't inheriting from `ApplicationJob` / `ActiveJob::Base`.
#### Filesystem
diff --git a/doc/development/testing_guide/end_to_end/best_practices.md b/doc/development/testing_guide/end_to_end/best_practices.md
index 042879b47aa..e2a0d267ba1 100644
--- a/doc/development/testing_guide/end_to_end/best_practices.md
+++ b/doc/development/testing_guide/end_to_end/best_practices.md
@@ -65,3 +65,31 @@ This library [saves the screenshots in the RSpec's `after` hook](https://github.
Given this fact, we should limit the use of `before(:all)` to only those operations where a screenshot is not
necessary in case of failure and QA logs would be enough for debugging.
+
+## Ensure tests do not leave the browser logged in
+
+All QA tests expect to be able to log in at the start of the test.
+
+That's not possible if a test leaves the browser logged in when it finishes. Normally this isn't a problem because [Capybara resets the session after each test](https://github.com/teamcapybara/capybara/blob/9ebc5033282d40c73b0286e60217515fd1bb0b5d/lib/capybara/rspec.rb#L18). But Capybara does that in an `after` block, so when a test logs in in an `after(:context)` block, the browser returns to a logged in state *after* Capybara had logged it out. And so the next test will fail.
+
+For an example see: <https://gitlab.com/gitlab-org/gitlab/issues/34736>
+
+Ideally, any actions peformed in an `after(:context)` (or [`before(:context)`](#limit-the-use-of-beforeall-hook)) block would be performed via the API. But if it's necessary to do so via the UI (e.g., if API functionality doesn't exist), make sure to log out at the end of the block.
+
+```ruby
+after(:all) do
+ login unless Page::Main::Menu.perform(&:signed_in?)
+
+ # Do something while logged in
+
+ Page::Main::Menu.perform(&:sign_out)
+end
+```
+
+## Tag tests that require Administrator access
+
+We don't run tests that require Administrator access against our Production environments.
+
+When you add a new test that requires Administrator access, apply the RSpec metadata `:requires_admin` so that the test will not be included in the test suites executed against Production and other environments on which we don't want to run those tests.
+
+Note: When running tests locally or configuring a pipeline, the environment variable `QA_CAN_TEST_ADMIN_FEATURES` can be set to `false` to skip tests that have the `:requires_admin` tag.
diff --git a/doc/development/testing_guide/end_to_end/feature_flags.md b/doc/development/testing_guide/end_to_end/feature_flags.md
new file mode 100644
index 00000000000..bf1e70be9cb
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/feature_flags.md
@@ -0,0 +1,27 @@
+# Testing with feature flags
+
+To run a specific test with a feature flag enabled you can use the `QA::Runtime::Feature` class to enabled and disable feature flags ([via the API](../../../api/features.md)).
+
+Note that administrator authorization is required to change feature flags. `QA::Runtime::Feature` will automatically authenticate as an administrator as long as you provide an appropriate access token via `GITLAB_QA_ADMIN_ACCESS_TOKEN` (recommended), or provide `GITLAB_ADMIN_USERNAME` and `GITLAB_ADMIN_PASSWORD`.
+
+```ruby
+context "with feature flag enabled" do
+ before do
+ Runtime::Feature.enable('feature_flag_name')
+ end
+
+ it "feature flag test" do
+ # Execute a test with a feature flag enabled
+ end
+
+ after do
+ Runtime::Feature.disable('feature_flag_name')
+ end
+end
+```
+
+## Running a scenario with a feature flag enabled
+
+It's also possible to run an entire scenario with a feature flag enabled, without having to edit existing tests or write new ones.
+
+Please see the [QA readme](https://gitlab.com/gitlab-org/gitlab/tree/master/qa#running-tests-with-a-feature-flag-enabled) for details.
diff --git a/doc/development/testing_guide/end_to_end/flows.md b/doc/development/testing_guide/end_to_end/flows.md
new file mode 100644
index 00000000000..fb1d82914aa
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/flows.md
@@ -0,0 +1,56 @@
+# Flows in GitLab QA
+
+Flows are frequently used sequences of actions. They are a higher level
+of abstraction than page objects. Flows can include multiple page objects,
+or any other relevant code.
+
+For example, the sign in flow encapsulates two steps that are included
+in every browser UI test.
+
+```ruby
+# QA::Flow::Login
+
+def sign_in(as: nil)
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as) }
+end
+
+# When used in a test
+
+it 'performs a test after signing in as the default user' do
+ Flow::Login.sign_in
+
+ # Perform the test
+end
+```
+
+`QA::Flow::Login` provides an even more useful flow, allowing a test to easily switch users.
+
+```ruby
+# QA::Flow::Login
+
+def while_signed_in(as: nil)
+ Page::Main::Menu.perform(&:sign_out_if_signed_in)
+
+ sign_in(as: as)
+
+ yield
+
+ Page::Main::Menu.perform(&:sign_out)
+end
+
+# When used in a test
+
+it 'performs a test as one user and verifies as another' do
+ user1 = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
+ user2 = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2)
+
+ Flow::Login.while_signed_in(as: user1) do
+ # Perform some setup as user1
+ end
+
+ Flow::Login.sign_in(as: user2)
+
+ # Perform the rest of the test as user2
+end
+```
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index a9fb4be284e..19885f5756f 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -130,6 +130,8 @@ Continued reading:
- [Quick Start Guide](quick_start_guide.md)
- [Style Guide](style_guide.md)
- [Best Practices](best_practices.md)
+- [Testing with feature flags](feature_flags.md)
+- [Flows](flows.md)
## Where can I ask for help?
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 28111c18378..554995fa2e2 100644
--- a/doc/development/testing_guide/end_to_end/page_objects.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -167,6 +167,65 @@ There are two supported methods of defining elements within a view.
Any existing `.qa-selector` class should be considered deprecated
and we should prefer the `data-qa-selector` method of definition.
+### Dynamic element selection
+
+> Introduced in GitLab 12.5
+
+A common occurrence in automated testing is selecting a single "one-of-many" element.
+In a list of several items, how do you differentiate what you are selecting on?
+The most common workaround for this is via text matching. Instead, a better practice is
+by matching on that specific element by a unique identifier, rather than by text.
+
+We got around this by adding the `data-qa-*` extensible selection mechanism.
+
+#### Examples
+
+**Example 1**
+
+Given the following Rails view (using GitLab Issues as an example):
+
+```haml
+%ul.issues-list
+ - @issues.each do |issue|
+ %li.issue{data: { qa_selector: 'issue', qa_issue_title: issue.title } }= link_to issue
+```
+
+We can select on that specific issue by matching on the Rails model.
+
+```ruby
+class Page::Project::Issues::Index < Page::Base
+ def has_issue?(issue)
+ has_element? :issue, issue_title: issue
+ end
+end
+```
+
+In our test, we can validate that this particular issue exists.
+
+```ruby
+describe 'Issue' do
+ it 'has an issue titled "hello"' do
+ Page::Project::Issues::Index.perform do |index|
+ expect(index).to have_issue('hello')
+ end
+ end
+end
+```
+
+**Example 2**
+
+*By an index...*
+
+```haml
+%ol
+ - @some_model.each_with_index do |model, idx|
+ %li.model{ data: { qa_selector: 'model', qa_index: idx } }
+```
+
+```ruby
+expect(the_page).to have_element(:model, index: 1) #=> select on the first model that appears in the list
+```
+
### Exceptions
In some cases it might not be possible or worthwhile to add a selector.
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index 0823c2e02b8..3a96f8204fc 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -83,6 +83,7 @@ This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-foss/m
- In JS tests, shifting elements can cause Capybara to misclick when the element moves at the exact time Capybara sends the click
- [Dropdowns rendering upward or downward due to window size and scroll position](https://gitlab.com/gitlab-org/gitlab/merge_requests/17660)
- [Lazy loaded images can cause Capybara to misclick](https://gitlab.com/gitlab-org/gitlab/merge_requests/18713)
+- [Triggering JS events before the event handlers are set up](https://gitlab.com/gitlab-org/gitlab/merge_requests/18742)
#### Capybara viewport size related issues
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 314995ca9b3..236f175cee5 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -119,6 +119,50 @@ Global mocks introduce magic and can affect how modules are imported in your tes
When in doubt, construct mocks in your test file using [`jest.mock()`](https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options), [`jest.spyOn()`](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname), etc.
+### Data-driven tests
+
+Similar to [RSpec's parameterized tests](best_practices.md#table-based--parameterized-tests),
+Jest supports data-driven tests for:
+
+- Individual tests using [`test.each`](https://jestjs.io/docs/en/api#testeachtable-name-fn-timeout) (aliased to `it.each`).
+- Groups of tests using [`describe.each`](https://jestjs.io/docs/en/api#describeeachtable-name-fn-timeout).
+
+These can be useful for reducing repetition within tests. Each option can take an array of
+data values or a tagged template literal.
+
+For example:
+
+```javascript
+// function to test
+const icon = status => status ? 'pipeline-passed' : 'pipeline-failed'
+const message = status => status ? 'pipeline-passed' : 'pipeline-failed'
+
+// test with array block
+it.each([
+ [false, 'pipeline-failed'],
+ [true, 'pipeline-passed']
+])('icon with %s will return %s',
+ (status, icon) => {
+ expect(renderPipeline(status)).toEqual(icon)
+ }
+);
+
+// test suite with tagged template literal block
+describe.each`
+ status | icon | message
+ ${false} | ${'pipeline-failed'} | ${'Pipeline failed - boo-urns'}
+ ${true} | ${'pipeline-passed'} | ${'Pipeline succeeded - win!'}
+`('pipeline component', ({ status, icon, message }) => {
+ it(`returns icon ${icon} with status ${status}`, () => {
+ expect(icon(status)).toEqual(message)
+ })
+
+ it(`returns message ${message} with status ${status}`, () => {
+ expect(message(status)).toEqual(message)
+ })
+});
+```
+
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine] as its test
@@ -457,6 +501,39 @@ it('waits for an event', () => {
});
```
+#### Ensuring that tests are isolated
+
+Tests are normally architected in a pattern which requires a recurring setup and breakdown of the component under test. This is done by making use of the `beforeEach` and `afterEach` hooks.
+
+Example
+
+```javascript
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(Component);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+```
+
+When looking at this initially you'd suspect that the component is setup before each test and then broken down afterwards, providing isolation between tests.
+
+This is however not entirely true as the `destroy` method does not remove everything which has been mutated on the `wrapper` object. For functional components, destroy only removes the rendered DOM elements from the document.
+
+In order to ensure that a clean wrapper object and DOM are being used in each test, the breakdown of the component should rather be performed as follows:
+
+```javascript
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+```
+
+See also the [Vue Test Utils documention on `destroy`](https://vue-test-utils.vuejs.org/api/wrapper/#destroy).
+
#### Migrating flaky Karma tests to Jest
Some of our Karma tests are flaky because they access the properties of a shared scope.
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 3dd403f148e..ecfcbc731e1 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -189,10 +189,10 @@ that the `review-apps-ce/ee` cluster is unhealthy. Leading indicators may be hea
The following items may help diagnose this:
-- [Instance group CPU Utilization in GCP](https://console.cloud.google.com/compute/instanceGroups/details/us-central1-b/gke-review-apps-ee-preemp-n1-standard-8affc0f5-grp?project=gitlab-review-apps&tab=monitoring&graph=GCE_CPU&duration=P30D) - helpful to identify if nodes are problematic or the entire cluster is trending towards unhealthy
-- [Instance Group size in GCP](https://console.cloud.google.com/compute/instanceGroups/details/us-central1-b/gke-review-apps-ee-preemp-n1-standard-8affc0f5-grp?project=gitlab-review-apps&tab=monitoring&graph=GCE_SIZE&duration=P30D) - aids in identifying load spikes on the cluster. Kubernetes will add nodes up to 220 based on total resource requests.
-- `kubectl top nodes --sort-by=cpu` - can identify if node spikes are common or load on specific nodes which may get rebalanced by the Kubernetes scheduler.
-- `kubectl top pods --sort-by=cpu` -
+- [Review Apps Health 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.
+- `kubectl top nodes | sort --key 3 --numeric` - can identify if node spikes are common or load on specific nodes which may get rebalanced by the Kubernetes scheduler.
+- `kubectl top pods | sort --key 2 --numeric` -
- [K9s] - K9s is a powerful command line dashboard which allows you to filter by labels. This can help identify trends with apps exceeding the [review-app resource requests](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/review_apps/base-config.yaml). Kubernetes will schedule pods to nodes based on resource requests and allow for CPU usage up to the limits.
- In K9s you can sort or add filters by typing the `/` character
- `-lrelease=<review-app-slug>` - filters down to all pods for a release. This aids in determining what is having issues in a single deployment
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
index 7c926c83a36..53b50b6332c 100644
--- a/doc/development/understanding_explain_plans.md
+++ b/doc/development/understanding_explain_plans.md
@@ -705,6 +705,43 @@ For more information about the available options, run:
/chatops run explain --help
```
+### `#database-lab`
+
+Another tool GitLab employees can use is a chatbot powered by [Joe](https://gitlab.com/postgres-ai/joe), available in the [`#database-lab`](https://gitlab.slack.com/archives/CLJMDRD8C) channel on Slack.
+Unlike chatops, it gives you a way to execute DDL statements (like creating indexes and tables) and get query plan not only for `SELECT` but also `UPDATE` and `DELETE`.
+
+For example, in order to test new index you can do the following:
+
+Create the index:
+
+```
+exec CREATE INDEX index_projects_marked_for_deletion ON projects (marked_for_deletion_at) WHERE marked_for_deletion_at IS NOT NULL
+```
+
+Analyze the table to update its statistics:
+
+```
+exec ANALYZE projects
+```
+
+Get the query plan:
+
+```
+explain SELECT * FROM projects WHERE marked_for_deletion_at < CURRENT_DATE
+```
+
+Once done you can rollback your changes:
+
+```
+reset
+```
+
+For more information about the available options, run:
+
+```
+help
+```
+
## Further reading
A more extensive guide on understanding query plans can be found in
diff --git a/doc/development/utilities.md b/doc/development/utilities.md
index 38e416d68e4..25869a0d2b5 100644
--- a/doc/development/utilities.md
+++ b/doc/development/utilities.md
@@ -1,6 +1,6 @@
# GitLab utilities
-We developed a number of utilities to ease development.
+We have developed a number of utilities to help ease development:
## `MergeHash`
@@ -51,15 +51,15 @@ Refer to: <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/mer
Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/override.rb>:
-- This utility could help us check if a particular method would override
- another method or not. It has the same idea of Java's `@Override` annotation
- or Scala's `override` keyword. However we only do this check when
+- This utility can help you check if one method would override
+ another or not. It is the same concept as Java's `@Override` annotation
+ or Scala's `override` keyword. However, you should only do this check when
`ENV['STATIC_VERIFICATION']` is set to avoid production runtime overhead.
- This is useful to check:
+ This is useful for checking:
- - If we have typos in overriding methods.
- - If we renamed the overridden methods, making original overriding methods
- overrides nothing.
+ - If you have typos in overriding methods.
+ - If you renamed the overridden methods, which make the original override methods
+ irrelevant.
Here's a simple example:
@@ -100,11 +100,11 @@ Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/stro
- Memoize the value even if it is `nil` or `false`.
- We often do `@value ||= compute`, however this doesn't work well if
- `compute` might eventually give `nil` and we don't want to compute again.
- Instead we could use `defined?` to check if the value is set or not.
- However it's tedious to write such pattern, and `StrongMemoize` would
- help us use such pattern.
+ We often do `@value ||= compute`. However, this doesn't work well if
+ `compute` might eventually give `nil` and you don't want to compute again.
+ Instead you could use `defined?` to check if the value is set or not.
+ It's tedious to write such pattern, and `StrongMemoize` would
+ help you use such pattern.
Instead of writing patterns like this:
@@ -118,7 +118,7 @@ Refer to <https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/utils/stro
end
```
- We could write it like:
+ You could write it like:
``` ruby
class Find
@@ -151,7 +151,7 @@ and the cache key would be based on the class name, method name,
optionally customized instance level values, optionally customized
method level values, and optional method arguments.
-A simple example that only uses the instance level customised values:
+A simple example that only uses the instance level customised values is:
``` ruby
class UserAccess
@@ -169,8 +169,8 @@ end
This way, the result of `can_push_to_branch?` would be cached in
`RequestStore.store` based on the cache key. If `RequestStore` is not
-currently active, then it would be stored in a hash saved in an
-instance variable, so the cache logic would be the same.
+currently active, then it would be stored in a hash, and saved in an
+instance variable so the cache logic would be the same.
We can also set different strategies for different methods:
@@ -184,3 +184,83 @@ class Commit
request_cache(:author) { author_email }
end
```
+
+## `ReactiveCaching`
+
+The `ReactiveCaching` concern is used to fetch some data in the background and
+store it in the Rails cache, keeping it up-to-date for as long as it is being
+requested. If the data hasn't been requested for `reactive_cache_lifetime`,
+it will stop being refreshed, and then be removed.
+
+Example of use:
+
+```ruby
+class Foo < ApplicationRecord
+ include ReactiveCaching
+
+ after_save :clear_reactive_cache!
+
+ def calculate_reactive_cache
+ # Expensive operation here. The return value of this method is cached
+ end
+
+ def result
+ with_reactive_cache do |data|
+ # ...
+ end
+ end
+end
+```
+
+In this example, the first time `#result` is called, it will return `nil`.
+However, it will enqueue a background worker to call `#calculate_reactive_cache`
+and set an initial cache lifetime of ten minutes.
+
+The background worker needs to find or generate the object on which
+`with_reactive_cache` was called.
+The default behaviour can be overridden by defining a custom
+`reactive_cache_worker_finder`.
+Otherwise, the background worker will use the class name and primary key to get
+the object using the ActiveRecord `find_by` method.
+
+```ruby
+class Bar
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->() { ["bar", "thing"] }
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ def self.from_cache(var1, var2)
+ # This method will be called by the background worker with "bar1" and
+ # "bar2" as arguments.
+ new(var1, var2)
+ end
+
+ def initialize(var1, var2)
+ # ...
+ end
+
+ def calculate_reactive_cache
+ # Expensive operation here. The return value of this method is cached
+ end
+
+ def result
+ with_reactive_cache("bar1", "bar2") do |data|
+ # ...
+ end
+ end
+end
+```
+
+Each time the background job completes, it stores the return value of
+`#calculate_reactive_cache`. It is also re-enqueued to run again after
+`reactive_cache_refresh_interval`, therefore, it will keep the stored value up to date.
+Calculations are never run concurrently.
+
+Calling `#result` while a value is cached will call the block given to
+`#with_reactive_cache`, yielding the cached value. It will also extend the
+lifetime by the `reactive_cache_lifetime` value.
+
+Once the lifetime has expired, no more background jobs will be enqueued and
+calling `#result` will again return `nil` - starting the process all over
+again.
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index fc3d36910f2..258a85d0474 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -23,6 +23,7 @@ The following are guides to basic GitLab functionality:
- [Create a group](../user/group/index.md#create-a-new-group), to combine and administer
projects together.
- [Create a branch](create-branch.md), to make changes to files stored in a project's repository.
+- [Feature branch workflow](feature_branch_workflow.md).
- [Fork a project](fork-project.md), to duplicate projects so they can be worked on in parallel.
- [Add a file](add-file.md), to add new files to a project's repository.
- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue),
@@ -30,7 +31,7 @@ The following are guides to basic GitLab functionality:
- [Create a merge request](add-merge-request.md), to request changes made in a branch
be merged into a project's repository.
- See how these features come together in the [GitLab Flow introduction video](https://youtu.be/InKNIvky2KE)
- and [GitLab Flow page](../workflow/gitlab_flow.md).
+ and [GitLab Flow page](../topics/gitlab_flow.md).
## Working with Git from the command line
diff --git a/doc/gitlab-basics/feature_branch_workflow.md b/doc/gitlab-basics/feature_branch_workflow.md
new file mode 100644
index 00000000000..2b641126d0d
--- /dev/null
+++ b/doc/gitlab-basics/feature_branch_workflow.md
@@ -0,0 +1,35 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/workflow.html'
+---
+
+# Feature branch workflow
+
+1. Clone project:
+
+ ```bash
+ git clone git@example.com:project-name.git
+ ```
+
+1. Create branch with your feature:
+
+ ```bash
+ git checkout -b $feature_name
+ ```
+
+1. Write code. Commit changes:
+
+ ```bash
+ git commit -am "My feature is ready"
+ ```
+
+1. Push your branch to GitLab:
+
+ ```bash
+ git push origin $feature_name
+ ```
+
+1. Review your code on commits page.
+
+1. Create a merge request.
+
+1. Your team lead will review the code &amp; merge it to the main branch.
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 5c19985121d..e92491a0821 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -8,4 +8,4 @@ A fork is a copy of an original repository that you put in another namespace
where you can experiment and apply changes that you can later decide whether or
not to share, without affecting the original project.
-It takes just a few steps to [fork a project in GitLab](../workflow/forking_workflow.md#creating-a-fork).
+It takes just a few steps to [fork a project in GitLab](../user/project/repository/forking_workflow.md#creating-a-fork).
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 05329993c64..1f43b151d5d 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -287,7 +287,7 @@ git reset HEAD~1
This leaves the changed files and folders unstaged in your local repository.
CAUTION: **Warning:**
-A Git commit should not usually be reverse, particularly if you already pushed it
+A Git commit should not usually be reversed, particularly if you already pushed it
to the remote repository. Although you can undo a commit, the best option is to avoid
the situation altogether by working carefully.
diff --git a/doc/install/README.md b/doc/install/README.md
index b906deadca9..441826687aa 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -7,7 +7,7 @@ type: index
# Installation **(CORE ONLY)**
GitLab can be installed in most GNU/Linux distributions and in a number
-of cloud providers. To get the best experience from GitLab you need to balance
+of cloud providers. To get the best experience from GitLab, you need to balance
performance, reliability, ease of administration (backups, upgrades and troubleshooting),
and cost of hosting.
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 2dea763688e..c1dde05196c 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -539,7 +539,7 @@ which would otherwise take much space.
In particular, you can store in S3:
-- [The Git LFS objects](../../workflow/lfs/lfs_administration.md#s3-for-omnibus-installations) ((Omnibus GitLab installations))
+- [The Git LFS objects](../../administration/lfs/lfs_administration.md#s3-for-omnibus-installations) ((Omnibus GitLab installations))
- [The Container Registry images](../../administration/packages/container_registry.md#container-registry-storage-driver) (Omnibus GitLab installations)
- [The GitLab CI/CD job artifacts](../../administration/job_artifacts.md#using-object-storage) (Omnibus GitLab installations)
diff --git a/doc/install/installation.md b/doc/install/installation.md
index dd4b5544659..98094ca1185 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -13,7 +13,7 @@ If you want to install on RHEL/CentOS, we recommend using the
[Omnibus packages](https://about.gitlab.com/install/).
This guide is long because it covers many cases and includes all commands you
-need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
+need, this is [one of the few installation scripts that actually work out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880).
The following steps have been known to work. **Use caution when you deviate**
from this guide. Make sure you don't violate any assumptions GitLab makes about
its environment. For example, many people run into permission problems because
@@ -35,7 +35,7 @@ After this termination runit will detect Sidekiq is not running and will start i
Since installations from source don't use runit for process supervision, Sidekiq
can't be terminated and its memory usage will grow over time.
-## Select version to install
+## Select a version to install
Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-7-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
@@ -56,7 +56,7 @@ of this page:
| |-- repositories
```
-- `/home/git/.ssh` - Contains OpenSSH settings. Specifically the `authorized_keys`
+- `/home/git/.ssh` - Contains OpenSSH settings. Specifically, the `authorized_keys`
file managed by GitLab Shell.
- `/home/git/gitlab` - GitLab core software.
- `/home/git/gitlab-shell` - Core add-on component of GitLab. Maintains SSH
@@ -183,7 +183,7 @@ sudo make prefix=/usr/local install
# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git
```
-For the [Custom Favicon](../customization/favicon.md) to work, GraphicsMagick
+For the [Custom Favicon](../user/admin_area/appearance.md#favicon) to work, GraphicsMagick
needs to be installed.
```sh
@@ -315,7 +315,7 @@ use of extensions and concurrent index removal, you need at least PostgreSQL 9.2
sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
```
-1. Create the GitLab production database and grant all privileges on database:
+1. Create the GitLab production database and grant all privileges on the database:
```sh
sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;"
@@ -397,7 +397,7 @@ sudo usermod -aG redis git
## 8. GitLab
```sh
-# We'll install GitLab into home directory of the user "git"
+# We'll install GitLab into the home directory of the user "git"
cd /home/git
```
@@ -424,7 +424,7 @@ cd /home/git/gitlab
# Copy the example GitLab config
sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml
-# Update GitLab config file, follow the directions at top of file
+# Update GitLab config file, follow the directions at top of the file
sudo -u git -H editor config/gitlab.yml
# Copy the example secrets file
@@ -465,7 +465,7 @@ nproc
# Enable cluster mode if you expect to have a high load instance
# Set the number of workers to at least the number of cores
-# Ex. change amount of workers to 3 for 2GB RAM server
+# Ex. change the amount of workers to 3 for 2GB RAM server
sudo -u git -H editor config/unicorn.rb
# Copy the example Rack attack config
@@ -588,7 +588,7 @@ You can specify a different Git repository by providing it as an extra parameter
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
```
-### Install GitLab-Elasticsearch-indexer`
+### Install GitLab-Elasticsearch-indexer
GitLab-Elasticsearch-Indexer uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Elasticsearch-Indexer in `/home/git/gitlab-elasticsearch-indexer`
@@ -670,7 +670,7 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# or you can skip the question by adding force=yes
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes
-# When done you see 'Administrator account created:'
+# When done, you see 'Administrator account created:'
```
NOTE: **Note:**
@@ -684,7 +684,7 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PA
The `secrets.yml` file stores encryption keys for sessions and secure variables.
Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups.
-Otherwise your secrets are exposed if one of your backups is compromised.
+Otherwise, your secrets are exposed if one of your backups is compromised.
### Install Init Script
@@ -835,7 +835,7 @@ initial administrator account. Enter your desired password and you'll be
redirected back to the login screen.
The default account's username is **root**. Provide the password you created
-earlier and login. After login you can change the username if you wish.
+earlier and login. After login, you can change the username if you wish.
**Enjoy!**
@@ -905,7 +905,7 @@ for the changes to take effect.
### Custom Redis Connection
-If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
+If you'd like to connect to a Redis server on a non-standard port or a different host, you can configure its connection string via the `config/resque.yml` file.
```
# example
@@ -921,7 +921,7 @@ production:
url: unix:/path/to/redis/socket
```
-Also you can use environment variables in the `config/resque.yml` file:
+Also, you can use environment variables in the `config/resque.yml` file:
```
# example
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index 010e56fb097..181d4414a9b 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -23,8 +23,6 @@ tools that will help us achieve our goal.
For a video demonstration on installing GitLab on OpenShift, check the article [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/blog/2016/11/14/idea-to-production/).
----
-
## Prerequisites
CAUTION: **Caution:** This information is no longer up to date, as the current versions
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 8b53ee7c3e1..ecd6516bd2e 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -35,7 +35,7 @@ Please see the [installation from source guide](installation.md) and the [instal
### Microsoft Windows
GitLab is developed for Linux-based operating systems.
-It does **not** run on Microsoft Windows, and we have no plans to support it in the near future. For the latest development status view this [issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/46567).
+It does **not** run on Microsoft Windows, and we have no plans to support it in the near future. For the latest development status view this [issue](https://gitlab.com/gitlab-org/gitlab/issues/22337).
Please consider using a virtual machine to run GitLab.
## Ruby versions
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 3a08303bf20..3f33aa94cb9 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -2,45 +2,71 @@
comments: false
---
-# GitLab Integration
-
-GitLab integrates with multiple third-party services to allow external issue
-trackers and external authentication.
-
-See the documentation below for details on how to configure these services.
-
-- [Akismet](akismet.md) Configure Akismet to stop spam
-- [Auth0 OmniAuth](auth0.md) Enable the Auth0 OmniAuth provider
-- [Bitbucket](bitbucket.md) Import projects from Bitbucket.org and login to your GitLab instance with your Bitbucket.org account
-- [CAS](cas.md) Configure GitLab to sign in using CAS
-- [External issue tracker](external-issue-tracker.md) Redmine, Jira, etc.
-- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
-- [Jenkins](jenkins.md) Integrate with the Jenkins CI
-- [Jira](../user/project/integrations/jira.md) Integrate with the Jira issue tracker
-- [Kerberos](kerberos.md) Integrate with Kerberos
-- [LDAP](ldap.md) Set up sign in via LDAP
-- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
-- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
-- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
-- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
-- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
-- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
-- [Trello](trello_power_up.md) Integrate Trello with GitLab
-
-> GitLab Enterprise Edition contains [advanced Jenkins support](jenkins.md).
+# GitLab integrations
+
+GitLab can be integrated with external services for enhanced functionality.
+
+## Issue trackers
+
+You can use an [external issue tracker](external-issue-tracker.md) at the same time as the GitLab issue tracker, or use only the external issue tracker.
+
+GitLab can be integrated with the following external issue trackers:
+
+- Jira
+- Redmine
+- Bugzilla
+- YouTrack
+
+## Authentication sources
+
+GitLab can be configured to authenticate access requests with the following authentication sources:
+
+- Enable the [Auth0 OmniAuth](auth0.md) provider.
+- Enable sign in with [Bitbucket](bitbucket.md) accounts.
+- Configure GitLab to sign in using [CAS](cas.md).
+- Integrate with [Kerberos](kerberos.md).
+- Enable sign in via [LDAP](ldap.md).
+- Enable [OAuth2 provider](oauth_provider.md) application creation.
+- Use [OmniAuth](omniauth.md) to enable sign in via Twitter, GitHub, GitLab.com, Google,
+Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure or Authentiq ID.
+- Use GitLab as an [OpenID Connect](openid_connect_provider.md) identity provider.
+- Configure GitLab as a [SAML](saml.md) 2.0 Service Provider.
+
+## Security enhancements
+
+GitLab can be integrated with the following external services to enhance security:
+
+- [Akismet](akismet.md) helps reduce spam.
+- Google [reCAPTCHA](recaptcha.md) helps verify new users.
+
+GitLab also provides features to improve the security of your own application. For more details see [GitLab Secure](../user/application_security/index.md).
+
+## Continuous integration
+
+GitLab can be integrated with the following external service for continuous integration:
+
+- [Jenkins](jenkins.md) CI. **(STARTER)**
+
+## Feature enhancements
+
+GitLab can be integrated with the following enhancements:
+
+- Add GitLab actions to [Gmail actions buttons](gmail_action_buttons_for_gitlab.md).
+- Configure [PlantUML](../administration/integration/plantuml.md) to use diagrams in AsciiDoc documents.
+- Attach merge requests to [Trello](trello_power_up.md) cards.
+- Enable integrated code intelligence powered by [Sourcegraph](sourcegraph.md).
## Project services
-Integration with services such as Campfire, Flowdock, HipChat,
-Pivotal Tracker, and Slack are available in the form of a [Project Service][].
+Integration with services such as Campfire, Flowdock, HipChat, Pivotal Tracker, and Slack are available as [Project Services](../user/project/integrations/project_services.md).
+
+## Troubleshooting
-[Project Service]: ../user/project/integrations/project_services.md
+### SSL certificate errors
-## SSL certificate errors
+When trying to integrate GitLab with services that are using self-signed certificates, it is very likely that SSL certificate errors will occur in different parts of the application, most likely Sidekiq.
-When trying to integrate GitLab with services that are using self-signed certificates,
-it is very likely that SSL certificate errors will occur on different parts of the
-application, most likely Sidekiq. There are 2 approaches you can take to solve this:
+There are two approaches you can take to solve this:
1. Add the root certificate to the trusted chain of the OS.
1. If using Omnibus, you can add the certificate to GitLab's trusted certificates.
@@ -61,12 +87,12 @@ in to GitLab Omnibus.
It is enough to concatenate the certificate to the main trusted certificate
however it may be overwritten during upgrades:
-```bash
+```shell
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
```
After that restart GitLab with:
-```bash
+```shell
sudo gitlab-ctl restart
```
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 63ffa69e606..7cead234709 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -24,7 +24,7 @@ Bitbucket.org.
GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
GitLab. You are encouraged to upgrade your GitLab instance if you haven't done so
already. If you're using GitLab 8.14 or below, [use the previous integration
-docs][bb-old].
+docs](https://gitlab.com/gitlab-org/gitlab/blob/8-14-stable-ee/doc/integration/bitbucket.md).
To enable the Bitbucket OmniAuth provider you must register your application
with Bitbucket.org. Bitbucket will generate an application ID and secret key for
@@ -135,9 +135,6 @@ GitLab and [start importing your projects][bb-import].
If you want to import projects from Bitbucket, but don't want to enable signing in,
you can [disable Sign-Ins in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
-[init-oauth]: omniauth.md#initial-omniauth-configuration
-[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
-[bb-old]: https://gitlab.com/gitlab-org/gitlab/blob/8-14-stable/doc/integration/bitbucket.md
-[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
+[bb-import]: ../user/project/import/bitbucket.md
[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index da53987ce1b..5c77bd5bcd9 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -583,3 +583,12 @@ Here are some common pitfalls and how to overcome them:
AWS has [fixed limits](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/aes-limits.html)
for this setting ("Maximum Size of HTTP Request Payloads"), based on the size of
the underlying instance.
+
+### Reverting to basic search
+
+Sometimes there may be issues with your Elasticsearch index data and as such
+GitLab will allow you to revert to "basic search" when there are no search
+results and assuming that basic search is supported in that scope. This "basic
+search" will behave as though you don't have Elasticsearch enabled at all for
+your instance and search using other data sources (ie. Postgres data and Git
+data).
diff --git a/doc/integration/img/sourcegraph_admin_v12_5.png b/doc/integration/img/sourcegraph_admin_v12_5.png
new file mode 100644
index 00000000000..23e38f56619
--- /dev/null
+++ b/doc/integration/img/sourcegraph_admin_v12_5.png
Binary files differ
diff --git a/doc/integration/img/sourcegraph_demo_v12_5.png b/doc/integration/img/sourcegraph_demo_v12_5.png
new file mode 100644
index 00000000000..c70448c0a8a
--- /dev/null
+++ b/doc/integration/img/sourcegraph_demo_v12_5.png
Binary files differ
diff --git a/doc/integration/img/sourcegraph_popover_v12_5.png b/doc/integration/img/sourcegraph_popover_v12_5.png
new file mode 100644
index 00000000000..878d6143646
--- /dev/null
+++ b/doc/integration/img/sourcegraph_popover_v12_5.png
Binary files differ
diff --git a/doc/integration/img/sourcegraph_user_preferences_v12_5.png b/doc/integration/img/sourcegraph_user_preferences_v12_5.png
new file mode 100644
index 00000000000..2c0e138e296
--- /dev/null
+++ b/doc/integration/img/sourcegraph_user_preferences_v12_5.png
Binary files differ
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index b72be55aca3..099cab0f5b8 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -572,7 +572,7 @@ installations from source. Restart Unicorn using the `sudo gitlab-ctl restart un
command on Omnibus installations and `sudo service gitlab restart` on installations
from source.
-You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer/)
+You may also find the [SAML Tracer](https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/)
(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace)
(Chrome) browser extensions useful in your debugging.
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index b8842ef3a43..bc2f190920c 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -18,6 +18,7 @@ Taking the trigger term as `project-name`, the commands are:
| `/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 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
new file mode 100644
index 00000000000..5e7cbdfbac3
--- /dev/null
+++ b/doc/integration/sourcegraph.md
@@ -0,0 +1,128 @@
+---
+type: reference, how-to
+---
+
+# Sourcegraph integration
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16556) in GitLab 12.5. Please note that this integration is in BETA and [behind a feature flag](#enable-the-sourcegraph-feature-flag).
+
+[Sourcegraph](https://sourcegraph.com) provides code intelligence features, natively integrated into the GitLab UI.
+
+For GitLab.com users, see [Sourcegraph for GitLab.com](#sourcegraph-for-gitlabcom).
+
+![Sourcegraph demo](img/sourcegraph_demo_v12_5.png)
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, watch the video [Sourcegraph's new GitLab native integration](https://www.youtube.com/watch?v=LjVxkt4_sEA).
+
+NOTE: **Note:**
+This feature requires user opt-in. After Sourcegraph has been enabled for your GitLab instance,
+you can choose to enable Sourcegraph [through your user preferences](#enable-sourcegraph-in-user-preferences).
+
+## Set up for self-managed GitLab instances **(CORE ONLY)**
+
+Before you can enable Sourcegraph code intelligence in GitLab you will need to:
+
+- Enable the `sourcegraph` feature flag for your GitLab instance.
+- Configure a Sourcegraph instance with your GitLab instance as an external service.
+
+### Enable the Sourcegraph feature flag
+
+NOTE: **Note:**
+If you are running a self-managed instance, the Sourcegraph integration will not be available
+unless the feature flag `sourcegraph` is enabled. This can be done from the Rails console
+by instance administrators.
+
+Use these commands to start the Rails console:
+
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+Then run the following command to enable the feature flag:
+
+```
+Feature.enable(:sourcegraph)
+```
+
+You can also enable the feature flag only for specific projects with:
+
+```
+Feature.enable(:sourcegraph, Project.find_by_full_path('my_group/my_project'))
+```
+
+### Set up a self-managed Sourcegraph instance
+
+If you are new to Sourcegraph, head over to the [Sourcegraph installation documentation](https://docs.sourcegraph.com/admin) and get your instance up and running.
+
+### Connect your Sourcegraph instance to your GitLab instance
+
+1. Navigate to the site admin area in Sourcegraph.
+1. [Configure your GitLab external service](https://docs.sourcegraph.com/admin/external_service/gitlab).
+You can skip this step if you already have your GitLab repositories searchable in Sourcegraph.
+1. Validate that you can search your repositories from GitLab in your Sourcegraph instance by running a test query.
+1. Add your GitLab instance URL to the [`corsOrigin` setting](https://docs.sourcegraph.com/admin/config/site_config#corsOrigin) in your site configuration.
+
+### Configure your GitLab instance with Sourcegraph
+
+1. In GitLab, go to **Admin Area > Settings > Integrations**.
+1. Expand the **Sourcegraph** configuration section.
+1. Check **Enable Sourcegraph**.
+1. Set the Sourcegraph URL to your Sourcegraph instance, e.g., `https://sourcegraph.example.com`.
+
+![Sourcegraph admin settings](img/sourcegraph_admin_v12_5.png)
+
+## Enable Sourcegraph in user preferences
+
+If a GitLab administrator has enabled Sourcegraph, you can enable this feature in your user preferences.
+
+1. In GitLab, click your avatar in the top-right corner, then click **Settings**. On the left-hand nav, click **Preferences**.
+1. Under **Integrations**, find the **Sourcegraph** section.
+1. Check **Enable Sourcegraph**.
+
+![Sourcegraph user preferences](img/sourcegraph_user_preferences_v12_5.png)
+
+## Using Sourcegraph code intelligence
+
+Once enabled, participating projects will have a code intelligence popover available in
+the following code views:
+
+- Merge request diffs
+- Commit view
+- File view
+
+When visiting one of these views, you can now hover over a code reference to see a popover with:
+
+- Details on how this reference was defined.
+- **Go to definition**, which navigates to the line of code where this reference was defined.
+- **Find references**, which navigates to the configured Sourcegraph instance, showing a list of references to the hilighted code.
+
+![Sourcegraph demo](img/sourcegraph_popover_v12_5.png)
+
+## Sourcegraph for GitLab.com
+
+Sourcegraph powered code intelligence will be incrementally rolled out on GitLab.com.
+It will eventually become available for all public projects, but for now, it is only
+available for some specific [`gitlab-org` projects](https://gitlab.com/gitlab-org/).
+This means that you can see it working and use it to dig into the code of these projects,
+but you cannot use it on your own project on GitLab.com yet.
+
+If you would like to use it in your own projects as of GitLab 12.5, you can do so by
+setting up a self-managed GitLab instance.
+
+Follow the epic [&2201](https://gitlab.com/groups/gitlab-org/-/epics/2201) for
+updates.
+
+## Sourcegraph and Privacy
+
+From Sourcegraph's [extension documentation](https://docs.sourcegraph.com/integration/browser_extension#privacy) which is the
+engine behind the native GitLab integration:
+
+> Sourcegraph integrations never send any logs, pings, usage statistics, or telemetry to Sourcegraph.com.
+> They will only connect to Sourcegraph.com as required to provide code intelligence or other functionality on public code.
+> As a result, no private code, private repository names, usernames, or any other specific data is sent to Sourcegraph.com.
diff --git a/doc/integration/ultra_auth.md b/doc/integration/ultra_auth.md
index fb950ba989a..83b2d7fe096 100644
--- a/doc/integration/ultra_auth.md
+++ b/doc/integration/ultra_auth.md
@@ -8,7 +8,7 @@ To enable UltraAuth OmniAuth provider, you must use UltraAuth's credentials for
To get the credentials (a pair of Client ID and Client Secret), you must register an application on UltraAuth.
1. Sign in to [UltraAuth](https://ultraauth.com).
-1. Navigate to [Create an App](https://ultraauth.com/select-strategy) and click on "Ruby on Rails".
+1. Navigate to **Create an App** and click on **Ruby on Rails**.
1. Scroll down the page that is displayed to locate the **Client ID** and **Client Secret**.
Keep this page open as you continue configuration.
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 33b23372280..58cb11423d5 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -18,13 +18,13 @@ Create issues, labels, milestones, cast your vote, and review issues.
- [Create an issue](../user/project/issues/managing_issues.md#create-a-new-issue)
- [Assign labels to issues](../user/project/labels.md)
- [Use milestones as an overview of your project's tracker](../user/project/milestones/index.md)
-- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
+- [Use voting to express your like/dislike to issues and merge requests](../user/award_emojis.md)
## Collaborate
Create merge requests and review code.
-- [Fork a project and contribute to it](../workflow/forking_workflow.md)
+- [Fork a project and contribute to it](../user/project/repository/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
- [Automatically close issues from merge requests](../user/project/issues/managing_issues.md#closing-issues-automatically)
- [Automatically merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index d118c2f40cb..ef94236d711 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -30,21 +30,68 @@ The following table describes the version types and their release cadence:
## Patch releases
-Patch releases usually only include bug fixes and are only done for the current
-stable release. That said, in some cases, we may backport it to previous stable
+Our current policy is to support **only the current stable release** at any given time.
+
+Patch releases **only include bug fixes** for the current stable released version of
+GitLab.
+
+These two policies are in place because:
+
+1. GitLab has Community and Enterprise distributions, doubling the amount of work
+necessary to test/release the software.
+1. Backporting to more than one release creates a high development, quality assurance,
+and support cost.
+1. Supporting parallel version discourages incremental upgrades which over time accumulate in
+complexity and create upgrade challenges for all users. GitLab has a dedicated team ensuring that
+incremental upgrades (and installations) are as simple as possible.
+1. The number of changes created in the GitLab application is high, which contributes to backporting complexity to older releases. In number of cases, backporting has to go through the same
+review process a new change goes through.
+1. Ensuring that tests pass on older release is a considerable challenge in some cases, and as such is very time consuming.
+
+Including new features in patch releases is not possible as that would break [Semantic Versioning].
+Breaking [Semantic Versioning] has the following consequences for users that
+have to adhere to various internal requirements (e.g. org. compliance, verifying new features and similar):
+
+1. Inability to quickly upgrade to leverage bug fixes included in patch versions.
+1. Inability to quickly upgrade to leverage security fixes included in patch versions.
+1. Requirements consisting of extensive testing for not only stable GitLab release, but every patch version.
+
+In cases where a strategic user has a requirement to test a feature before it is
+officially released, we can offer to create a Release Candidate (RC) version that will
+include the specific feature. This should be needed only in extreme cases, and can be requested for consideration by raising an issue in [release/tasks] issue tracker.
+It is important to note that the Release Candidate will also contain other
+features and changes as it is not possible to easily isolate a specific feature (similar reasons as noted above). The Release Candidate will be no different than any code that is deployed to GitLab.com or is publicly accessible.
+
+### Backporting to older releases
+
+Backporting to more than one stable release is reserved for [security releases](#security-releases).
+In some cases however, we may need to backport *a bug fix* to more than one stable
release, depending on the severity of the bug.
-For instance, if we release `10.1.1` with a fix for a severe bug introduced in
-`10.0.0`, we could backport the fix to a new `10.0.x` patch release.
+Decision on whether backporting a change will be performed is done at the discretion of the [current release managers][release-managers], similar to what is described in the [managing bugs] process, based on *all* of the following:
+
+1. Estimated [severity][severity-labels] of the bug: Highest possible impact to users based on the current definition of severity.
+
+1. Estimated [priority][priority-labels] of the bug: Immediate impact on all impacted users based on the above estimated severity.
+
+1. Potentially incurring data loss and/or security breach.
+
+1. Potentially affecting one or more strategic accounts due to a proven inability by the user to upgrade to the current stable version.
+
+If *all* of the above are satisfied, the backport releases can be created for
+the current stable stable release, and two previous monthly releases.
+For instance, if we release `11.2.1` with a fix for a severe bug introduced in
+`11.0.0`, we could backport the fix to a new `11.0.x`, and `11.1.x` patch release.
+
+To request backporting to more than one stable release for consideration, raise an issue in [release/tasks] issue tracker.
### Security releases
Security releases are a special kind of patch release that only include security
fixes and patches (see below).
-Our current policy is to support one stable release at any given time, but for
-medium-level security issues, we may backport security fixes to the previous two
-monthly releases.
+Our current policy is to backport security fixes to the previous two
+monthly releases in addition to the current stable release.
For very serious security issues, there is
[precedent](https://about.gitlab.com/blog/2016/05/02/cve-2016-4340-patches/)
@@ -91,3 +138,9 @@ Please see the table below for some examples:
More information about the release procedures can be found in our
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
[Responsible Disclosure Policy](https://about.gitlab.com/security/disclosure/).
+
+[release-managers]: https://about.gitlab.com/community/release-managers/
+[priority-definition]: ../development/contributing/issue_workflow.md#priority-labels
+[severity-labels]: ../development/contributing/issue_workflow.html#severity-labels
+[managing bugs]: https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md#managing-bugs
+[release/tasks]: https://gitlab.com/gitlab-org/release/tasks/issues
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index fe9617c75ad..006e998b1ab 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -877,7 +877,7 @@ including (but not restricted to):
- [Custom Pages domains](../user/project/pages/custom_domains_ssl_tls_certification/index.md)
- [Project error tracking](../user/project/operations/error_tracking.md)
- [Runner authentication](../ci/runners/README.md)
-- [Project mirroring](../workflow/repository_mirroring.md)
+- [Project mirroring](../user/project/repository/repository_mirroring.md)
- [Web hooks](../user/project/integrations/webhooks.md)
In cases like CI/CD variables and Runner authentication, you might
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 67bf7cbd828..937f15554b4 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -88,7 +88,7 @@ gitlab-rake gitlab:cleanup:orphan_job_artifact_files DRY_RUN=false
You can also limit the number of files to delete with `LIMIT`:
```shell
-gitlab-rake gitlab:cleanup:orphan_job_artifact_files LIMIT=100`
+gitlab-rake gitlab:cleanup:orphan_job_artifact_files LIMIT=100
```
This will only delete up to 100 files from disk. You can use this to
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index b9af1ac108f..cb9ad2b694c 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -9,19 +9,24 @@ local network, these may be vulnerable to exploitation via Webhooks.
With [Webhooks](../user/project/integrations/webhooks.md), you and your project
maintainers and owners can set up URLs to be triggered when specific changes
-occur in your projects. Normally, these requests are sent to external web services
-specifically set up for this purpose, that process the request and its attached
-data in some appropriate way.
+occur in your projects. Normally, these requests are sent to external web
+services specifically set up for this purpose, that process the request and its
+attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't
point to an external, but to an internal service, that may do something
completely unintended when the webhook is triggered and the POST request is
sent.
-Because Webhook requests are made by the GitLab server itself, these have
-complete access to everything running on the server (`http://localhost:123`) or
-within the server's local network (`http://192.168.1.12:345`), even if these
-services are otherwise protected and inaccessible from the outside world.
+Webhook requests are made by the GitLab server itself and use a single
+(optional) secret token per hook for authorization (instead of a user or
+repo-specific token). As a result, these may have broader access than
+intended to everything running on the server hosting the webhook (which
+may include the GitLab server or API itself, e.g., `http://localhost:123`).
+Depending on the called webhook, this may also result in network access
+to other servers within that webhook server's local network (e.g.,
+`http://192.168.1.12:345`), even if these services are otherwise protected
+and inaccessible from the outside world.
If a web service does not require authentication, Webhooks can be used to
trigger destructive commands by getting the GitLab server to make POST requests
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 07b426b7f28..01d86331a0a 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -51,7 +51,7 @@ GitLab supports RSA, DSA, ECDSA, and ED25519 keys. Their difference lies on
the signing algorithm, and some of them have advantages over the others. For
more information, you can read this
[nice article on ArchWiki](https://wiki.archlinux.org/index.php/SSH_keys#Choosing_the_authentication_key_type).
-We'll focus on ED25519 and RSA and here.
+We'll focus on ED25519 and RSA here.
NOTE: **Note:**
As an admin, you can [restrict which keys should be permitted and their minimum length](../security/ssh_keys_restrictions.md).
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index a1373639a87..93549ac4de5 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -95,58 +95,85 @@ Auto DevOps.
To make full use of Auto DevOps, you will need:
+- **Kubernetes** (for Auto Review Apps, Auto Deploy, and Auto Monitoring)
+
+ To enable deployments, you will need:
+
+ 1. A [Kubernetes 1.12+ cluster](../../user/project/clusters/index.md) for the project. The easiest
+ way is to add a [new cluster using the GitLab UI](../../user/project/clusters/add_remove_clusters.md#add-new-cluster).
+ 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.
+
+ 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
+ [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
+ [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)
+
+ 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
+ [GitLab-managed app for Ingress](../../user/clusters/applications.md#ingress),
+ the URL endpoint will be automatically configured for you.
+
+ You will then need to [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)
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).
+ If you have 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.
-- **Base domain** (for Auto Review Apps and Auto Deploy)
-
- You will need a domain configured with wildcard DNS which is going to be used
- by all of your Auto DevOps applications.
-
- Read the [specifics](#auto-devops-base-domain).
-- **Kubernetes** (for Auto Review Apps, Auto Deploy, and Auto Monitoring)
-
- To enable deployments, you will need:
+ that are assigned to specific projects (the default if you have installed the
+ GitLab Runner managed application).
- - Kubernetes 1.5+.
- - A [Kubernetes cluster][kubernetes-clusters] for the project.
- - A load balancer. You can use NGINX Ingress by deploying it to your
- Kubernetes cluster by either:
- - Using the [`nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress) Helm chart.
- - Installing the Ingress [GitLab Managed App](../../user/clusters/applications.md#ingress).
- **Prometheus** (for Auto Monitoring)
- To enable Auto Monitoring, you
- will need Prometheus installed somewhere (inside or outside your cluster) and
- configured to scrape your Kubernetes cluster. To get response metrics
- (in addition to system metrics), you need to
- [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-nginx-ingress-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
+ 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 needs to 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
+ [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-nginx-ingress-monitoring).
If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
Auto Deploy, and Auto Monitoring will be silently skipped.
+One all requirements are met, you can go ahead and [enable Auto DevOps](#enablingdisabling-auto-devops).
+
## Auto DevOps base domain
-The Auto DevOps base domain is required if you want to make use of [Auto
-Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined
-in any of the following places:
+The Auto DevOps base domain is required if you want to make use of
+[Auto Review Apps](#auto-review-apps), [Auto Deploy](#auto-deploy), and
+[Auto Monitoring](#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
@@ -156,9 +183,15 @@ in any of the following places:
The base domain variable `KUBE_INGRESS_BASE_DOMAIN` follows the same order of precedence
as other environment [variables](../../ci/variables/README.md#priority-of-environment-variables).
-NOTE: **Note**
-`AUTO_DEVOPS_DOMAIN` environment variable is deprecated and
-[is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-foss/issues/56959).
+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
+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
+[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:
@@ -179,77 +212,28 @@ 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).
-NOTE: **Note:**
-From GitLab 11.8, `KUBE_INGRESS_BASE_DOMAIN` replaces `AUTO_DEVOPS_DOMAIN`.
-Support for `AUTO_DEVOPS_DOMAIN` was [removed in GitLab
-12.0](https://gitlab.com/gitlab-org/gitlab-foss/issues/56959).
-
-## 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).
-
-In the [Auto DevOps template] (used behind the scenes by Auto DevOps), there
-are currently 3 defined environment names that you need to know:
-
-- `review/` (every environment starting with `review/`)
-- `staging`
-- `production`
-
-Those environments are tied to jobs that use [Auto Deploy](#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).
-
-The following table is an example of how the three different clusters would
-be configured.
-
-| 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](#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](#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.
-
- ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
-
-1. After the clusters are created, navigate to each one 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
- specified Auto DevOps domains.
-1. Navigate to each cluster's page, through **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
-cluster with the `review/*` environment scope. Similarly, you can check the
-other environments.
-
## Enabling/Disabling Auto DevOps
-When first using Auto Devops, review the [requirements](#requirements) to ensure all necessary components to make
+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).
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.
-### At the instance level (Administrators only)
+### At the project level
-Even when disabled at the instance level, group owners and project maintainers can still enable
-Auto DevOps at the group and project level, respectively.
+If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
-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 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](#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.
+
### At the group level
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/52447) in GitLab 11.10.
@@ -266,19 +250,16 @@ When enabling or disabling Auto DevOps at group-level, group configuration will
the subgroups and projects inside that group, unless Auto DevOps is specifically enabled or disabled on
the subgroup or project.
-### At the project level
+### At the instance level (Administrators only)
-If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+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 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](#auto-deploy)
- and choose the [deployment strategy](#deployment-strategy).
+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. 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.
-
### Enable for a percentage of projects
There is also a feature flag to enable Auto DevOps by default to your chosen percentage of projects.
@@ -310,6 +291,53 @@ The available options are:
- `master` branch is directly deployed to staging.
- Manual actions are provided for incremental rollout to production.
+## 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).
+
+In the [Auto DevOps template] (used behind the scenes by Auto DevOps), there
+are currently 3 defined environment names that you need to know:
+
+- `review/` (every environment starting with `review/`)
+- `staging`
+- `production`
+
+Those environments are tied to jobs that use [Auto Deploy](#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).
+
+The following table is an example of how the three different clusters would
+be configured.
+
+| 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](#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](#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.
+
+ ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
+
+1. After the clusters are created, navigate to each one 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
+ specified Auto DevOps domains.
+1. Navigate to each cluster's page, through **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
+cluster with the `review/*` environment scope. Similarly, you can check the
+other environments.
+
## Stages of Auto DevOps
The following sections describe the stages of Auto DevOps. Read them carefully
@@ -670,9 +698,28 @@ workers:
terminationGracePeriodSeconds: 60
```
-### Auto Monitoring
+#### 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:
+
+```shell
+/bin/herokuish procfile exec $COMMAND
+```
+
+This might be neccessary, for example, when:
+
+- Attaching using `kubectl exec`.
+- Using GitLab's [Web Terminal](../../ci/environments.md#web-terminals).
+
+For example, to start a Rails console from the application root directory, run:
+
+```sh
+/bin/herokuish procfile exec bin/rails c
+```
-See the [requirements](#requirements) for Auto Monitoring to enable this stage.
+### Auto Monitoring
Once your application is deployed, Auto Monitoring makes it possible to monitor
your application's server and response metrics right out of the box. Auto
@@ -687,18 +734,15 @@ The metrics include:
- **Response Metrics:** latency, throughput, error rate
- **System Metrics:** CPU utilization, memory utilization
-In order to make use of monitoring you need to:
+To make use of Auto Monitoring:
-1. [Deploy Prometheus](../../user/project/integrations/prometheus.md) into your Kubernetes cluster
-1. If you would like response metrics, ensure you are running at least version
- 0.9.0 of NGINX Ingress and
- [enable Prometheus metrics](https://github.com/kubernetes/ingress-nginx/blob/master/docs/examples/customization/custom-vts-metrics-prometheus/nginx-vts-metrics-conf.yaml).
-1. Finally, [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"`.
-
-To view the metrics, open the
-[Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments).
+1. [Install and configure the requirements](#requirements).
+1. [Enable Auto DevOps](#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)
+ to view the metrics of your deployed application. To view the metrics of the
+ whole Kubernetes cluster, navigate to **Operations > Metrics**.
![Auto Metrics](img/auto_monitoring.png)
@@ -725,6 +769,8 @@ or a `.buildpacks` file in your project:
CAUTION: **Caution:**
Using multiple buildpacks isn't yet supported by Auto DevOps.
+CAUTION: **Caution:** When using the `.buildpacks` file, Auto Test will not work. The buildpack [heroku-buildpack-multi](https://github.com/heroku/heroku-buildpack-multi/) (which is used under the hood to parse the `.buildpacks` file) doesn't provide the necessary commands `bin/test-compile` and `bin/test`. Make sure to provide the project variable `BUILDPACK_URL` instead.
+
### Custom `Dockerfile`
If your project has a `Dockerfile` in the root of the project repo, Auto DevOps
@@ -924,6 +970,7 @@ applications.
| `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | From GitLab 11.11, used to set the name of the Helm repository. Defaults to `gitlab`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | From GitLab 11.11, used to set a username to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD`. |
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | From GitLab 11.11, used to set a password to connect to the Helm repository. Defaults to no credentials. Also set `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME`. |
+| `AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE` | From GitLab 12.5, used in combination with [Modsecurity feature flag](../../user/clusters/applications.md#web-application-firewall-modsecurity) to toggle [Modsecurity's `SecRuleEngine`](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRuleEngine) behavior. Defaults to `DetectionOnly`. |
| `BUILDPACK_URL` | Buildpack's full URL. Can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`. For example `https://github.com/heroku/heroku-buildpack-ruby.git#v142`. |
| `CANARY_ENABLED` | From GitLab 11.0, used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments-premium). |
| `CANARY_PRODUCTION_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. Takes precedence over `CANARY_REPLICAS`. Defaults to 1. |
@@ -968,7 +1015,6 @@ 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`. |
-| `DS_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled. Defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](../../user/application_security/dependency_scanning/index.md#remote-checks). |
#### Disable jobs
@@ -1301,7 +1347,6 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
```
[ce-37115]: https://gitlab.com/gitlab-org/gitlab-foss/issues/37115
-[kubernetes-clusters]: ../../user/project/clusters/index.md
[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../../ci/review_apps/index.md
[container-registry]: ../../user/packages/container_registry/index.md
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index d9bdd73221f..ce3a3dd5ca6 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -25,7 +25,7 @@ Google account (for example, one that you use to access Gmail, Drive, etc.) or c
TIP: **Tip:**
Every new Google Cloud Platform (GCP) account receives [$300 in credit](https://console.cloud.google.com/freetrial),
and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
-Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?PCN=a0n60000006Vpz4AAC) and apply for credit.
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form) and apply for credit.
## Creating a new project from a template
@@ -212,7 +212,7 @@ under **Settings > CI/CD > Environment variables**.
### Working with branches
-Following the [GitLab flow](../../workflow/gitlab_flow.md#working-with-feature-branches),
+Following the [GitLab flow](../gitlab_flow.md#working-with-feature-branches),
let's create a feature branch that will add some content to the application.
Under your repository, navigate to the following file: `app/views/welcome/index.html.erb`.
@@ -280,6 +280,6 @@ and customized to fit your workflow. Here are some helpful resources for further
1. [Multiple Kubernetes clusters](index.md#using-multiple-kubernetes-clusters-premium) **(PREMIUM)**
1. [Incremental rollout to production](index.md#incremental-rollout-to-production-premium) **(PREMIUM)**
1. [Disable jobs you don't need with environment variables](index.md#environment-variables)
-1. [Use a static IP for your cluster](../../user/project/clusters/index.md#using-a-static-ip)
+1. [Use a static IP for your cluster](../../user/clusters/applications.md#using-a-static-ip)
1. [Use your own buildpacks to build your application](index.md#custom-buildpacks)
1. [Prometheus monitoring](../../user/project/integrations/prometheus.md)
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index d6e1f83b876..4325980a60c 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -32,7 +32,7 @@ The following resources will help you get started with Git:
- 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](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
+ - [Squashing commits](../gitlab_flow.md#squashing-commits-with-rebase)
### Concepts
@@ -85,7 +85,7 @@ The following relate to Git Large File Storage:
- [Getting Started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
- [Migrate an existing Git repo with Git LFS](migrate_to_git_lfs/index.md)
-- [GitLab Git LFS user documentation](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
-- [GitLab Git LFS admin documentation](../../workflow/lfs/lfs_administration.md)
-- [git-annex to Git-LFS migration guide](../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
+- [GitLab Git LFS user documentation](../../administration/lfs/manage_large_binaries_with_git_lfs.md)
+- [GitLab Git LFS admin documentation](../../administration/lfs/lfs_administration.md)
+- [git-annex to Git-LFS migration guide](../../administration/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/migrate_to_git_lfs/index.md b/doc/topics/git/migrate_to_git_lfs/index.md
index 0c30b45c552..eec1c3c10c1 100644
--- a/doc/topics/git/migrate_to_git_lfs/index.md
+++ b/doc/topics/git/migrate_to_git_lfs/index.md
@@ -163,9 +163,9 @@ but commented out to help encourage others to add to it in the future. -->
## References
- [Getting Started with Git LFS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
-- [Migrate from Git Annex to Git LFS](../../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
-- [GitLab's Git LFS user documentation](../../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
-- [GitLab's Git LFS administrator documentation](../../../workflow/lfs/lfs_administration.md)
+- [Migrate from Git Annex to Git LFS](../../../administration/lfs/migrate_from_git_annex_to_git_lfs.md)
+- [GitLab's Git LFS user documentation](../../../administration/lfs/manage_large_binaries_with_git_lfs.md)
+- [GitLab's Git LFS administrator documentation](../../../administration/lfs/lfs_administration.md)
- Alternative method to [migrate an existing repo to Git LFS](https://github.com/git-lfs/git-lfs/wiki/Tutorial#migrating-existing-repository-data-to-lfs)
<!--
diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md
index ce1b551ddb6..e6f84ee8251 100644
--- a/doc/topics/git/partial_clone.md
+++ b/doc/topics/git/partial_clone.md
@@ -39,16 +39,20 @@ Follow [Git for enormous repositories](https://gitlab.com/groups/gitlab-org/-/ep
## Enabling partial clone
-GitLab 12.1 uses Git 2.21.0 which has an arbitrary file access security
-vulnerability when `uploadpack.allowFilter` is enabled, and should not be
-enabled in production environments.
+> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1553) in GitLab 12.4.
-A feature flag is planned to enable `uploadpack.allowFilter` and
-`uploadpack.allowAnySHA1InWant` once the version of Git used by GitLab has been
-updated to Git 2.22.0.
+To enable partial clone, use the [feature flags API](../../api/features.md).
+For example:
-Follow [this issue](https://gitlab.com/gitlab-org/gitaly/issues/1553) for
-updated.
+```sh
+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)
+```
## Excluding objects by size
diff --git a/doc/topics/gitlab_flow.md b/doc/topics/gitlab_flow.md
new file mode 100644
index 00000000000..0fab4de8454
--- /dev/null
+++ b/doc/topics/gitlab_flow.md
@@ -0,0 +1,330 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/gitlab_flow.html'
+---
+
+# Introduction to GitLab Flow
+
+![GitLab Flow](img/gitlab_flow.png)
+
+Git allows a wide variety of branching strategies and workflows.
+Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems.
+Therefore, we propose GitLab flow as a clearly defined set of best practices.
+It combines [feature-driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
+
+Organizations coming to Git from other version control systems frequently find it hard to develop a productive workflow.
+This article describes GitLab flow, which integrates the Git workflow with an issue tracking system.
+It offers a simple, transparent, and effective way to work with Git.
+
+![Four stages (working copy, index, local repo, remote repo) and three steps between them](img/gitlab_flow_four_stages.png)
+
+When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues.
+Most version control systems have only one step: committing from the working copy to a shared server.
+In Git, you add files from the working copy to the staging area. After that, you commit them to your local repo.
+The third step is pushing to a shared remote repository.
+After getting used to these three steps, the next challenge is the branching model.
+
+![Multiple long-running branches and merging in all directions](img/gitlab_flow_messy_flow.png)
+
+Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy.
+The biggest problem is that many long-running branches emerge that all contain part of the changes.
+People have a hard time figuring out which branch has the latest code, or which branch to deploy to production.
+Frequently, the reaction to this problem is to adopt a standardized pattern such as [Git flow](https://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html).
+We think there is still room for improvement. In this document, we describe a set of practices we call GitLab flow.
+
+For a video introduction of how this works in GitLab, see [GitLab Flow](https://youtu.be/InKNIvky2KE).
+
+## Git flow and its problems
+
+![Git Flow timeline by Vincent Driessen, used with permission](img/gitlab_flow_gitdashflow.png)
+
+Git flow was one of the first proposals to use Git branches, and it has received a lot of attention.
+It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes.
+The development happens on the `develop` branch, moves to a release branch, and is finally merged into the `master` branch.
+
+Git flow is a well-defined standard, but its complexity introduces two problems.
+The first problem is that developers must use the `develop` branch and not `master`. `master` is reserved for code that is released to production.
+It is a convention to call your default branch `master` and to mostly branch from and merge to this.
+Since most tools automatically use the `master` branch as the default, it is annoying to have to switch to another branch.
+
+The second problem of Git flow is the complexity introduced by the hotfix and release branches.
+These branches can be a good idea for some organizations but are overkill for the vast majority of them.
+Nowadays, most organizations practice continuous delivery, which means that your default branch can be deployed.
+Continuous delivery removes the need for hotfix and release branches, including all the ceremony they introduce.
+An example of this ceremony is the merging back of release branches.
+Though specialized tools do exist to solve this, they require documentation and add complexity.
+Frequently, developers make mistakes such as merging changes only into `master` and not into the `develop` branch.
+The reason for these errors is that Git flow is too complicated for most use cases.
+For example, many projects do releases but don't need to do hotfixes.
+
+## GitHub flow as a simpler alternative
+
+![Master branch with feature branches merged in](img/gitlab_flow_github_flow.png)
+
+In reaction to Git flow, GitHub created a simpler alternative.
+[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch.
+This flow is clean and straightforward, and many organizations have adopted it with great success.
+Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/git/simple-git-workflow-is-simple), although they rebase feature branches.
+Merging everything into the `master` branch and frequently deploying means you minimize the amount of unreleased code, which is in line with lean and continuous delivery best practices.
+However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues.
+With GitLab flow, we offer additional guidance for these questions.
+
+## Production branch with GitLab flow
+
+![Master branch and production branch with an arrow that indicates a deployment](img/gitlab_flow_production_branch.png)
+
+GitHub flow assumes you can deploy to production every time you merge a feature branch.
+While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible.
+One case is where you don't control the timing of a release, for example, an iOS application that is released when it passes App Store validation.
+Another case is when you have deployment windows &mdash; for example, workdays from 10&nbsp;AM to 4&nbsp;PM when the operations team is at full capacity &mdash; but you also merge code at other times.
+In these cases, you can make a production branch that reflects the deployed code.
+You can deploy a new version by merging `master` into the production branch.
+If you need to know what code is in production, you can just checkout the production branch to see.
+The approximate time of deployment is easily visible as the merge commit in the version control system.
+This time is pretty accurate if you automatically deploy your production branch.
+If you need a more exact time, you can have your deployment script create a tag on each deployment.
+This flow prevents the overhead of releasing, tagging, and merging that happens with Git flow.
+
+## Environment branches with GitLab flow
+
+![Multiple branches with the code cascading from one to another](img/gitlab_flow_environment_branches.png)
+
+It might be a good idea to have an environment that is automatically updated to the `master` branch.
+Only, in this case, the name of this environment might differ from the branch name.
+Suppose you have a staging environment, a pre-production environment, and a production environment.
+In this case, deploy the `master` branch to staging.
+To deploy to pre-production, create a merge request from the `master` branch to the pre-production branch.
+Go live by merging the pre-production branch into the production branch.
+This workflow, where commits only flow downstream, ensures that everything is tested in all environments.
+If you need to cherry-pick a commit with a hotfix, it is common to develop it on a feature branch and merge it into `master` with a merge request.
+In this case, do not delete the feature branch yet.
+If `master` passes automatic testing, you then merge the feature branch into the other branches.
+If this is not possible because more manual testing is required, you can send merge requests from the feature branch to the downstream branches.
+
+## Release branches with GitLab flow
+
+![Master and multiple release branches that vary in length with cherry-picks from master](img/gitlab_flow_release_branches.png)
+
+You only need to work with release branches if you need to release software to the outside world.
+In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc.
+Create stable branches using `master` as a starting point, and branch as late as possible.
+By doing this, you minimize the length of time during which you have to apply bug fixes to multiple branches.
+After announcing a release branch, only add serious bug fixes to the branch.
+If possible, first merge these bug fixes into `master`, and then cherry-pick them into the release branch.
+If you start by merging into the release branch, you might forget to cherry-pick them into `master`, and then you'd encounter the same bug in subsequent releases.
+Merging into `master` and then cherry-picking into release is called an "upstream first" policy, which is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/en/blog/a-community-for-using-openstack-with-red-hat-rdo).
+Every time you include a bug fix in a release branch, increase the patch version (to comply with [Semantic Versioning](https://semver.org/)) by setting a new tag.
+Some projects also have a stable branch that points to the same commit as the latest released branch.
+In this flow, it is not common to have a production branch (or Git flow `master` branch).
+
+## Merge/pull requests with GitLab flow
+
+![Merge request with inline comments](img/gitlab_flow_mr_inline_comments.png)
+
+Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches.
+Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch.
+Tools such as GitLab and others choose the name "merge request" since the final action is to merge the feature branch.
+In this article, we'll refer to them as merge requests.
+
+If you work on a feature branch for more than a few hours, it is good to share the intermediate result with the rest of the team.
+To do this, create a merge request without assigning it to anyone.
+Instead, mention people in the description or a comment, for example, "/cc @mark @susan."
+This indicates that the merge request is not ready to be merged yet, but feedback is welcome.
+Your team members can comment on the merge request in general or on specific lines with line comments.
+The merge request serves as a code review tool, and no separate code review tools should be needed.
+If the review reveals shortcomings, anyone can commit and push a fix.
+Usually, the person to do this is the creator of the merge request.
+The diff in the merge request automatically updates when new commits are pushed to the branch.
+
+When you are ready for your feature branch to be merged, assign the merge request to the person who knows most about the codebase you are changing.
+Also, mention any other people from whom you would like feedback.
+After the assigned person feels comfortable with the result, they can merge the branch.
+If the assigned person does not feel comfortable, they can request more changes or close the merge request without merging.
+
+In GitLab, it is common to protect the long-lived branches, e.g., the `master` branch, so that [most developers can't modify them](../user/permissions.md).
+So, if you want to merge into a protected branch, assign your merge request to someone with maintainer permissions.
+
+After you merge a feature branch, you should remove it from the source control software.
+In GitLab, you can do this when merging.
+Removing finished branches ensures that the list of branches shows only work in progress.
+It also ensures that if someone reopens the issue, they can use the same branch name without causing problems.
+
+NOTE: **Note:**
+When you reopen an issue you need to create a new merge request.
+
+![Remove checkbox for branch in merge requests](img/gitlab_flow_remove_checkbox.png)
+
+## Issue tracking with GitLab flow
+
+![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](img/gitlab_flow_merge_request.png)
+
+GitLab flow is a way to make the relation between the code and the issue tracker more transparent.
+
+Any significant change to the code should start with an issue that describes the goal.
+Having a reason for every code change helps to inform the rest of the team and to keep the scope of a feature branch small.
+In GitLab, each change to the codebase starts with an issue in the issue tracking system.
+If there is no issue yet, create the issue, as long as the change will take a significant amount of work, i.e., more than 1 hour.
+In many organizations, raising an issue is part of the development process because they are used in sprint planning.
+The issue title should describe the desired state of the system.
+For example, the issue title "As an administrator, I want to remove users without receiving an error" is better than "Admin can't remove users."
+
+When you are ready to code, create a branch for the issue from the `master` branch.
+This branch is the place for any work related to this change.
+
+NOTE: **Note:**
+The name of a branch might be dictated by organizational standards.
+
+When you are done or want to discuss the code, open a merge request.
+A merge request is an online place to discuss the change and review the code.
+
+If you open the merge request but do not assign it to anyone, it is a "Work In Progress" merge request.
+These are used to discuss the proposed implementation but are not ready for inclusion in the `master` branch yet.
+Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready.
+
+When you think the code is ready, assign the merge request to a reviewer.
+The reviewer can merge the changes when they think the code is ready for inclusion in the `master` branch.
+When they press the merge button, GitLab merges the code and creates a merge commit that makes this event easily visible later on.
+Merge requests always create a merge commit, even when the branch could be merged without one.
+This merge strategy is called "no fast-forward" in Git.
+After the merge, delete the feature branch since it is no longer needed.
+In GitLab, this deletion is an option when merging.
+
+Suppose that a branch is merged but a problem occurs and the issue is reopened.
+In this case, it is no problem to reuse the same branch name since the first branch was deleted when it was merged.
+At any time, there is at most one branch for every issue.
+It is possible that one feature branch solves more than one issue.
+
+## Linking and closing issues from merge requests
+
+![Merge request showing the linked issues that will be closed](img/gitlab_flow_close_issue_mr.png)
+
+Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12."
+GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request.
+
+To automatically close linked issues, mention them with the words "fixes" or "closes," for example, "fixes #14" or "closes #67." GitLab closes these issues when the code is merged into the default branch.
+
+If you have an issue that spans across multiple repositories, create an issue for each repository and link all issues to a parent issue.
+
+## Squashing commits with rebase
+
+![Vim screen showing the rebase view](img/gitlab_flow_rebase.png)
+
+With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them.
+This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical.
+
+However, you should never rebase commits you have pushed to a remote server.
+Rebasing creates new commits for all your changes, which can cause confusion because the same change would have multiple identifiers.
+It also causes merge errors for anyone working on the same branch because their history would not match with yours.
+Also, if someone has already reviewed your code, rebasing makes it hard to tell what changed since the last review.
+
+You should also never rebase commits authored by other people.
+Not only does this rewrite history, but it also loses authorship information.
+Rebasing prevents the other authors from being attributed and sharing part of the [`git blame`](https://git-scm.com/docs/git-blame).
+
+If a merge involves many commits, it may seem more difficult to undo.
+You might think to solve this by squashing all the changes into one commit before merging, but as discussed earlier, it is a bad idea to rebase commits that you have already pushed.
+Fortunately, there is an easy way to undo a merge with all its commits.
+The way to do this is by reverting the merge commit.
+Preserving this ability to revert a merge is a good reason to always use the "no fast-forward" (`--no-ff`) strategy when you merge manually.
+
+NOTE: **Note:**
+If you revert a merge commit and then change your mind, revert the revert commit to redo the merge.
+Git does not allow you to merge the code again otherwise.
+
+## Reducing merge commits in feature branches
+
+![List of sequential merge commits](img/gitlab_flow_merge_commits.png)
+
+Having lots of merge commits can make your repository history messy.
+Therefore, you should try to avoid merge commits in feature branches.
+Often, people avoid merge commits by just using rebase to reorder their commits after the commits on the `master` branch.
+Using rebase prevents a merge commit when merging `master` into your feature branch, and it creates a neat linear history.
+However, as discussed in [the section about rebasing](#squashing-commits-with-rebase), you should never rebase commits you have pushed to a remote server.
+This restriction makes it impossible to rebase work in progress that you already shared with your team, which is something we recommend.
+
+Rebasing also creates more work, since every time you rebase, you have to resolve similar conflicts.
+Sometimes you can reuse recorded resolutions (`rerere`), but merging is better since you only have to resolve conflicts once.
+Atlassian has a more thorough explanation of the tradeoffs between merging and rebasing [on their blog](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase).
+
+A good way to prevent creating many merge commits is to not frequently merge `master` into the feature branch.
+There are three reasons to merge in `master`: utilizing new code, resolving merge conflicts, and updating long-running branches.
+
+If you need to utilize some code that was introduced in `master` after you created the feature branch, you can often solve this by just cherry-picking a commit.
+
+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.
+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.
+The solution here is to keep your feature branches short-lived.
+Most feature branches should take less than one day of work.
+If your feature branches often take more than a day of work, try to split your features into smaller units of work.
+
+If you need to keep a feature branch open for more than a day, there are a few strategies to keep it up-to-date.
+One option is to use continuous integration (CI) to merge in `master` at the start of the day.
+Another option is to only merge in from well-defined points in time, for example, a tagged release.
+You could also use [feature toggles](https://martinfowler.com/bliki/FeatureToggle.html) to hide incomplete features so you can still merge back into `master` every day.
+
+> **Note:** Don't confuse automatic branch testing with continuous integration.
+> Martin Fowler makes this distinction in [his article about feature branches](https://martinfowler.com/bliki/FeatureBranch.html):
+>
+> "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
+> That's continuous building, and a Good Thing, but there's no *integration*, so it's not CI."
+
+In conclusion, you should try to prevent merge commits, but not eliminate them.
+Your codebase should be clean, but your history should represent what actually happened.
+Developing software happens in small, messy steps, and it is OK to have your history reflect this.
+You can use tools to view the network graphs of commits and understand the messy history that created your code.
+If you rebase code, the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers.
+
+## Commit often and push frequently
+
+Another way to make your development work easier is to commit often.
+Every time you have a working set of tests and code, you should make a commit.
+Splitting up work into individual commits provides context for developers looking at your code later.
+Smaller commits make it clear how a feature was developed, and they make it easy to roll back to a specific good point in time or to revert one code change without reverting several unrelated changes.
+
+Committing often also makes it easy to share your work, which is important so that everyone is aware of what you are working on.
+You should push your feature branch frequently, even when it is not yet ready for review.
+By sharing your work in a feature branch or [a merge request](#mergepull-requests-with-gitlab-flow), you prevent your team members from duplicating work.
+Sharing your work before it's complete also allows for discussion and feedback about the changes, which can help improve the code before it gets to review.
+
+## How to write a good commit message
+
+![Good and bad commit message](img/gitlab_flow_good_commit.png)
+
+A commit message should reflect your intention, not just the contents of the commit.
+It is easy to see the changes in a commit, so the commit message should explain why you made those changes.
+An example of a good commit message is: "Combine templates to reduce duplicate code in the user views."
+The words "change," "improve," "fix," and "refactor" don't add much information to a commit message.
+For example, "Improve XML generation" could be better written as "Properly escape special characters in XML generation."
+For more information about formatting commit messages, please see this excellent [blog post by Tim Pope](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
+
+## Testing before merging
+
+![Merge requests showing the test states: red, yellow, and green](img/gitlab_flow_ci_mr.png)
+
+In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only.
+Developers had to ensure their code did not break the `master` branch.
+When using GitLab flow, developers create their branches from this `master` branch, so it is essential that it never breaks.
+Therefore, each merge request must be tested before it is accepted.
+CI software like Travis CI and GitLab CI show the build results right in the merge request itself to make this easy.
+
+There is one drawback to testing merge requests: the CI server only tests the feature branch itself, not the merged result.
+Ideally, the server could also test the `master` branch after each change.
+However, retesting on every commit to `master` is computationally expensive and means you are more frequently waiting for test results.
+Since feature branches should be short-lived, testing just the branch is an acceptable risk.
+If new commits in `master` cause merge conflicts with the feature branch, merge `master` back into the branch to make the CI server re-run the tests.
+As said before, if you often have feature branches that last for more than a few days, you should make your issues smaller.
+
+## Working with feature branches
+
+![Shell output showing git pull output](img/gitlab_flow_git_pull.png)
+
+When creating a feature branch, always branch from an up-to-date `master`.
+If you know before you start that your work depends on another branch, you can also branch from there.
+If you need to merge in another branch after starting, explain the reason in the merge commit.
+If you have not pushed your commits to a shared location yet, you can also incorporate changes by rebasing on `master` or another feature branch.
+Do not merge from upstream again if your code can work and merge cleanly without doing so.
+Merging only when needed prevents creating merge commits in your feature branch that later end up littering the `master` history.
diff --git a/doc/workflow/img/gitlab_flow.png b/doc/topics/img/gitlab_flow.png
index a6f3c947843..a6f3c947843 100644
--- a/doc/workflow/img/gitlab_flow.png
+++ b/doc/topics/img/gitlab_flow.png
Binary files differ
diff --git a/doc/workflow/img/ci_mr.png b/doc/topics/img/gitlab_flow_ci_mr.png
index 85a609cb814..85a609cb814 100644
--- a/doc/workflow/img/ci_mr.png
+++ b/doc/topics/img/gitlab_flow_ci_mr.png
Binary files differ
diff --git a/doc/workflow/img/close_issue_mr.png b/doc/topics/img/gitlab_flow_close_issue_mr.png
index 70de2fb6cee..70de2fb6cee 100644
--- a/doc/workflow/img/close_issue_mr.png
+++ b/doc/topics/img/gitlab_flow_close_issue_mr.png
Binary files differ
diff --git a/doc/workflow/img/environment_branches.png b/doc/topics/img/gitlab_flow_environment_branches.png
index 0aff33c6bb8..0aff33c6bb8 100644
--- a/doc/workflow/img/environment_branches.png
+++ b/doc/topics/img/gitlab_flow_environment_branches.png
Binary files differ
diff --git a/doc/workflow/img/four_stages.png b/doc/topics/img/gitlab_flow_four_stages.png
index 3ef6a33d2d4..3ef6a33d2d4 100644
--- a/doc/workflow/img/four_stages.png
+++ b/doc/topics/img/gitlab_flow_four_stages.png
Binary files differ
diff --git a/doc/workflow/img/git_pull.png b/doc/topics/img/gitlab_flow_git_pull.png
index 0e56e59471c..0e56e59471c 100644
--- a/doc/workflow/img/git_pull.png
+++ b/doc/topics/img/gitlab_flow_git_pull.png
Binary files differ
diff --git a/doc/workflow/img/gitdashflow.png b/doc/topics/img/gitlab_flow_gitdashflow.png
index 65900853d84..65900853d84 100644
--- a/doc/workflow/img/gitdashflow.png
+++ b/doc/topics/img/gitlab_flow_gitdashflow.png
Binary files differ
diff --git a/doc/workflow/img/github_flow.png b/doc/topics/img/gitlab_flow_github_flow.png
index 21a22becdb6..21a22becdb6 100644
--- a/doc/workflow/img/github_flow.png
+++ b/doc/topics/img/gitlab_flow_github_flow.png
Binary files differ
diff --git a/doc/workflow/img/good_commit.png b/doc/topics/img/gitlab_flow_good_commit.png
index ceb0d4b1691..ceb0d4b1691 100644
--- a/doc/workflow/img/good_commit.png
+++ b/doc/topics/img/gitlab_flow_good_commit.png
Binary files differ
diff --git a/doc/workflow/img/merge_commits.png b/doc/topics/img/gitlab_flow_merge_commits.png
index 4a80811c6e3..4a80811c6e3 100644
--- a/doc/workflow/img/merge_commits.png
+++ b/doc/topics/img/gitlab_flow_merge_commits.png
Binary files differ
diff --git a/doc/workflow/img/merge_request.png b/doc/topics/img/gitlab_flow_merge_request.png
index 010e95983fc..010e95983fc 100644
--- a/doc/workflow/img/merge_request.png
+++ b/doc/topics/img/gitlab_flow_merge_request.png
Binary files differ
diff --git a/doc/workflow/img/messy_flow.png b/doc/topics/img/gitlab_flow_messy_flow.png
index 4fa22d2bb5d..4fa22d2bb5d 100644
--- a/doc/workflow/img/messy_flow.png
+++ b/doc/topics/img/gitlab_flow_messy_flow.png
Binary files differ
diff --git a/doc/workflow/img/mr_inline_comments.png b/doc/topics/img/gitlab_flow_mr_inline_comments.png
index a18801f56e4..a18801f56e4 100644
--- a/doc/workflow/img/mr_inline_comments.png
+++ b/doc/topics/img/gitlab_flow_mr_inline_comments.png
Binary files differ
diff --git a/doc/workflow/img/production_branch.png b/doc/topics/img/gitlab_flow_production_branch.png
index c132d51bfb6..c132d51bfb6 100644
--- a/doc/workflow/img/production_branch.png
+++ b/doc/topics/img/gitlab_flow_production_branch.png
Binary files differ
diff --git a/doc/workflow/img/rebase.png b/doc/topics/img/gitlab_flow_rebase.png
index fe865177ba8..fe865177ba8 100644
--- a/doc/workflow/img/rebase.png
+++ b/doc/topics/img/gitlab_flow_rebase.png
Binary files differ
diff --git a/doc/workflow/img/release_branches.png b/doc/topics/img/gitlab_flow_release_branches.png
index 0a7f61d0248..0a7f61d0248 100644
--- a/doc/workflow/img/release_branches.png
+++ b/doc/topics/img/gitlab_flow_release_branches.png
Binary files differ
diff --git a/doc/workflow/img/remove_checkbox.png b/doc/topics/img/gitlab_flow_remove_checkbox.png
index fb0e792b37b..fb0e792b37b 100644
--- a/doc/workflow/img/remove_checkbox.png
+++ b/doc/topics/img/gitlab_flow_remove_checkbox.png
Binary files differ
diff --git a/doc/topics/index.md b/doc/topics/index.md
index b51f24b02e4..71048ec5aa4 100644
--- a/doc/topics/index.md
+++ b/doc/topics/index.md
@@ -11,6 +11,7 @@ tutorials, technical overviews, blog posts) and videos.
- [Authentication](authentication/index.md)
- [Continuous Integration (GitLab CI)](../ci/README.md)
- [Git](git/index.md)
+- [GitLab Flow](gitlab_flow.md)
- [GitLab Installation](../install/README.md)
- [GitLab Pages](../user/project/pages/index.md)
diff --git a/doc/university/README.md b/doc/university/README.md
index 8f5a5038bb9..9725cb14fc5 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -129,7 +129,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres
1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
1. [GitLab Flow Overview](https://about.gitlab.com/blog/2014/09/29/gitlab-flow/)
1. [Always Start with an Issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/)
-1. [GitLab Flow Documentation](../workflow/gitlab_flow.md)
+1. [GitLab Flow Documentation](../topics/gitlab_flow.md)
### 2.5. GitLab Comparisons
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 1c77fbeb8d6..ebdd453ff3c 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -170,7 +170,7 @@ Some tickets need specific knowledge or a deep understanding of a particular com
Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
-- Set up and try [Git LFS](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
+- Set up and try [Git LFS](../../administration/lfs/manage_large_binaries_with_git_lfs.md)
- Get to know the [GitLab API](../../api/README.md), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](../../user/project/import/svn.md)
- Set up [GitLab CI](../../ci/quick_start/README.md)
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
index 66e645a0af8..b80eb031aee 100644
--- a/doc/university/training/gitlab_flow.md
+++ b/doc/university/training/gitlab_flow.md
@@ -38,7 +38,7 @@ type: reference
## More details
-For more information, read through the [GitLab Flow](../../workflow/gitlab_flow.md)
+For more information, read through the [GitLab Flow](../../topics/gitlab_flow.md)
documentation.
<!-- ## Troubleshooting
diff --git a/doc/update/README.md b/doc/update/README.md
index 965f29bc8aa..6834deb1a85 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -69,7 +69,13 @@ before continuing the upgrading procedure. While this won't require downtime
between upgrading major/minor releases, allowing the background migrations to
finish. The time necessary to complete these migrations can be reduced by
increasing the number of Sidekiq workers that can process jobs in the
-`background_migration` queue.
+`background_migration` queue. To check the size of this queue,
+[start a Rails console session](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session)
+and run the command below:
+
+```ruby
+Sidekiq::Queue.new('background_migration').size
+```
As a rule of thumb, any database smaller than 10 GB won't take too much time to
upgrade; perhaps an hour at most per minor release. Larger databases however may
diff --git a/doc/user/admin_area/activating_deactivating_users.md b/doc/user/admin_area/activating_deactivating_users.md
new file mode 100644
index 00000000000..78a07f4a04e
--- /dev/null
+++ b/doc/user/admin_area/activating_deactivating_users.md
@@ -0,0 +1,66 @@
+---
+type: howto
+---
+
+# Activating and deactivating users
+
+GitLab administrators can deactivate and activate users.
+
+## Deactivating a user
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
+
+In order to temporarily prevent access by a GitLab user that has no recent activity, administrators
+can choose to deactivate the user.
+
+Deactivating a user is functionally identical to [blocking a user](blocking_unblocking_users.md),
+with the following differences:
+
+- It does not prohibit the user from logging back in via the UI.
+- Once a deactivated user logs back into the GitLab UI, their account is set to active.
+
+A deactivated user:
+
+- Cannot access Git repositories or the API.
+- Will not receive any notifications from GitLab.
+- Will not be able to use [slash commands](../../integration/slash_commands.md).
+
+Personal projects, and group and user history of the deactivated user will be left intact.
+
+A user can be deactivated from the Admin Area. To do this:
+
+1. Navigate to **Admin Area > Overview > Users**.
+1. Select a user.
+1. Under the **Account** tab, click **Deactivate user**.
+
+Please note that for the deactivation option to be visible to an admin, the user:
+
+- Must be currently active.
+- Should not have any activity in the last 180 days.
+
+Users can also be deactivated using the [GitLab API](../../api/users.html#deactivate-user).
+
+NOTE: **Note:**
+A deactivated user does not consume a [seat](../../subscriptions/index.md#managing-subscriptions).
+
+## Activating a user
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
+
+A deactivated user can be activated from the Admin Area.
+
+To do this:
+
+1. Navigate to **Admin Area > Overview > Users**.
+1. Click on the **Deactivated** tab.
+1. Select a user.
+1. Under the **Account** tab, click **Activate user**.
+
+Users can also be activated using the [GitLab API](../../api/users.html#activate-user).
+
+NOTE: **Note:**
+Activating a user will change the user's state to active and it consumes a
+[seat](../../subscriptions/index.md#managing-subscriptions).
+
+TIP: **Tip:**
+A deactivated user can also activate their account by themselves by simply logging back via the UI.
diff --git a/doc/user/admin_area/blocking_unblocking_users.md b/doc/user/admin_area/blocking_unblocking_users.md
new file mode 100644
index 00000000000..8868170169e
--- /dev/null
+++ b/doc/user/admin_area/blocking_unblocking_users.md
@@ -0,0 +1,48 @@
+---
+type: howto
+---
+
+# Blocking and unblocking users
+
+GitLab administrators block and unblock users.
+
+## Blocking a user
+
+In order to completely prevent access of a user to the GitLab instance, administrators can choose to
+block the user.
+
+Users can be blocked [via an abuse report](abuse_reports.md#blocking-users),
+or directly from the Admin Area. To do this:
+
+1. Navigate to **Admin Area > Overview > Users**.
+1. Select a user.
+1. Under the **Account** tab, click **Block user**.
+
+A blocked user:
+
+- Will not be able to login.
+- Cannot access Git repositories or the API.
+- Will not receive any notifications from GitLab.
+- Will not be able to use [slash commands](../../integration/slash_commands.md).
+
+Personal projects, and group and user history of the blocked user will be left intact.
+
+Users can also be blocked using the [GitLab API](../../api/users.html#block-user).
+
+NOTE: **Note:**
+A blocked user does not consume a [seat](../../subscriptions/index.md#managing-subscriptions).
+
+## Unblocking a user
+
+A blocked user can be unblocked from the Admin Area. To do this:
+
+1. Navigate to **Admin Area > Overview > Users**.
+1. Click on the **Blocked** tab.
+1. Select a user.
+1. Under the **Account** tab, click **Unblock user**.
+
+Users can also be unblocked using the [GitLab API](../../api/users.html#unblock-user).
+
+NOTE: **Note:**
+Unblocking a user will change the user's state to active and it consumes a
+[seat](../../subscriptions/index.md#managing-subscriptions).
diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md
index 5117b5f476f..4e24c25de8f 100644
--- a/doc/user/admin_area/diff_limits.md
+++ b/doc/user/admin_area/diff_limits.md
@@ -6,7 +6,7 @@ type: reference
You can set a maximum size for display of diff files (patches).
-For details about diff files, [View changes between files](../project/merge_requests/index.md#view-changes-between-file-versions).
+For details about diff files, [View changes between files](../project/merge_requests/reviewing_and_managing_merge_requests.md#view-changes-between-file-versions).
## Maximum diff patch size
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index c75a8bcac79..35cb2b42c56 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -112,8 +112,8 @@ To list users matching a specific criteria, click on one of the following tabs o
- **2FA Enabled**
- **2FA Disabled**
- **External**
-- **Blocked**
-- **Deactivated**
+- **[Blocked](blocking_unblocking_users.md)**
+- **[Deactivated](activating_deactivating_users.md)**
- **Without projects**
For each user, their username, email address, are listed, also the date their account was
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
index 6439607de33..103d7ecc573 100644
--- a/doc/user/admin_area/monitoring/health_check.md
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -13,7 +13,7 @@ type: concepts, howto
GitLab provides liveness and readiness probes to indicate service health and
reachability to required services. These probes report on the status of the
database connection, Redis connection, and access to the filesystem. These
-endpoints [can be provided to schedulers like Kubernetes][kubernetes] to hold
+endpoints [can be provided to schedulers like Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) to hold
traffic until the system is ready or restart the container as needed.
## IP whitelist
@@ -39,7 +39,11 @@ GET http://localhost/-/liveness
## Health
-Checks whether the application server is running. It does not verify the database or other services are running.
+Checks whether the application server is running.
+It does not verify the database or other services
+are running. This endpoint circumvents Rails Controllers
+and is implemented as additional middleware `BasicHealthCheck`
+very early into the request processing lifecycle.
```text
GET /-/health
@@ -59,10 +63,17 @@ GitLab OK
## Readiness
-The readiness probe checks whether the GitLab instance is ready to use. It checks the dependent services (Database, Redis, Gitaly etc.) and gives a status for each.
+The readiness probe checks whether the GitLab instance is ready
+to accept traffic via Rails Controllers. The check by default
+does validate only instance-checks.
+
+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
GET /-/readiness
+GET /-/readiness?all=1
```
Example request:
@@ -75,37 +86,30 @@ Example response:
```json
{
- "db_check":{
+ "master_check":[{
"status":"failed",
- "message": "unexpected Db check result: 0"
- },
- "redis_check":{
- "status":"ok"
- },
- "cache_check":{
- "status":"ok"
- },
- "queues_check":{
- "status":"ok"
- },
- "shared_state_check":{
- "status":"ok"
- },
- "gitaly_check":{
- "status":"ok",
- "labels":{
- "shard":"default"
- }
- }
- }
+ "message": "unexpected Master check result: false"
+ }],
+ ...
+}
```
+On failure, the endpoint will return a `503` HTTP status code.
+
+This check does hit the database and Redis if authenticated via `token`.
+
+This check is being exempt from Rack Attack.
+
## Liveness
DANGER: **Warning:**
-In Gitlab [12.4](https://about.gitlab.com/upcoming-releases/) the response body of the Liveness check will change to match the example below.
+In Gitlab [12.4](https://about.gitlab.com/upcoming-releases/)
+the response body of the Liveness check was changed
+to match the example below.
-The liveness probe checks whether the application server is alive. Unlike the [`health`](#health) check, this check hits the database.
+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
GET /-/liveness
@@ -127,7 +131,9 @@ On success, the endpoint will return a `200` HTTP status code, and a response li
}
```
-On failure, the endpoint will return a `500` HTTP status code.
+On failure, the endpoint will return a `503` HTTP status code.
+
+This check is being exempt from Rack Attack.
## Access token (Deprecated)
@@ -163,4 +169,3 @@ but commented out to help encourage others to add to it in the future. -->
[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
-[kubernetes]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
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 a1beee404eb..e443127a8a0 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -32,7 +32,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](../../../workflow/lfs/manage_large_binaries_with_git_lfs.md#git-lfs)
+1. Although you have enabled [Git LFS](../../../administration/lfs/manage_large_binaries_with_git_lfs.md#git-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/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index c60b3323105..f775dd8bbb4 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -134,6 +134,19 @@ Once that time passes, the jobs will be archived and no longer able to be
retried. Make it empty to never expire jobs. It has to be no less than 1 day,
for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.
+## Default CI configuration path
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/18073) in GitLab 12.5.
+
+The default CI configuration file path for new projects can be set in the Admin
+area of your GitLab instance (`.gitlab-ci.yml` if not set):
+
+1. Go to **Admin area > Settings > Continuous Integration and Deployment**.
+1. Input the new path in the **Default CI configuration path** field.
+1. Hit **Save changes** for the changes to take effect.
+
+It is also possible to specify a [custom CI configuration path for a specific project](../../project/pipelines/settings.md#custom-ci-configuration-path).
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 6026f9dc735..4611d5f5c77 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -8,7 +8,7 @@ You can customize some of the content in emails sent from your GitLab instance.
## Custom logo
-The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
+The logo in the header of some emails can be customized, see the [logo customization section](../appearance.md#navigation-bar).
## Custom additional text **(PREMIUM ONLY)**
diff --git a/doc/user/admin_area/settings/img/two_factor_grace_period.png b/doc/user/admin_area/settings/img/two_factor_grace_period.png
new file mode 100644
index 00000000000..e7fb52969aa
--- /dev/null
+++ b/doc/user/admin_area/settings/img/two_factor_grace_period.png
Binary files differ
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index 4ca91ae5339..42f496bfbfa 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -14,6 +14,7 @@ include:
- [Continuous Integration and Deployment](continuous_integration.md)
- [Email](email.md)
- [Sign up restrictions](sign_up_restrictions.md)
+- [Sign in restrictions](sign_in_restrictions.md)
- [Terms](terms.md)
- [Third party offers](third_party_offers.md)
- [Usage statistics](usage_statistics.md)
diff --git a/doc/user/admin_area/settings/sign_in_restrictions.md b/doc/user/admin_area/settings/sign_in_restrictions.md
new file mode 100644
index 00000000000..0975766400f
--- /dev/null
+++ b/doc/user/admin_area/settings/sign_in_restrictions.md
@@ -0,0 +1,56 @@
+---
+type: reference
+---
+
+# Sign-in restrictions **(CORE ONLY)**
+
+You can use sign-in restrictions to limit the authentication with password
+for web interface and Git over HTTP(S), two-factor authentication enforcing, as well as
+as configuring the home page URL and after sign-out path.
+
+## Password authentication enabled
+
+You can restrict the password authentication for web interface and Git over HTTP(S):
+
+- **Web interface**: When this feature is disabled, an [external authentication provider](../../../administration/auth/README.md) must be used.
+- **Git over HTTP(S)**: When this feature is disabled, a [Personal Access Token](../../profile/personal_access_tokens.md) must be used to authenticate.
+
+## Two-factor authentication
+
+When this feature enabled, all users will have to use the [two-factor authentication](../../profile/account/two_factor_authentication.md).
+
+Once the two-factor authentication is configured as mandatory, the users will be allowed
+to skip forced configuration of two-factor authentication for the configurable grace
+period in hours.
+
+![Two-factor grace period](img/two_factor_grace_period.png)
+
+## Sign-in information
+
+All users that are not logged-in will be redirected to the page represented by the configured
+"Home page URL" if value is not empty.
+
+All users will be redirect to the page represented by the configured "After sign out path"
+after sign out if value is not empty.
+
+If a "Sign in text" in Markdown format is provided, then every user will be presented with
+this message after logging-in.
+
+## Settings
+
+To access this feature:
+
+1. Navigate to the **Settings > General** in the Admin area.
+1. Expand the **Sign-in restrictions** section.
+
+<!-- ## 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/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 98126f72a78..81edd9eac34 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -31,7 +31,7 @@ patches will need to be backported, making sure active GitLab instances remain
secure.
If you disable version check, this information will not be collected. Enable or
-disable the version check at **Admin area > Settings > Usage statistics**.
+disable the version check at **Admin area > Settings > Metrics and profiling > Usage statistics**.
## Usage ping **(CORE ONLY)**
@@ -85,7 +85,7 @@ will be able to show [usage statistics](../../instance_statistics/index.md)
of your instance to your users.
This can be restricted to admins by selecting "Only admins" in the Instance
-Statistics visibility section under **Admin area > Settings > Usage statistics**.
+Statistics visibility section under **Admin area > Settings > Metrics and profiling > Usage statistics**.
<!-- ## Troubleshooting
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 f718e31e8bd..73406fd5037 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -177,7 +177,7 @@ For more details, see [SSH key restrictions](../../../security/ssh_keys_restrict
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3586) in GitLab 10.3.
-This option is enabled by default. By disabling it, both [pull and push mirroring](../../../workflow/repository_mirroring.md) will no longer
+This option is enabled by default. By disabling it, both [pull and push mirroring](../../project/repository/repository_mirroring.md) will no longer
work in every repository and can only be re-enabled by an admin on a per-project basis.
![Mirror settings](img/mirror_settings.png)
diff --git a/doc/user/analytics/cycle_analytics.md b/doc/user/analytics/cycle_analytics.md
index e17202645d3..c75f101b0e1 100644
--- a/doc/user/analytics/cycle_analytics.md
+++ b/doc/user/analytics/cycle_analytics.md
@@ -3,9 +3,10 @@
> - Introduced prior to GitLab 12.3 at the project level.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12077) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3 at the group level.
-Cycle Analytics measures the time spent to go from an [idea to production] - also known
-as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to
-reach production, along with the time typically spent in each DevOps stage along the way.
+Cycle Analytics measures the time spent to go from an
+[idea to production](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab)
+(also known as cycle time) for each of your projects. Cycle Analytics displays the median time
+spent in each stage defined in the process.
NOTE: **Note:**
Use the `cycle_analytics` feature flag to enable at the group level.
@@ -14,8 +15,8 @@ Cycle 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.
-Cycle Analytics is tightly coupled with the [GitLab flow] and calculates a separate median for each
-stage.
+Cycle Analytics is tightly coupled with the [GitLab flow](../../topics/gitlab_flow.md) and
+calculates a separate median for each stage.
## Overview
@@ -46,6 +47,16 @@ There are seven stages that are tracked as part of the Cycle Analytics calculati
- **Production** (Total)
- Total lifecycle time; i.e. the velocity of the project or team
+## Date ranges
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13216) in GitLab 12.4.
+
+GitLab provides the ability to filter analytics based on a date range. To filter results:
+
+1. Select a group.
+1. Optionally select a project.
+1. Select a date range using the available date pickers.
+
## How the data is measured
Cycle Analytics records cycle time and data based on the project issues with the
@@ -53,7 +64,8 @@ exception of the staging and production stages, where only data deployed to
production are measured.
Specifically, if your CI is not set up and you have not defined a `production`
-or `production/*` [environment], then you will not have any data for those stages.
+or `production/*` [environment](../../ci/yaml/README.md#environment), then you will not have any
+data for those stages.
Each stage of Cycle Analytics is further described in the table below.
@@ -64,11 +76,9 @@ Each stage of Cycle Analytics is further described in the table below.
| 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 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. |
| Review | Measures the median time taken to review the merge request that has closing issue pattern, between its creation and until it's merged. |
-| Staging | Measures the median time between merging the merge request with closing issue pattern until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. |
+| Staging | Measures the median time between merging the merge request with closing issue pattern until the very first deployment to production. It's tracked by the environment set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. |
| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
----
-
How this works, behind the scenes:
1. Issues and merge requests are grouped together in pairs, such that for each
@@ -81,12 +91,12 @@ How this works, behind the scenes:
we need for the stages, like issue creation date, merge request merge time,
etc.
-To sum up, anything that doesn't follow [GitLab flow] will not be tracked and the
+To sum up, anything that doesn't follow [GitLab flow](../../workflow/gitlab_flow.md) will not be tracked and the
Cycle Analytics dashboard will not present any data for:
-- merge requests that do not close an issue.
-- issues not labeled with a label present in the Issue Board or for issues not assigned a milestone.
-- staging and production stages, if the project has no `production` or `production/*`
+- Merge requests that do not close an issue.
+- Issues not labeled with a label present in the Issue Board or for issues not assigned a milestone.
+- Staging and production stages, if the project has no `production` or `production/*`
environment.
## Example workflow
@@ -107,7 +117,7 @@ environments is configured.
1. Push branch and create a merge request that contains the [issue closing pattern](../project/issues/managing_issues.md#closing-issues-automatically)
in its description at 14:00 (stop of **Code** stage / start of **Test** and
**Review** stages).
-1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and
+1. The CI starts running your scripts defined in [`.gitlab-ci.yml`](../../ci/yaml/README.md) and
takes 5min (stop of **Test** stage).
1. Review merge request, ensure that everything is OK and merge the merge
request at 19:00. (stop of **Review** stage / start of **Staging** stage).
@@ -151,7 +161,7 @@ The current permissions on the Project Cycle Analytics dashboard are:
- Internal projects - any authenticated user can access.
- Private projects - any member Guest and above can access.
-You can [read more about permissions][permissions] in general.
+You can [read more about permissions](../../ci/yaml/README.md) in general.
NOTE: **Note:**
As of GitLab 12.3, the project-level page is deprecated. You should access
@@ -169,14 +179,6 @@ For Cycle Analytics functionality introduced in GitLab 12.3 and later:
Learn more about Cycle Analytics in the following resources:
-- [Cycle Analytics feature page](https://about.gitlab.com/product/cycle-analytics/)
-- [Cycle Analytics feature preview](https://about.gitlab.com/blog/2016/09/16/feature-preview-introducing-cycle-analytics/)
-- [Cycle Analytics feature highlight](https://about.gitlab.com/blog/2016/09/21/cycle-analytics-feature-highlight/)
-
-[ce-5986]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/5986
-[ce-20975]: https://gitlab.com/gitlab-org/gitlab-foss/issues/20975
-[environment]: ../../ci/yaml/README.md#environment
-[GitLab flow]: ../../workflow/gitlab_flow.md
-[idea to production]: https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
-[permissions]: ../permissions.md
-[yml]: ../../ci/yaml/README.md
+- [Cycle Analytics feature page](https://about.gitlab.com/product/cycle-analytics/).
+- [Cycle Analytics feature preview](https://about.gitlab.com/blog/2016/09/16/feature-preview-introducing-cycle-analytics/).
+- [Cycle Analytics feature highlight](https://about.gitlab.com/blog/2016/09/21/cycle-analytics-feature-highlight/).
diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md
index aecbac15c98..40295e47e89 100644
--- a/doc/user/analytics/productivity_analytics.md
+++ b/doc/user/analytics/productivity_analytics.md
@@ -42,10 +42,19 @@ The following metrics and visualizations are available on a project or group lev
- Number of lines of code per commit.
- Number of files touched.
- Scatterplot showing all MRs merged on a certain date, together with the days it took to complete the action and a 30 day rolling median.
- - Users can zoom in and out on specific days of interest.
- Table showing the list of merge requests with their respective time duration metrics.
- Users can sort by any of the above metrics.
+## Date ranges
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13188) in GitLab 12.4.
+
+GitLab has the ability to filter analytics based on a date range. To filter results:
+
+1. Select a group.
+1. Optionally select a project.
+1. Select a date range using the available date pickers.
+
## Permissions
The **Productivity Analytics** dashboard can be accessed only:
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 14dae56f087..931755c6305 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -40,10 +40,9 @@ to perform audits for your Docker-based apps.
To enable Container Scanning in your pipeline, you need:
- A 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.
+ [`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
+ [`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html)
+ executor.
- Docker `18.09.03` or higher installed on the machine where the Runners are
running. If you're using the shared Runners on GitLab.com, this is already
the case.
@@ -150,17 +149,18 @@ container_scanning:
Container Scanning can be [configured](#overriding-the-container-scanning-template)
using environment variables.
-| Environment Variable | Description | Default |
-| ------ | ------ | ------ |
-| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` |
-| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` |
-| `DOCKER_PASSWORD` | Password for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_PASSWORD` |
-| `CLAIR_OUTPUT` | Severity level threshold. Vulnerabilities with severity level higher than or equal to this threshold will be outputted. Supported levels are `Unknown`, `Negligible`, `Low`, `Medium`, `High`, `Critical` and `Defcon1`. | `Unknown` |
-| `REGISTRY_INSECURE` | Allow [Klar](https://github.com/optiopay/klar) to access insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | `"false"` |
-| `CLAIR_VULNERABILITIES_DB_URL` | This variable is explicitly set in the [services section](https://gitlab.com/gitlab-org/gitlab/blob/30522ca8b901223ac8c32b633d8d67f340b159c1/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L17-19) of the `Container-Scanning.gitlab-ci.yml` file and defaults to `clair-vulnerabilities-db`. This value represents the address that the [postgres 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 scanning tool](https://gitlab.com/gitlab-org/security-products/analyzers/klar/#running-the-scanning-tool) section of the [klar readme](https://gitlab.com/gitlab-org/security-products/analyzers/klar). | `clair-vulnerabilities-db` |
-| `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` |
-| `CLAIR_DB_IMAGE_TAG` | The Docker image tag for the [postgres 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` |
+| Environment Variable | Description | Default |
+| ------ | ------ | ------ |
+| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` |
+| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` |
+| `DOCKER_PASSWORD` | Password for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_PASSWORD` |
+| `CLAIR_OUTPUT` | Severity level threshold. Vulnerabilities with severity level higher than or equal to this threshold will be outputted. Supported levels are `Unknown`, `Negligible`, `Low`, `Medium`, `High`, `Critical` and `Defcon1`. | `Unknown` |
+| `REGISTRY_INSECURE` | Allow [Klar](https://github.com/optiopay/klar) to access insecure registries (HTTP only). Should only be set to `true` when testing the image locally. | `"false"` |
+| `CLAIR_VULNERABILITIES_DB_URL` | This variable is explicitly set in the [services section](https://gitlab.com/gitlab-org/gitlab/blob/30522ca8b901223ac8c32b633d8d67f340b159c1/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L17-19) of the `Container-Scanning.gitlab-ci.yml` file and defaults to `clair-vulnerabilities-db`. This value represents the address that the [postgres 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 scanning tool](https://gitlab.com/gitlab-org/security-products/analyzers/klar/#running-the-scanning-tool) section of the [GitLab klar analyzer readme](https://gitlab.com/gitlab-org/security-products/analyzers/klar). | `clair-vulnerabilities-db` |
+| `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` |
+| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [postgres 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 air-gapped installation. | `arminc/clair-db:latest` |
+| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [postgres 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` |
## Security Dashboard
@@ -178,6 +178,47 @@ Once a vulnerability is found, you can interact with it. Read more on how to
For more information about the vulnerabilities database update, check the
[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
+## Running Container Scanning in an offline air-gapped installation
+
+Container Scanning can be executed on an offline air-gapped GitLab Ultimate installation using the following process:
+
+1. Host the following Docker images on a [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](https://gitlab.com/gitlab-org/security-products/analyzers/klar)
+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
+ include:
+ - template: Container-Scanning.gitlab-ci.yml
+
+ container_scanning:
+ image: $CI_REGISTRY/namespace/gitlab-klar-analyzer
+ variables:
+ CLAIR_DB_IMAGE: $CI_REGISTRY/namespace/clair-vulnerabilities-db
+ ```
+
+It may be worthwhile to set up a [scheduled pipeline](../../project/pipelines/schedules.md) to automatically build a new version of the vulnerabilities database on a preset schedule. You can use the following `.gitlab-yml.ci` as a template:
+
+```yaml
+image: docker:stable
+
+services:
+ - docker:stable-dind
+
+stages:
+ - build
+
+build_latest_vulnerabilities:
+ stage: build
+ script:
+ - docker pull arminc/clair-db:latest
+ - docker tag arminc/clair-db:latest $CI_REGISTRY/namespace/clair-vulnerabilities-db
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ - docker push $CI_REGISTRY/namespace/clair-vulnerabilities-db
+```
+
+The above template will work for a GitLab Docker registry running on a local installation, however, if you're using a non-GitLab Docker registry, you'll need to change the `$CI_REGISTRY` value and the `docker login` credentials to match the details of your local registry.
+
## Troubleshooting
### docker: Error response from daemon: failed to copy xattrs
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 951c4b9dd73..d285b5ff585 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -339,3 +339,33 @@ 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. -->
+
+## Troubleshooting
+
+### Running out of memory
+
+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.
+This results in the following error:
+
+```
+[zap.out] java.lang.OutOfMemoryError: Java heap space
+```
+
+Fortunately, it is straightforward to increase the amount of memory available
+for DAST by overwriting the `script` key in the DAST template:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+dast:
+ script:
+ - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
+ - /analyze -t $DAST_WEBSITE -z"-Xmx3072m"
+```
+
+Here, DAST is being allocated 3072 MB.
+Change the number after `-Xmx` to the required memory amount.
diff --git a/doc/user/application_security/dependency_list/img/dependency_list_v12_4.png b/doc/user/application_security/dependency_list/img/dependency_list_v12_4.png
new file mode 100644
index 00000000000..4687987b763
--- /dev/null
+++ b/doc/user/application_security/dependency_list/img/dependency_list_v12_4.png
Binary files differ
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
index 8366e943ccc..2828d487153 100644
--- a/doc/user/application_security/dependency_list/index.md
+++ b/doc/user/application_security/dependency_list/index.md
@@ -17,7 +17,7 @@ sidebar.
## Viewing dependencies
-![Dependency List](img/dependency_list_v12_3.png)
+![Dependency List](img/dependency_list_v12_4.png)
Dependencies are displayed with the following information:
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 9f87d79025e..0e46052b0bd 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -37,7 +37,7 @@ The results are sorted by the severity of the vulnerability:
## Requirements
-To run a Dependency Scanning job, you need GitLab Runner with the
+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,
@@ -47,6 +47,8 @@ CAUTION: **Caution:**
If you use your own Runners, make sure that the Docker version you have installed
is **not** `19.03.00`. 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)
+
## Supported languages and package managers
The following languages and dependency managers are supported.
@@ -59,27 +61,10 @@ The following languages and dependency managers are supported.
| Go ([Golang](https://golang.org/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/7132 "Dependency Scanning for Go")) | not available |
| 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://docs.pipenv.org/en/latest/basics/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available |
+| 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://poetry.eustace.io/)) | 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) |
-## Remote checks
-
-While some tools pull a local database to check vulnerabilities, some others
-like Gemnasium require sending data to GitLab central servers to analyze them:
-
-1. Gemnasium scans the dependencies of your project locally and sends a list of
- packages to GitLab central servers.
-1. The servers return the list of known vulnerabilities for all versions of
- these packages.
-1. The client picks up the relevant vulnerabilities by comparing with the versions
- of the packages that are used by the project.
-
-The Gemnasium client does **NOT** send the exact package versions your project relies on.
-
-You can disable the remote checks by [using](#customizing-the-dependency-scanning-settings)
-the `DS_DISABLE_REMOTE_CHECKS` environment variable and setting it to `"true"`.
-
## Configuration
For GitLab 11.9 and later, to enable Dependency Scanning, you must
@@ -116,7 +101,7 @@ include:
template: Dependency-Scanning.gitlab-ci.yml
variables:
- DS_DISABLE_REMOTE_CHECKS: "true"
+ DS_PYTHON_VERSION: 2
```
Because template is [evaluated before](../../../ci/yaml/README.md#include) the pipeline
@@ -150,7 +135,7 @@ using environment variables.
| `DS_PYTHON_VERSION` | 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)| |
| `DS_PIP_DEPENDENCY_PATH` | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) | |
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). | |
-| `DS_DISABLE_REMOTE_CHECKS` | Do not send any data to GitLab. Used in the [Gemnasium analyzer](#remote-checks). | |
+| `DS_DISABLE_DIND` | Disable Docker in Docker and run analyzers [individually](#disabling-docker-in-docker-for-dependency-scanning).| |
| `DS_PULL_ANALYZER_IMAGES` | Pull the images from the Docker registry (set to `0` to disable). | |
| `DS_EXCLUDED_PATHS` | Exclude vulnerabilities from output based on the paths. A comma-separated list of patterns. Patterns can be globs, file or folder paths. Parent directories will also match patterns. | `DS_EXCLUDED_PATHS=doc,spec` |
| `DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | 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`. | |
@@ -158,6 +143,50 @@ using environment variables.
| `DS_RUN_ANALYZER_TIMEOUT` | 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`. | |
| `PIP_INDEX_URL` | Base URL of Python Package Index (default `https://pypi.org/simple`). | |
| `PIP_EXTRA_INDEX_URL` | 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. | |
+| `MAVEN_CLI_OPTS` | List of command line arguments that will be passed to the maven analyzer during the project's build phase (see example for [using private repos](#using-private-maven-repos)). | |
+
+### Using private Maven repos
+
+If you have a private Maven repository which requires login credentials,
+you can use the `MAVEN_CLI_OPTS` environment variable to pass variables
+specified in your settings (e.g., username, password, etc.).
+
+For example, if you have a settings file in your project source (e.g., `mysettings.xml`)
+that looks like the following, you can specify the variables
+[by adding an entry under your project's settings](../../../ci/variables/README.md#via-the-ui),
+so that you don't have to expose your private data in `.gitlab-ci.yml` (e.g., adding
+`MAVEN_CLI_OPTS` with value `--settings mysettings.xml -Dprivate.username=foo -Dprivate.password=bar`).
+
+```xml
+<!-- mysettings.xml -->
+<settings>
+ ...
+ <servers>
+ <server>
+ <id>private_server</id>
+ <username>${private.username}</username>
+ <password>${private.password}</password>
+ </server>
+ </servers>
+</settings>
+```
+
+### Disabling Docker in Docker for Dependency Scanning
+
+> [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:
+
+```yaml
+include:
+ template: Dependency-Scanning.gitlab-ci.yml
+
+variables:
+ DS_DISABLE_DIND: "true"
+```
+
+This will create individual `<analyzer-name>-dependency_scanning` jobs for each analyzer that runs in your CI/CD pipeline.
## Interacting with the vulnerabilities
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index e9f5898950e..dbbcb606ac7 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -203,14 +203,34 @@ An approval will be optional when a license report:
- Contains no software license violations.
- Contains only new licenses that are `approved` or unknown.
-<!-- ## Troubleshooting
+## 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.
+### Getting error message `sast job: stage parameter should be [some stage name here]`
-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. -->
+When including a security job template like [`SAST`](sast/index.md#configuration),
+the following error can be raised, depending on your GitLab CI/CD configuration:
+
+```
+Found errors in your .gitlab-ci.yml:
+
+* sast job: stage parameter should be unit-tests
+```
+
+This error appears when the stage (nammed `test`) of the included job isn't declared
+in `.gitlab-ci.yml`.
+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`:
+
+ ```yaml
+ include:
+ template: SAST.gitlab-ci.yml
+
+ sast:
+ stage: unit-tests
+ ```
+
+[Learn more on overriding the SAST template](sast/index.md#overriding-the-sast-template).
+All the security scanning tools define their stage, so this error can occur with
+all of them.
diff --git a/doc/user/application_security/license_compliance/index.md b/doc/user/application_security/license_compliance/index.md
index 75a3b33e32e..3cf8301adca 100644
--- a/doc/user/application_security/license_compliance/index.md
+++ b/doc/user/application_security/license_compliance/index.md
@@ -94,8 +94,20 @@ always take the latest License Compliance artifact available. Behind the scenes,
[GitLab License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
is used to detect the languages/frameworks and in turn analyzes the licenses.
-The License Compliance settings can be changed through environment variables by using the
-[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [License Compliance documentation](https://gitlab.com/gitlab-org/security-products/license-management#settings).
+The License Compliance settings can be changed through [environment variables](#available-variables) by using the
+[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
+
+### Available 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) |
### Installing custom dependencies
diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md
index 76a566f7514..6eb2ca71e71 100644
--- a/doc/user/application_security/sast/analyzers.md
+++ b/doc/user/application_security/sast/analyzers.md
@@ -25,7 +25,7 @@ SAST supports the following official analyzers:
- [`security-code-scan`](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan) (Security Code Scan (.NET))
- [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix))
- [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT))
-- [`tslint`](https://gitlab.com/gitlab-org/security-products/analyzers/tslint) (TSLint (Typescript))
+- [`tslint`](https://gitlab.com/gitlab-org/security-products/analyzers/tslint) (TSLint (TypeScript))
The analyzers are published as Docker images that SAST will use to launch
dedicated containers for each analysis.
@@ -111,6 +111,9 @@ This configuration doesn't benefit from the integrated detection step.
SAST has to fetch and spawn each Docker image to establish whether the
custom analyzer can scan the source code.
+CAUTION: **Caution:**
+Custom analyzers are not spawned automatically when [Docker In Docker](index.md#disabling-docker-in-docker-for-sast) is disabled.
+
## Analyzers Data
| Property \ Tool | Apex | Bandit | Brakeman | ESLint security | Find Sec Bugs | Flawfinder | Go AST Scanner | NodeJsScan | Php CS Security Audit | Security code Scan (.NET) | TSLint Security | Sobelow |
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index cb54d9f3853..615eb072ea7 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -78,7 +78,7 @@ The following table shows which languages, package managers and frameworks are s
| Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 |
| 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
@@ -146,7 +146,15 @@ sast:
CI_DEBUG_TRACE: "true"
```
-### Using a variable to pass username and password to a private Maven repository
+### Using environment variables to pass credentials for private repositories
+
+Some analyzers require downloading the project's dependencies in order to
+perform the analysis. In turn, such dependencies may live in private Git
+repositories and thus require credentials like username and password to download them.
+Depending on the analyzer, such credentials can be provided to
+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 Apache Maven repository that requires login credentials,
you can use the `MAVEN_CLI_OPTS` [environment variable](#available-variables)
@@ -184,14 +192,14 @@ SAST can be [configured](#customizing-the-sast-settings) using environment varia
The following are Docker image-related variables.
-| Environment variable | Description |
-|-------------------------------|--------------------------------------------------------------------------------|
-| `SAST_ANALYZER_IMAGES` | Comma separated list of custom images. Default images are still enabled. Read more about [customizing analyzers](analyzers.md). |
-| `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` | 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_PULL_ANALYZER_IMAGES` | Pull the images from the Docker registry (set to 0 to disable). Read more about [customizing analyzers](analyzers.md). |
+| Environment variable | 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_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` | 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_PULL_ANALYZER_IMAGES` | 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). |
#### Vulnerability filters
@@ -216,6 +224,9 @@ The following variables configure timeouts.
| `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).
+
#### Analyzer settings
Some analyzers can be customized with environment variables.
@@ -234,6 +245,19 @@ Some analyzers can be customized with environment variables.
| `SBT_PATH` | spotbugs | Path to the `sbt` executable. |
| `FAIL_NEVER` | spotbugs | Set to `1` to ignore compilation failure. |
+#### 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
+to the underlying SAST analyzer images if
+[the SAST vendored template](#configuration) is used.
+
+CAUTION: **Caution:**
+Variables having names starting with these prefixes will **not** be propagated to the SAST Docker container and/or
+analyzer containers: `DOCKER_`, `CI`, `GITLAB_`, `FF_`, `HOME`, `PWD`, `OLDPWD`, `PATH`, `SHLVL`, `HOSTNAME`.
+
## Reports JSON format
CAUTION: **Caution:**
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
deleted file mode 100644
index 1fe76a9e08f..00000000000
--- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.png
new file mode 100644
index 00000000000..682dcbec63f
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_4.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 0e26206f070..688d231d568 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -71,12 +71,12 @@ Once you're on the dashboard, at the top you should see a series of filters for:
- Report type
- Project
-To the right of the filters, you should see a **Hide dismissed** toggle button.
+To the right of the filters, you should see a **Hide dismissed** toggle button ([available in GitLab Ultimate 12.5](https://gitlab.com/gitlab-org/gitlab/issues/9102)).
NOTE: **Note:**
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_3.png)
+![dashboard with action buttons and metrics](img/group_security_dashboard_v12_4.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.
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index dc6f859e881..c3e2e6bca5b 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -7,7 +7,7 @@ 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).
You can install them after you
-[create a cluster](../project/clusters/index.md#adding-and-removing-clusters).
+[create a cluster](../project/clusters/add_remove_clusters.md).
## Installing applications
@@ -40,6 +40,7 @@ The following applications can be installed:
- [GitLab Runner](#gitlab-runner)
- [JupyterHub](#jupyterhub)
- [Knative](#knative)
+- [Crossplane](#crossplane)
With the exception of Knative, the applications will be installed in a dedicated
namespace called `gitlab-managed-apps`.
@@ -82,19 +83,21 @@ 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.
-NOTE: **Note:**
-The
-[jetstack/cert-manager](https://github.com/jetstack/cert-manager)
-chart is used to install this application with a
-[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/cert_manager/values.yaml)
-file. Prior to GitLab 12.3,
-the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
-chart was used.
+The chart used to install this application depends on the version of GitLab used. In:
-NOTE: **Note:**
-If you have installed cert-manager prior to GitLab 12.3, Let's Encrypt will
-[block requests from older versions of cert-manager](https://community.letsencrypt.org/t/blocking-old-cert-manager-versions/98753).
-To resolve this, uninstall cert-manager (consider [backing up any additional configuration](https://docs.cert-manager.io/en/latest/tasks/backup-restore-crds.html)), then install cert-manager again.
+- GitLab 12.3 and newer, the [jetstack/cert-manager](https://github.com/jetstack/cert-manager)
+ chart is used with a [`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/cert_manager/values.yaml)
+ file.
+- GitLab 12.2 and older, the [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager)
+ chart was used.
+
+If you have installed Cert-Manager prior to GitLab 12.3, Let's Encrypt will
+[block requests from older versions of Cert-Manager](https://community.letsencrypt.org/t/blocking-old-cert-manager-versions/98753).
+
+To resolve this:
+
+1. Uninstall Cert-Manager (consider [backing up any additional configuration](https://docs.cert-manager.io/en/latest/tasks/backup-restore-crds.html)).
+1. Install Cert-Manager again.
### GitLab Runner
@@ -105,10 +108,20 @@ To resolve this, uninstall cert-manager (consider [backing up any additional con
project that is used to run your jobs and send the results back to
GitLab. It is used in conjunction with [GitLab
CI/CD](../../ci/README.md), the open-source continuous integration
-service included with GitLab that coordinates the jobs. When installing
-the GitLab Runner via the applications, it will run in **privileged
-mode** by default. Make sure you read the [security
-implications](../project/clusters/index.md#security-implications) before doing so.
+service included with GitLab that coordinates the jobs.
+
+If the project is on GitLab.com, shared Runners are available
+(the first 2000 minutes are free, you can
+[buy more later](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes))
+and you do not have to deploy one if they are enough for your needs. If a
+project-specific Runner is desired, or there are no shared Runners, it is easy
+to deploy one.
+
+Note that the deployed Runner will be set as **privileged**, which means it will essentially
+have root access to the underlying machine. This is required to build Docker images,
+so it is the default. Make sure you read the
+[security implications](../project/clusters/index.md#security-implications)
+before deploying one.
NOTE: **Note:**
The [`runner/gitlab-runner`](https://gitlab.com/gitlab-org/charts/gitlab-runner)
@@ -127,11 +140,112 @@ web proxy for your applications and is useful if you want to use [Auto
DevOps](../../topics/autodevops/index.md) or deploy your own web apps.
NOTE: **Note:**
+With the following procedure, a load balancer must be installed in your cluster
+to obtain the endpoint. You can use either
+Ingress, or Knative's own load balancer ([Istio](https://istio.io)) if using Knative.
+
+In order to publish your web application, you first need to find the endpoint which will be either an IP
+address or a hostname associated with your load balancer.
+
+To install it, click on the **Install** button for Ingress. GitLab will attempt
+to determine the external endpoint and it should be available within a few minutes.
+
+#### Determining the external endpoint automatically
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/17052) in GitLab 10.6.
+
+After you install Ingress, the external endpoint should be available within a few minutes.
+
+TIP: **Tip:**
+This endpoint can be used for the
+[Auto DevOps base domain](../../topics/autodevops/index.md#auto-devops-base-domain)
+using the `KUBE_INGRESS_BASE_DOMAIN` environment variable.
+
+If the endpoint doesn't appear and your cluster runs on Google Kubernetes Engine:
+
+1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes.
+1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas).
+1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions.
+
+Once installed, you may see a `?` for "Ingress IP Address" depending on the
+cloud provider. For EKS specifically, this is because the ELB is created
+with a DNS name, not an IP address. If GitLab is still unable to
+determine the endpoint of your Ingress or Knative application, you can
+[determine it manually](#determining-the-external-endpoint-manually).
+
+NOTE: **Note:**
The [`stable/nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/ingress/values.yaml)
file.
+#### Determining the external endpoint manually
+
+If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
+**Advanced settings**, or go directly to the
+[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
+and select the proper project and cluster. Then click **Connect** and execute
+the `gcloud` command in a local terminal or using the **Cloud Shell**.
+
+If the cluster is not on GKE, follow the specific instructions for your
+Kubernetes provider to configure `kubectl` with the right credentials.
+The output of the following examples will show the external endpoint of your
+cluster. This information can then be used to set up DNS entries and forwarding
+rules that allow external access to your deployed applications.
+
+If you installed Ingress via the **Applications**, run the following command:
+
+```bash
+kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
+```
+
+Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
+
+```bash
+kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
+```
+
+For Istio/Knative, the command will be different:
+
+```bash
+kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
+```
+
+Otherwise, you can list the IP addresses of all load balancers:
+
+```bash
+kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
+```
+
+NOTE: **Note:**
+If EKS is used, an [Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
+will also be created, which will incur additional AWS costs.
+
+NOTE: **Note:**
+You may see a trailing `%` on some Kubernetes versions, **do not include it**.
+
+The Ingress is now available at this address and will route incoming requests to
+the proper service based on the DNS name in the request. To support this, a
+wildcard DNS CNAME record should be created for the desired domain name. For example,
+`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
+
+#### Using a static IP
+
+By default, an ephemeral external IP address is associated to the cluster's load
+balancer. If you associate the ephemeral IP with your DNS and the IP changes,
+your apps will not be able to be reached, and you'd have to change the DNS
+record again. In order to avoid that, you should change it into a static
+reserved IP.
+
+Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
+
+#### Pointing your DNS at the external endpoint
+
+Once you've set up the external endpoint, you should associate it with a [wildcard DNS
+record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
+in order to be able to reach your apps. If your external endpoint is an IP address,
+use an A record. If your external endpoint is a hostname, use a CNAME record.
+
#### Web Application Firewall (ModSecurity)
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/65192) in GitLab 12.3 (enabled using `ingress_modsecurity` [feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-in-development)).
@@ -150,7 +264,7 @@ This feature:
For example:
```sh
- kubectl -n gitlab-managed-apps exec -it $(kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' | awk '{print $1}') -- tail -f /var/log/modsec_audit.log
+ kubectl -n gitlab-managed-apps exec -it $(kubectl get pods -n gitlab-managed-apps | grep 'ingress-controller' | awk '{print $1}') -- tail -f /var/log/modsec/audit.log
```
There is a small performance overhead by enabling `modsecurity`. However, if this is
@@ -242,7 +356,7 @@ server to use the external IP address for that domain. For any
application created and installed, they will be accessible as
`<program_name>.<kubernetes_namespace>.<domain_name>`. This will require
your Kubernetes cluster to have [RBAC
-enabled](../project/clusters/index.md#rbac-cluster-resources).
+enabled](../project/clusters/add_remove_clusters.md#rbac-cluster-resources).
NOTE: **Note:**
The [`knative/knative`](https://storage.googleapis.com/triggermesh-charts)
@@ -257,12 +371,52 @@ chart is used to install this application.
open-source monitoring and alerting system useful to supervise your
deployed applications.
+GitLab is able to monitor applications automatically, using the
+[Prometheus integration](../project/integrations/prometheus.md). Kubernetes container CPU and
+memory metrics are automatically collected, and response metrics are retrieved
+from NGINX Ingress as well.
+
+To enable monitoring, simply install Prometheus into the cluster with the
+**Install** button.
+
NOTE: **Note:**
The [`stable/prometheus`](https://github.com/helm/charts/tree/master/stable/prometheus)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/prometheus/values.yaml)
file.
+### Crossplane
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34702) in GitLab 12.5 for project-level clusters.
+
+[Crossplane](https://crossplane.io/docs) is a multi-cloud control plane useful for
+managing applications and infrastructure across multiple clouds. It extends the
+Kubernetes API using:
+
+- Custom resources.
+- Controllers that watch those custom resources.
+
+Crossplane allows provisioning and lifecycle management of infrastructure components
+across cloud providers in a uniform manner by abstracting cloud provider-specific
+configurations.
+
+The Crossplane GitLab-managed application:
+
+- Installs Crossplane with a provider of choice on a Kubernetes cluster attached to the
+ project repository.
+- Can then be used to provision infrastructure or managed applications such as
+ PostgreSQL (for example, CloudSQL from GCP or RDS from AWS) and other services
+ required by the application via the Auto DevOps pipeline.
+
+For information on configuring Crossplane installed on the cluster, see
+[Crossplane configuration](crossplane.md).
+
+NOTE: **Note:**
+[`alpha/crossplane`](https://charts.crossplane.io/alpha/) chart v0.4.1 is used to
+install Crossplane using the
+[`values.yaml`](https://github.com/crossplaneio/crossplane/blob/master/cluster/charts/crossplane/values.yaml.tmpl)
+file.
+
## Upgrading applications
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24789) in GitLab 11.8.
@@ -296,13 +450,14 @@ The applications below can be uninstalled.
| Application | GitLab version | Notes |
| ----------- | -------------- | ----- |
-| Cert-Manager | 12.2+ | The associated private key will be deleted and cannot be restored. Deployed applications will continue to use HTTPS, but certificates will not be renewed. Before uninstalling, you may wish to [back up your configuration](https://docs.cert-manager.io/en/latest/tasks/backup-restore-crds.html) or [revoke your certificates](https://letsencrypt.org/docs/revoking/) |
+| Cert-Manager | 12.2+ | The associated private key will be deleted and cannot be restored. Deployed applications will continue to use HTTPS, but certificates will not be renewed. Before uninstalling, you may wish to [back up your configuration](https://docs.cert-manager.io/en/latest/tasks/backup-restore-crds.html) or [revoke your certificates](https://letsencrypt.org/docs/revoking/). |
| GitLab Runner | 12.2+ | Any running pipelines will be canceled. |
| Helm | 12.2+ | The associated Tiller pod, the `gitlab-managed-apps` namespace, and all of its resources will be deleted and cannot be restored. |
| Ingress | 12.1+ | The associated load balancer and IP will be deleted and cannot be restored. Furthermore, it can only be uninstalled if JupyterHub is not installed. |
| JupyterHub | 12.1+ | All data not committed to GitLab will be deleted and cannot be restored. |
| Knative | 12.1+ | The associated IP will be deleted and cannot be restored. |
| Prometheus | 11.11+ | All data will be deleted and cannot be restored. |
+| Crossplane | 12.5+ | All data will be deleted and cannot be restored. |
To uninstall an application:
diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md
new file mode 100644
index 00000000000..37210b22f6f
--- /dev/null
+++ b/doc/user/clusters/crossplane.md
@@ -0,0 +1,292 @@
+# Crossplane configuration
+
+Once Crossplane [is installed](applications.md#crossplane), it must be configured for
+use.
+
+The process of configuring Crossplane includes:
+
+1. Configuring RBAC permissions.
+1. Configuring Crossplane with a cloud provider.
+1. Configure managed service access.
+1. Setting up Resource classes.
+1. Using Auto DevOps configuration options.
+1. Connect to the PostgreSQL instance.
+
+To allow Crossplane to provision cloud services such as PostgreSQL, the cloud provider
+stack must be configured with a user account. For example:
+
+- A service account for GCP.
+- An IAM user for AWS.
+
+Important notes:
+
+- This guide uses GCP as an example. However, the process for AWS and Azure will be
+similar.
+- Crossplane requires the Kubernetes cluster to be VPC native with Alias IPs enabled so
+that the IP address of the pods are routable within the GCP network.
+
+First, we need to declare some environment variables with configuration that will be used throughout this guide:
+
+```sh
+export PROJECT_ID=crossplane-playground # the GCP project where all resources reside.
+export NETWORK_NAME=default # the GCP network where your GKE is provisioned.
+export REGION=us-central1 # the GCP region where the GKE cluster is provisioned.
+```
+
+## Configure RBAC permissions
+
+- For a non-GitLab managed cluster(s), ensure that the service account for the token provided can manage resources in the `database.crossplane.io` API group.
+Manually grant GitLab's service account the ability to manage resources in the
+`database.crossplane.io` API group. The Aggregated ClusterRole allows us to do that.
+​
+NOTE: **Note:**
+For a non-GitLab managed cluster, ensure that the service account for the token provided can manage resources in the `database.crossplane.io` API group.
+​1. Save the following YAML as `crossplane-database-role.yaml`:
+
+```sh
+cat > crossplane-database-role.yaml <<EOF
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: crossplane-database-role
+ labels:
+ rbac.authorization.k8s.io/aggregate-to-edit: "true"
+rules:
+- apiGroups:
+ - database.crossplane.io
+ resources:
+ - postgresqlinstances
+ verbs:
+ - get
+ - list
+ - create
+ - update
+ - delete
+ - patch
+ - watch
+EOF
+```
+
+Once the file is created, apply it with the following command in order to create the necessary role:
+
+```sh
+kubectl apply -f crossplane-database-role.yaml
+```
+
+## Configure Crossplane with a cloud provider
+
+See [Configure Your Cloud Provider Account](https://crossplane.io/docs/v0.4/cloud-providers.html)
+to configure the installed cloud provider stack with a user account.
+
+Note that the Secret and the Provider resource referencing the Secret needs to be
+applied to the `gitlab-managed-apps` namespace in the guide. Make sure you change that
+while following the process.
+
+[Configure Providers](https://crossplane.io/docs/v0.4/cloud-providers.html)
+
+## Configure Managed Service Access
+
+We need to configure connectivity between the PostgreSQL database and the GKE cluster.
+This can done by either:
+
+- Using Crossplane as demonstrated below.
+- Directly in the GCP console by
+[configuring private services access](https://cloud.google.com/vpc/docs/configure-private-services-access).
+Create a GlobalAddress and Connection resources:
+
+```sh
+cat > network.yaml <<EOF
+---
+# gitlab-ad-globaladdress defines the IP range that will be allocated for cloud services connecting to the instances in the given Network.
+
+apiVersion: compute.gcp.crossplane.io/v1alpha3
+kind: GlobalAddress
+metadata:
+ name: gitlab-ad-globaladdress
+spec:
+ providerRef:
+ name: gcp-provider
+ reclaimPolicy: Delete
+ name: gitlab-ad-globaladdress
+ purpose: VPC_PEERING
+ addressType: INTERNAL
+ prefixLength: 16
+ network: projects/$PROJECT_ID/global/networks/$NETWORK_NAME
+---
+# gitlab-ad-connection is what allows cloud services to use the allocated GlobalAddress for communication. Behind
+# the scenes, it creates a VPC peering to the network that those service instances actually live.
+
+apiVersion: servicenetworking.gcp.crossplane.io/v1alpha3
+kind: Connection
+metadata:
+ name: gitlab-ad-connection
+spec:
+ providerRef:
+ name: gcp-provider
+ reclaimPolicy: Delete
+ parent: services/servicenetworking.googleapis.com
+ network: projects/$PROJECT_ID/global/networks/$NETWORK_NAME
+ reservedPeeringRangeRefs:
+ - name: gitlab-ad-globaladdress
+EOF
+```
+
+Apply the settings specified in the file with the following command:
+
+```sh
+kubectl apply -f network.yaml
+```
+
+You can verify creation of the network resources with the following commands.
+Verify that the status of both of these resources is ready and is synced.
+
+```sh
+kubectl describe connection.servicenetworking.gcp.crossplane.io gitlab-ad-connection
+kubectl describe globaladdress.compute.gcp.crossplane.io gitlab-ad-globaladdress
+```
+
+## Setting up Resource classes
+
+Resource classes are a way of defining a configuration for the required managed service. We will define the Postgres Resource class
+
+- Define a gcp-postgres-standard.yaml resourceclass which contains
+
+1. A default CloudSQLInstanceClass.
+1. A CloudSQLInstanceClass with labels.
+
+```sh
+cat > gcp-postgres-standard.yaml <<EOF
+apiVersion: database.gcp.crossplane.io/v1beta1
+kind: CloudSQLInstanceClass
+metadata:
+ name: cloudsqlinstancepostgresql-standard
+ labels:
+ gitlab-ad-demo: "true"
+specTemplate:
+ writeConnectionSecretsToNamespace: gitlab-managed-apps
+ forProvider:
+ databaseVersion: POSTGRES_9_6
+ region: $REGION
+ settings:
+ tier: db-custom-1-3840
+ dataDiskType: PD_SSD
+ dataDiskSizeGb: 10
+ ipConfiguration:
+ privateNetwork: projects/$PROJECT_ID/global/networks/$NETWORK_NAME
+ # this should match the name of the provider created in the above step
+ providerRef:
+ name: gcp-provider
+ reclaimPolicy: Delete
+---
+apiVersion: database.gcp.crossplane.io/v1beta1
+kind: CloudSQLInstanceClass
+metadata:
+ name: cloudsqlinstancepostgresql-standard-default
+ annotations:
+ resourceclass.crossplane.io/is-default-class: "true"
+specTemplate:
+ writeConnectionSecretsToNamespace: gitlab-managed-apps
+ forProvider:
+ databaseVersion: POSTGRES_9_6
+ region: $REGION
+ settings:
+ tier: db-custom-1-3840
+ dataDiskType: PD_SSD
+ dataDiskSizeGb: 10
+ ipConfiguration:
+ privateNetwork: projects/$PROJECT_ID/global/networks/$NETWORK_NAME
+ # this should match the name of the provider created in the above step
+ providerRef:
+ name: gcp-provider
+ reclaimPolicy: Delete
+EOF
+```
+
+Apply the resource class configuration with the following command:
+
+```sh
+kubectl apply -f gcp-postgres-standard.yaml
+```
+
+Verify creation of the Resource class with the following command:
+
+```sh
+kubectl get cloudsqlinstanceclasses
+```
+
+The Resource Classes allow you to define classes of service for a managed service. We could create another `CloudSQLInstanceClass` which requests for a larger or a faster disk. It could also request for a specific version of the database.
+
+## Auto DevOps Configuration Options
+
+The Auto DevOps pipeline can be run with the following options:
+
+The Environment variables, `AUTO_DEVOPS_POSTGRES_MANAGED` and `AUTO_DEVOPS_POSTGRES_MANAGED_CLASS_SELECTOR` need to be set to provision PostgresQL using Crossplane
+
+Alertnatively, the following options can be overridden from the values for the helm chart.
+
+- `postgres.managed` set to true which will select a default resource class.
+ The resource class needs to be marked with the annotation
+ `resourceclass.crossplane.io/is-default-class: "true"`. The CloudSQLInstanceClass
+ `cloudsqlinstancepostgresql-standard-default` will be used to satisfy the claim.
+
+- `postgres.managed` set to `true` with `postgres.managedClassSelector`
+ providing the resource class to choose based on labels. In this case, the
+ value of `postgres.managedClassSelector.matchLabels.gitlab-ad-demo="true"`
+ will select the CloudSQLInstance class `cloudsqlinstancepostgresql-standard`
+ to satisfy the claim request.
+
+The Auto DevOps pipeline should provision a PostgresqlInstance when it runs succesfully.
+
+Verify creation of the PostgresQL Instance.
+
+```sh
+kubectl get postgresqlinstance
+```
+
+Sample Output: The `STATUS` field of the PostgresqlInstance transitions to `BOUND` when it is successfully provisioned.
+
+```
+NAME STATUS CLASS-KIND CLASS-NAME RESOURCE-KIND RESOURCE-NAME AGE
+staging-test8 Bound CloudSQLInstanceClass cloudsqlinstancepostgresql-standard CloudSQLInstance xp-ad-demo-24-staging-staging-test8-jj55c 9m
+```
+
+The endpoint of the PostgreSQL instance, and the user credentials, are present in a secret called `app-postgres` within the same project namespace.
+
+Verify the secret with the database information is created with the following command:
+
+```sh
+kubectl describe secret app-postgres
+```
+
+Sample Output:
+
+```
+Name: app-postgres
+Namespace: xp-ad-demo-24-staging
+Labels: <none>
+Annotations: crossplane.io/propagate-from-name: 108e460e-06c7-11ea-b907-42010a8000bd
+ crossplane.io/propagate-from-namespace: gitlab-managed-apps
+ crossplane.io/propagate-from-uid: 10c79605-06c7-11ea-b907-42010a8000bd
+
+Type: Opaque
+
+Data
+====
+privateIP: 8 bytes
+publicIP: 13 bytes
+serverCACertificateCert: 1272 bytes
+serverCACertificateCertSerialNumber: 1 bytes
+serverCACertificateCreateTime: 24 bytes
+serverCACertificateExpirationTime: 24 bytes
+username: 8 bytes
+endpoint: 8 bytes
+password: 27 bytes
+serverCACertificateCommonName: 98 bytes
+serverCACertificateInstance: 41 bytes
+serverCACertificateSha1Fingerprint: 40 bytes
+```
+
+## Connect to the PostgresQL instance
+
+Follow this [GCP guide](https://cloud.google.com/sql/docs/postgres/connect-kubernetes-engine) if you
+would like to connect to the newly provisioned Postgres database instance on CloudSQL.
diff --git a/doc/user/clusters/img/advanced-settings-cluster-management-project-v12_5.png b/doc/user/clusters/img/advanced-settings-cluster-management-project-v12_5.png
new file mode 100644
index 00000000000..63e2d1cd4e8
--- /dev/null
+++ b/doc/user/clusters/img/advanced-settings-cluster-management-project-v12_5.png
Binary files differ
diff --git a/doc/user/clusters/management_project.md b/doc/user/clusters/management_project.md
index 37308ad7175..83b6f6fe300 100644
--- a/doc/user/clusters/management_project.md
+++ b/doc/user/clusters/management_project.md
@@ -4,7 +4,7 @@ CAUTION: **Warning:**
This is an _alpha_ feature, and it is subject to change at any time without
prior notice.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17866) in GitLab 12.4
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32810) in GitLab 12.5
A project can be designated as the management project for a cluster.
A management project can be used to run deployment jobs with
@@ -20,14 +20,37 @@ This can be useful for:
## Permissions
Only the management project will receive `cluster-admin` privileges. All
-other projects will continue to receive [namespace scoped `edit` level privileges](../project/clusters/index.md#rbac-cluster-resources).
+other projects will continue to receive [namespace scoped `edit` level privileges](../project/clusters/add_remove_clusters.md#rbac-cluster-resources).
+
+Management projects are restricted to the following:
+
+- For project-level clusters, the management project must in the same
+ namespace (or descendants) as the cluster's project.
+- For group-level clusters, the management project must in the same
+ group (or descendants) as as the cluster's group.
+- For instance-level clusters, there are no such restrictions.
## Usage
+To use a cluster management project for a cluster:
+
+1. Select the project.
+1. Configure your pipelines.
+1. Set an environment scope.
+
### Selecting a cluster management project
-This will be implemented as part of [this
-issue](https://gitlab.com/gitlab-org/gitlab/issues/32810).
+To select a cluster management project to use:
+
+1. Navigate to the appropriate configuration page. For a:
+ - [Project-level cluster](../project/clusters/index.md), navigate to your project's
+ **Operations > Kubernetes** page.
+ - [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes**
+ page.
+1. Select the project using **Cluster management project field** in the **Advanced settings**
+ section.
+
+![Selecting a cluster management project under Advanced settings](img/advanced-settings-cluster-management-project-v12_5.png)
### Configuring your pipeline
@@ -60,7 +83,7 @@ to a management project:
| Staging | `staging` |
| Production | `production` |
-The the following environments set in
+The following environments set in
[`.gitlab-ci.yml`](../../ci/yaml/README.md) will deploy to the
Development, Staging, and Production cluster respectively.
@@ -86,16 +109,3 @@ configure production cluster:
environment:
name: production
```
-
-## Disabling this feature
-
-This feature is enabled by default. To disable this feature, disable the
-feature flag `:cluster_management_project`.
-
-To check if the feature flag is enabled on your GitLab instance,
-please ask an administrator to execute the following in a Rails console:
-
-```ruby
-Feature.enabled?(:cluster_management_project) # Check if it's enabled or not.
-Feature.disable(:cluster_management_project) # Disable the feature flag.
-```
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 4742e7189b7..1fe456902a2 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -58,13 +58,18 @@ differentiate the new cluster from the rest.
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/index.md#access-controls) section for details on which resources will
+[Access controls](../../project/clusters/add_remove_clusters.md#access-controls) section for details on which resources will
be created.
-If you choose to manage your own cluster, project-specific resources will not be created
-automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.md), you will
-need to explicitly provide the `KUBE_NAMESPACE` [deployment variable](../../project/clusters/index.md#deployment-variables)
-that will be used by your deployment jobs.
+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 deployments with a cluster not managed by GitLab, you must ensure:
+
+- The project's deployment service account has permissions to deploy to
+ [`KUBE_NAMESPACE`](../../project/clusters/index.md#deployment-variables).
+- `KUBECONFIG` correctly reflects any changes to `KUBE_NAMESPACE`
+ (this is [not automatic](https://gitlab.com/gitlab-org/gitlab/issues/31519)). Editing
+ `KUBE_NAMESPACE` directly is discouraged.
NOTE: **Note:**
If you [install applications](#installing-applications) on your cluster, GitLab will create
@@ -147,7 +152,7 @@ are deployed to the Kubernetes cluster, see the documentation for
For important information about securely configuring GitLab Runners, see
[Security of
-Runners](../../project/clusters/index.md#security-of-gitlab-runners)
+Runners](../../project/clusters/add_remove_clusters.md#security-of-gitlab-runners)
documentation for project-level clusters.
<!-- ## Troubleshooting
diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
index a17c56c618b..a17c56c618b 100755..100644
--- a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
+++ b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view_v12.3.png b/doc/user/group/epics/img/epic_view_v12.3.png
index 79758cf3d52..79758cf3d52 100755..100644
--- a/doc/user/group/epics/img/epic_view_v12.3.png
+++ b/doc/user/group/epics/img/epic_view_v12.3.png
Binary files differ
diff --git a/doc/user/group/epics/img/epics_list_view_v12.3.png b/doc/user/group/epics/img/epics_list_view_v12.3.png
deleted file mode 100755
index c6817a503e7..00000000000
--- a/doc/user/group/epics/img/epics_list_view_v12.3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epics_list_view_v12.5.png b/doc/user/group/epics/img/epics_list_view_v12.5.png
new file mode 100644
index 00000000000..2520ee67abc
--- /dev/null
+++ b/doc/user/group/epics/img/epics_list_view_v12.5.png
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index f9690d4edfe..0753df70bc2 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -10,7 +10,7 @@ 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.
-![epics list view](img/epics_list_view_v12.3.png)
+![epics list view](img/epics_list_view_v12.5.png)
## Use cases
@@ -92,24 +92,44 @@ To remove a child epic from a parent epic:
## Start date and due date
-To set a **Start date** and **Due date** for an epic, you can choose either of the following:
+To set a **Start date** and **Due date** for an epic, select one of the following:
- **Fixed**: Enter a fixed value.
-- **From milestones:** Inherit a dynamic value from the issues added to the epic.
+- **From milestones**: Inherit a dynamic value from the issues added to the epic.
+- **Inherited**: Inherit a dynamic value from the issues added to the epic. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7332) in GitLab 12.5 to replace **From milestones**).
-If you select **From milestones** for the start date, GitLab will automatically set the
-date to be earliest start date across all milestones that are currently assigned
-to the issues that are added to the epic. Similarly, if you select "From milestones"
-for the due date, GitLab will set it to be the latest due date across all
-milestones that are currently assigned to those issues.
+### Milestones
-These are dynamic dates which are recalculated immediately if any of the following occur:
+If you select **From milestones** for the start date, GitLab will automatically set the date to be earliest
+start date across all milestones that are currently assigned to the issues that are added to the epic.
+Similarly, if you select **From milestones** for the due date, GitLab will set it to be the latest due date across
+all milestones that are currently assigned to those issues.
+
+These are dynamic dates which are recalculated if any of the following occur:
- Milestones are re-assigned to the issues.
- Milestone dates change.
- Issues are added or removed from the epic.
-## Roadmap
+### Inherited
+
+If you select **Inherited** for the start date, GitLab will scan all child epics and issues assigned to the epic,
+and will set the start date to match the earliest found start date or milestone. Similarly, if you select
+**Inherited** for the due date, GitLab will set the due date to match the latest due date or milestone
+found among its child epics and issues.
+
+These are dynamic dates and recalculated if any of the following occur:
+
+- A child epic's dates change.
+- Milestones are reassigned to an issue.
+- A milestone's dates change.
+- Issues are added to, or removed from, the epic.
+
+Because the epic's dates can inherit dates from its children, the start date and due date propagate from the bottom to the top.
+If the start date of a child epic on the lowest level changes, that becomes the earliest possible start date for its parent epic,
+then the parent epic's start date will reflect the change and this will propagate upwards to the top epic.
+
+## Roadmap in epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
@@ -121,6 +141,8 @@ have a [start or due date](#start-date-and-due-date), a
## 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:
@@ -267,7 +289,7 @@ Once you wrote your comment, you can either:
## Notifications
-- [Receive notifications](../../../workflow/notifications.md) for epic events.
+- [Receive notifications](../../profile/notifications.md) for epic events.
<!-- ## Troubleshooting
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index c4be08c842b..5f45a462f94 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -75,7 +75,7 @@ By doing so:
## Issues and merge requests within a group
Issues and merge requests are part of projects. For a given group, you can view all of the
-[issues](../project/issues/index.md#issues-list) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all projects in that group,
+[issues](../project/issues/index.md#issues-list) and [merge requests](../project/merge_requests/reviewing_and_managing_merge_requests.md#view-merge-requests-for-all-projects-in-a-group) across all projects in that group,
together in a single list view.
### Bulk editing issues and merge requests
@@ -123,7 +123,7 @@ For more details on creating groups, watch the video [GitLab Namespaces (users,
## Add users to a group
A benefit of putting multiple projects in one group is that you can
-give a user to access to all projects in the group with one action.
+give a user access to all projects in the group with one action.
Add members to a group by navigating to the group's dashboard and clicking **Members**.
@@ -135,14 +135,14 @@ Consider a group with two projects:
- On the **Group Members** page, you can now add a new user to the group.
- Now, because this user is a **Developer** member of the group, they automatically
- gets **Developer** access to **all projects** within that group.
+ get **Developer** access to **all projects** within that group.
To increase the access level of an existing user for a specific project,
add them again as a new member to the project with the desired permission level.
## Request access to a group
-As a group owner, you can enable or disable the ability for non members to request access to
+As a group owner, you can enable or disable the ability for non-members to request access to
your group. Go to the group settings, and click **Allow users to request access**.
As a user, you can request to be a member of a group, if that setting is enabled. Go to the group for which you'd like to be a member, and click the **Request Access** button on the right
@@ -353,10 +353,10 @@ content.
Restriction currently applies to:
- UI.
-- API access.
+- [From GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/issues/12874), API access.
- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/32113), Git actions via SSH.
-To avoid accidental lock-out, admins and group owners are are able to access
+To avoid accidental lock-out, admins and group owners are able to access
the group regardless of the IP restriction.
#### Allowed domain restriction **(PREMIUM)**
@@ -385,7 +385,7 @@ Some domains cannot be restricted. These are the most popular public email domai
To enable this feature:
1. Navigate to the group's **Settings > General** page.
-1. Expand the **Permissions, LFS, 2FA** section, and enter domain name into **Restrict membership by email** field.
+1. Expand the **Permissions, LFS, 2FA** section, and enter the domain name into **Restrict membership by email** field.
1. Click **Save changes**.
This will enable the domain-checking for all new users added to the group from this moment on.
@@ -421,8 +421,9 @@ Define project templates at a group level by setting a group as the template sou
#### Disabling email notifications
-You can disable all email notifications related to the group, which also includes
-it's subgroups and projects.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/23585) in GitLab 12.2.
+
+You can disable all email notifications related to the group, which includes its subgroups and projects.
To enable this feature:
@@ -444,7 +445,7 @@ To enable this feature:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/13294) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.0.
-A group owner can check the aggregated storage usage for all the project in a group, sub-groups included, in the **Storage** tab of the **Usage Quotas** page available to the group page settings list.
+A group owner can check the aggregated storage usage for all the projects in a group, sub-groups included, in the **Storage** tab of the **Usage Quotas** page available to the group page settings list.
![Group storage usage quota](img/group_storage_usage_quota.png)
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index bcd79bd04bf..a72cd990706 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -26,7 +26,7 @@ Epics in the view can be sorted by:
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
including the [epics list view](../epics/index.md).
-Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap).
+Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
## Timeline duration
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index ecf2934b877..6fd56414796 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -26,6 +26,23 @@ SAML SSO for GitLab.com groups does not sync users between providers without usi
![Issuer and callback for configuring SAML identity provider with GitLab.com](img/group_saml_configuration_information.png)
+### NameID
+
+GitLab.com uses the SAML NameID to identify users. The NameID element:
+
+- Is a required field in the SAML response.
+- Must be unique to each user.
+- Must be a persistent value that will never change, such as a randomly generated unique user ID.
+- Is case sensitive. The NameID must match exactly on subsequent login attempts, so should not rely on user input that could change between upper and lower case.
+- Should not be an email address or username. We strongly recommend against these as it is hard to guarantee they will never change, for example when a person's name changes. Email addresses are also case-insensitive, which can result in users being unable to sign in.
+
+CAUTION: **Warning:**
+Once users have signed into GitLab using the SSO SAML setup, changing the `NameID` will break the configuration and potentially lock users out of the GitLab group.
+
+#### NameID Format
+
+We recommend setting the NameID format to `Persistent` unless using a field (such as email) that requires a different format.
+
### SSO enforcement
SSO enforcement was:
@@ -58,25 +75,16 @@ Since use of the group managed account requires the use of SSO, users of group m
- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
- Contributions in the group (e.g. issues, merge requests) will remain intact.
-### NameID
+#### Assertions
-GitLab.com uses the SAML NameID to identify users. The NameID element:
+When using Group Manged Accounts, the following user details need to be passed to GitLab as SAML Assertions in order for us to be able to create a user:
-- Is a required field in the SAML response.
-- Must be unique to each user.
-- Must be a persistent value that will never change, such as a randomly generated unique user ID.
-- Is case sensitive. The NameID must match exactly on subsequent login attempts, so should not rely on user input that could change between upper and lower case.
-
-We strongly recommend against using Email as the NameID as it is hard to guarantee it will never change, for example when a person's name changes. Similarly usernames should be avoided if possible.
-
-### Assertions
-
-| Field | Supported keys |
-|-------|----------------|
+| Field | Supported keys |
+|-----------------|----------------|
| Email (required)| `email`, `mail` |
-| Full Name | `name` |
-| First Name | `first_name`, `firstname`, `firstName` |
-| Last Name | `last_name`, `lastname`, `lastName` |
+| Full Name | `name` |
+| First Name | `first_name`, `firstname`, `firstName` |
+| Last Name | `last_name`, `lastname`, `lastName` |
## Metadata configuration
@@ -108,17 +116,28 @@ NOTE: **Note:** GitLab is unable to provide support for IdPs that are not listed
| Azure | [Configuring single sign-on to applications](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-single-sign-on-non-gallery-applications) |
| Auth0 | [Auth0 as Identity Provider](https://auth0.com/docs/protocols/saml/saml-idp-generic) |
| G Suite | [Set up your own custom SAML application](https://support.google.com/a/answer/6087519?hl=en) |
-| JumpCloud | [Single Sign On (SSO) with GitLab](https://support.jumpcloud.com/customer/en/portal/articles/2810701-single-sign-on-sso-with-gitlab) |
+| JumpCloud | [Single Sign On (SSO) with GitLab](https://support.jumpcloud.com/support/s/article/single-sign-on-sso-with-gitlab-2019-08-21-10-36-47) |
| Okta | [Setting up a SAML application in Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) |
| OneLogin | [Use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f) |
| Ping Identity | [Add and configure a new SAML application](https://support.pingidentity.com/s/document-item?bundleId=pingone&topicId=xsh1564020480660-1.html) |
When [configuring your identify provider](#configuring-your-identity-provider), please consider the notes below for specific providers to help avoid common issues and as a guide for terminology used.
+### Okta setup notes
+
+| GitLab Setting | Okta Field |
+|--------------|----------------|
+| Identifier | Audience URI |
+| Assertion consumer service URL | Single sign on URL |
+
+Under Okta's **Single sign on URL** field, check the option **Use this for Recipient URL and Destination URL**.
+
+Set attribute statements according to the [assertions table](#assertions).
+
### OneLogin setup notes
-NOTE: **Note:**
-The GitLab app listed in the directory is for self-managed GitLab instances. Please use a generic SAML Test Connector.
+The GitLab app listed in the OneLogin app catalog is for self-managed GitLab instances.
+For GitLab.com, use a generic SAML Test Connector such as the SAML Test Connector (Advanced).
| GitLab Setting | OneLogin Field |
|--------------|----------------|
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 60b779b3f70..392b27bb42f 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -25,25 +25,6 @@ The following identity providers are supported:
## Requirements
- [Group SSO](index.md) needs to be configured.
-- The `scim_group` feature flag must be enabled:
-
- Run the following commands in a Rails console:
-
- ```sh
- # Omnibus GitLab
- gitlab-rails console
-
- # Installation from source
- cd /home/git/gitlab
- sudo -u git -H bin/rails console RAILS_ENV=production
- ```
-
- To enable SCIM for a group named `group_name`:
-
- ```ruby
- group = Group.find_by_full_path('group_name')
- Feature.enable(:group_scim, group)
- ```
## GitLab configuration
@@ -85,8 +66,13 @@ You can then test the connection by clicking on **Test Connection**. If the conn
1. Click **Delete** next to the `mail` mapping.
1. Map `userPrincipalName` to `emails[type eq "work"].value` and change it's **Matching precedence** to `2`.
1. Map `mailNickname` to `userName`.
-1. Create a new mapping by clicking **Add New Mapping** then set **Source attribute** to `objectId`, **Target attribute** to `id`, **Match objects using this attribute** to `Yes`, and **Matching precedence** to `1`.
-1. Create a new mapping by clicking **Add New Mapping** then set **Source attribute** to `objectId`, and **Target attribute** to `externalId`.
+1. Determine how GitLab will uniquely identify users.
+
+ - Use `objectId` unless users already have SAML linked for your group.
+ - If you already have users with SAML linked then use the `Name ID` value from the [SAML configuration](#azure). Using a different value will likely cause duplicate users and prevent users from accessing the GitLab group.
+
+1. Create a new mapping by clicking **Add New Mapping** then set **Source attribute** to the unique identifier determined above, **Target attribute** to `id`, **Match objects using this attribute** to `Yes`, and **Matching precedence** to `1`.
+1. Create a new mapping by clicking **Add New Mapping** then set **Source attribute** to the unique identifier determined above, and **Target attribute** to `externalId`.
1. Click the `userPrincipalName` mapping and change **Match objects using this attribute** to `No`.
Save your changes and you should have the following configuration:
@@ -118,6 +104,9 @@ You can then test the connection by clicking on **Test Connection**. If the conn
Once enabled, the synchronization details and any errors will appear on the
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.
+
## Troubleshooting
### Testing Azure connection: invalid credentials
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index a3606fadb89..52b7035389a 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -176,7 +176,7 @@ Here's a list of what you can't do with subgroups:
- [GitLab Pages](../../project/pages/index.md) supports projects hosted under
a subgroup, but not subgroup websites.
That means that only the highest-level group supports
- [group websites](../../project/pages/getting_started_part_one.md#gitlab-pages-domain-names),
+ [group websites](../../project/pages/getting_started_part_one.md#gitlab-pages-default-domain-names),
although you can have project websites under a subgroup.
- It is not possible to share a project with a group that's an ancestor of
the group the project is in. That means you can only share as you walk down
diff --git a/doc/workflow/img/todos_add_todo_sidebar.png b/doc/user/img/todos_add_todo_sidebar.png
index aefec7a2d9c..aefec7a2d9c 100644
--- a/doc/workflow/img/todos_add_todo_sidebar.png
+++ b/doc/user/img/todos_add_todo_sidebar.png
Binary files differ
diff --git a/doc/workflow/img/todos_icon.png b/doc/user/img/todos_icon.png
index 9fee4337a75..9fee4337a75 100644
--- a/doc/workflow/img/todos_icon.png
+++ b/doc/user/img/todos_icon.png
Binary files differ
diff --git a/doc/workflow/img/todos_index.png b/doc/user/img/todos_index.png
index 99c1575d157..99c1575d157 100644
--- a/doc/workflow/img/todos_index.png
+++ b/doc/user/img/todos_index.png
Binary files differ
diff --git a/doc/workflow/img/todos_mark_done_sidebar.png b/doc/user/img/todos_mark_done_sidebar.png
index 2badd880b40..2badd880b40 100644
--- a/doc/workflow/img/todos_mark_done_sidebar.png
+++ b/doc/user/img/todos_mark_done_sidebar.png
Binary files differ
diff --git a/doc/workflow/img/todo_list_item.png b/doc/user/img/todos_todo_list_item.png
index 91bbf9e5373..91bbf9e5373 100644
--- a/doc/workflow/img/todo_list_item.png
+++ b/doc/user/img/todos_todo_list_item.png
Binary files differ
diff --git a/doc/user/incident_management/img/incident_management_settings.png b/doc/user/incident_management/img/incident_management_settings.png
new file mode 100644
index 00000000000..25ad4fd08b7
--- /dev/null
+++ b/doc/user/incident_management/img/incident_management_settings.png
Binary files differ
diff --git a/doc/user/incident_management/index.md b/doc/user/incident_management/index.md
new file mode 100644
index 00000000000..5ac27d227a1
--- /dev/null
+++ b/doc/user/incident_management/index.md
@@ -0,0 +1,131 @@
+---
+description: "GitLab - Incident Management. GitLab offers solutions for handling incidents in your applications and services"
+---
+
+# 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.
+
+## Configuring 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**.
+
+![Incident Management Settings](img/incident_management_settings.png)
+
+### Automatically 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.
+
+### 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. Select the template from the **Issue Template** dropdown.
+
+## Alerting
+
+GitLab can react to the alerts that your applications and services may be
+triggering by automatically creating issues, and alerting developers via email.
+
+### Prometheus alerts
+
+Prometheus alerts can be set up in both:
+
+- [GitLab-managed Prometheus](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics-ultimate) and
+- [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).
+
+### Recovery 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.
+
+## Embedded metrics
+
+Metrics can be embedded anywhere where GitLab Markdown is used, for example,
+descriptions and comments on issues and merge requests.
+
+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
+
+Learn how to embed [GitLab hosted metric charts](../project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown).
+
+### Grafana metrics
+
+Learn how to embed [Grafana hosted metric charts](../project/integrations/prometheus.md#embedding-grafana-charts).
+
+## Slack integration
+
+Slack slash commands allow you to control GitLab and view content right inside
+Slack, without having to leave it.
+
+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 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).
+
+### Alerting
+
+You can let GitLab know of alerts that may be triggering in your applications and services. GitLab can react to these by automatically creating Issues, and alerting developers via Email.
+
+#### Prometheus Alerts
+
+Prometheus alerts can be setup in both GitLab-managed Prometheus installs and self-managed Prometheus installs.
+
+Documentation for each method can be found here:
+
+- [GitLab-managed Prometheus](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics-ultimate)
+- [Self-managed Prometheus](../project/integrations/prometheus.md#external-prometheus-instances)
+
+#### 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.
+
+More information on setting this up, including how to customize the payload [can be found here](../project/integrations/generic_alerts.md).
+
+#### Recovery Alerts
+
+Coming soon: GitLab can automatically close Issues that have been automatically created when we receive notification that the alert is resolved.
+
+### Configuring Incidents
+
+Incident Management features can be easily enabled & disabled via the Project settings page. Head to Project -> Settings -> Operations -> Incidents.
+
+#### Auto-creation
+
+GitLab Issues can automatically be created as a result of an Alert notification. An Issue created this way will contain error information to help you further debug the error.
diff --git a/doc/user/index.md b/doc/user/index.md
index ee5d4a0a07b..ab953b6d8bf 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -50,15 +50,15 @@ GitLab is a Git-based platform that integrates a great number of essential tools
With GitLab Enterprise Edition, you can also:
- Provide support with [Service Desk](project/service_desk.md).
-- Improve collaboration with
- [Merge Request Approvals](project/merge_requests/index.md#merge-request-approvals-starter),
- [Multiple Assignees for Issues](project/issues/multiple_assignees_for_issues.md),
- and [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards).
+- Improve collaboration with:
+ - [Merge Request Approvals](project/merge_requests/merge_request_approvals.md). **(STARTER)**
+ - [Multiple Assignees for Issues](project/issues/multiple_assignees_for_issues.md). **(STARTER)**
+ - [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards).
- Create formal relationships between issues with [Related Issues](project/issues/related_issues.md).
- Use [Burndown Charts](project/milestones/burndown_charts.md) to track progress during a sprint or while working on a new version of their software.
- Leverage [Elasticsearch](../integration/elasticsearch.md) with [Advanced Global Search](search/advanced_global_search.md) and [Advanced Syntax Search](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance.
- [Authenticate users with Kerberos](../integration/kerberos.md).
-- [Mirror a repository](../workflow/repository_mirroring.md) from elsewhere on your local server.
+- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
- [Export issues as CSV](project/issues/csv_export.md).
- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](../ci/multi_project_pipeline_graphs.md).
- [Lock files](project/file_lock.md) to prevent conflicts.
@@ -130,12 +130,12 @@ gather feedback through [resolvable threads](discussions/index.md#resolvable-com
Read through the [GFM documentation](markdown.md) to learn how to apply
the best of GitLab Flavored Markdown in your threads, comments,
-issues and merge requests descriptions, and everywhere else GMF is
+issues and merge requests descriptions, and everywhere else GFM is
supported.
## Todos
-Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md)
+Never forget to reply to your collaborators. [GitLab Todos](todos.md)
are a tool for working faster and more effectively with your team,
by listing all user or group mentions, as well as issues and merge
requests you're assigned to.
@@ -150,6 +150,11 @@ requests you're assigned to.
you have quick access to. You can also gather feedback on them through
[Discussions](#Discussions).
+## Keyboard shortcuts
+
+There are many [keyboard shortcuts](shortcuts.md) in GitLab to help you navigate between
+pages and accomplish tasks faster.
+
## Integrations
[Integrate GitLab](../integration/README.md) with your preferred tool,
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 0b4bb43b4bf..3bd0dcafc19 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -405,6 +405,7 @@ GFM will recognize the following:
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
+| scoped label by name | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` |
| project milestone by ID | `%123` | `namespace/project%123` | `project%123` |
| one-word milestone by name | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` |
| multi-word milestone by name | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` |
@@ -417,9 +418,10 @@ GFM will recognize the following:
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#task-lists).
-You can add task lists anywhere markdown is supported, but you can only "click" to
-toggle the boxes if they are in issues, merge requests, or comments. In other places
-you must edit the markdown manually to change the status by adding or removing the `x`.
+You can add task lists anywhere Markdown is supported, but you can only "click"
+to toggle the boxes if they are in issues, merge requests, or comments. In other
+places you must edit the Markdown manually to change the status by adding or
+removing an `x` within the square brackets.
To create a task list, add a specially-formatted Markdown list. You can use either
unordered or ordered lists:
diff --git a/doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.png b/doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.png
deleted file mode 100644
index d4db5e88672..00000000000
--- a/doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/operations_dashboard/index.md b/doc/user/operations_dashboard/index.md
index 649a95a5f3a..cdb80cca6f7 100644
--- a/doc/user/operations_dashboard/index.md
+++ b/doc/user/operations_dashboard/index.md
@@ -5,10 +5,7 @@
The Operations Dashboard provides a summary of each project's operational health,
including pipeline and alert status.
-The dashboard can be accessed via the top bar, by clicking on the new
-dashboard icon:
-
-![Operations Dashboard icon in top bar](img/index_operations_dashboard_top_bar_icon.png)
+The dashboard can be accessed via the top bar, by clicking **More > Operations**.
## Adding a project to the dashboard
@@ -28,6 +25,10 @@ last commit, pipeline status, and when it was last deployed.
![Operations Dashboard with projects](img/index_operations_dashboard_with_projects.png)
+## Arranging projects on a dashboard
+
+You can drag project cards to change their order. The card order is currently only saved to your browser, so will not change the dashboard for other people.
+
## Making it the default dashboard when you sign in
The Operations Dashboard can also be made the default GitLab dashboard shown when
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index f81756f7979..953c7472f4d 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -1,6 +1,6 @@
# GitLab Conan Repository **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8248) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8248) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.5.
With the GitLab Conan Repository, every
project can have its own space to store Conan packages.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index d6358d72348..60f4dbc0abb 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -1,4 +1,4 @@
-# Dependency Proxy **(PREMIUM)**
+# Dependency Proxy **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 5f5d86ab17e..d8b59ae63d0 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -42,6 +42,9 @@ it is not possible due to a naming collision. For example:
| `gitlab-org/gitlab` | `@gitlab-org/gitlab` | Yes |
| `gitlab-org/gitlab` | `@foo/bar` | No |
+CAUTION: **When updating the path of a user/group or transferring a (sub)group/project:**
+If you update the root namespace of a project with NPM packages, your changes will be rejected. To be allowed to do that, make sure to remove any NPM package first. Don't forget to update your `.npmrc` files to follow the above naming convention and run `npm publish` if necessary.
+
Now, you can configure your project to authenticate with the GitLab NPM
Registry.
@@ -105,6 +108,21 @@ Then, you could run `npm publish` either locally or via GitLab CI/CD:
- **GitLab CI/CD:** Set an `NPM_TOKEN` [variable](../../../ci/variables/README.md)
under your project's **Settings > CI/CD > Variables**.
+
+### Authenticating with a CI job token
+
+> [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.
+The token will inherit the permissions of the user that generates the pipeline.
+
+Add a corresponding section to your `.npmrc` file:
+
+```ini
+@foo:registry=https://gitlab.com/api/v4/packages/npm/
+//gitlab.com/api/v4/packages/npm/:_authToken=${env.CI_JOB_TOKEN}
+//gitlab.com/api/v4/projects/{env.CI_PROJECT_ID>/packages/npm/:_authToken=${env.CI_JOB_TOKEN}
+```
## Uploading packages
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 90874eca2eb..70660e5e22f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -51,11 +51,11 @@ The following table depicts the various user permission levels in a project.
| View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View licenses in Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
-| View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View GitLab Pages protected by [access control](project/pages/introduction.md#gitlab-pages-access-control-core) | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View wiki pages | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| See a list of jobs | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
@@ -83,7 +83,7 @@ The following table depicts the various user permission levels in a project.
| Push to non-protected branches | | | ✓ | ✓ | ✓ |
| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
| Remove non-protected branches | | | ✓ | ✓ | ✓ |
-| Create new merge request | | | ✓ | ✓ | ✓ |
+| Create new merge request | | ✓ | ✓ | ✓ | ✓ |
| Assign merge requests | | | ✓ | ✓ | ✓ |
| Label merge requests | | | ✓ | ✓ | ✓ |
| Lock merge request threads | | | ✓ | ✓ | ✓ |
@@ -103,6 +103,7 @@ The following table depicts the various user permission levels in a project.
| Apply code change suggestions | | | ✓ | ✓ | ✓ |
| Create and edit wiki pages | | | ✓ | ✓ | ✓ |
| Rewrite/remove Git tags | | | ✓ | ✓ | ✓ |
+| Manage Feature Flags **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Use environment terminals | | | | ✓ | ✓ |
| Run Web IDE's Interactive Web Terminals **(ULTIMATE ONLY)** | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
@@ -120,6 +121,7 @@ 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)** | | | | ✓ | ✓ |
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ |
| Manage Error Tracking | | | | ✓ | ✓ |
@@ -168,7 +170,7 @@ the [documentation on Cycle Analytics permissions](analytics/cycle_analytics.md#
Developers and users with higher permission level can use all
the functionality of the Issue Board, that is create/delete lists
-and drag issues around. Read though the
+and drag issues around. Read through the
[documentation on Issue Boards permissions](project/issue_board.md#permissions)
to learn more.
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index be761ca7558..beea063672e 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -32,63 +32,6 @@ As an administrator, you can delete a user account by:
- **Delete user and contributions** to delete the user and
their associated records.
-### Blocking a user
-
-In addition to blocking a user
-[via an abuse report](../../admin_area/abuse_reports.md#blocking-users),
-a user can be blocked directly from the Admin area. To do this:
-
-1. Navigate to **Admin Area > Overview > Users**.
-1. Selecting a user.
-1. Under the **Account** tab, click **Block user**.
-
-### Deactivating a user
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
-
-A user can be deactivated from the Admin area. Deactivating a user is functionally identical to blocking a user, with the following differences:
-
-- It does not prohibit the user from logging back in via the UI.
-- Once a deactivated user logs back into the GitLab UI, their account is set to active.
-
-A deactivated user:
-
-- Cannot access Git repositories or the API.
-- Will not receive any notifications from GitLab.
-- Will not be able to use [slash commands](../../../integration/slash_commands.md).
-
-Personal projects, group and user history of the deactivated user will be left intact.
-
-NOTE: **Note:**
-A deactivated user does not consume a [seat](../../../subscriptions/index.md#managing-subscriptions).
-
-To do this:
-
-1. Navigate to **Admin Area > Overview > Users**.
-1. Select a user.
-1. Under the **Account** tab, click **Deactivate user**.
-
-Please note that for the deactivation option to be visible to an admin, the user:
-
-- Must be currently active.
-- Should not have any activity in the last 180 days.
-
-### Activating a user
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
-
-A deactivated user can be activated from the Admin area. Activating a user sets their account to active state.
-
-To do this:
-
-1. Navigate to **Admin Area > Overview > Users**.
-1. Click on the **Deactivated** tab.
-1. Select a user.
-1. Under the **Account** tab, click **Activate user**.
-
-TIP: **Tip:**
-A deactivated user can also activate their account by themselves by simply logging back via the UI.
-
## Associated Records
> - Introduced for issues in
diff --git a/doc/workflow/img/notification_global_settings.png b/doc/user/profile/img/notification_global_settings.png
index 699f726c442..699f726c442 100644
--- a/doc/workflow/img/notification_global_settings.png
+++ b/doc/user/profile/img/notification_global_settings.png
Binary files differ
diff --git a/doc/workflow/img/notification_group_settings.png b/doc/user/profile/img/notification_group_settings.png
index ed5e9459216..ed5e9459216 100644
--- a/doc/workflow/img/notification_group_settings.png
+++ b/doc/user/profile/img/notification_group_settings.png
Binary files differ
diff --git a/doc/workflow/img/notification_project_settings.png b/doc/user/profile/img/notification_project_settings.png
index e2db2037d94..e2db2037d94 100644
--- a/doc/workflow/img/notification_project_settings.png
+++ b/doc/user/profile/img/notification_project_settings.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 9c4001f0a79..06e4eac6623 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -53,7 +53,7 @@ From there, you can:
[use GitLab as an OAuth provider](../../integration/oauth_provider.md#introduction-to-oauth)
- Manage [personal access tokens](personal_access_tokens.md) to access your account via API and authorized applications
- Add and delete emails linked to your account
-- Choose which email to use for notifications, web-based commits, and display on your public profile
+- Choose which email to use for [notifications](notifications.md), web-based commits, and display on your public profile
- Manage [SSH keys](../../ssh/README.md) to access your account via SSH
- Manage your [preferences](preferences.md#syntax-highlighting-theme)
to customize your own GitLab experience
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
new file mode 100644
index 00000000000..388576a48db
--- /dev/null
+++ b/doc/user/profile/notifications.md
@@ -0,0 +1,231 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/notifications.html'
+---
+
+# GitLab Notification Emails
+
+GitLab Notifications allow you to stay informed about what's happening in GitLab. With notifications enabled, you can receive updates about activity in issues, merge requests, and epics. Notifications are sent via email.
+
+## Receiving notifications
+
+You will receive notifications for one of the following reasons:
+
+- You participate in an issue, merge request, or epic. In this context, _participate_ means comment, or edit.
+- You enable notifications in an issue, merge request, or epic. To enable notifications, click the **Notifications** toggle in the sidebar to _on_.
+
+While notifications are enabled, you will receive notification of actions occurring in that issue, merge request, or epic.
+
+NOTE: **Note:**
+Notifications can be blocked by an admin, preventing them from being sent.
+
+## Tuning your notifications
+
+The quantity of notifications can be overwhelming. GitLab allows you to tune the notifications you receive. For example, you may want to be notified about all activity in a specific project, but for others, only be notified when you are mentioned by name.
+
+You can tune the notifications you receive by combining your notification settings:
+
+- [Global notification settings](#global-notification-settings)
+- [Notification scope](#notification-scope)
+- [Notification levels](#notification-levels)
+
+### Editing notification settings
+
+To edit your notification settings:
+
+1. Click on your profile picture and select **Settings**.
+1. Click **Notifications** in the left sidebar.
+1. Edit the desired notification settings. Edited settings are automatically saved and enabled.
+
+These notification settings apply only to you. They do not affect the notifications received by anyone else in the same project or group.
+
+![notification settings](img/notification_global_settings.png)
+
+## Global notification settings
+
+Your **Global notification settings** are the default settings unless you select different values for a project or a group.
+
+- Notification email
+ - This is the email address your notifications will be sent to.
+- Global notification level
+ - This is the default [notification level](#notification-levels) which applies to all your notifications.
+- Receive notifications about your own activity.
+ - Check this checkbox if you want to receive notification about your own activity. Default: Not checked.
+
+### Notification scope
+
+You can tune the scope of your notifications by selecting different notification levels for each project and group.
+
+Notification scope is applied in order of precedence (highest to lowest):
+
+- Project
+ - For each project, you can select a notification level. Your project setting overrides the group setting.
+- Group
+ - For each group, you can select a notification level. Your group setting overrides your default setting.
+- Global (default)
+ - Your global, or _default_, notification level applies if you have not selected a notification level for the project or group in which the activity occurred.
+
+#### Project notifications
+
+You can select a notification level for each project. This can be useful if you need to closely monitor activity in select projects.
+
+![notification settings](img/notification_project_settings.png)
+
+To select a notification level for a project, use either of these methods:
+
+1. Click on your profile picture and select **Settings**.
+1. Click **Notifications** in the left sidebar.
+1. Locate the project in the **Projects** section.
+1. Select the desired [notification level](#notification-levels).
+
+Or:
+
+1. Navigate to the project's page.
+1. Click the notification dropdown, marked with a bell icon.
+1. Select the desired [notification level](#notification-levels).
+
+#### Group notifications
+
+You can select a notification level and email address for each group.
+
+![notification settings](img/notification_group_settings.png)
+
+##### Group notification level
+
+To select a notification level for a group, use either of these methods:
+
+1. Click on your profile picture and select **Settings**.
+1. Click **Notifications** in the left sidebar.
+1. Locate the project in the **Groups** section.
+1. Select the desired [notification level](#notification-levels).
+
+---
+
+1. Navigate to the group's page.
+1. Click the notification dropdown, marked with a bell icon.
+1. Select the desired [notification level](#notification-levels).
+
+##### Group notification email address
+
+> Introduced in GitLab 12.0
+
+You can select an email address to receive notifications for each group you belong to. This could be useful, for example, if you work freelance, and want to keep email about clients' projects separate.
+
+1. Click on your profile picture and select **Settings**.
+1. Click **Notifications** in the left sidebar.
+1. Locate the project in the **Groups** section.
+1. Select the desired email address.
+
+### Notification levels
+
+For each project and group you can select one of the following levels:
+
+| Level | Description |
+|:------------|:------------|
+| Global | Your global settings apply. |
+| Watch | Receive notifications for any activity. |
+| On mention | Receive notifications when `@mentioned` in comments. |
+| Participate | Receive notifications for threads you have participated in. |
+| Disabled | Turns off notifications. |
+| Custom | Receive notifications for custom selected events. |
+
+## Notification events
+
+Users will be notified of the following events:
+
+| Event | Sent to | Settings level |
+|------------------------------|---------------------|------------------------------|
+| New SSH key added | User | Security email, always sent. |
+| New email added | User | Security email, always sent. |
+| Email changed | User | Security email, always sent. |
+| Password changed | User | Security email, always sent. |
+| New user created | User | Sent on user creation, except for OmniAuth (LDAP)|
+| User added to project | User | Sent when user is added to project |
+| Project access level changed | User | Sent when user project access level is changed |
+| User added to group | User | Sent when user is added to group |
+| Group access level changed | User | Sent when user group access level is changed |
+| Project moved | Project members (1) | (1) not disabled |
+| New release | Project members | Custom notification |
+
+## Issue / Epics / Merge request events
+
+In most of the below cases, the notification will be sent to:
+
+- Participants:
+ - the author and assignee of the issue/merge request
+ - authors of comments on the issue/merge request
+ - anyone mentioned by `@username` in the title or description of the issue, merge request or epic **(ULTIMATE)**
+ - anyone with notification level "Participating" or higher that is mentioned by `@username` in any of the comments on the issue, merge request, or epic **(ULTIMATE)**
+- Watchers: users with notification level "Watch"
+- Subscribers: anyone who manually subscribed to the issue, merge request, or epic **(ULTIMATE)**
+- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
+
+| Event | Sent to |
+|------------------------|---------|
+| New issue | |
+| Close issue | |
+| Reassign issue | The above, plus the old assignee |
+| Reopen issue | |
+| Due issue | Participants and Custom notification level with this event selected |
+| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| New merge request | |
+| Push to merge request | Participants and Custom notification level with this event selected |
+| Reassign merge request | The above, plus the old assignee |
+| Close merge request | |
+| Reopen merge request | |
+| Merge merge request | |
+| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
+| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+| Failed pipeline | The author of the pipeline |
+| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
+| New epic **(ULTIMATE)** | |
+| Close epic **(ULTIMATE)** | |
+| Reopen epic **(ULTIMATE)** | |
+
+In addition, if the title or description of an Issue or Merge Request is
+changed, notifications will be sent to any **new** mentions by `@username` as
+if they had been mentioned in the original text.
+
+You won't receive notifications for Issues, Merge Requests or Milestones created
+by yourself (except when an issue is due). You will only receive automatic
+notifications when somebody else comments or adds changes to the ones that
+you've created or mentions you.
+
+If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
+If a user has also set the merge request to automatically merge once pipeline succeeds,
+then that user will also be notified.
+
+## Email Headers
+
+Notification emails include headers that provide extra content about the notification received:
+
+| Header | Description |
+|-----------------------------|-------------------------------------------------------------------------|
+| X-GitLab-Project | The name of the project the notification belongs to |
+| X-GitLab-Project-Id | The ID of the project |
+| X-GitLab-Project-Path | The path of the project |
+| X-GitLab-(Resource)-ID | The ID of the resource the notification is for, where resource is `Issue`, `MergeRequest`, `Commit`, etc|
+| X-GitLab-Discussion-ID | Only in comment emails, the ID of the thread the comment is from |
+| X-GitLab-Pipeline-Id | Only in pipeline emails, the ID of the pipeline the notification is for |
+| X-GitLab-Reply-Key | A unique token to support reply by email |
+| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
+| List-Id | The path of the project in a RFC 2919 mailing list identifier useful for email organization, for example, with Gmail filters |
+
+### X-GitLab-NotificationReason
+
+This header holds the reason for the notification to have been sent out,
+where reason can be `mentioned`, `assigned`, `own_activity`, etc.
+Only one reason is sent out according to its priority:
+
+- `own_activity`
+- `assigned`
+- `mentioned`
+
+The reason in this header will also be shown in the footer of the notification email. For example an email with the
+reason `assigned` will have this sentence in the footer:
+`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
+
+NOTE: **Note:**
+Only reasons listed above have been implemented so far.
+Further implementation is [being discussed](https://gitlab.com/gitlab-org/gitlab/issues/20689).
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 82a6d2b3703..b299c74c8f4 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -82,7 +82,7 @@ You have 8 options here that you can use for your default dashboard view:
- Your projects' activity
- Starred projects' activity
- Your groups
-- Your [Todos](../../workflow/todos.md)
+- Your [Todos](../todos.md)
- Assigned Issues
- Assigned Merge Requests
- Operations Dashboard **(PREMIUM)**
@@ -128,6 +128,19 @@ You can choose one of the following options as the first day of the week:
If you select **System Default**, the system-wide default setting will be used.
+## Integrations
+
+Configure your preferences with third-party services which provide enhancements to your GitLab experience.
+
+### Sourcegraph
+
+NOTE: **Note:**
+This setting is only visible if Sourcegraph has been enabled by a GitLab administrator.
+
+Manage the availability of integrated code intelligence features powered by
+Sourcegraph. View [the Sourcegraph feature documentation](../../integration/sourcegraph.md#enable-sourcegraph-in-user-preferences)
+for more information.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
new file mode 100644
index 00000000000..150a451dfe5
--- /dev/null
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -0,0 +1,730 @@
+# Adding and removing Kubernetes clusters
+
+GitLab can integrate with the following Kubernetes providers:
+
+- Google Kubernetes Engine (GKE).
+- Amazon Elastic Kubernetes Service (EKS).
+
+TIP: **Tip:**
+Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
+and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
+Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form) and apply for credit.
+
+## Access controls
+
+When creating a cluster in GitLab, you will be asked if you would like to create either:
+
+- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) cluster.
+- An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/reference/access-authn-authz/abac/) cluster.
+
+NOTE: **Note:**
+[RBAC](#rbac-cluster-resources) is recommended and the GitLab default.
+
+GitLab creates the necessary service accounts and privileges to install and run
+[GitLab managed applications](index.md#installing-applications). When GitLab creates the cluster,
+a `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace
+to manage the newly created cluster.
+
+NOTE: **Note:**
+Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/51716) in GitLab 11.5.
+
+When you install Helm into your cluster, the `tiller` service account
+is created with `cluster-admin` privileges in the `gitlab-managed-apps`
+namespace.
+
+This service account will be:
+
+- Added to the installed Helm Tiller
+- Used by Helm to install and run [GitLab managed applications](index.md#installing-applications).
+
+Helm will also create additional service accounts and other resources for each
+installed application. Consult the documentation of the Helm charts for each application
+for details.
+
+If you are [adding an existing Kubernetes cluster](add_remove_clusters.md#add-existing-cluster),
+ensure the token of the account has administrator privileges for the cluster.
+
+The resources created by GitLab differ depending on the type of cluster.
+
+### Important notes
+
+Note the following about access controls:
+
+- Environment-specific resources are only created if your cluster is
+ [managed by GitLab](index.md#gitlab-managed-clusters).
+- If your cluster was created before GitLab 12.2, it will use a single namespace for all project
+ environments.
+
+### RBAC cluster resources
+
+GitLab creates the following resources for RBAC clusters.
+
+| Name | Type | Details | Created when |
+|:----------------------|:---------------------|:-----------------------------------------------------------------------------------------------------------|:-----------------------|
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new cluster |
+| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
+| Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
+| Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
+| Environment namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster |
+
+### ABAC cluster resources
+
+GitLab creates the following resources for ABAC clusters.
+
+| Name | Type | Details | Created when |
+|:----------------------|:---------------------|:-------------------------------------|:---------------------------|
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
+| Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
+| Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
+
+### Security of GitLab Runners
+
+GitLab Runners have the [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#the-privileged-mode)
+enabled by default, which allows them to execute special commands and running
+Docker in Docker. This functionality is needed to run some of the
+[Auto DevOps](../../../topics/autodevops/index.md)
+jobs. This implies the containers are running in privileged mode and you should,
+therefore, be aware of some important details.
+
+The privileged flag gives all capabilities to the running container, which in
+turn can do almost everything that the host can do. Be aware of the
+inherent security risk associated with performing `docker run` operations on
+arbitrary images as they effectively have root access.
+
+If you don't want to use GitLab Runner in privileged mode, either:
+
+- Use shared Runners on GitLab.com. They don't have this security issue.
+- Set up your own Runners using configuration described at
+ [Shared Runners](../../gitlab_com/index.md#shared-runners). This involves:
+ 1. Making sure that you don't have it installed via
+ [the applications](index.md#installing-applications).
+ 1. Installing a Runner
+ [using `docker+machine`](https://docs.gitlab.com/runner/executors/docker_machine.html).
+
+## Add new cluster
+
+### GKE cluster
+
+GitLab supports:
+
+- Creating a new GKE cluster using the GitLab UI.
+- Providing credentials to add an [existing Kubernetes cluster](#add-existing-cluster).
+
+Starting from [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/25925), all the GKE clusters provisioned by GitLab are [VPC-native](https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips).
+
+NOTE: **Note:**
+The [Google authentication integration](../../../integration/google.md) must
+be enabled in GitLab at the instance level. If that's not the case, ask your
+GitLab administrator to enable it. On GitLab.com, this is enabled.
+
+#### GKE Requirements
+
+Before creating your first cluster on Google Kubernetes Engine with GitLab's
+integration, make sure the following requirements are met:
+
+- A [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
+ is set up and you have permissions to access it.
+- The Kubernetes Engine API and related service are enabled. It should work immediately but may take up to 10 minutes after you create a project. For more information see the
+ ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin).
+
+Also note the following:
+
+- Starting from [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-foss/issues/55902), all GKE clusters
+ created by GitLab are RBAC-enabled. Take a look at the [RBAC section](#rbac-cluster-resources) for
+ more information.
+- Starting from [GitLab 12.5](https://gitlab.com/gitlab-org/gitlab/merge_requests/18341), the
+ cluster's pod address IP range will be set to /16 instead of the regular /14. /16 is a CIDR
+ notation.
+
+NOTE: **Note:**
+GitLab requires basic authentication enabled and a client certificate issued for the cluster in
+order to setup an [initial service account](#access-controls). Starting from [GitLab
+11.10](https://gitlab.com/gitlab-org/gitlab-foss/issues/58208), the cluster creation process will
+explicitly request that basic authentication and client certificate is enabled.
+
+#### Creating the cluster on GKE
+
+If all of the above requirements are met, you can proceed to create and add a
+new Kubernetes cluster to your project:
+
+1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions](../../permissions.md) and above to access the Kubernetes page.
+
+1. Click **Add Kubernetes cluster**.
+1. Click **Create with Google Kubernetes Engine**.
+1. Connect your Google account if you haven't done already by clicking the
+ **Sign in with Google** button.
+1. Choose your cluster's settings:
+ - **Kubernetes cluster name** - The name you wish to give the cluster.
+ - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
+ - **Google Cloud Platform project** - Choose the project you created in your GCP
+ console that will host the Kubernetes cluster. Learn more about
+ [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
+ - **Zone** - Choose the [region zone](https://cloud.google.com/compute/docs/regions-zones/)
+ under which the cluster will be created.
+ - **Number of nodes** - Enter the number of nodes you wish the cluster to have.
+ - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
+ of the Virtual Machine instance that the cluster will be based on.
+ - **Enable Cloud Run on GKE (beta)** - Check this if you want to use Cloud Run on GKE for this cluster.
+ See the [Cloud Run on GKE section](#cloud-run-on-gke) for more information.
+ - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster.
+ See the [Managed clusters section](index.md#gitlab-managed-clusters) for more information.
+1. Finally, click the **Create Kubernetes cluster** button.
+
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](index.md#installing-applications).
+
+#### Cloud Run on GKE
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16566) in GitLab 12.4.
+
+You can choose to use Cloud Run on GKE in place of installing Knative and Istio
+separately after the cluster has been created. This means that Cloud Run
+(Knative), Istio, and HTTP Load Balancing will be enabled on the cluster at
+create time and cannot be [installed or uninstalled](../../clusters/applications.md) separately.
+
+### EKS Cluster
+
+GitLab supports:
+
+- Creating a new EKS cluster using the GitLab UI
+ ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/22392) in GitLab 12.5).
+- Providing credentials to add an [existing Kubernetes cluster](#add-existing-cluster).
+
+#### EKS Requirements
+
+Before creating your first cluster on Amazon EKS with GitLab's integration,
+make sure the following requirements are met:
+
+- An [Amazon Web Services](https://aws.amazon.com/) account is set up and you are able to log in.
+- You have permissions to manage IAM resources.
+
+##### Additional requirements for self-managed instances
+
+If you are using a self-managed GitLab instance, GitLab must first
+be configured with a set of Amazon credentials. These credentials
+will be used to assume an Amazon IAM role provided by the user
+creating the cluster. Create an IAM user and ensure it has permissions
+to assume the role(s) that your users will use to create EKS clusters.
+
+For example, the following policy document allows assuming a role whose name starts with
+`gitlab-eks-` in account `123456789012`:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": {
+ "Effect": "Allow",
+ "Action": "sts:AssumeRole",
+ "Resource": "arn:aws:iam::123456789012:role/gitlab-eks-*"
+ }
+}
+```
+
+Generate an access key for the IAM user, and configure GitLab with the credentials:
+
+1. Navigate to **Admin Area > Settings > Integrations** and expand the **Amazon EKS** section.
+1. Check **Enable Amazon EKS integration**.
+1. Enter the account ID and access key credentials into the respective
+ `Account ID`, `Access key ID` and `Secret access key` fields.
+1. Click **Save changes**.
+
+#### Creating the cluster on EKS
+
+If all of the above requirements are met, you can proceed to create and add a
+new Kubernetes cluster to your project:
+
+1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions](../../permissions.md) and above to access the Kubernetes page.
+
+1. Click **Add Kubernetes cluster**.
+1. Click **Amazon EKS**. You will be provided with an `Account ID` and `External ID` to use in the next step.
+1. In the [IAM Management Console](https://console.aws.amazon.com/iam/home), create an IAM role:
+ 1. From the left panel, select **Roles**.
+ 1. Click **Create role**.
+ 1. Under `Select type of trusted entity`, select **Another AWS account**.
+ 1. Enter the Account ID from GitLab into the `Account ID` field.
+ 1. Check **Require external ID**.
+ 1. Enter the External ID from GitLab into the `External ID` field.
+ 1. Click **Next: Permissions**.
+ 1. Click **Create Policy**, which will open a new window.
+ 1. Select the **JSON** tab, and paste in the following snippet in place of the existing content:
+
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "autoscaling:CreateAutoScalingGroup",
+ "autoscaling:DescribeAutoScalingGroups",
+ "autoscaling:DescribeScalingActivities",
+ "autoscaling:UpdateAutoScalingGroup",
+ "autoscaling:CreateLaunchConfiguration",
+ "autoscaling:DescribeLaunchConfigurations",
+ "cloudformation:CreateStack",
+ "cloudformation:DescribeStacks",
+ "ec2:AuthorizeSecurityGroupEgress",
+ "ec2:AuthorizeSecurityGroupIngress",
+ "ec2:RevokeSecurityGroupEgress",
+ "ec2:RevokeSecurityGroupIngress",
+ "ec2:CreateSecurityGroup",
+ "ec2:createTags",
+ "ec2:DescribeImages",
+ "ec2:DescribeKeyPairs",
+ "ec2:DescribeRegions",
+ "ec2:DescribeSecurityGroups",
+ "ec2:DescribeSubnets",
+ "ec2:DescribeVpcs",
+ "eks:CreateCluster",
+ "eks:DescribeCluster",
+ "iam:AddRoleToInstanceProfile",
+ "iam:AttachRolePolicy",
+ "iam:CreateRole",
+ "iam:CreateInstanceProfile",
+ "iam:GetRole",
+ "iam:ListRoles",
+ "iam:PassRole",
+ "ssm:GetParameters"
+ ],
+ "Resource": "*"
+ }
+ ]
+ }
+ ```
+
+ NOTE: **Note:**
+ These permissions give GitLab the ability to create resources, but not delete them.
+ This means that if an error is encountered during the creation process, changes will
+ not be rolled back and you must remove resources manually. You can do this by deleting
+ the relevant [CloudFormation stack](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-delete-stack.html)
+
+ 1. Click **Review policy**.
+ 1. Enter a suitable name for this policy, and click **Create Policy**. You can now close this window.
+ 1. Switch back to the "Create role" window, and select the policy you just created.
+ 1. Click **Next: Tags**, and optionally enter any tags you wish to associate with this role.
+ 1. Click **Next: Review**.
+ 1. Enter a role name and optional description into the fields provided.
+ 1. Click **Create role**, the new role name will appear at the top. Click on its name and copy the `Role ARN` from the newly created role.
+1. In GitLab, enter the copied role ARN into the `Role ARN` field.
+1. Click **Authenticate with AWS**.
+1. Choose your cluster's settings:
+ - **Kubernetes cluster name** - The name you wish to give the cluster.
+ - **Environment scope** - The [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
+ - **Kubernetes version** - The Kubernetes version to use. Currently the only version supported is 1.14.
+ - **Role name** - Select the [IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html)
+ to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf.
+ - **Region** - The [region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html)
+ in which the cluster will be created.
+ - **Key pair name** - Select the [key pair](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
+ that you can use to connect to your worker nodes if required.
+ - **VPC** - Select a [VPC](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html)
+ to use for your EKS Cluster resources.
+ - **Subnets** - Choose the [subnets](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Subnets.html)
+ in your VPC where your worker nodes will run.
+ - **Security group** - Choose the [security group](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html)
+ to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.
+ - **Instance type** - The [instance type](https://aws.amazon.com/ec2/instance-types/) of your worker nodes.
+ - **Node count** - The number of worker nodes.
+ - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster.
+ See the [Managed clusters section](index.md#gitlab-managed-clusters) for more information.
+1. Finally, click the **Create Kubernetes cluster** button.
+
+After about 10 minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](index.md#installing-applications).
+
+## Add existing cluster
+
+If you have either of the following types of clusters already, you can add them to a project:
+
+- [Google Kubernetes Engine cluster](#add-existing-gke-cluster).
+- [Amazon Elastic Kubernetes Service](#add-existing-eks-cluster).
+
+NOTE: **Note:**
+Kubernetes integration is not supported for arm64 clusters. See the issue
+[Helm Tiller fails to install on arm64 cluster](https://gitlab.com/gitlab-org/gitlab-foss/issues/64044) for details.
+
+### Add existing GKE cluster
+
+To add an existing Kubernetes cluster to your project:
+
+1. Navigate to your project's **Operations > Kubernetes** page.
+
+ NOTE: **Note:**
+ You need Maintainer [permissions](../../permissions.md) and above to access the Kubernetes page.
+
+1. Click **Add Kubernetes cluster**.
+1. Click **Add an existing Kubernetes cluster** and fill in the details:
+ - **Kubernetes cluster name** (required) - The name you wish to give the cluster.
+ - **Environment scope** (required) - The
+ [associated environment](index.md#setting-the-environment-scope-premium) to this cluster.
+ - **API URL** (required) -
+ It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
+ exposes several APIs, we want the "base" URL that is common to all of them,
+ e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+ Get the API URL by running this command:
+
+ ```sh
+ kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'
+ ```
+
+ - **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We will use the certificate created by default.
+ - List the secrets with `kubectl get secrets`, and one should named similar to
+ `default-token-xxxxx`. Copy that token name for use below.
+ - Get the certificate by running this command:
+
+ ```sh
+
+ kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
+
+ ```
+
+ NOTE: **Note:**
+ If the command returns the entire certificate chain, you need copy the *root ca*
+ certificate at the bottom of the chain.
+
+ - **Token** -
+ GitLab authenticates against Kubernetes using service tokens, which are
+ scoped to a particular `namespace`.
+ **The token used should belong to a service account with
+ [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+ privileges.** To create this service account:
+
+ 1. Create a file called `gitlab-admin-service-account.yaml` with contents:
+
+ ```yaml
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: gitlab-admin
+ namespace: kube-system
+ ---
+ apiVersion: rbac.authorization.k8s.io/v1beta1
+ kind: ClusterRoleBinding
+ metadata:
+ name: gitlab-admin
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: gitlab-admin
+ namespace: kube-system
+ ```
+
+ 1. Apply the service account and cluster role binding to your cluster:
+
+ ```bash
+ kubectl apply -f gitlab-admin-service-account.yaml
+ ```
+
+ You will need the `container.clusterRoleBindings.create` permission
+ to create cluster-level roles. If you do not have this permission,
+ you can alternatively enable Basic Authentication and then run the
+ `kubectl apply` command as an admin:
+
+ ```bash
+ kubectl apply -f gitlab-admin-service-account.yaml --username=admin --password=<password>
+ ```
+
+ NOTE: **Note:**
+ Basic Authentication can be turned on and the password credentials
+ can be obtained using the Google Cloud Console.
+
+ Output:
+
+ ```bash
+ serviceaccount "gitlab-admin" created
+ clusterrolebinding "gitlab-admin" created
+ ```
+
+ 1. Retrieve the token for the `gitlab-admin` service account:
+
+ ```bash
+ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')
+ ```
+
+ Copy the `<authentication_token>` value from the output:
+
+ ```yaml
+ Name: gitlab-admin-token-b5zv4
+ Namespace: kube-system
+ Labels: <none>
+ Annotations: kubernetes.io/service-account.name=gitlab-admin
+ kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
+
+ Type: kubernetes.io/service-account-token
+
+ Data
+ ====
+ ca.crt: 1025 bytes
+ namespace: 11 bytes
+ token: <authentication_token>
+ ```
+
+ NOTE: **Note:**
+ For GKE clusters, you will need the
+ `container.clusterRoleBindings.create` permission to create a cluster
+ role binding. You can follow the [Google Cloud
+ documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
+ to grant access.
+
+ - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster.
+ See the [Managed clusters section](index.md#gitlab-managed-clusters) for more information.
+
+ - **Project namespace** (optional) - You don't have to fill it in; by leaving
+ it blank, GitLab will create one for you. Also:
+ - Each project should have a unique namespace.
+ - The project namespace is not necessarily the namespace of the secret, if
+ you're using a secret with broader permissions, like the secret from `default`.
+ - You should **not** use `default` as the project namespace.
+ - If you or someone created a secret specifically for the project, usually
+ with limited permissions, the secret's namespace and project namespace may
+ be the same.
+
+1. Finally, click the **Create Kubernetes cluster** button.
+
+After a couple of minutes, your cluster will be ready to go. You can now proceed
+to install some [pre-defined applications](index.md#installing-applications).
+
+### Add existing EKS cluster
+
+In this section, we will show how to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab and begin
+deploying applications.
+
+#### Requirements
+
+To integrate with with EKS, you will need:
+
+- An account on GitLab, like [GitLab.com](https://gitlab.com).
+- An Amazon EKS cluster (with worker nodes properly configured).
+- `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl).
+
+If you don't have an Amazon EKS cluster, one can be created by following the
+[EKS getting started guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html).
+
+#### Configuring and connecting the EKS cluster
+
+From the left side bar, hover over **Operations > Kubernetes > Add Kubernetes cluster**,
+then click **Add an existing Kubernetes cluster**.
+
+A few details from the EKS cluster will be required to connect it to GitLab:
+
+1. **Retrieve the certificate**: A valid Kubernetes certificate is needed to
+ authenticate to the EKS cluster. We will use the certificate created by default.
+ Open a shell and use `kubectl` to retrieve it:
+
+ - List the secrets with `kubectl get secrets`, and one should named similar to
+ `default-token-xxxxx`. Copy that token name for use below.
+ - Get the certificate with:
+
+ ```sh
+ kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
+ ```
+
+1. **Create admin token**: A `cluster-admin` token is required to install and
+ manage Helm Tiller. GitLab establishes mutual SSL auth with Helm Tiller
+ and creates limited service accounts for each application. To create the
+ token we will create an admin service account as follows:
+
+ 1. Create a file called `eks-admin-service-account.yaml` with contents:
+
+ ```yaml
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 1. Apply the service account to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-service-account.yaml
+ ```
+
+ Output:
+
+ ```bash
+ serviceaccount "eks-admin" created
+ ```
+
+ 1. Create a file called `eks-admin-cluster-role-binding.yaml` with contents:
+
+ ```yaml
+ apiVersion: rbac.authorization.k8s.io/v1beta1
+ kind: ClusterRoleBinding
+ metadata:
+ name: eks-admin
+ roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: eks-admin
+ namespace: kube-system
+ ```
+
+ 1. Apply the cluster role binding to your cluster:
+
+ ```bash
+ kubectl apply -f eks-admin-cluster-role-binding.yaml
+ ```
+
+ Output:
+
+ ```bash
+ clusterrolebinding "eks-admin" created
+ ```
+
+ 1. Retrieve the token for the `eks-admin` service account:
+
+ ```bash
+ kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep eks-admin | awk '{print $1}')
+ ```
+
+ Copy the `<authentication_token>` value from the output:
+
+ ```yaml
+ Name: eks-admin-token-b5zv4
+ Namespace: kube-system
+ Labels: <none>
+ Annotations: kubernetes.io/service-account.name=eks-admin
+ kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
+
+ Type: kubernetes.io/service-account-token
+
+ Data
+ ====
+ ca.crt: 1025 bytes
+ namespace: 11 bytes
+ token: <authentication_token>
+ ```
+
+1. The API server endpoint is also required, so GitLab can connect to the cluster.
+ This is displayed on the AWS EKS console, when viewing the EKS cluster details.
+
+You now have all the information needed to connect the EKS cluster:
+
+- Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
+- Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
+- API URL: Paste in the API server endpoint retrieved above.
+- CA Certificate: Paste the certificate data from the earlier step, as-is.
+- Paste the admin token value.
+- Project namespace: This can be left blank to accept the default namespace, based on the project name.
+
+![Add Cluster](img/add_cluster.png)
+
+Click on **Add Kubernetes cluster**, the cluster is now connected to GitLab.
+At this point, [Kubernetes deployment variables](index.md#deployment-variables) will
+automatically be available during CI/CD jobs, making it easy to interact with the cluster.
+
+If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
+
+#### Disable Role-Based Access Control (RBAC) (optional)
+
+When connecting a cluster via GitLab integration, you may specify whether the
+cluster is RBAC-enabled or not. This will affect how GitLab interacts with the
+cluster for certain operations. If you **did not** check the "RBAC-enabled cluster"
+checkbox at creation time, GitLab will assume RBAC is disabled for your cluster
+when interacting with it. If so, you must disable RBAC on your cluster for the
+integration to work properly.
+
+![rbac](img/rbac.png)
+
+NOTE: **Note**: Disabling RBAC means that any application running in the cluster,
+or user who can authenticate to the cluster, has full API access. This is a
+[security concern](index.md#security-implications), and may not be desirable.
+
+To effectively disable RBAC, global permissions can be applied granting full access:
+
+```bash
+kubectl create clusterrolebinding permissive-binding \
+ --clusterrole=cluster-admin \
+ --user=admin \
+ --user=kubelet \
+ --group=system:serviceaccounts
+```
+
+#### Create a default Storage Class
+
+Amazon EKS doesn't have a default Storage Class out of the box, which means
+requests for persistent volumes will not be automatically fulfilled. As part
+of Auto DevOps, the deployed Postgres instance requests persistent storage,
+and without a default storage class it will fail to start.
+
+If a default Storage Class doesn't already exist and is desired, follow Amazon's
+[guide on storage classes](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html)
+to create one.
+
+Alternatively, disable Postgres by setting the project variable
+[`POSTGRES_ENABLED`](../../../topics/autodevops/#environment-variables) to `false`.
+
+#### Deploy the app to EKS
+
+With RBAC disabled and services deployed,
+[Auto DevOps](../../../topics/autodevops/index.md) can now be leveraged
+to build, test, and deploy the app.
+
+[Enable Auto DevOps](../../../topics/autodevops/index.md#at-the-project-level)
+if not already enabled. If a wildcard DNS entry was created resolving to the
+Load Balancer, enter it in the `domain` field under the Auto DevOps settings.
+Otherwise, the deployed app will not be externally available outside of the cluster.
+
+![Deploy Pipeline](img/pipeline.png)
+
+A new pipeline will automatically be created, which will begin to build, test,
+and deploy the app.
+
+After the pipeline has finished, your app will be running in EKS and available
+to users. Click on **CI/CD > Environments**.
+
+![Deployed Environment](img/environment.png)
+
+You will see a list of the environments and their deploy status, as well as
+options to browse to the app, view monitoring metrics, and even access a shell
+on the running pod.
+
+## Enabling or disabling integration
+
+After you have successfully added your cluster information, you can enable the
+Kubernetes cluster integration:
+
+1. Click the **Enabled/Disabled** switch
+1. Hit **Save** for the changes to take effect
+
+To disable the Kubernetes cluster integration, follow the same procedure.
+
+## Removing integration
+
+To remove the Kubernetes cluster integration from your project, simply click the
+**Remove integration** button. You will then be able to follow the procedure
+and add a Kubernetes cluster again.
+
+When removing the cluster integration, note:
+
+- You need Maintainer [permissions](../../permissions.md) and above to remove a Kubernetes cluster
+ integration.
+- When you remove a cluster, you only remove its relationship to GitLab, not the cluster itself. To
+ remove the cluster, you can do so by visiting the GKE dashboard or using `kubectl`.
+
+## Learn more
+
+To learn more on automatically deploying your applications,
+read about [Auto DevOps](../../../topics/autodevops/index.md).
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png b/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png
deleted file mode 100644
index 61ed85e5cd9..00000000000
--- a/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_project.png b/doc/user/project/clusters/eks_and_gitlab/img/create_project.png
deleted file mode 100644
index b02ab4b9064..00000000000
--- a/doc/user/project/clusters/eks_and_gitlab/img/create_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png b/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png
deleted file mode 100644
index 0d9fcc838d9..00000000000
--- a/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index 22576b84926..fda8cd6340e 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -1,278 +1,5 @@
-# Connecting and deploying to an Amazon EKS cluster
+---
+redirect_to: '../add_remove_clusters.md#add-existing-eks-cluster'
+---
-In this tutorial, we will show how to integrate an
-[Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab and begin
-deploying applications.
-
-## Introduction
-
-For an end-to-end walkthrough we will:
-
-1. Start with a new project based on the sample Ruby on Rails template.
-1. Integrate an EKS cluster.
-1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application.
-
-You will need:
-
-1. An account on GitLab, like [GitLab.com](https://gitlab.com).
-1. An Amazon EKS cluster (with worker nodes properly configured).
-1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl).
-
-If you don't have an Amazon EKS cluster, one can be created by following the
-[EKS getting started guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html).
-
-## Creating a new project
-
-On GitLab, create a new project by clicking on the `+` icon in the top navigation
-bar and selecting **New project**.
-
-On the new project screen, click on the **Create from template** tab, and select
-"Use template" for the Ruby on Rails sample project.
-
-Give the project a name, and then select **Create project**.
-
-![Create Project](img/create_project.png)
-
-## Configuring and connecting the EKS cluster
-
-From the left side bar, hover over **Operations > Kubernetes > Add Kubernetes cluster**,
-then click **Add an existing Kubernetes cluster**.
-
-A few details from the EKS cluster will be required to connect it to GitLab:
-
-1. **Retrieve the certificate**: A valid Kubernetes certificate is needed to
- authenticate to the EKS cluster. We will use the certificate created by default.
- Open a shell and use `kubectl` to retrieve it:
-
- - List the secrets with `kubectl get secrets`, and one should named similar to
- `default-token-xxxxx`. Copy that token name for use below.
- - Get the certificate with:
-
- ```sh
- kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
- ```
-
-1. **Create admin token**: A `cluster-admin` token is required to install and
- manage Helm Tiller. GitLab establishes mutual SSL auth with Helm Tiller
- and creates limited service accounts for each application. To create the
- token we will create an admin service account as follows:
-
- 2.1. Create a file called `eks-admin-service-account.yaml` with contents:
-
- ```yaml
- apiVersion: v1
- kind: ServiceAccount
- metadata:
- name: eks-admin
- namespace: kube-system
- ```
-
- 2.2. Apply the service account to your cluster:
-
- ```bash
- kubectl apply -f eks-admin-service-account.yaml
- ```
-
- Output:
-
- ```bash
- serviceaccount "eks-admin" created
- ```
-
- 2.3. Create a file called `eks-admin-cluster-role-binding.yaml` with contents:
-
- ```yaml
- apiVersion: rbac.authorization.k8s.io/v1beta1
- kind: ClusterRoleBinding
- metadata:
- name: eks-admin
- roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: cluster-admin
- subjects:
- - kind: ServiceAccount
- name: eks-admin
- namespace: kube-system
- ```
-
- 2.4. Apply the cluster role binding to your cluster:
-
- ```bash
- kubectl apply -f eks-admin-cluster-role-binding.yaml
- ```
-
- Output:
-
- ```bash
- clusterrolebinding "eks-admin" created
- ```
-
- 2.5. Retrieve the token for the `eks-admin` service account:
-
- ```bash
- kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep eks-admin | awk '{print $1}')
- ```
-
- Copy the `<authentication_token>` value from the output:
-
- ```yaml
- Name: eks-admin-token-b5zv4
- Namespace: kube-system
- Labels: <none>
- Annotations: kubernetes.io/service-account.name=eks-admin
- kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
-
- Type: kubernetes.io/service-account-token
-
- Data
- ====
- ca.crt: 1025 bytes
- namespace: 11 bytes
- token: <authentication_token>
- ```
-
-1. The API server endpoint is also required, so GitLab can connect to the cluster.
- This is displayed on the AWS EKS console, when viewing the EKS cluster details.
-
-You now have all the information needed to connect the EKS cluster:
-
-- Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
-- Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
-- API URL: Paste in the API server endpoint retrieved above.
-- CA Certificate: Paste the certificate data from the earlier step, as-is.
-- Paste the admin token value.
-- Project namespace: This can be left blank to accept the default namespace, based on the project name.
-
-![Add Cluster](img/add_cluster.png)
-
-Click on **Add Kubernetes cluster**, the cluster is now connected to GitLab.
-At this point, [Kubernetes deployment variables](../#deployment-variables) will
-automatically be available during CI/CD jobs, making it easy to interact with the cluster.
-
-If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here.
-
-## Disable Role-Based Access Control (RBAC) (optional)
-
-When connecting a cluster via GitLab integration, you may specify whether the
-cluster is RBAC-enabled or not. This will affect how GitLab interacts with the
-cluster for certain operations. If you **did not** check the "RBAC-enabled cluster"
-checkbox at creation time, GitLab will assume RBAC is disabled for your cluster
-when interacting with it. If so, you must disable RBAC on your cluster for the
-integration to work properly.
-
-![rbac](img/rbac.png)
-
-NOTE: **Note**: Disabling RBAC means that any application running in the cluster,
-or user who can authenticate to the cluster, has full API access. This is a
-[security concern](../index.md#security-implications), and may not be desirable.
-
-To effectively disable RBAC, global permissions can be applied granting full access:
-
-```bash
-kubectl create clusterrolebinding permissive-binding \
- --clusterrole=cluster-admin \
- --user=admin \
- --user=kubelet \
- --group=system:serviceaccounts
-```
-
-## Deploy services to the cluster
-
-GitLab supports one-click deployment of helpful services to the cluster, many of
-which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a
-list of applications is now available to deploy.
-
-First, install Helm Tiller, a package manager for Kubernetes. This enables
-deployment of the other applications.
-
-![Deploy Apps](img/deploy_apps.png)
-
-### Deploying NGINX Ingress (optional)
-
-Next, if you would like the deployed app to be reachable on the internet, deploy
-the Ingress. Note that this will also cause an
-[Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
-to be created, which will incur additional AWS costs.
-
-Once installed, you may see a `?` for "Ingress IP Address". This is because the
-created ELB is available at a DNS name, not an IP address. To get the DNS name,
-run:
-
-```sh
-kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"
-```
-
-Note that you may see a trailing `%` on some Kubernetes versions, **do not include it**.
-
-The Ingress is now available at this address and will route incoming requests to
-the proper service based on the DNS name in the request. To support this, a
-wildcard DNS CNAME record should be created for the desired domain name. For example,
-`*.myekscluster.com` would point to the Ingress hostname obtained earlier.
-
-![Create DNS](img/create_dns.png)
-
-### Deploying the GitLab Runner (optional)
-
-If the project is on GitLab.com, free shared Runners are available and you do
-not have to deploy one. If a project specific Runner is desired, or there are no
-shared Runners, it is easy to deploy one.
-
-Simply click on the **Install** button for the GitLab Runner. It is important to
-note that the Runner deployed is set as **privileged**, which means it essentially
-has root access to the underlying machine. This is required to build docker images,
-and so is on by default.
-
-### Deploying Prometheus (optional)
-
-GitLab is able to monitor applications automatically, utilizing
-[Prometheus](../../integrations/prometheus.html). Kubernetes container CPU and
-memory metrics are automatically collected, and response metrics are retrieved
-from NGINX Ingress as well.
-
-To enable monitoring, simply install Prometheus into the cluster with the
-**Install** button.
-
-## Create a default Storage Class
-
-Amazon EKS doesn't have a default Storage Class out of the box, which means
-requests for persistent volumes will not be automatically fulfilled. As part
-of Auto DevOps, the deployed Postgres instance requests persistent storage,
-and without a default storage class it will fail to start.
-
-If a default Storage Class doesn't already exist and is desired, follow Amazon's
-[guide on storage classes](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html)
-to create one.
-
-Alternatively, disable Postgres by setting the project variable
-[`POSTGRES_ENABLED`](../../../../topics/autodevops/#environment-variables) to `false`.
-
-## Deploy the app to EKS
-
-With RBAC disabled and services deployed,
-[Auto DevOps](../../../../topics/autodevops/index.md) can now be leveraged
-to build, test, and deploy the app.
-
-[Enable Auto DevOps](../../../../topics/autodevops/index.md#at-the-project-level)
-if not already enabled. If a wildcard DNS entry was created resolving to the
-Load Balancer, enter it in the `domain` field under the Auto DevOps settings.
-Otherwise, the deployed app will not be externally available outside of the cluster.
-
-![Deploy Pipeline](img/pipeline.png)
-
-A new pipeline will automatically be created, which will begin to build, test,
-and deploy the app.
-
-After the pipeline has finished, your app will be running in EKS and available
-to users. Click on **CI/CD > Environments**.
-
-![Deployed Environment](img/environment.png)
-
-You will see a list of the environments and their deploy status, as well as
-options to browse to the app, view monitoring metrics, and even access a shell
-on the running pod.
-
-## Learn more
-
-To learn more on automatically deploying your applications,
-read about [Auto DevOps](../../../../topics/autodevops/index.md).
+This document was moved to [another location](../add_remove_clusters.md#add-existing-eks-cluster).
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png b/doc/user/project/clusters/img/add_cluster.png
index 94ec83f1514..94ec83f1514 100644
--- a/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png
+++ b/doc/user/project/clusters/img/add_cluster.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/environment.png b/doc/user/project/clusters/img/environment.png
index 4714c447026..4714c447026 100644
--- a/doc/user/project/clusters/eks_and_gitlab/img/environment.png
+++ b/doc/user/project/clusters/img/environment.png
Binary files differ
diff --git a/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png b/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png
deleted file mode 100644
index 73c2ecd182a..00000000000
--- a/doc/user/project/clusters/img/kubernetes_pod_logs_v12_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/clusters/img/kubernetes_pod_logs_v12_5.png b/doc/user/project/clusters/img/kubernetes_pod_logs_v12_5.png
new file mode 100644
index 00000000000..e54637e7218
--- /dev/null
+++ b/doc/user/project/clusters/img/kubernetes_pod_logs_v12_5.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png b/doc/user/project/clusters/img/pipeline.png
index 0eb00d0faa7..0eb00d0faa7 100644
--- a/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png
+++ b/doc/user/project/clusters/img/pipeline.png
Binary files differ
diff --git a/doc/user/project/clusters/eks_and_gitlab/img/rbac.png b/doc/user/project/clusters/img/rbac.png
index 517e4f7ca44..517e4f7ca44 100644
--- a/doc/user/project/clusters/eks_and_gitlab/img/rbac.png
+++ b/doc/user/project/clusters/img/rbac.png
Binary files differ
diff --git a/doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_5.png b/doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_5.png
new file mode 100644
index 00000000000..f113b0353f2
--- /dev/null
+++ b/doc/user/project/clusters/img/sidebar_menu_pod_logs_v12_5.png
Binary files differ
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 9ecb785d6fe..c5c2c2c07e7 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -28,12 +28,11 @@ Using the GitLab project Kubernetes integration, you can:
- Use [Deploy Boards](#deploy-boards-premium). **(PREMIUM)**
- Use [Canary Deployments](#canary-deployments-premium). **(PREMIUM)**
- View [Pod logs](#pod-logs-ultimate). **(ULTIMATE)**
-
-You can also:
-
-- Connect and deploy to an [Amazon EKS cluster](eks_and_gitlab/index.html).
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
+See [Adding and removing Kubernetes clusters](add_remove_clusters.md) for details on how to
+set up integrations.
+
### Deploy Boards **(PREMIUM)**
GitLab's Deploy Boards offer a consolidated view of the current health and
@@ -98,236 +97,10 @@ pods are annotated with:
`$CI_ENVIRONMENT_SLUG` and `$CI_PROJECT_PATH_SLUG` are the values of
the CI variables.
-## Adding and removing clusters
-
-There are two options when adding a new cluster to your project:
-
-- Associate your account with Google Kubernetes Engine (GKE) to
- [create new clusters](#add-new-gke-cluster) from within GitLab.
-- Provide credentials to an
- [existing Kubernetes cluster](#add-existing-kubernetes-cluster).
-
-### Add new GKE cluster
-
-TIP: **Tip:**
-Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
-and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
-Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?PCN=a0n60000006Vpz4AAC) and apply for credit.
-
-NOTE: **Note:**
-The [Google authentication integration](../../../integration/google.md) must
-be enabled in GitLab at the instance level. If that's not the case, ask your
-GitLab administrator to enable it. On GitLab.com, this is enabled.
-
-#### Requirements
-
-Before creating your first cluster on Google Kubernetes Engine with GitLab's
-integration, make sure the following requirements are met:
-
-- A [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
- is set up and you have permissions to access it.
-- The Kubernetes Engine API and related service are enabled. It should work immediately but may take up to 10 minutes after you create a project. For more information see the
- ["Before you begin" section of the Kubernetes Engine docs](https://cloud.google.com/kubernetes-engine/docs/quickstart#before-you-begin).
-
-#### Creating the cluster
-
-If all of the above requirements are met, you can proceed to create and add a
-new Kubernetes cluster to your project:
-
-1. Navigate to your project's **Operations > Kubernetes** page.
-
- NOTE: **Note:**
- You need Maintainer [permissions](../../permissions.md) and above to access the Kubernetes page.
-
-1. Click **Add Kubernetes cluster**.
-1. Click **Create with Google Kubernetes Engine**.
-1. Connect your Google account if you haven't done already by clicking the
- **Sign in with Google** button.
-1. From there on, choose your cluster's settings:
- - **Kubernetes cluster name** - The name you wish to give the cluster.
- - **Environment scope** - The [associated environment](#setting-the-environment-scope-premium) to this cluster.
- - **Google Cloud Platform project** - Choose the project you created in your GCP
- console that will host the Kubernetes cluster. Learn more about
- [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
- - **Zone** - Choose the [region zone](https://cloud.google.com/compute/docs/regions-zones/)
- under which the cluster will be created.
- - **Number of nodes** - Enter the number of nodes you wish the cluster to have.
- - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
- of the Virtual Machine instance that the cluster will be based on.
- - **Enable Cloud Run on GKE (beta)** - Check this if you want to use Cloud Run on GKE for this cluster. See the [Cloud Run on GKE section](#cloud-run-on-gke) for more information.
- - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](#gitlab-managed-clusters) for more information.
-1. Finally, click the **Create Kubernetes cluster** button.
-
-After a couple of minutes, your cluster will be ready to go. You can now proceed
-to install some [pre-defined applications](#installing-applications).
-
-NOTE: **Note:**
-GitLab requires basic authentication enabled and a client certificate issued for
-the cluster in order to setup an [initial service
-account](#access-controls). Starting from [GitLab
-11.10](https://gitlab.com/gitlab-org/gitlab-foss/issues/58208), the cluster
-creation process will explicitly request that basic authentication and
-client certificate is enabled.
-
-NOTE: **Note:**
-Starting from [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-foss/issues/55902), all GKE clusters created by GitLab are RBAC enabled. Take a look at the [RBAC section](#rbac-cluster-resources) for more information.
-
-### Add existing Kubernetes cluster
-
-NOTE: **Note:**
-Kubernetes integration is not supported for arm64 clusters. See the issue [Helm Tiller fails to install on arm64 cluster](https://gitlab.com/gitlab-org/gitlab-foss/issues/64044) for details.
-
-To add an existing Kubernetes cluster to your project:
-
-1. Navigate to your project's **Operations > Kubernetes** page.
-
- NOTE: **Note:**
- You need Maintainer [permissions](../../permissions.md) and above to access the Kubernetes page.
-
-1. Click **Add Kubernetes cluster**.
-1. Click **Add an existing Kubernetes cluster** and fill in the details:
- - **Kubernetes cluster name** (required) - The name you wish to give the cluster.
- - **Environment scope** (required) - The
- [associated environment](#setting-the-environment-scope-premium) to this cluster.
- - **API URL** (required) -
- It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
- exposes several APIs, we want the "base" URL that is common to all of them,
- e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-
- Get the API URL by running this command:
-
- ```sh
- kubectl cluster-info | grep 'Kubernetes master' | awk '/http/ {print $NF}'
- ```
-
- - **CA certificate** (required) - A valid Kubernetes certificate is needed to authenticate to the cluster. We will use the certificate created by default.
- - List the secrets with `kubectl get secrets`, and one should named similar to
- `default-token-xxxxx`. Copy that token name for use below.
- - Get the certificate by running this command:
-
- ```sh
- kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
- ```
-
- - **Token** -
- GitLab authenticates against Kubernetes using service tokens, which are
- scoped to a particular `namespace`.
- **The token used should belong to a service account with
- [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
- privileges.** To create this service account:
-
- 1. Create a file called `gitlab-admin-service-account.yaml` with contents:
-
- ```yaml
- apiVersion: v1
- kind: ServiceAccount
- metadata:
- name: gitlab-admin
- namespace: kube-system
- ---
- apiVersion: rbac.authorization.k8s.io/v1beta1
- kind: ClusterRoleBinding
- metadata:
- name: gitlab-admin
- roleRef:
- apiGroup: rbac.authorization.k8s.io
- kind: ClusterRole
- name: cluster-admin
- subjects:
- - kind: ServiceAccount
- name: gitlab-admin
- namespace: kube-system
- ```
-
- 1. Apply the service account and cluster role binding to your cluster:
-
- ```bash
- kubectl apply -f gitlab-admin-service-account.yaml
- ```
-
- Output:
-
- ```bash
- serviceaccount "gitlab-admin" created
- clusterrolebinding "gitlab-admin" created
- ```
-
- 1. Retrieve the token for the `gitlab-admin` service account:
-
- ```bash
- kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')
- ```
-
- Copy the `<authentication_token>` value from the output:
-
- ```yaml
- Name: gitlab-admin-token-b5zv4
- Namespace: kube-system
- Labels: <none>
- Annotations: kubernetes.io/service-account.name=gitlab-admin
- kubernetes.io/service-account.uid=bcfe66ac-39be-11e8-97e8-026dce96b6e8
-
- Type: kubernetes.io/service-account-token
-
- Data
- ====
- ca.crt: 1025 bytes
- namespace: 11 bytes
- token: <authentication_token>
- ```
-
- NOTE: **Note:**
- For GKE clusters, you will need the
- `container.clusterRoleBindings.create` permission to create a cluster
- role binding. You can follow the [Google Cloud
- documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
- to grant access.
-
- - **GitLab-managed cluster** - Leave this checked if you want GitLab to manage namespaces and service accounts for this cluster. See the [Managed clusters section](#gitlab-managed-clusters) for more information.
-
- - **Project namespace** (optional) - You don't have to fill it in; by leaving
- it blank, GitLab will create one for you. Also:
- - Each project should have a unique namespace.
- - The project namespace is not necessarily the namespace of the secret, if
- you're using a secret with broader permissions, like the secret from `default`.
- - You should **not** use `default` as the project namespace.
- - If you or someone created a secret specifically for the project, usually
- with limited permissions, the secret's namespace and project namespace may
- be the same.
-
-1. Finally, click the **Create Kubernetes cluster** button.
-
-After a couple of minutes, your cluster will be ready to go. You can now proceed
-to install some [pre-defined applications](#installing-applications).
-
-### Enabling or disabling integration
-
-After you have successfully added your cluster information, you can enable the
-Kubernetes cluster integration:
-
-1. Click the **Enabled/Disabled** switch
-1. Hit **Save** for the changes to take effect
-
-To disable the Kubernetes cluster integration, follow the same procedure.
-
-### Removing integration
-
-NOTE: **Note:**
-You need Maintainer [permissions](../../permissions.md) and above to remove a Kubernetes cluster integration.
-
-NOTE: **Note:**
-When you remove a cluster, you only remove its relation to GitLab, not the
-cluster itself. To remove the cluster, you can do so by visiting the GKE
-dashboard or using `kubectl`.
-
-To remove the Kubernetes cluster integration from your project, simply click the
-**Remove integration** button. You will then be able to follow the procedure
-and add a Kubernetes cluster again.
-
## Cluster configuration
-This section covers important considerations for configuring Kubernetes
-clusters with GitLab.
+After [adding a Kubernetes cluster](add_remove_clusters.md) to GitLab, read this section that covers
+important considerations for configuring Kubernetes clusters with GitLab.
### Security implications
@@ -340,15 +113,6 @@ functionalities needed to successfully build and deploy a containerized
application. Bear in mind that the same credentials are used for all the
applications running on the cluster.
-### Cloud Run on GKE
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16566) in GitLab 12.4.
-
-You can choose to use Cloud Run on GKE in place of installing Knative and Istio
-separately after the cluster has been created. This means that Cloud Run
-(Knative), Istio, and HTTP Load Balancing will be enabled on the cluster at
-create time and cannot be [installed or uninstalled](../../clusters/applications.md) separately.
-
### GitLab-managed clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22011) in GitLab 11.5.
@@ -356,7 +120,7 @@ create time and cannot be [installed or uninstalled](../../clusters/applications
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](#access-controls) section for details on which resources will
+[Access controls](add_remove_clusters.md#access-controls) section for details on which resources will
be created.
If you choose to manage your own cluster, project-specific resources will not be created
@@ -386,97 +150,6 @@ you can either:
- Create an `A` record that points to the Ingress IP address with your domain provider.
- Enter a wildcard DNS address using a service such as nip.io or xip.io. For example, `192.168.1.1.xip.io`.
-### Access controls
-
-When creating a cluster in GitLab, you will be asked if you would like to create either:
-
-- An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/reference/access-authn-authz/abac/) cluster.
-- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) cluster.
-
-NOTE: **Note:**
-[RBAC](#rbac-cluster-resources) is recommended and the GitLab default.
-
-GitLab creates the necessary service accounts and privileges to install and run
-[GitLab managed applications](#installing-applications). When GitLab creates the cluster,
-a `gitlab` service account with `cluster-admin` privileges is created in the `default` namespace
-to manage the newly created cluster.
-
- NOTE: **Note:**
- Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/51716) in GitLab 11.5.
-
-When you install Helm into your cluster, the `tiller` service account
-is created with `cluster-admin` privileges in the `gitlab-managed-apps`
-namespace. This service account will be added to the installed Helm Tiller and will
-be used by Helm to install and run [GitLab managed applications](#installing-applications).
-Helm will also create additional service accounts and other resources for each
-installed application. Consult the documentation of the Helm charts for each application
-for details.
-
-If you are [adding an existing Kubernetes cluster](#add-existing-kubernetes-cluster),
-ensure the token of the account has administrator privileges for the cluster.
-
-The resources created by GitLab differ depending on the type of cluster.
-
-#### ABAC cluster resources
-
-GitLab creates the following resources for ABAC clusters.
-
-| Name | Type | Details | Created when |
-|:----------------------|:---------------------|:-------------------------------------|:---------------------------|
-| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
-| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
-| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
-| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
-| Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
-| Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
-| Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
-
-#### RBAC cluster resources
-
-GitLab creates the following resources for RBAC clusters.
-
-| Name | Type | Details | Created when |
-|:----------------------|:---------------------|:-----------------------------------------------------------------------------------------------------------|:---------------------------|
-| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
-| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster |
-| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
-| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
-| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
-| Environment namespace | `Namespace` | Contains all environment-specific resources | Deploying to a cluster |
-| Environment namespace | `ServiceAccount` | Uses namespace of environment | Deploying to a cluster |
-| Environment namespace | `Secret` | Token for environment ServiceAccount | Deploying to a cluster |
-| Environment namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster |
-
-NOTE: **Note:**
-Environment-specific resources are only created if your cluster is [managed by GitLab](#gitlab-managed-clusters).
-
-NOTE: **Note:**
-If your cluster was created before GitLab 12.2, it will use a single namespace for all project environments.
-
-#### Security of GitLab Runners
-
-GitLab Runners have the [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#the-privileged-mode)
-enabled by default, which allows them to execute special commands and running
-Docker in Docker. This functionality is needed to run some of the
-[Auto DevOps](../../../topics/autodevops/index.md)
-jobs. This implies the containers are running in privileged mode and you should,
-therefore, be aware of some important details.
-
-The privileged flag gives all capabilities to the running container, which in
-turn can do almost everything that the host can do. Be aware of the
-inherent security risk associated with performing `docker run` operations on
-arbitrary images as they effectively have root access.
-
-If you don't want to use GitLab Runner in privileged mode, either:
-
-- Use shared Runners on GitLab.com. They don't have this security issue.
-- Set up your own Runners using configuration described at
- [Shared Runners](../../gitlab_com/index.md#shared-runners). This involves:
- 1. Making sure that you don't have it installed via
- [the applications](#installing-applications).
- 1. Installing a Runner
- [using `docker+machine`](https://docs.gitlab.com/runner/executors/docker_machine.html).
-
### Setting the environment scope **(PREMIUM)**
When adding more than one Kubernetes cluster to your project, you need to differentiate
@@ -545,93 +218,12 @@ differentiate the new cluster with the rest.
## Installing applications
-GitLab can install and manage some applications in your project-level
-cluster. For more information on installing, upgrading, uninstalling,
-and troubleshooting applications for your project cluster, see
+GitLab can install and manage some applications like Helm, GitLab Runner, Ingress,
+Prometheus, etc., in your project-level cluster. For more information on
+installing, upgrading, uninstalling, and troubleshooting applications for
+your project cluster, see
[GitLab Managed Apps](../../clusters/applications.md).
-### Getting the external endpoint
-
-NOTE: **Note:**
-With the following procedure, a load balancer must be installed in your cluster
-to obtain the endpoint. You can use either
-[Ingress](#installing-applications), or Knative's own load balancer
-([Istio](https://istio.io)) if using [Knative](#installing-applications).
-
-In order to publish your web application, you first need to find the endpoint which will be either an IP
-address or a hostname associated with your load balancer.
-
-#### Automatically determining the external endpoint
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/17052) in GitLab 10.6.
-
-After you install [Ingress or Knative](#installing-applications), GitLab attempts to determine the external endpoint
-and it should be available within a few minutes. If the endpoint doesn't appear
-and your cluster runs on Google Kubernetes Engine:
-
-1. Check your [Kubernetes cluster on Google Kubernetes Engine](https://console.cloud.google.com/kubernetes) to ensure there are no errors on its nodes.
-1. Ensure you have enough [Quotas](https://console.cloud.google.com/iam-admin/quotas) on Google Kubernetes Engine. For more information, see [Resource Quotas](https://cloud.google.com/compute/quotas).
-1. Check [Google Cloud's Status](https://status.cloud.google.com/) to ensure they are not having any disruptions.
-
-If GitLab is still unable to determine the endpoint of your Ingress or Knative application, you can
-manually determine it by following the steps below.
-
-#### Manually determining the external endpoint
-
-If the cluster is on GKE, click the **Google Kubernetes Engine** link in the
-**Advanced settings**, or go directly to the
-[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
-and select the proper project and cluster. Then click **Connect** and execute
-the `gcloud` command in a local terminal or using the **Cloud Shell**.
-
-If the cluster is not on GKE, follow the specific instructions for your
-Kubernetes provider to configure `kubectl` with the right credentials.
-The output of the following examples will show the external endpoint of your
-cluster. This information can then be used to set up DNS entries and forwarding
-rules that allow external access to your deployed applications.
-
-If you installed the Ingress [via the **Applications**](#installing-applications),
-run the following command:
-
-```bash
-kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
-```
-
-Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
-
-```bash
-kubectl get service --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
-```
-
-For Istio/Knative, the command will be different:
-
-```bash
-kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
-```
-
-Otherwise, you can list the IP addresses of all load balancers:
-
-```bash
-kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} '
-```
-
-#### Using a static IP
-
-By default, an ephemeral external IP address is associated to the cluster's load
-balancer. If you associate the ephemeral IP with your DNS and the IP changes,
-your apps will not be able to be reached, and you'd have to change the DNS
-record again. In order to avoid that, you should change it into a static
-reserved IP.
-
-Read how to [promote an ephemeral external IP address in GKE](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip).
-
-#### Pointing your DNS at the external endpoint
-
-Once you've set up the external endpoint, you should associate it with a [wildcard DNS
-record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) such as `*.example.com.`
-in order to be able to reach your apps. If your external endpoint is an IP address,
-use an A record. If your external endpoint is a hostname, use a CNAME record.
-
## Deploying to a Kubernetes cluster
A Kubernetes cluster can be the destination for a deployment job. If
@@ -654,8 +246,8 @@ GitLab CI/CD build environment.
| Variable | Description |
| -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. |
-| `KUBE_TOKEN` | The Kubernetes token of the [environment service account](#access-controls). |
-| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>-<environment>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
+| `KUBE_TOKEN` | The Kubernetes token of the [environment service account](add_remove_clusters.md#access-controls). |
+| `KUBE_NAMESPACE` | The namespace associated with the project's deployment service account. In the format `<project_name>-<project_id>-<environment>`. For GitLab-managed clusters, a matching namespace is automatically created by GitLab in the cluster. |
| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
@@ -668,6 +260,16 @@ service account of the cluster integration.
NOTE: **Note:**
If your cluster was created before GitLab 12.2, default `KUBE_NAMESPACE` will be set to `<project_name>-<project_id>`.
+When deploying a custom namespace:
+
+- The custom namespace must exist in your cluster.
+- The project's deployment service account must have permission to deploy to the namespace.
+- `KUBECONFIG` must be updated to use the custom namespace instead of the GitLab-provided default (this is [not automatic](https://gitlab.com/gitlab-org/gitlab/issues/31519)).
+- If deploying with Auto DevOps, you must *also* override `KUBE_NAMESPACE` with the custom namespace.
+
+CAUTION: **Caution:**
+GitLab does not save custom namespaces in the database. So while deployments work with custom namespaces, GitLab's integration for already-deployed environments will not pick up the customized values. For example, [Deploy Boards](../deploy_boards.md) will not work as intended for those deployments. For more information, see the [related issue](https://gitlab.com/gitlab-org/gitlab/issues/27630).
+
### Troubleshooting
Before the deployment jobs starts, GitLab creates the following specifically for
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 4036eaf0bfb..797ddf784cc 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -11,7 +11,31 @@ Everything you need to build, test, deploy, and run your app at scale.
## Overview
-[Kubernetes](https://kubernetes.io) pod logs can be viewed directly within GitLab. Logs can be displayed by clicking on a specific pod from [Deploy Boards](../deploy_boards.md):
+[Kubernetes](https://kubernetes.io) pod logs can be viewed directly within GitLab.
+
+![Pod logs](img/kubernetes_pod_logs_v12_5.png)
+
+## Requirements
+
+[Deploying to a Kubernetes environment](../deploy_boards.md#enabling-deploy-boards) is required in order to be able to use Pod Logs.
+
+## Usage
+
+To access pod logs, you must have the right [permissions](../../permissions.md#project-members-permissions).
+
+You can access them in two ways.
+
+### From the project sidebar
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22011) in GitLab 12.5.
+
+Go to **Operations > Pod logs** on the sidebar menu.
+
+![Sidebar menu](img/sidebar_menu_pod_logs_v12_5.png)
+
+### From Deploy Boards
+
+Logs can be displayed by clicking on a specific pod from [Deploy Boards](../deploy_boards.md):
1. Go to **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).
@@ -23,9 +47,3 @@ Everything you need to build, test, deploy, and run your app at scale.
- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/5769), environments.
Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/6502).
-
- ![Deploy Boards pod list](img/kubernetes_pod_logs_v12_4.png)
-
-## Requirements
-
-[Enabling Deploy Boards](../deploy_boards.md#enabling-deploy-boards) is required in order to be able to use Pod Logs.
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
index 7b17ec68234..bffae4c5069 100644
--- a/doc/user/project/clusters/runbooks/index.md
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -35,7 +35,7 @@ for an overview of how this is accomplished in GitLab!**
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 [GitLab's GKE integration](../index.md#add-new-gke-cluster).
+ The simplest way to get started is to add a cluster using one of [GitLab's integrations](../add_remove_clusters.md#add-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.
@@ -60,7 +60,7 @@ the components outlined above and the preloaded demo runbook.
### 1. Add a Kubernetes cluster
-Follow the steps outlined in [Add new GKE cluster](../index.md#add-new-gke-cluster)
+Follow the steps outlined in [Add new cluster](../add_remove_clusters.md#add-new-cluster)
to add a Kubernetes cluster to your project.
### 2. Install Helm Tiller, Ingress, and JupyterHub
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 9a9857bd5da..ffd7b0c0f2a 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -13,14 +13,16 @@ GitLab supports several ways deploy Serverless applications in both Kubernetes E
Currently we support:
-- [Knative](#knative): Build Knative applications with Knative and gitlabktl on GKE
-- [AWS Lambda](aws.md): Create serverless applications via the Serverless Framework and GitLab CI
+- [Knative](#knative): Build Knative applications with Knative and gitlabktl on GKE.
+- [AWS Lambda](aws.md): Create serverless applications via the Serverless Framework and GitLab CI.
## Knative
Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/knative/).
-Knative extends Kubernetes to provide a set of middleware components that are useful to build modern, source-centric, container-based applications. Knative brings some significant benefits out of the box through its main components:
+Knative extends Kubernetes to provide a set of middleware components that are useful to build
+modern, source-centric, container-based applications. Knative brings some significant benefits out
+of the box through its main components:
- [Serving](https://github.com/knative/serving): Request-driven compute that can scale to zero.
- [Eventing](https://github.com/knative/eventing): Management and delivery of events.
@@ -39,7 +41,7 @@ To run Knative on GitLab, you will need:
- If you are planning on deploying a serverless application, clone the sample [Knative Ruby App](https://gitlab.com/knative-examples/knative-ruby-app) to get started.
1. **Kubernetes Cluster:** An RBAC-enabled Kubernetes cluster is required to deploy Knative.
- The simplest way to get started is to add a cluster using [GitLab's GKE integration](../index.md#add-new-gke-cluster).
+ The simplest way to get started is to add a cluster using [GitLab's GKE integration](../add_remove_clusters.md#gke-cluster).
The set of minimum recommended cluster specifications to run Knative is 3 nodes, 6 vCPUs, and 22.50 GB memory.
1. **Helm Tiller:** Helm is a package manager for Kubernetes and is required to install
Knative.
@@ -62,20 +64,22 @@ To run Knative on GitLab, you will need:
using our [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes).
1. **Prometheus** (optional): Installing Prometheus allows you to monitor the scale and traffic of your serverless function/application.
See [Installing Applications](../index.md#installing-applications) for more information.
+1. **Logging** (optional): Configuring logging allows you to view and search request logs for your serverless function/application.
+ See [Configuring logging](#configuring-logging) for more information.
## Installing Knative via GitLab's Kubernetes integration
NOTE: **Note:**
The minimum recommended cluster size to run Knative is 3-nodes, 6 vCPUs, and 22.50 GB memory. **RBAC must be enabled.**
-1. [Add a Kubernetes cluster](../index.md) and [install Helm](../index.md#installing-applications).
+1. [Add a Kubernetes cluster](../add_remove_clusters.md) and [install Helm](../index.md#installing-applications).
1. Once Helm has been successfully installed, scroll down to the Knative app section. Enter the domain to be used with
your application/functions (e.g. `example.com`) and click **Install**.
![install-knative](img/install-knative.png)
1. After the Knative installation has finished, you can wait for the IP address or hostname to be displayed in the
- **Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../#manually-determining-the-external-endpoint).
+ **Knative Endpoint** field or [retrieve the Istio Ingress Endpoint manually](../../../clusters/applications.md#determining-the-external-endpoint-manually).
NOTE: **Note:**
Running `kubectl` commands on your cluster requires setting up access to the cluster first.
@@ -108,7 +112,7 @@ You must do the following:
1. Follow the steps to
[add an existing Kubernetes
- cluster](../index.md#add-existing-kubernetes-cluster).
+ cluster](../add_remove_clusters.md#add-existing-cluster).
1. Ensure GitLab can manage Knative:
- For a non-GitLab managed cluster, ensure that the service account for the token
@@ -164,13 +168,61 @@ You must do the following:
or [serverless applications](#deploying-serverless-applications) onto your
cluster.
-## Deploying functions
+## Configuring logging
-> Introduced in GitLab 11.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/33330) in GitLab 12.5.
+
+### Prerequisites
+
+- A GitLab-managed cluster.
+- `kubectl` installed and working.
+
+Running `kubectl` commands on your cluster requires setting up access to the
+cluster first. For clusters created on:
+
+- GKE, see [GKE Cluster Access](https://cloud.google.com/kubernetes-engine/docs/how-to/cluster-access-for-kubectl)
+- Other platforms, see [Install and Set Up kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
+
+### Enable request log template
+
+Run the following command to enable request logs:
+
+```shell
+kubectl edit cm -n knative-serving config-observability
+```
+
+Copy the `logging.request-log-template` from the `data._example` field to the data field one level up in the hierarchy.
+
+### Enable request logs
+
+Run the following commands to install Elasticsearch, Kibana, and Filebeat into a `kube-logging` namespace and configure all nodes to forward logs using Filebeat:
-Using functions is useful for dealing with independent events without needing
-to maintain a complex unified infrastructure. This allows you to focus on a
-single task that can be executed/scaled automatically and independently.
+```shell
+kubectl apply -f https://gitlab.com/gitlab-org/serverless/configurations/knative/raw/v0.7.0/kube-logging-filebeat.yaml
+kubectl label nodes --all beta.kubernetes.io/filebeat-ready="true"
+```
+
+### Viewing request logs
+
+To view request logs:
+
+1. Run `kubectl proxy`.
+1. Navigate to Kibana UI.
+
+Or:
+
+1. Open the Kibana UI.
+1. Click on **Discover**, then select `filebeat-*` from the dropdown on the left.
+1. Enter `kubernetes.container.name:"queue-proxy" AND message:/httpRequest/` into the search box.
+
+## Supported runtimes
+
+Serverless functions for GitLab can be written in 6 supported languages:
+
+- NodeJS and Ruby, with GitLab-managed and OpenFaas runtimes.
+- C#, Go, PHP, and Python with OpenFaaS runtimes only.
+
+### GitLab managed runtimes
Currently the following [runtimes](https://gitlab.com/gitlab-org/serverless/runtimes) are offered:
@@ -180,6 +232,31 @@ Currently the following [runtimes](https://gitlab.com/gitlab-org/serverless/runt
`Dockerfile` presence is assumed when a runtime is not specified.
+### OpenFaaS runtimes
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29253) in GitLab 12.5.
+
+[OpenFaaS classic runtimes](https://github.com/openfaas/templates#templates-in-store) can be used with GitLab serverless.
+Runtimes are specified using the pattern: `openfaas/classic/<template_name>`. The following
+example shows how to define a function in `serverless.yml` using an OpenFaaS runtime:
+
+```yaml
+hello:
+ source: ./hello
+ runtime: openfaas/classic/ruby
+ description: "Ruby function using OpenFaaS classic runtime"
+```
+
+`handler` is not needed for OpenFaaS functions. The location of the handler is defined
+by the conventions of the runtime.
+
+See the [`ruby-openfaas-function`](https://gitlab.com/knative-examples/ruby-openfaas-function)
+project for an example of a function using an OpenFaaS runtime.
+
+## Deploying functions
+
+> Introduced in GitLab 11.6.
+
You can find and import all the files referenced in this doc in the
**[functions example project](https://gitlab.com/knative-examples/functions)**.
@@ -311,10 +388,49 @@ The sample function can now be triggered from any HTTP client using a simple `PO
![function execution](img/function-execution.png)
+### Running functions locally
+
+Running a function locally is a good way to quickly verify behavior during development.
+
+Running functions locally requires:
+
+- Go 1.12 or newer installed.
+- Docker Engine installed and running.
+- `gitlabktl` installed using the Go package manager:
+
+ ```shell
+ GO111MODULE=on go get gitlab.com/gitlab-org/gitlabktl
+ ```
+
+To run a function locally:
+
+1. Navigate to the root of your GitLab serverless project.
+1. Build your function into a Docker image:
+
+ ```shell
+ gitlabktl serverless build
+ ```
+
+1. Run your function in Docker:
+
+ ```shell
+ docker run -itp 8080:8080 <your_function_name>
+ ```
+
+1. Invoke your function:
+
+ ```shell
+ curl http://localhost:8080
+ ```
+
## Deploying Serverless applications
> Introduced in GitLab 11.5.
+Serverless applications are the building block of serverless functions. They are useful in scenarios where an existing
+runtime does not meet the needs of an application, such as one written in a language that has no runtime available. Note
+though that serverless applications should be stateless!
+
NOTE: **Note:**
You can reference and import the sample [Knative Ruby App](https://gitlab.com/knative-examples/knative-ruby-app) to get started.
diff --git a/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
index f813b60dcd9..f813b60dcd9 100755..100644
--- a/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
+++ b/doc/user/project/img/code_owners_approval_new_protected_branch_v12_4.png
Binary files differ
diff --git a/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
index 59da6874d14..59da6874d14 100755..100644
--- a/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
+++ b/doc/user/project/img/code_owners_approval_protected_branch_v12_4.png
Binary files differ
diff --git a/doc/workflow/time_tracking/img/time_tracking_example_v12_2.png b/doc/user/project/img/time_tracking_example_v12_2.png
index 31d8c490ed1..31d8c490ed1 100644
--- a/doc/workflow/time_tracking/img/time_tracking_example_v12_2.png
+++ b/doc/user/project/img/time_tracking_example_v12_2.png
Binary files differ
diff --git a/doc/workflow/time_tracking/img/time_tracking_sidebar_v8_16.png b/doc/user/project/img/time_tracking_sidebar_v8_16.png
index 22124afed6f..22124afed6f 100644
--- a/doc/workflow/time_tracking/img/time_tracking_sidebar_v8_16.png
+++ b/doc/user/project/img/time_tracking_sidebar_v8_16.png
Binary files differ
diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md
index f883e4474e2..94ab9d9195b 100644
--- a/doc/user/project/import/gitea.md
+++ b/doc/user/project/import/gitea.md
@@ -75,7 +75,5 @@ You also can:
![Gitea importer page](img/import_projects_from_gitea_importer_v12_3.png)
----
-
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index 0aeca7f73ad..9b98c52c4b8 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -125,7 +125,7 @@ your GitHub repositories are listed.
## Mirroring and pipeline status sharing
-Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep
+Depending your GitLab tier, [project mirroring](../repository/repository_mirroring.md) can be set up to keep
your imported project in sync with its GitHub copy.
Additionally, you can configure GitLab to send pipeline status updates back GitHub with the
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 7ae288996da..c173d3d3e11 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -26,6 +26,7 @@ When you create a project in GitLab, you'll have access to a large number of
from messing with history or pushing code without review
- [Protected tags](protected_tags.md): Control over who has
permission to create tags, and prevent accidental update or deletion
+ - [Repository mirroring](repository/repository_mirroring.md)
- [Signing commits](gpg_signed_commits/index.md): use GPG to sign your commits
- [Deploy tokens](deploy_tokens/index.md): Manage project-based deploy tokens that allow permanent access to the repository and Container Registry.
- [Web IDE](web_ide/index.md)
@@ -44,7 +45,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Review Apps](../../ci/review_apps/index.md): Live preview the results
of the changes proposed in a merge request in a per-branch basis
- [Labels](labels.md): Organize issues and merge requests by labels
-- [Time Tracking](../../workflow/time_tracking.md): Track estimate time
+- [Time Tracking](time_tracking.md): Track estimate time
and time spent on
the conclusion of an issue or merge request
- [Milestones](milestones/index.md): Work towards a target date
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index ec43696fdee..62310dd9177 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -1,6 +1,6 @@
# Generic alerts integration **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4.
GitLab can accept alerts from any source via a generic webhook receiver.
When you set up the generic alerts integration, a unique endpoint will
@@ -16,7 +16,7 @@ authored by the GitLab Alert Bot.
To set up the generic alerts integration:
1. Navigate to **Settings > Integrations** in a project.
-1. Click on **Alert endpoint**.
+1. Click on **Alerts endpoint**.
1. Toggle the **Active** alert setting. The `URL` and `Authorization Key` for the webhook configuration can be found there.
## Customizing the payload
@@ -37,12 +37,12 @@ Example request:
```sh
curl --request POST \
--data '{"title": "Incident title"}' \
- --header "Authorization: Bearer <autorization_key>" \
+ --header "Authorization: Bearer <authorization_key>" \
--header "Content-Type: application/json" \
<url>
```
-The `<autorization_key>` and `<url>` values can be found when [setting up generic alerts](#setting-up-generic-alerts).
+The `<authorization_key>` and `<url>` values can be found when [setting up generic alerts](#setting-up-generic-alerts).
Example payload:
diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
index 50adb5993e5..c1e6f93de30 100644
--- a/doc/user/project/integrations/gitlab_slack_application.md
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -31,7 +31,7 @@ integration settings.
Keep in mind that you need to have the appropriate permissions for your Slack
team in order to be able to install a new application, read more in Slack's
-docs on [Adding an app to your team][slack-docs].
+docs on [Adding an app to your team](https://slack.com/help/articles/202035138).
To enable GitLab's service for your Slack team:
@@ -60,6 +60,5 @@ project, you would do:
/gitlab gitlab-org/gitlab issue show 1001
```
-[slack-docs]: https://get.slack.help/hc/en-us/articles/202035138-Adding-apps-to-your-team
[slash commands]: ../../../integration/slash_commands.md
[slack-manual]: slack_slash_commands.md
diff --git a/doc/user/project/integrations/img/embed_metrics_issue_template.png b/doc/user/project/integrations/img/embed_metrics_issue_template.png
new file mode 100644
index 00000000000..3c6a243e5c1
--- /dev/null
+++ b/doc/user/project/integrations/img/embed_metrics_issue_template.png
Binary files differ
diff --git a/doc/user/project/integrations/img/grafana_panel_v12_5.png b/doc/user/project/integrations/img/grafana_panel_v12_5.png
new file mode 100644
index 00000000000..18c17b910cd
--- /dev/null
+++ b/doc/user/project/integrations/img/grafana_panel_v12_5.png
Binary files differ
diff --git a/doc/user/project/integrations/img/grafana_sharing_dialog_v12_5.png b/doc/user/project/integrations/img/grafana_sharing_dialog_v12_5.png
new file mode 100644
index 00000000000..fae62dd50df
--- /dev/null
+++ b/doc/user/project/integrations/img/grafana_sharing_dialog_v12_5.png
Binary files differ
diff --git a/doc/user/project/integrations/img/heatmap_panel_type.png b/doc/user/project/integrations/img/heatmap_panel_type.png
new file mode 100644
index 00000000000..a2b3911ec68
--- /dev/null
+++ b/doc/user/project/integrations/img/heatmap_panel_type.png
Binary files differ
diff --git a/doc/user/project/integrations/img/http_proxy_access_v12_5.png b/doc/user/project/integrations/img/http_proxy_access_v12_5.png
new file mode 100644
index 00000000000..0036a916a12
--- /dev/null
+++ b/doc/user/project/integrations/img/http_proxy_access_v12_5.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_dashboard_anomaly_panel_type.png b/doc/user/project/integrations/img/prometheus_dashboard_anomaly_panel_type.png
new file mode 100644
index 00000000000..5cba6fa9038
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_dashboard_anomaly_panel_type.png
Binary files differ
diff --git a/doc/user/project/integrations/img/rendered_grafana_embed_v12_5.png b/doc/user/project/integrations/img/rendered_grafana_embed_v12_5.png
new file mode 100644
index 00000000000..6cabe4193bd
--- /dev/null
+++ b/doc/user/project/integrations/img/rendered_grafana_embed_v12_5.png
Binary files differ
diff --git a/doc/user/project/integrations/img/select_query_variables_v12_5.png b/doc/user/project/integrations/img/select_query_variables_v12_5.png
new file mode 100644
index 00000000000..23503577327
--- /dev/null
+++ b/doc/user/project/integrations/img/select_query_variables_v12_5.png
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 6d2a0563ec1..874a1092b73 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -59,7 +59,7 @@ When connecting to **Jira Cloud**, which supports authentication via API token,
> higher is required.
> - GitLab 8.14 introduced a new way to integrate with Jira which greatly simplified
> the configuration options you have to enter. If you are using an older version,
-> [follow this documentation][jira-repo-old-docs].
+> [follow this documentation](https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable-ee/doc/project_services/jira.md).
> - In order to support Oracle's Access Manager, GitLab will send additional cookies
> to enable Basic Auth. The cookie being added to each request is `OBBasicAuth` with
> a value of `fromDialog`.
@@ -205,4 +205,3 @@ authenticate with the Jira site. You will need to log in to your Jira instance
and complete the CAPTCHA.
[services-templates]: services_templates.md
-[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable/doc/project_services/jira.md
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index e385ee53636..315039f82b3 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -58,7 +58,7 @@ Click on the service links to see further configuration instructions and details
## Push hooks limit
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31009) in GitLab 12.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/17874) in GitLab 12.4.
If a single push includes changes to more than three branches or tags, services
supported by `push_hooks` and `tag_push_hooks` events won't be executed.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index d7666d00e76..d3d4afefb59 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -117,7 +117,7 @@ You can view the performance dashboard for an environment by [clicking on the mo
Custom metrics can be monitored by adding them on the monitoring dashboard page. Once saved, they will be displayed on the environment performance dashboard provided that either:
-- A [connected Kubernetes cluster](../clusters/index.md#adding-and-removing-clusters) with the environment scope of `*` is used and [Prometheus installed on the cluster](#enabling-prometheus-integration), or
+- A [connected Kubernetes cluster](../clusters/add_remove_clusters.md) with the environment scope of `*` is used and [Prometheus installed on the cluster](#enabling-prometheus-integration)
- Prometheus is [manually configured](#manual-configuration-of-prometheus).
![Add New Metric](img/prometheus_add_metric.png)
@@ -139,7 +139,7 @@ GitLab supports a limited set of [CI variables](../../../ci/variables/README.htm
- CI_ENVIRONMENT_SLUG
- KUBE_NAMESPACE
-To specify a variable in a query, enclose it in curly braces with a leading percent. For example: `%{ci_environment_slug}`.
+To specify a variable in a query, enclose it in quotation marks with curly braces with a leading percent. For example: `"%{ci_environment_slug}"`.
### Defining custom dashboards per project
@@ -211,11 +211,11 @@ The following tables outline the details of expected properties.
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------- |
-| `type` | enum | no, defaults to `area-chart` | Specifies the chart type to use, can be `area-chart` or `line-chart` |
+| `type` | enum | no, defaults to `area-chart` | Specifies the chart type to use, can be: `area-chart`, `line-chart` or `anomaly-chart`. |
| `title` | string | yes | Heading for the panel. |
| `y_label` | string | no, but highly encouraged | Y-Axis label for the panel. |
| `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. |
+| `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`. |
**Metrics (`metrics`) properties:**
@@ -231,20 +231,20 @@ The following tables outline the details of expected properties.
The below panel types are supported in monitoring dashboards.
-##### Area
+##### Area or Line Chart
-To add an area panel type to a dashboard, look at the following sample dashboard file:
+To add an area chart panel type to a dashboard, look at the following sample dashboard file:
```yaml
dashboard: 'Dashboard Title'
panel_groups:
- group: 'Group Title'
panels:
- - type: area-chart
- title: "Chart Title"
+ - type: area-chart # or line-chart
+ title: 'Area Chart Title'
y_label: "Y-Axis"
metrics:
- - id: 10
+ - id: area_http_requests_total
query_range: 'http_requests_total'
label: "Metric of Ages"
unit: "count"
@@ -255,10 +255,52 @@ Note the following properties:
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
| type | string | no | Type of panel to be rendered. Optional for area panel types |
-| query_range | yes | required | For area panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
+| query_range | string | required | For area panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
![area panel type](img/prometheus_dashboard_area_panel_type.png)
+##### Anomaly chart
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16530) in GitLab 12.5.
+
+To add an anomaly chart panel type to a dashboard, add add a panel with *exactly* 3 metrics.
+
+The first metric represents the current state, and the second and third metrics represent the upper and lower limit respectively:
+
+```yaml
+dashboard: 'Dashboard Title'
+panel_groups:
+ - group: 'Group Title'
+ panels:
+ - type: anomaly-chart
+ title: "Chart Title"
+ y_label: "Y-Axis"
+ metrics:
+ - id: anomaly_requests_normal
+ query_range: 'http_requests_total'
+ label: "# of Requests"
+ unit: "count"
+ metrics:
+ - id: anomaly_requests_upper_limit
+ query_range: 10000
+ label: "Max # of requests"
+ unit: "count"
+ metrics:
+ - id: anomaly_requests_lower_limit
+ query_range: 2000
+ label: "Min # of requests"
+ unit: "count"
+```
+
+Note the following properties:
+
+| Property | Type | Required | Description |
+| ------ | ------ | ------ | ------ |
+| type | string | required | Must be `anomaly-chart` for anomaly panel types |
+| query_range | yes | required | For anomaly panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) in every metric. |
+
+![anomaly panel type](img/prometheus_dashboard_anomaly_panel_type.png)
+
##### Single Stat
To add a single stat panel type to a dashboard, look at the following sample dashboard file:
@@ -286,6 +328,42 @@ Note the following properties:
![single stat panel type](img/prometheus_dashboard_single_stat_panel_type.png)
+##### Heatmaps
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30581) in GitLab 12.5.
+
+To add a heatmap panel type to a dashboard, look at the following sample dashboard file:
+
+```yaml
+dashboard: 'Dashboard Title'
+panel_groups:
+ - group: 'Group Title'
+ panels:
+ - title: "Heatmap"
+ type: "heatmap"
+ metrics:
+ - id: 10
+ query: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)'
+ unit: req/sec
+ label: "Status code"
+```
+
+Note the following properties:
+
+| Property | Type | Required | Description |
+| ------ | ------ | ------ | ------ |
+| type | string | yes | Type of panel to be rendered. For heatmap panel types, set to `heatmap` |
+| query_range | yes | yes | For area panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
+
+![heatmap panel type](img/heatmap_panel_type.png)
+
+### 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.
+
### Downloading data as CSV
Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
@@ -332,9 +410,12 @@ 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.
+
### Taking action on incidents **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
+>- [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:
@@ -355,6 +436,8 @@ 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.
+
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.
Since [version 12.2](https://gitlab.com/gitlab-org/gitlab-foss/issues/63373), GitLab will tag each incident issue with the `incident` label automatically. If the label does not yet exist, it will be created automatically as well.
@@ -389,6 +472,8 @@ Prometheus server.
## Embedding metric charts within GitLab Flavored Markdown
+### Embedding GitLab-managed Kubernetes metrics
+
> [Introduced][ce-29691] in GitLab 12.2.
It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
@@ -414,9 +499,19 @@ The following requirements must be met for the metric to unfurl:
![Embedded Metrics](img/embed_metrics.png)
-### Embedding live Grafana charts
+### 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.
+
+![Embedded Metrics in issue templates](img/embed_metrics_issue_template.png)
+
+### Embedding Grafana charts
-It is also possible to embed live [Grafana](https://docs.gitlab.com/omnibus/settings/grafana.html) charts within issues, as a [Direct Linked Rendered Image](https://grafana.com/docs/reference/sharing/#direct-link-rendered-image).
+Grafana metrics can be embedded in [GitLab Flavored Markdown](../../markdown.md).
+
+#### Embedding charts via Grafana Rendered Images
+
+It is possible to embed live [Grafana](https://docs.gitlab.com/omnibus/settings/grafana.html) charts in issues, as a [direct linked rendered image](https://grafana.com/docs/reference/sharing/#direct-link-rendered-image).
The sharing dialog within Grafana provides the link, as highlighted below.
@@ -435,6 +530,41 @@ This will render like so:
<img src="https://dashboards.gitlab.com/render/d-solo/RZmbBr7mk/gitlab-triage?orgId=1&refresh=30s&var-env=gprd&var-environment=gprd&var-prometheus=prometheus-01-inf-gprd&var-prometheus_app=prometheus-app-01-inf-gprd&var-backend=All&var-type=All&var-stage=main&panelId=1247&width=1000&height=300"/>
+#### Embedding charts via integration with Grafana HTTP API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/31376) in GitLab 12.5.
+
+Each project can support integration with one Grafana instance. This configuration allows a user to copy a link to a panel in Grafana, then paste it into a GitLab markdown field. The chart will be rendered in the GitLab chart format.
+
+Prerequisites for embedding from a Grafana instance:
+
+1. The datasource must be a Prometheus instance.
+1. The datasource must be proxyable, so the HTTP Access setting should be set to `Server`.
+
+![HTTP Proxy Access](img/http_proxy_access_v12_5.png)
+
+##### Setting up the Grafana integration
+
+1. [Generate an Admin-level API Token in Grafana.](https://grafana.com/docs/http_api/auth/#create-api-token)
+1. In your GitLab project, navigate to **Settings > Operations > Grafana Authentication**.
+1. To enable the integration, check the "Active" checkbox.
+1. For "Grafana URL", enter the base URL of the Grafana instance.
+1. For "API Token", enter the Admin API Token you just generated.
+1. Click **Save Changes**.
+
+##### Generating a link to a chart
+
+1. In Grafana, navigate to the dashboard you wish to embed a panel from.
+ ![Grafana Metric Panel](img/grafana_panel_v12_5.png)
+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.
+1. If your Prometheus queries use Grafana's custom template variables, ensure that "Template variables" and "Current time range" options are toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported.
+ ![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)
+
## Troubleshooting
If the "No data found" screen continues to appear, it could be due to:
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index d630956c109..93f6dbb0302 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
### About managed NGINX Ingress deployments
-NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
+NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../../clusters/applications.md#ingress).
NGINX is configured for Prometheus monitoring, by setting:
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 83eac44666c..a1dcb105196 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
@@ -30,7 +30,7 @@ For other deployments, there is [some configuration](#manually-setting-up-nginx-
### About managed NGINX Ingress deployments
-NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../clusters/index.md#getting-the-external-endpoint).
+NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's Endpoint](../../../clusters/applications.md#ingress).
NGINX is configured for Prometheus monitoring, by setting:
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 103d50a94e8..403972941b2 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -287,7 +287,7 @@ Different issue board features are available in different [GitLab tiers](https:/
| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists |
|----------|--------------------------------|------------------------------|---------------------------|----------------|
-| Core / Free | 1 | 1 | No | No |
+| Core / Free | Multiple | 1 | No | No |
| Starter / Bronze | Multiple | 1 | Yes | No |
| Premium / Silver | Multiple | Multiple | Yes | Yes |
| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
diff --git a/doc/user/project/issues/associate_zoom_meeting.md b/doc/user/project/issues/associate_zoom_meeting.md
new file mode 100644
index 00000000000..24775204c9f
--- /dev/null
+++ b/doc/user/project/issues/associate_zoom_meeting.md
@@ -0,0 +1,42 @@
+# Associate a Zoom meeting with an issue
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/16609) in GitLab 12.4.
+
+In order to communicate synchronously for incidents management,
+GitLab allows 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.
+
+## Adding a zoom meeting to an issue
+
+To associate a zoom meeting with an issue, you can use GitLab's
+[quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
+
+In an issue, leave a comment using the `/zoom` quick action followed by a valid Zoom link:
+
+```sh
+/zoom https://zoom.us/j/123456789
+```
+
+If the Zoom meeting URL is valid and you have at least [Reporter permissions](../../permissions.md),
+a system alert will notify you that the addition of the meeting URL was successful.
+The issue's description will be automatically edited to include the Zoom link, and a button will
+appear right under the issue's title.
+
+![Link Zoom Call in Issue](img/zoom-quickaction-button.png)
+
+You are only allowed to attach a single Zoom meeting to an issue. If you attempt
+to add a second Zoom meeting using the `/zoom` quick action, it won't work, you
+need to [remove it](#removing-an-existing-zoom-meeting-from-an-issue) first.
+
+## Removing an existing Zoom meeting from an issue
+
+Similarly to adding a zoom meeting, you can remove it with a quick action:
+
+```sh
+/remove_zoom
+```
+
+If you have at least [Reporter permissions](../../permissions.md),
+a system alert will notify you that the meeting URL was successfully removed.
diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md
index fb7fdde7b94..b97bcd47f61 100644
--- a/doc/user/project/issues/csv_export.md
+++ b/doc/user/project/issues/csv_export.md
@@ -67,8 +67,8 @@ Data will be encoded with a comma as the column delimiter, with `"` used to quot
| Milestone | Title of the issue milestone |
| Weight | Issue weight |
| Labels | Title of any labels joined with a `,` |
-| Time Estimate | [Time estimate](../../../workflow/time_tracking.md#estimates) in seconds |
-| Time Spent | [Time spent](../../../workflow/time_tracking.md#time-spent) in seconds |
+| Time Estimate | [Time estimate](../time_tracking.md#estimates) in seconds |
+| Time Spent | [Time spent](../time_tracking.md#time-spent) in seconds |
## Limitations
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index 169da7049a6..594f73dbfbe 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -22,24 +22,26 @@ For an overview, see the video [Design Management (GitLab 12.2)](https://www.you
## Requirements
Design Management requires
-[Large File Storage (LFS)](../../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
+[Large File Storage (LFS)](../../../administration/lfs/manage_large_binaries_with_git_lfs.md)
to be enabled:
- For GitLab.com, LFS is already enabled.
- For self-managed instances, a GitLab administrator must have
- [enabled LFS globally](../../../workflow/lfs/lfs_administration.md).
+ [enabled LFS globally](../../../administration/lfs/lfs_administration.md).
- For both GitLab.com and self-managed instances: LFS must be enabled for the project itself.
If enabled globally, LFS will be enabled by default to all projects. To enable LFS on the
project level, navigate to your project's **Settings > General**, expand **Visibility, project features, permissions**
and enable **Git Large File Storage**.
+Design Management requires that projects are using
+[hashed storage](../../../administration/repository_storage_types.html#hashed-storage)
+(the default storage type since v10.0).
+
## Limitations
- Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`.
The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab/issues/12771).
- Design uploads are limited to 10 files at a time.
-- Design Management is
- [not yet supported in the project export](https://gitlab.com/gitlab-org/gitlab/issues/11090).
- Design Management data
[isn't deleted when a project is destroyed](https://gitlab.com/gitlab-org/gitlab/issues/13429) yet.
- Design Management data [won't be moved](https://gitlab.com/gitlab-org/gitlab/issues/13426)
@@ -112,12 +114,16 @@ viewed by browsing previous versions.
## Adding annotations to designs
-When a design image is displayed, you can add annotations to it by clicking on
-the image. A badge is added to the image and a form is displayed to start a new
-discussion. For example:
+When a design is uploaded, you can add annotations by clicking on
+the image on the exact location you'd like to add the note to.
+A badge is added to the image identifying the annotation, from
+which you can start a new discussion:
![Starting a new discussion on design](img/adding_note_to_design_1.png)
-When submitted, the form saves a badge linked to the discussion on the image. Different discussions have different badge numbers. For example:
+Different discussions have different badge numbers:
![Discussions on design annotations](img/adding_note_to_design_2.png)
+
+From GitLab 12.5 on, new annotations will be outputted to the issue activity,
+so that everyone involved can participate in the discussion.
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index 240859651e2..b19d5dc1650 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -33,7 +33,7 @@ the icon and the date colored red. You can sort issues by those that are
![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png)
-Due dates also appear in your [todos list](../../../workflow/todos.md).
+Due dates also appear in your [todos list](../../todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
diff --git a/doc/workflow/issue_weight/issue.png b/doc/user/project/issues/img/issue_weight.png
index 3800b5940b8..3800b5940b8 100644
--- a/doc/workflow/issue_weight/issue.png
+++ b/doc/user/project/issues/img/issue_weight.png
Binary files differ
diff --git a/doc/user/project/issues/img/select_all_designs_v12_4.png b/doc/user/project/issues/img/select_all_designs_v12_4.png
deleted file mode 100644
index b08b04c1214..00000000000
--- a/doc/user/project/issues/img/select_all_designs_v12_4.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/zoom-quickaction-button.png b/doc/user/project/issues/img/zoom-quickaction-button.png
index d6d691b2267..c95a56b43e8 100644
--- a/doc/user/project/issues/img/zoom-quickaction-button.png
+++ b/doc/user/project/issues/img/zoom-quickaction-button.png
Binary files differ
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index 01f4eb5b912..92da4235afa 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -41,7 +41,7 @@ after it is closed.
#### 2. To Do
-You can add issues to and remove issues from your [GitLab To-Do List](../../../workflow/todos.md).
+You can add issues to and remove issues from your [GitLab To-Do List](../../todos.md).
The button to do this has a different label depending on whether the issue is already on your To-Do List or not. If the issue is:
@@ -83,9 +83,9 @@ Select a [milestone](../milestones/index.md) to attribute that issue to.
#### 6. Time Tracking
-Use [GitLab Quick Actions](../quick_actions.md) to [track estimates and time spent on issues](../../../workflow/time_tracking.md).
-You can add an [estimate of the time it will take](../../../workflow/time_tracking.md#estimates)
-to resolve the issue, and also add [the time spent](../../../workflow/time_tracking.md#time-spent)
+Use [GitLab Quick Actions](../quick_actions.md) to [track estimates and time spent on issues](../time_tracking.md).
+You can add an [estimate of the time it will take](../time_tracking.md#estimates)
+to resolve the issue, and also add [the time spent](../time_tracking.md#time-spent)
on the resolution of the issue.
#### 7. Due date
@@ -109,7 +109,7 @@ from which you can select **Create new label**.
#### 9. Weight **(STARTER)**
-[Assign a weight](../../../workflow/issue_weight.md) to an issue.
+[Assign a weight](issue_weight.md) to an issue.
Larger values are used to indicate more effort is required to complete the issue. Only
positive values or zero are allowed.
@@ -131,7 +131,7 @@ or were mentioned in the description or threads.
#### 13. Notifications
-Click on the icon to enable/disable [notifications](../../../workflow/notifications.md#issue--epics--merge-request-events)
+Click on the icon to enable/disable [notifications](../../profile/notifications.md#issue--epics--merge-request-events)
for the issue. This will automatically enable if you participate in the issue in any way.
- **Enable**: If you are not a participant in the discussion on that issue, but
@@ -162,7 +162,7 @@ allowing many formatting options.
You can mention a user or a group present in your GitLab instance with `@username` or
`@groupname` and they will be notified via todos and email, unless they have disabled
all notifications in their profile settings. This is controlled in the
-[notification settings](../../../workflow/notifications.md).
+[notification settings](../../profile/notifications.md).
Mentions for yourself (the current logged in user), will be highlighted in a different
color, allowing you to easily see which comments involve you, helping you focus on
@@ -257,4 +257,4 @@ You can attach and remove Zoom meetings to issues using the `/zoom` and `/remove
Attaching a [Zoom](https://zoom.us) call an issue
results in a **Join Zoom meeting** button at the top of the issue, just under the header.
-![Link Zoom Call in Issue](img/zoom-quickaction-button.png)
+Read more how to [add or remove a zoom meeting](associate_zoom_meeting.md).
diff --git a/doc/user/project/issues/issue_weight.md b/doc/user/project/issues/issue_weight.md
new file mode 100644
index 00000000000..4b8d2318e9b
--- /dev/null
+++ b/doc/user/project/issues/issue_weight.md
@@ -0,0 +1,25 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/issue_weight.html'
+---
+
+# Issue weight **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/76) in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
+
+When you have a lot of issues, it can be hard to get an overview.
+By adding a weight to each issue, you can get a better idea of how much time,
+value or complexity a given issue has or will cost.
+
+You can set the weight of an issue during its creation, by simply changing the
+value in the dropdown menu. You can set it to a non-negative integer
+value from 0, 1, 2, and so on. (The database stores a 4-byte value, so the
+upper bound is essentially limitless).
+You can remove weight from an issue
+as well.
+
+This value will appear on the right sidebar of an individual issue, as well as
+in the issues page next to a distinctive balance scale icon.
+
+As an added bonus, you can see the total sum of all issues on the milestone page.
+
+![issue page](img/issue_weight.png)
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index cfd6d4eaf4b..d8356abdd1c 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -210,8 +210,8 @@ The following can be filtered by labels:
## Subscribing to labels
From the project label list page and the group label list page, you can subscribe
-to [notifications](../../workflow/notifications.md) of a given label, to alert you
-that the label has been assigned to an epic, issue, and merge request.
+to [notifications](../profile/notifications.md) of a given label, to alert you
+that the label has been assigned to an epic, issue, or merge request.
![Labels subscriptions](img/labels_subscriptions_v12_1.png)
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 92681e741de..69bdfe10e3f 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -66,6 +66,18 @@ will scan your source code for code quality issues. The report will be saved as
that you can later download and analyze. Due to implementation limitations we always
take the latest Code Quality artifact available.
+By default, report artifacts are not downloadable. If you need them downloadable on the
+job details page, you can add `gl-code-quality-report.json` to the artifact paths like so:
+
+```yaml
+include:
+ - template: Code-Quality.gitlab-ci.yml
+
+code_quality:
+ artifacts:
+ paths: [gl-code-quality-report.json]
+```
+
The included `code_quality` job is running in the `test` stage, so it needs to be included in your CI config, like so:
```yaml
@@ -91,7 +103,7 @@ old job definitions are still maintained they have been deprecated and may be re
in the next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
configuration to reflect that change.
-For GitLab 11.5 and earlier, the job should look like:
+For GitLab 11.5 and later, the job should look like:
```yaml
code_quality:
diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md
new file mode 100644
index 00000000000..084ebf32a92
--- /dev/null
+++ b/doc/user/project/merge_requests/creating_merge_requests.md
@@ -0,0 +1,156 @@
+---
+type: index, reference
+---
+
+# Creating merge requests
+
+Merge requests are the primary method of making changes to files in a GitLab project.
+Changes are proposed by creating and submitting a merge request, which is then
+[reviewed, and accepted (or rejected)](reviewing_and_managing_merge_requests.md),
+all within GitLab.
+
+## Creating new merge requests
+
+You can start creating a new merge request by clicking the **New merge request** button
+on the **Merge Requests** page in a project. Then you must choose the source project and
+branch that contain your changes, and the target project and branch where you want to merge
+the changes into. Click on **Compare branches and continue** to go to the next step
+and start filling in the merge request details.
+
+When viewing the commits on a branch other than master in **Repository > Commits**, you
+can click on the **Create merge request** button, and a new merge request will be started
+using the current branch as the source, and `master` in the current project as the target.
+
+If you have recently pushed changes to GitLab, the **Create merge request** button will
+also appear in the top right of the:
+
+- **Project** page.
+- **Repository > Files** page.
+- **Merge Requests** page.
+
+In this case, the merge request will use the most recent branch you pushed changes
+to as the source branch, and `master` in the current project as the target.
+
+## Workflow for new merge requests
+
+On the **New Merge Request** page, you can start by filling in the title and description
+for the merge request. If there are are already commits on the branch, the title will
+be pre-filled with the first line of the first commit message, and the description will
+be pre-filled with any additional lines in the commit message. The title is the only
+field that is mandatory in all cases.
+
+From here, you can also:
+
+- Set the merge request as a [work in progress](work_in_progress_merge_requests.md).
+- Select the [assignee](#assignee), or [assignees](#multiple-assignees-starter). **(STARTER)**
+- Select a [milestone](../milestones/index.md).
+- Select [labels](../labels.md).
+- Add any [merge request dependencies](merge_request_dependencies.md). **(PREMIUM)**
+- Select [approval options](merge_request_approvals.md). **(STARTER)**
+- Verify the source and target branches are correct.
+- Enable the [delete source branch when merge request is accepted](#deleting-the-source-branch) option.
+- Enable the [squash commits when merge request is accepted](squash_and_merge.md) option.
+- If the merge request is from a fork, enable [Allow collaboration on merge requests across forks](allow_collaboration.md).
+
+Many of these can be set when pushing changes from the command line, with
+[Git push options](../push_options.md).
+
+### Merge requests to close issues
+
+If the merge request is being created to resolve an issue, you can add a note in the
+description which will set it to [automatically close the issue](../issues/managing_issues.md#closing-issues-automatically)
+when merged.
+
+If the issue is [confidential](../issues/confidential_issues.md), you may want to
+use a different workflow for [merge requests for confidential issues](../issues/confidential_issues.md#merge-requests-for-confidential-issues),
+to prevent confidential information from being exposed.
+
+## Assignee
+
+Choose an assignee to designate someone as the person responsible for the first
+[review of the merge request](reviewing_and_managing_merge_requests.md). Open the
+drop down box to search for the user you wish to assign, and the merge request will be
+added to their [assigned merge request list](../../search/index.md#issues-and-merge-requests).
+
+### Multiple assignees **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2004) in [GitLab Starter 11.11](https://about.gitlab.com/pricing/).
+
+Multiple people often review merge requests at the same time. GitLab allows you to
+have multiple assignees for merge requests to indicate everyone that is reviewing or
+accountable for it.
+
+![multiple assignees for merge requests sidebar](img/multiple_assignees_for_merge_requests_sidebar.png)
+
+To assign multiple assignees to a merge request:
+
+1. From a merge request, expand the right sidebar and locate the **Assignees** section.
+1. Click on **Edit** and from the dropdown menu, select as many users as you want
+ to assign the merge request to.
+
+Similarly, assignees are removed by deselecting them from the same dropdown menu.
+
+It's also possible to manage multiple assignees:
+
+- When creating a merge request.
+- Using [quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
+
+## Deleting the source branch
+
+When creating a merge request, select the "Delete source branch when merge
+request accepted" option and the source branch will be deleted when the merge
+request is merged. To make this option enabled by default for all new merge
+requests, enable it in the [project's settings](../settings/index.md#merge-request-settings).
+
+This option is also visible in an existing merge request next to the merge
+request button and can be selected/deselected before merging. It's only visible
+to users with [Maintainer permissions](../../permissions.md) in the source project.
+
+If the user viewing the merge request does not have the correct permissions to
+delete the source branch and the source branch is set for deletion, the merge
+request widget will show the "Deletes source branch" text.
+
+![Delete source branch status](img/remove_source_branch_status.png)
+
+## Create new merge requests by email
+
+_This feature needs [incoming email](../../../administration/incoming_email.md)
+to be configured by a GitLab administrator to be available for CE/EE users, and
+it's available on GitLab.com._
+
+You can create a new merge request by sending an email to a user-specific email
+address. The address can be obtained on the merge requests page by clicking on
+a **Email a new merge request to this project** button. The subject will be
+used as the source branch name for the new merge request and the target branch
+will be the default branch for the project. The message body (if not empty)
+will be used as the merge request description. You need
+["Reply by email"](../../../administration/reply_by_email.md) enabled to use
+this feature. If it's not enabled to your instance, you may ask your GitLab
+administrator to do so.
+
+This is a private email address, generated just for you. **Keep it to yourself**
+as anyone who gets ahold of it can create issues or merge requests as if they were you.
+You can add this address to your contact list for easy access.
+
+![Create new merge requests by email](img/create_from_email.png)
+
+_In GitLab 11.7, we updated the format of the generated email address.
+However the older format is still supported, allowing existing aliases
+or contacts to continue working._
+
+### Adding patches when creating a merge request via e-mail
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22723) in GitLab 11.5.
+
+You can add commits to the merge request being created by adding
+patches as attachments to the email. All attachments with a filename
+ending in `.patch` will be considered patches and they will be processed
+ordered by name.
+
+The combined size of the patches can be 2MB.
+
+If the source branch from the subject does not exist, it will be
+created from the repository's HEAD or the specified target branch to
+apply the patches. The target branch can be specified using the
+[`/target_branch` quick action](../quick_actions.md). If the source
+branch already exists, the patches will be applied on top of it.
diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png
deleted file mode 100644
index bbb131e86e9..00000000000
--- a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_5.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_5.png
new file mode 100644
index 00000000000..24c8c8f8c11
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_5.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
index c704129685f..c704129685f 100755..100644
--- a/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
+++ b/doc/user/project/merge_requests/img/mr_approvals_by_code_owners_v12_4.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 2ab7c3fb15b..1ca8c882ac7 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -1,5 +1,5 @@
---
-type: index, reference, concepts
+type: index, reference
---
# Merge requests
@@ -9,45 +9,9 @@ to source code that exist as commits on a given Git branch.
![Merge request view](img/merge_request.png)
-## Overview
-
-A Merge Request (**MR**) is the basis of GitLab as a code collaboration
-and version control platform.
-It is as simple as the name implies: a _request_ to _merge_ one branch into another.
-
-With GitLab merge requests, you can:
-
-- Compare the changes between two [branches](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell#_git_branching)
-- [Review and discuss](../../discussions/index.md#threads) the proposed modifications inline
-- Live preview the changes when [Review Apps](../../../ci/review_apps/index.md) is configured for your project
-- Build, test, and deploy your code in a per-branch basis with built-in [GitLab CI/CD](../../../ci/README.md)
-- Prevent the merge request from being merged before it's ready with [WIP MRs](#work-in-progress-merge-requests)
-- View the deployment process through [Pipeline Graphs](../../../ci/pipelines.md#visualizing-pipelines)
-- [Automatically close the issue(s)](../../project/issues/managing_issues.md#closing-issues-automatically) that originated the implementation proposed in the merge request
-- Assign it to any registered user, and change the assignee how many times you need
-- Assign a [milestone](../../project/milestones/index.md) and track the development of a broader implementation
-- Organize your issues and merge requests consistently throughout the project with [labels](../../project/labels.md)
-- Add a time estimation and the time spent with that merge request with [Time Tracking](../../../workflow/time_tracking.md#time-tracking)
-- [Resolve merge conflicts from the UI](#resolve-conflicts)
-- Enable [fast-forward merge requests](#fast-forward-merge-requests)
-- Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch
-- [Create new merge requests by email](#create-new-merge-requests-by-email)
-- [Allow collaboration](allow_collaboration.md) so members of the target project can push directly to the fork
-- [Squash and merge](squash_and_merge.md) for a cleaner commit history
-
-With **[GitLab Enterprise Edition][ee]**, you can also:
-
-- Prepare a full review and submit it once it's ready with [Merge Request Reviews](../../discussions/index.md#merge-request-reviews-premium) **(PREMIUM)**
-- View the deployment process across projects with [Multi-Project Pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)**
-- Request [approvals](merge_request_approvals.md) from your managers **(STARTER)**
-- Analyze the impact of your changes with [Code Quality reports](code_quality.md) **(STARTER)**
-- Manage the licenses of your dependencies with [License Compliance](../../application_security/license_compliance/index.md) **(ULTIMATE)**
-- Analyze your source code for vulnerabilities with [Static Application Security Testing](../../application_security/sast/index.md) **(ULTIMATE)**
-- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](../../application_security/dast/index.md) **(ULTIMATE)**
-- Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **(ULTIMATE)**
-- Analyze your Docker images for vulnerabilities with [Container Scanning](../../application_security/container_scanning/index.md) **(ULTIMATE)**
-- Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing-premium) **(PREMIUM)**
-- Specify merge order dependencies with [Merge Request Dependencies](#merge-request-dependencies-premium) **(PREMIUM)**
+A Merge Request (**MR**) is the basis of GitLab as a code collaboration and version
+control platform. It is as simple as the name implies: a _request_ to _merge_ one
+branch into another.
## Use cases
@@ -58,8 +22,11 @@ A. Consider you are a software developer working in a team:
1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md) **(STARTER)**
1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD
1. You avoid using dependencies whose license is not compatible with your project with [License Compliance reports](../../application_security/license_compliance/index.md) **(ULTIMATE)**
-1. You request the [approval](#merge-request-approvals-starter) from your manager
-1. Your manager pushes a commit with their final review, [approves the merge request](merge_request_approvals.md), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
+1. You request the [approval](merge_request_approvals.md) from your manager **(STARTER)**
+1. Your manager:
+ 1. Pushes a commit with their final review
+ 1. [Approves the merge request](merge_request_approvals.md) **(STARTER)**
+ 1. Sets it to [merge when pipeline succeeds](merge_when_pipeline_succeeds.md)
1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#whenmanual) for GitLab CI/CD
1. Your implementations were successfully shipped to your customer
@@ -71,547 +38,112 @@ B. Consider you're a web developer writing a webpage for your company's website:
1. You request your web designers for their implementation
1. You request the [approval](merge_request_approvals.md) from your manager **(STARTER)**
1. Once approved, your merge request is [squashed and merged](squash_and_merge.md), and [deployed to staging with GitLab Pages](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
-1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
-
-## Merge requests per project
-
-View all the merge requests within a project by navigating to **Project > Merge Requests**.
-
-When you access your project's merge requests, GitLab will present them in a list,
-and you can use the tabs available to quickly filter by open and closed. You can also [search and filter the results](../../search/index.md#issues-and-merge-requests-per-project).
-
-![Project merge requests list view](img/project_merge_requests_list_view.png)
-
-## Merge requests per group
-
-View merge requests in all projects in the group, including all projects of all descendant subgroups of the group. Navigate to **Group > Merge Requests** to view these merge requests. This view also has the open and closed merge requests tabs.
-
-You can [search and filter the results](../../search/index.md#issues-and-merge-requests-per-group) from here.
-
-![Group Issues list view](img/group_merge_requests_list_view.png)
-
-## Deleting the source branch
-
-When creating a merge request, select the "Delete source branch when merge
-request accepted" option and the source branch will be deleted when the merge
-request is merged.
-
-This option is also visible in an existing merge request next to the merge
-request button and can be selected/deselected before merging. It's only visible
-to users with [Maintainer permissions](../../permissions.md) in the source project.
-
-If the user viewing the merge request does not have the correct permissions to
-delete the source branch and the source branch is set for deletion, the merge
-request widget will show the "Deletes source branch" text.
-
-![Delete source branch status](img/remove_source_branch_status.png)
-
-## Allow collaboration on merge requests across forks
-
-When a user opens a merge request from a fork, they are given the option to allow
-upstream maintainers to collaborate with them on the source branch. This allows
-the maintainers of the upstream project to make small fixes or rebase branches
-before merging, reducing the back and forth of accepting community contributions.
-
-[Learn more about allowing upstream members to push to forks.](allow_collaboration.md)
+1. Your production team [cherry picks](cherry_pick_changes.md) the merge commit into production
+
+## Creating merge requests
+
+While making changes to files in the `master` branch of a repository is possible, it is not
+the common workflow. In most cases, a user will make changes in a [branch](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell#_git_branching),
+then [create a merge request](creating_merge_requests.md) to request that the changes
+be merged into another branch (often the `master` branch).
+
+It is then [reviewed](#reviewing-and-managing-merge-requests), possibly updated after
+discussions and suggestions, and finally approved and merged into the target branch.
+Creating and reviewing merge requests is one of the most fundamental parts of working
+with GitLab.
+
+When [creating merge requests](creating_merge_requests.md), there are a number of features
+to be aware of:
+
+| Feature | Description |
+|-----------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Adding patches when creating a merge request via e-mail](creating_merge_requests.md#adding-patches-when-creating-a-merge-request-via-e-mail) | Add commits to a merge request created by e-mail, by adding patches as e-mail attachments. |
+| [Allow collaboration on merge requests across forks](allow_collaboration.md) | Allows the maintainers of an upstream project to collaborate on a fork, to make fixes or rebase branches before merging, reducing the back and forth of accepting community contributions. |
+| [Assignee](creating_merge_requests.md#assignee) | Add an assignee to indicate who is reviewing or accountable for it. |
+| [Automatic issue closing](../../project/issues/managing_issues.md#closing-issues-automatically) | Set a merge request to close defined issues automatically as soon as it is merged. |
+| [Create new merge requests by email](creating_merge_requests.md#create-new-merge-requests-by-email) | Create new merge requests by sending an email to a user-specific email address. |
+| [Deleting the source branch](creating_merge_requests.md#deleting-the-source-branch) | Select the "Delete source branch when merge request accepted" option and the source branch will be deleted when the merge request is merged. |
+| [Git push options](../push_options.md) | Use Git push options to create or update merge requests when pushing changes to GitLab with Git, without needing to use the GitLab interface. |
+| [Labels](../../project/labels.md) | Organize your issues and merge requests consistently throughout the project. |
+| [Merge request approvals](merge_request_approvals.md) **(STARTER)** | Set the number of necessary approvals and predefine a list of approvers that will need to approve every merge request in a project. |
+| [Merge Request dependencies](merge_request_dependencies.md) **(PREMIUM)** | Specify that a merge request depends on other merge requests, enforcing a desired order of merging. |
+| [Merge Requests for Confidential Issues](../issues/confidential_issues.md#merge-requests-for-confidential-issues) | Create merge requests to resolve confidential issues for preventing leakage or early release of sensitive data through regular merge requests. |
+| [Milestones](../../project/milestones/index.md) | Track merge requests to achieve a broader goal in a certain period of time. |
+| [Multiple assignees](creating_merge_requests.md#multiple-assignees-starter) **(STARTER)** | Have multiple assignees for merge requests to indicate everyone that is reviewing or accountable for it. |
+| [Squash and merge](squash_and_merge.md) | Squash all changes present in a merge request into a single commit when merging, to allow for a neater commit history. |
+| [Work In Progress merge requests](work_in_progress_merge_requests.md) | Prevent the merge request from being merged before it's ready |
+
+## Reviewing and managing merge requests
+
+Once a merge request has been [created](#creating-merge-requests) and submitted, there
+are many powerful features that you can use during the review process to make sure only
+the changes you want are merged into the repository.
+
+For managers and administrators, it is also important to be able to view and manage
+all the merge requests in a group or project. When [reviewing or managing merge requests](reviewing_and_managing_merge_requests.md),
+there are a number of features to be aware of:
+
+| Feature | Description |
+|-------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Bulk editing merge requests](../../project/bulk_editing.md) | Update the attributes of multiple merge requests simultaneously. |
+| [Cherry-pick changes](cherry_pick_changes.md) | Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button in a merged merge requests or a commit. |
+| [Commenting on any file line in merge requests](reviewing_and_managing_merge_requests.md#commenting-on-any-file-line-in-merge-requests) | Make comments directly on the exact line of a file you want to talk about. |
+| [Discuss changes in threads in merge requests reviews](../../discussions/index.md) | Keep track of the progress during a code review by making and resolving comments. |
+| [Fast-forward merge requests](fast_forward_merge.md) | For a linear Git history and a way to accept merge requests without creating merge commits |
+| [Find the merge request that introduced a change](versions.md) | When viewing the commit details page, GitLab will link to the merge request(s) containing that commit. |
+| [Ignore whitespace changes in Merge Request diff view](reviewing_and_managing_merge_requests.md#ignore-whitespace-changes-in-Merge-Request-diff-view) | Hide whitespace changes from the diff view for a to focus on more important changes. |
+| [Incrementally expand merge request diffs](reviewing_and_managing_merge_requests.md#incrementally-expand-merge-request-diffs) | View the content directly above or below a change, to better understand the context of that change. |
+| [Live preview with Review Apps](reviewing_and_managing_merge_requests.md#live-preview-with-review-apps) | Live preview the changes when Review Apps are configured for your project |
+| [Merge request diff file navigation](reviewing_and_managing_merge_requests.md#merge-request-diff-file-navigation) | Quickly jump to any changed file within the diff view. |
+| [Merge requests versions](versions.md) | Select and compare the different versions of merge request diffs |
+| [Merge when pipeline succeeds](merge_when_pipeline_succeeds.md) | Set a merge request that looks ready to merge to merge automatically when CI pipeline succeeds. |
+| [Perform a Review](../../discussions/index.md#merge-request-reviews-premium) **(PREMIUM)** | Start a review in order to create multiple comments on a diff and publish them once you're ready. |
+| [Pipeline status in merge requests](reviewing_and_managing_merge_requests.md#pipeline-status-in-merge-requests) | If using [GitLab CI/CD](../../../ci/README.md), see pre and post-merge pipelines information, and which deployments are in progress. |
+| [Post-merge pipeline status](reviewing_and_managing_merge_requests.md#post-merge-pipeline-status) | When a merge request is merged, see the post-merge pipeline status of the branch the merge request was merged into. |
+| [Resolve conflicts](resolve_conflicts.md) | GitLab can provide the option to resolve certain merge request conflicts in the GitLab UI. |
+| [Revert changes](revert_changes.md) | Revert changes from any commit from within a merge request. |
+| [Semi-linear history merge requests](reviewing_and_managing_merge_requests.md#semi-linear-history-merge-requests) | Enable semi-linear history merge requests as another security layer to guarantee the pipeline is passing in the target branch |
+| [Suggest changes](../../discussions/index.md#suggest-changes) | Add suggestions to change the content of merge requests directly into merge request threads, and easily apply them to the codebase directly from the UI. |
+| [Time Tracking](../time_tracking.md#time-tracking) | Add a time estimation and the time spent with that merge request. |
+| [View changes between file versions](reviewing_and_managing_merge_requests.md#view-changes-between-file-versions) | View what will be changed when a merge request is merged. |
+| [View group merge requests](reviewing_and_managing_merge_requests.md#view-merge-requests-for-all-projects-in-a-group) | List and view the merge requests within a group. |
+| [View project merge requests](reviewing_and_managing_merge_requests.md#view-project-merge-requests) | List and view the merge requests within a project. |
+
+## Testing and reports in merge requests
+
+GitLab has the ability to test the changes included in a merge request, and can display
+or link to useful information directly in the merge request page:
+
+| Feature | Description |
+|--------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Browser Performance Testing](browser_performance_testing.md) **(PREMIUM)** | Quickly determine the performance impact of pending code changes. |
+| [Code Quality](code_quality.md) **(STARTER)** | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
+| [Display arbitrary job artifacts](../../../ci/yaml/README.md#artifactsexpose_as) | Configure CI pipelines with the `artifacts:expose_as` parameter to directly link to selected [artifacts](../pipelines/job_artifacts.md) in merge requests. |
+| [GitLab CI/CD](../../../ci/README.md) | Build, test, and deploy your code in a per-branch basis with built-in CI/CD. |
+| [JUnit test reports](../../../ci/junit_test_reports.md) | Configure your CI jobs to use JUnit test reports, and let GitLab display a report on the merge request so that it’s easier and faster to identify the failure without having to check the entire job log. |
+| [Metrics Reports](../../../ci/metrics_reports.md) **(PREMIUM)** | Display the Metrics Report on the merge request so that it's fast and easy to identify changes to important metrics. |
+| [Multi-Project pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)** | When you set up GitLab CI/CD across multiple projects, you can visualize the entire pipeline, including all cross-project interdependencies. |
+| [Pipelines for merge requests](../../../ci/merge_request_pipelines/index.md) | Customize a specific pipeline structure for merge requests in order to speed the cycle up by running only important jobs. |
+| [Pipeline Graphs](../../../ci/pipelines.md#visualizing-pipelines) | View the status of pipelines within the merge request, including the deployment process. |
+
+### Security Reports **(ULTIMATE)**
+
+In addition to the reports listed above, GitLab can do many types of [Security reports](../../application_security/index.md),
+generated by scanning and reporting any vulnerabilities found in your project:
+
+| Feature | Description |
+|-----------------------------------------------------------------------------------------|------------------------------------------------------------------|
+| [Container Scanning](../../application_security/container_scanning/index.md) | Analyze your Docker images for known vulnerabilities. |
+| [Dynamic Application Security Testing (DAST)](../../application_security/dast/index.md) | Analyze your running web applications for known vulnerabilities. |
+| [Dependency Scanning](../../application_security/dependency_scanning/index.md) | Analyze your dependencies for known vulnerabilities. |
+| [License Compliance](../../application_security/license_compliance/index.md) | Manage the licenses of your dependencies. |
+| [Static Application Security Testing (SAST)](../../application_security/sast/index.md) | Analyze your source code for known vulnerabilities. |
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
-1. Working with [protected branches][] in a single repository
+1. Working with [protected branches](../protected_branches.md) in a single repository
1. Working with forks of an authoritative project
[Learn more about the authorization for merge requests.](authorization_for_merge_requests.md)
-
-## Cherry-pick changes
-
-Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
-in a merged merge requests or a commit.
-
-[Learn more about cherry-picking changes.](cherry_pick_changes.md)
-
-## Semi-linear history merge requests
-
-A merge commit is created for every merge, but the branch is only merged if
-a fast-forward merge is possible. This ensures that if the merge request build
-succeeded, the target branch build will also succeed after merging.
-
-Navigate to a project's settings, select the **Merge commit with semi-linear
-history** option under **Merge Requests: Merge method** and save your changes.
-
-## Fast-forward merge requests
-
-If you prefer a linear Git history and a way to accept merge requests without
-creating merge commits, you can configure this on a per-project basis.
-
-[Read more about fast-forward merge requests.](fast_forward_merge.md)
-
-## Merge when pipeline succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or
-more CI jobs running, you can set it to be merged automatically when CI
-pipeline succeeds. This way, you don't have to wait for the pipeline to finish
-and remember to merge the request manually.
-
-[Learn more about merging when pipeline succeeds.](merge_when_pipeline_succeeds.md)
-
-## Resolve threads in merge requests reviews
-
-Keep track of the progress during a code review with resolving comments.
-Resolving comments prevents you from forgetting to address feedback and lets
-you hide threads that are no longer relevant.
-
-[Read more about resolving threads in merge requests reviews.](../../discussions/index.md)
-
-## View changes between file versions
-
-The **Changes** tab of a merge request shows the changes to files between branches or
-commits. This view of changes to a file is also known as a **diff**. By default, the diff view
-compares the file in the merge request branch and the file in the target branch.
-
-The diff view includes the following:
-
-- The file's name and path.
-- The number of lines added and deleted.
-- Buttons for the following options:
- - Toggle comments for this file; useful for inline reviews.
- - Edit the file in the merge request's branch.
- - Show full file, in case you want to look at the changes in context with the rest of the file.
- - View file at the current commit.
- - Preview the changes with [Review Apps](../../../ci/review_apps/index.md).
-- The changed lines, with the specific changes highlighted.
-
-![Example screenshot of a source code diff](img/merge_request_diff_v12_2.png)
-
-## Commenting on any file line in merge requests
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/13950) in GitLab 11.5.
-
-GitLab provides a way of leaving comments in any part of the file being changed
-in a Merge Request. To do so, click the **...** button in the gutter of the Merge Request diff UI to expand the diff lines and leave a comment, just as you would for a changed line.
-
-![Comment on any diff file line](img/comment-on-any-diff-line.png)
-
-## Perform a Review **(PREMIUM)**
-
-Start a review in order to create multiple comments on a diff and publish them once you're ready.
-Starting a review allows you to get all your thoughts in order and ensure you haven't missed anything
-before submitting all your comments.
-
-[Learn more about Merge Request Reviews](../../discussions/index.md#merge-request-reviews-premium)
-
-## Squash and merge
-
-GitLab allows you to squash all changes present in a merge request into a single
-commit when merging, to allow for a neater commit history.
-
-[Learn more about squash and merge.](squash_and_merge.md)
-
-## Suggest changes
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/18008) in GitLab 11.6.
-
-As a reviewer, you can add suggestions to change the content in
-merge request threads, and users with appropriate [permission](../../permissions.md)
-can easily apply them to the codebase directly from the UI. Read
-through the documentation on [Suggest changes](../../discussions/index.md#suggest-changes)
-to learn more.
-
-## Multiple assignees **(STARTER)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2004)
-in [GitLab Starter 11.11](https://about.gitlab.com/pricing/).
-
-Multiple people often review merge requests at the same time. GitLab allows you to have multiple assignees for merge requests to indicate everyone that is reviewing or accountable for it.
-
-![multiple assignees for merge requests sidebar](img/multiple_assignees_for_merge_requests_sidebar.png)
-
-To assign multiple assignees to a merge request:
-
-1. From a merge request, expand the right sidebar and locate the **Assignees** section.
-1. Click on **Edit** and from the dropdown menu, select as many users as you want
- to assign the merge request to.
-
-Similarly, assignees are removed by deselecting them from the same dropdown menu.
-
-It's also possible to manage multiple assignees:
-
-- When creating a merge request.
-- Using [quick actions](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
-
-## Resolve conflicts
-
-When a merge request has conflicts, GitLab may provide the option to resolve
-those conflicts in the GitLab UI.
-
-[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
-
-## Create new merge requests by email
-
-_This feature needs [incoming email](../../../administration/incoming_email.md)
-to be configured by a GitLab administrator to be available for CE/EE users, and
-it's available on GitLab.com._
-
-You can create a new merge request by sending an email to a user-specific email
-address. The address can be obtained on the merge requests page by clicking on
-a **Email a new merge request to this project** button. The subject will be
-used as the source branch name for the new merge request and the target branch
-will be the default branch for the project. The message body (if not empty)
-will be used as the merge request description. You need
-["Reply by email"](../../../administration/reply_by_email.md) enabled to use
-this feature. If it's not enabled to your instance, you may ask your GitLab
-administrator to do so.
-
-This is a private email address, generated just for you. **Keep it to yourself**
-as anyone who gets ahold of it can create issues or merge requests as if they were you.
-You can add this address to your contact list for easy access.
-
-![Create new merge requests by email](img/create_from_email.png)
-
-_In GitLab 11.7, we updated the format of the generated email address.
-However the older format is still supported, allowing existing aliases
-or contacts to continue working._
-
-### Adding patches when creating a merge request via e-mail
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22723) in GitLab 11.5.
-
-You can add commits to the merge request being created by adding
-patches as attachments to the email. All attachments with a filename
-ending in `.patch` will be considered patches and they will be processed
-ordered by name.
-
-The combined size of the patches can be 2MB.
-
-If the source branch from the subject does not exist, it will be
-created from the repository's HEAD or the specified target branch to
-apply the patches. The target branch can be specified using the
-[`/target_branch` quick action](../quick_actions.md). If the source
-branch already exists, the patches will be applied on top of it.
-
-## Use Git push options with merge requests
-
-Use [Git push options](../push_options.md) to create or update merge requests when
-pushing changes to GitLab with Git, without needing to use the GitLab interface.
-
-## Find the merge request that introduced a change
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/2383) in GitLab 10.5.
-
-When viewing the commit details page, GitLab will link to the merge request (or
-merge requests, if it's in more than one) containing that commit.
-
-This only applies to commits that are in the most recent version of a merge
-request - if a commit was in a merge request, then rebased out of that merge
-request, they will not be linked.
-
-[Read more about merge request versions](versions.md)
-
-## Revert changes
-
-GitLab implements Git's powerful feature to revert any commit with introducing
-a **Revert** button in merge requests and commit details.
-
-[Learn more about reverting changes in the UI](revert_changes.md)
-
-## Merge requests versions
-
-Every time you push to a branch that is tied to a merge request, a new version
-of merge request diff is created. When you visit a merge request that contains
-more than one pushes, you can select and compare the versions of those merge
-request diffs.
-
-[Read more about merge request versions](versions.md)
-
-## Work In Progress merge requests
-
-To prevent merge requests from accidentally being accepted before they're
-completely ready, GitLab blocks the "Accept" button for merge requests that
-have been marked as a **Work In Progress**.
-
-[Learn more about setting a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
-
-## Merge Requests for Confidential Issues
-
-Create [merge requests to resolve confidential issues](../issues/confidential_issues.md#merge-requests-for-confidential-issues)
-for preventing leakage or early release of sensitive data through regular merge requests.
-
-## Merge request approvals **(STARTER)**
-
-> Included in [GitLab Starter](https://about.gitlab.com/product/).
-
-If you want to make sure every merge request is approved by one or more people,
-you can enforce this workflow by using merge request approvals. Merge request
-approvals allow you to set the number of necessary approvals and predefine a
-list of approvers that will need to approve every merge request in a project.
-
-[Read more about merge request approvals.](merge_request_approvals.md)
-
-## Code Quality **(STARTER)**
-
-> Introduced in [GitLab Starter](https://about.gitlab.com/product/) 9.3.
-
-If you are using [GitLab CI][ci], you can analyze your source code quality using
-the [Code Climate][cc] analyzer [Docker image][cd]. Going a step further, GitLab
-can show the Code Climate report right in the merge request widget area.
-
-[Read more about Code Quality reports.](code_quality.md)
-
-## Metrics Reports **(PREMIUM)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9788) in [GitLab Premium](https://about.gitlab.com/product/) 11.10.
-Requires GitLab Runner 11.10 and above.
-
-If you are using [GitLab CI][ci], you can configure your job to output custom
-metrics and GitLab will display the Metrics Report on the merge request so
-that it's fast and easy to identify changes to important metrics.
-
-[Read more about Metrics Report](../../../ci/metrics_reports.md).
-
-## Browser Performance Testing **(PREMIUM)**
-
-> Introduced in [GitLab Premium](https://about.gitlab.com/product/) 10.3.
-
-If your application offers a web interface and you are using [GitLab CI/CD][ci], you can quickly determine the performance impact of pending code changes. GitLab uses [Sitespeed.io][sitespeed], a free and open source tool for measuring the performance of web sites, to analyze the performance of specific pages.
-
-GitLab runs the [Sitespeed.io container][sitespeed-container] and displays the difference in overall performance scores between the source and target branches.
-
-[Read more about Browser Performance Testing.](browser_performance_testing.md)
-
-## Merge Request Dependencies **(PREMIUM)**
-
-> Introduced in [GitLab Premium](https://about.gitlab.com/product/) 12.2.
-
-A single logical change may be split across several merge requests, across
-several projects. When this happens, the order in which MRs are merged is
-important.
-
-GitLab allows you to specify that a merge request depends on other MRs. With
-this relationship in place, the merge request cannot be merged until all of its
-dependencies have also been merged, helping to maintain the consistency of a
-single logical change.
-
-[Read more about merge request dependencies.](merge_request_dependencies.md)
-
-## Security reports **(ULTIMATE)**
-
-GitLab can scan and report any vulnerabilities found in your project.
-
-[Read more about security reports.](../../application_security/index.md)
-
-## JUnit test reports
-
-Configure your CI jobs to use JUnit test reports, and let GitLab display a report
-on the merge request so that it’s easier and faster to identify the failure
-without having to check the entire job log.
-
-[Read more about JUnit test reports](../../../ci/junit_test_reports.md).
-
-## Merge request diff file navigation
-
-When reviewing changes in the **Changes** tab the diff can be navigated using
-the file tree or file list. As you scroll through large diffs with many
-changes, you can quickly jump to any changed file using the file tree or file
-list.
-
-![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
-
-### Incrementally expand merge request diffs
-
-By default, the diff shows only the parts of a file which are changed.
-To view more unchanged lines above or below a change click on the
-**Expand up** or **Expand down** icons. You can also click on **Show all lines**
-to expand the entire file.
-
-![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png)
-
-## Ignore whitespace changes in Merge Request diff view
-
-If you click the **Hide whitespace changes** button, you can see the diff
-without whitespace changes (if there are any). This is also working when on a
-specific commit page.
-
-![MR diff](img/merge_request_diff.png)
-
->**Tip:**
-You can append `?w=1` while on the diffs page of a merge request to ignore any
-whitespace changes.
-
-## Live preview with Review Apps
-
-If you configured [Review Apps](https://about.gitlab.com/product/review-apps/) for your project,
-you can preview the changes submitted to a feature-branch through a merge request
-in a per-branch basis. No need to checkout the branch, install and preview locally;
-all your changes will be available to preview by anyone with the Review Apps link.
-
-With GitLab's [Route Maps](../../../ci/review_apps/index.md#route-maps) set, the
-merge request widget takes you directly to the pages changed, making it easier and
-faster to preview proposed modifications.
-
-[Read more about Review Apps](../../../ci/review_apps/index.md).
-
-## Pipelines for merge requests
-
-When a developer updates a merge request, a pipeline should quickly report back
-its result to the developer, but often pipelines take long time to complete
-because general branch pipelines contain unnecessary jobs from the merge request standpoint.
-You can customize a specific pipeline structure for merge requests in order to
-speed the cycle up by running only important jobs.
-
-Learn more about [pipelines for merge requests](../../../ci/merge_request_pipelines/index.md).
-
-## Pipeline status in merge requests
-
-If you've set up [GitLab CI/CD](../../../ci/README.md) in your project,
-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
-successfully deployed to it, the deployed environment and the link to the
-Review App will be shown as well.
-
-### Post-merge pipeline status
-
-When a merge request is merged, you can see the post-merge pipeline status of
-the branch the merge request was merged into. For example, when a merge request
-is merged into the master branch and then triggers a deployment to the staging
-environment.
-
-Deployments that are ongoing will be shown, as well as the deploying/deployed state
-for environments. If it's the first time the branch is deployed, the link
-will return a `404` error until done. During the deployment, the stop button will
-be disabled. If the pipeline fails to deploy, the deployment info will be hidden.
-
-![Merge request pipeline](img/merge_request_pipeline.png)
-
-For more information, [read about pipelines](../../../ci/pipelines.md).
-
-## Bulk editing merge requests
-
-Find out about [bulk editing merge requests](../../project/bulk_editing.md).
-
-## Troubleshooting
-
-Sometimes things don't go as expected in a merge request, here are some
-troubleshooting steps.
-
-### Merge request cannot retrieve the pipeline status
-
-This can occur if Sidekiq doesn't pick up the changes fast enough.
-
-#### Sidekiq
-
-Sidekiq didn't process the CI state change fast enough. Please wait a few
-seconds and the status will update automatically.
-
-#### Bug
-
-Merge Request pipeline statuses can't be retrieved when the following occurs:
-
-1. A Merge Request is created
-1. The Merge Request is closed
-1. Changes are made in the project
-1. The Merge Request is reopened
-
-To enable the pipeline status to be properly retrieved, close and reopen the
-Merge Request again.
-
-## Tips
-
-Here are some tips that will help you be more efficient with merge requests in
-the command line.
-
-> **Note:**
-This section might move in its own document in the future.
-
-### Checkout merge requests locally
-
-A merge request contains all the history from a repository, plus the additional
-commits added to the branch associated with the merge request. Here's a few
-tricks to checkout a merge request locally.
-
-Please note that you can checkout a merge request locally even if the source
-project is a fork (even a private fork) of the target project.
-
-#### Checkout locally by adding a Git alias
-
-Add the following alias to your `~/.gitconfig`:
-
-```
-[alias]
- mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
-```
-
-Now you can check out a particular merge request from any repository and any
-remote. For example, to check out the merge request with ID 5 as shown in GitLab
-from the `origin` remote, do:
-
-```
-git mr origin 5
-```
-
-This will fetch the merge request into a local `mr-origin-5` branch and check
-it out.
-
-#### Checkout locally by modifying `.git/config` for a given repository
-
-Locate the section for your GitLab remote in the `.git/config` file. It looks
-like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-foss.git
- fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-You can open the file with:
-
-```
-git config -e
-```
-
-Now add the following line to the above section:
-
-```
-fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-In the end, it should look like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-foss.git
- fetch = +refs/heads/*:refs/remotes/origin/*
- fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests:
-
-```
-git fetch origin
-
-...
-From https://gitlab.com/gitlab-org/gitlab-foss.git
- * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-And to check out a particular merge request:
-
-```
-git checkout origin/merge-requests/1
-```
-
-All the above can be done with the [`git-mr`](https://gitlab.com/glensc/git-mr) script.
-
-[protected branches]: ../protected_branches.md
-[ci]: ../../../ci/README.md
-[cc]: https://codeclimate.com/
-[cd]: https://hub.docker.com/r/codeclimate/codeclimate/
-[sitespeed]: https://www.sitespeed.io
-[sitespeed-container]: https://hub.docker.com/r/sitespeedio/sitespeed.io/
-[ee]: https://about.gitlab.com/pricing/ "GitLab Enterprise Edition"
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 2aa92ba2316..76c348eb93e 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -75,9 +75,9 @@ request approval rules:
1. Click **Add approvers** to create a new approval rule.
1. Just like in [GitLab Starter](#editing-approvals), select the approval members and approvals required.
1. Give the approval rule a name that describes the set of approvers selected.
-1. Click **Add approvers** to submit the new rule.
+1. Click **Add approval rule** to submit the new rule.
- ![Approvals premium project edit](img/approvals_premium_project_edit_v12_3.png)
+ ![Approvals premium project edit](img/approvals_premium_project_edit_v12_5.png)
## Multiple approval rules **(PREMIUM)**
@@ -219,8 +219,6 @@ and the project level approvers are changed after a merge request is created,
the merge request retains the previous approvers.
However, the approvers can be changed by [editing the merge request](#overriding-the-merge-request-approvals-default-settings).
----
-
The default approval settings can now be overridden when creating a
[merge request](index.md) or by editing it after it's been created:
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index dab2184448a..6630179ea47 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -85,3 +85,8 @@ 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. -->
+
+## Use it from the command line
+
+You can use [Push Options](../push_options.md) to trigger this feature when
+pushing.
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
new file mode 100644
index 00000000000..f693b0b1e72
--- /dev/null
+++ b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md
@@ -0,0 +1,251 @@
+---
+type: index, reference
+---
+
+# Reviewing and managing merge requests
+
+Merge requests are the primary method of making changes to files in a GitLab project.
+Changes are proposed by [creating and submitting a merge request](creating_merge_requests.md),
+which is then reviewed, and accepted (or rejected).
+
+## View project merge requests
+
+View all the merge requests within a project by navigating to **Project > Merge Requests**.
+
+When you access your project's merge requests, GitLab will present them in a list,
+and you can use the tabs available to quickly filter by open and closed. You can also [search and filter the results](../../search/index.md#issues-and-merge-requests-per-project).
+
+![Project merge requests list view](img/project_merge_requests_list_view.png)
+
+## View merge requests for all projects in a group
+
+View merge requests in all projects in the group, including all projects of all descendant subgroups of the group. Navigate to **Group > Merge Requests** to view these merge requests. This view also has the open and closed merge requests tabs.
+
+You can [search and filter the results](../../search/index.md#issues-and-merge-requests-per-group) from here.
+
+![Group Issues list view](img/group_merge_requests_list_view.png)
+
+## Semi-linear history merge requests
+
+A merge commit is created for every merge, but the branch is only merged if
+a fast-forward merge is possible. This ensures that if the merge request build
+succeeded, the target branch build will also succeed after merging.
+
+Navigate to a project's settings, select the **Merge commit with semi-linear history**
+option under **Merge Requests: Merge method** and save your changes.
+
+## View changes between file versions
+
+The **Changes** tab, below the main merge request details and next to the discussion tab,
+shows the changes to files between branches or commits. This view of changes to a
+file is also known as a **diff**. By default, the diff view compares the file in the
+merge request branch and the file in the target branch.
+
+The diff view includes the following:
+
+- The file's name and path.
+- The number of lines added and deleted.
+- Buttons for the following options:
+ - Toggle comments for this file; useful for inline reviews.
+ - Edit the file in the merge request's branch.
+ - Show full file, in case you want to look at the changes in context with the rest of the file.
+ - View file at the current commit.
+ - Preview the changes with [Review Apps](../../../ci/review_apps/index.md).
+- The changed lines, with the specific changes highlighted.
+
+![Example screenshot of a source code diff](img/merge_request_diff_v12_2.png)
+
+### Merge request diff file navigation
+
+When reviewing changes in the **Changes** tab the diff can be navigated using
+the file tree or file list. As you scroll through large diffs with many
+changes, you can quickly jump to any changed file using the file tree or file
+list.
+
+![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
+
+### Incrementally expand merge request diffs
+
+By default, the diff shows only the parts of a file which are changed.
+To view more unchanged lines above or below a change click on the
+**Expand up** or **Expand down** icons. You can also click on **Show all lines**
+to expand the entire file.
+
+![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png)
+
+### Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Commenting on any file line in merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/13950) in GitLab 11.5.
+
+GitLab provides a way of leaving comments in any part of the file being changed
+in a Merge Request. To do so, click the **...** button in the gutter of the Merge Request diff UI to expand the diff lines and leave a comment, just as you would for a changed line.
+
+![Comment on any diff file line](img/comment-on-any-diff-line.png)
+
+## Live preview with Review Apps
+
+If you configured [Review Apps](https://about.gitlab.com/product/review-apps/) for your project,
+you can preview the changes submitted to a feature-branch through a merge request
+in a per-branch basis. No need to checkout the branch, install and preview locally;
+all your changes will be available to preview by anyone with the Review Apps link.
+
+With GitLab's [Route Maps](../../../ci/review_apps/index.md#route-maps) set, the
+merge request widget takes you directly to the pages changed, making it easier and
+faster to preview proposed modifications.
+
+[Read more about Review Apps](../../../ci/review_apps/index.md).
+
+## Pipeline status in merge requests
+
+If you've set up [GitLab CI/CD](../../../ci/README.md) in your project,
+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
+successfully deployed to it, the deployed environment and the link to the
+Review App will be shown as well.
+
+### Post-merge pipeline status
+
+When a merge request is merged, you can see the post-merge pipeline status of
+the branch the merge request was merged into. For example, when a merge request
+is merged into the master branch and then triggers a deployment to the staging
+environment.
+
+Deployments that are ongoing will be shown, as well as the deploying/deployed state
+for environments. If it's the first time the branch is deployed, the link
+will return a `404` error until done. During the deployment, the stop button will
+be disabled. If the pipeline fails to deploy, the deployment info will be hidden.
+
+![Merge request pipeline](img/merge_request_pipeline.png)
+
+For more information, [read about pipelines](../../../ci/pipelines.md).
+
+## Troubleshooting
+
+Sometimes things don't go as expected in a merge request, here are some
+troubleshooting steps.
+
+### Merge request cannot retrieve the pipeline status
+
+This can occur if Sidekiq doesn't pick up the changes fast enough.
+
+#### Sidekiq
+
+Sidekiq didn't process the CI state change fast enough. Please wait a few
+seconds and the status will update automatically.
+
+#### Bug
+
+Merge Request pipeline statuses can't be retrieved when the following occurs:
+
+1. A Merge Request is created
+1. The Merge Request is closed
+1. Changes are made in the project
+1. The Merge Request is reopened
+
+To enable the pipeline status to be properly retrieved, close and reopen the
+Merge Request again.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
+#### Checkout locally by adding a Git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any
+remote. For example, to check out the merge request with ID 5 as shown in GitLab
+from the `origin` remote, do:
+
+```
+git mr origin 5
+```
+
+This will fetch the merge request into a local `mr-origin-5` branch and check
+it out.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-foss.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-foss.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-foss.git
+ * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+All the above can be done with the [`git-mr`](https://gitlab.com/glensc/git-mr) script.
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index fbe216c3aed..ffd0efb365a 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -35,6 +35,17 @@ changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png)
+## Find the merge request that introduced a change
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/2383) in GitLab 10.5.
+
+When viewing the commit details page, GitLab will link to the merge request (or
+merge requests, if it's in more than one) containing that commit.
+
+This only applies to commits that are in the most recent version of a merge
+request - if a commit was in a merge request, then rebased out of that merge
+request, they will not be linked.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 105854ccd33..21a4e3d8ead 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -12,23 +12,21 @@ Milestones allow you to organize issues and merge requests into a cohesive group
## Milestones as Agile sprints
-Milestones can be used as Agile sprints.
-Set the milestone start date and due date to represent
-the start and end of your Agile sprint.
-Set the milestone title to the name of your Agile sprint,
-such as `November 2018 sprint`.
-Add an issue to your Agile sprint by associating
-the milestone to the issue.
+Milestones can be used as Agile sprints so that you can track all issues and merge requests related to a particular sprint. To do so:
+
+1. Set the milestone start date and due date to represent the start and end of your Agile sprint.
+1. Set the milestone title to the name of your Agile sprint, such as `November 2018 sprint`.
+1. Add an issue to your Agile sprint by associating the desired milestone from the issue's right-hand sidebar.
## Milestones as releases
-Milestones can be used as releases.
-Set the milestone due date to represent the release date of your release.
-(And leave the milestone start date blank.)
-Set the milestone title to the version of your release,
-such as `Version 9.4`.
-Add an issue to your release by associating
-the milestone to the issue.
+Similarily, milestones can be used as releases. To do so:
+
+1. Set the milestone due date to represent the release date of your release and leave the milestone start date blank.
+1. Set the milestone title to the version of your release, such as `Version 9.4`.
+1. Add an issue to your release by associating the desired milestone from the issue's right-hand sidebar.
+
+Additionally, you can integrate milestones with GitLab's [Releases feature](../releases/index.md#releases-associated-with-milestones).
## Project milestones and group milestones
@@ -103,30 +101,18 @@ When filtering by milestone, in addition to choosing a specific project mileston
## Milestone view
-Not all features in the project milestone view are available in the group milestone view. This table summarizes the differences:
-
-| Feature | Project milestone view | Group milestone view |
-|--------------------------------------|:----------------------:|:--------------------:|
-| Title an description | ✓ | ✓ |
-| Issues assigned to milestone | ✓ | |
-| Merge requests assigned to milestone | ✓ | |
-| Participants and labels used | ✓ | |
-| Percentage complete | ✓ | ✓ |
-| Start date and due date | ✓ | ✓ |
-| Total issue time spent | ✓ | ✓ |
-| Total issue weight | ✓ | |
-| Burndown chart **[STARTER}** | ✓ | ✓ |
-
The milestone view shows the title and description.
-### Project milestone features
-
-These features are only available for project milestones and not group milestones.
+There are also tabs below these that show the following:
-- Issues assigned to the milestone are displayed in three columns: Unstarted issues, ongoing issues, and completed issues.
-- Merge requests assigned to the milestone are displayed in four columns: Work in progress merge requests, waiting for merge, rejected, and closed.
-- Participants and labels that are used in issues and merge requests that have the milestone assigned are displayed.
-- [Burndown chart](#project-burndown-charts-starter).
+- Issues
+ Shows all issues assigned to the milestone. These are displayed in three columns: Unstarted issues, ongoing issues, and completed issues.
+- Merge requests
+ Shows all merge requests assigned to the milestone. These are displayed in four columns: Work in progress merge requests, waiting for merge, rejected, and closed.
+- Participants
+ Shows all assignees of issues assigned to the milestone.
+- Labels
+ Shows all labels that are used in issues assigned to the milestone.
### Project Burndown Charts **(STARTER)**
@@ -144,9 +130,8 @@ The milestone sidebar on the milestone view shows the following:
- Percentage complete, which is calculated as number of closed issues divided by total number of issues.
- The start date and due date.
-- The total time spent on all issues that have the milestone assigned.
-
-For project milestones only, the milestone sidebar shows the total issue weight of all issues that have the milestone assigned.
+- The total time spent on all issues assigned to the milestone.
+- The total issue weight of all issues assigned to the milestone.
![Project milestone page](img/milestones_project_milestone_page.png)
diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md
index 1b319c5641c..04eda026bc3 100644
--- a/doc/user/project/operations/error_tracking.md
+++ b/doc/user/project/operations/error_tracking.md
@@ -6,7 +6,7 @@ Error tracking allows developers to easily discover and view the errors that the
## Sentry error tracking
-[Sentry](https://sentry.io/) is an open source error tracking system. GitLab allows administrators to connect Sentry to GitLab, to allow users to view a list of Sentry errors in GitLab itself.
+[Sentry](https://sentry.io/) is an open source error tracking system. GitLab allows administrators to connect Sentry to GitLab, to allow users to view a list of Sentry errors in GitLab.
### Deploying Sentry
@@ -20,6 +20,7 @@ You will need at least Maintainer [permissions](../../permissions.md) to enable
GitLab provides an easy way to connect Sentry to your project:
1. Sign up to Sentry.io or [deploy your own](#deploying-sentry) Sentry instance.
+1. [Create](https://docs.sentry.io/guides/integrate-frontend/create-new-project/) a new Sentry project. For each GitLab project that you want to integrate, we recommend that you create a new Sentry project.
1. [Find or generate](https://docs.sentry.io/api/auth/) a Sentry auth token for your Sentry project.
Make sure to give the token at least the following scopes: `event:read` and `project:read`.
1. Navigate to your project’s **Settings > Operations**.
@@ -31,11 +32,27 @@ GitLab provides an easy way to connect Sentry to your project:
1. Click **Save changes** for the changes to take effect.
1. You can now visit **Operations > Error Tracking** in your project's sidebar to [view a list](#error-tracking-list) of Sentry errors.
+### Enabling Gitlab issues links
+
+You may also want to enable Sentry's GitLab integration by following the steps in the [Sentry documentation](https://docs.sentry.io/workflow/integrations/global-integrations/#gitlab)
+
## Error Tracking List
NOTE: **Note:**
You will need at least Reporter [permissions](../../permissions.md) to view the Error Tracking list.
The Error Tracking list may be found at **Operations > Error Tracking** in your project's sidebar.
+Errors can be filtered by title.
![Error Tracking list](img/error_tracking_list.png)
+
+## Error Details
+
+From error list, users can navigate to the error details page by clicking the title of any error.
+
+This page has:
+
+- A link to Sentry issue.
+- A full stack trace along with other details.
+
+![Error Details](img/error_details_v12_5.png)
diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md
index 08df92959c3..c05f8fa8bc4 100644
--- a/doc/user/project/operations/feature_flags.md
+++ b/doc/user/project/operations/feature_flags.md
@@ -81,7 +81,14 @@ NOTE: **NOTE**
We'd highly recommend you to use the [Environment](../../../ci/environments.md)
feature in order to quickly assess which flag is enabled per environment.
-## Rollout strategy
+## Feature Flag strategies
+
+GitLab Feature Flag adopts [Unleash](https://unleash.github.io)
+as the feature flag engine. In unleash, there is a concept of rulesets for granular feature flag controls,
+called [strategies](https://unleash.github.io/docs/activation_strategy).
+Supported strategies for GitLab Feature Flags are described below.
+
+### Rollout strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8240) in GitLab 12.2.
@@ -91,13 +98,13 @@ The status of an environment spec ultimately determines whether or not a feature
For instance, a feature will always be disabled for every user if the matching environment spec has a disabled status, regardless of the chosen rollout strategy.
However, a feature will be enabled for 50% of logged-in users if the matching environment spec has an enabled status along with a **Percent rollout (logged in users)** strategy set to 50%.
-### All users
+#### All users
Enables the feature for all users. It is implemented using the Unleash
[`default`](https://unleash.github.io/docs/activation_strategy#default)
activation strategy.
-### Percent rollout (logged in users)
+#### Percent rollout (logged in users)
Enables the feature for a percentage of authenticated users. It is
implemented using the Unleash
@@ -112,7 +119,7 @@ CAUTION: **Caution:**
If this strategy is selected, then the Unleash client **must** be given a user
ID for the feature to be enabled. See the [Ruby example](#ruby-application-example) below.
-## Target users
+### Target users strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8240) in GitLab 12.2.
@@ -134,7 +141,7 @@ In order to use Feature Flags, you need to first
[get the access credentials](#configuring-feature-flags) from GitLab and then
prepare your application and hook it with a [client library](#client-libraries).
-### Configuring Feature Flags
+## Configuring Feature Flags
To get the access credentials that your application will need to talk to GitLab:
@@ -153,7 +160,7 @@ if **Instance ID** will be single token or multiple tokens, assigned to the
**Environment**. Also, **Application name** could describe the version of
application instead of the running environment.
-### Client libraries
+## Client libraries
GitLab currently implements a single backend that is compatible with
[Unleash](https://github.com/Unleash/unleash#client-implementations) clients.
@@ -178,7 +185,7 @@ Community contributed clients:
- [Unofficial .Net Core Unleash client](https://github.com/onybo/unleash-client-core)
- [Unleash client for Python 3](https://github.com/aes/unleash-client-python)
-### Golang application example
+## Golang application example
Here's an example of how to integrate the feature flags in a Golang application:
@@ -219,7 +226,7 @@ func main() {
}
```
-### Ruby application example
+## Ruby application example
Here's an example of how to integrate the feature flags in a Ruby application.
@@ -249,3 +256,11 @@ else
puts "hello, world!"
end
```
+
+## Feature Flags API
+
+You can create, update, read, and delete Feature Flags via API
+to control them in an automated flow:
+
+- [Feature Flags API](../../../api/feature_flags.md)
+- [Feature Flag Specs API](../../../api/feature_flag_specs.md)
diff --git a/doc/user/project/operations/img/error_details_v12_5.png b/doc/user/project/operations/img/error_details_v12_5.png
new file mode 100644
index 00000000000..5e3e6300640
--- /dev/null
+++ b/doc/user/project/operations/img/error_details_v12_5.png
Binary files differ
diff --git a/doc/user/project/operations/img/error_tracking_list.png b/doc/user/project/operations/img/error_tracking_list.png
index 194c7b440a4..79b464e021e 100644
--- a/doc/user/project/operations/img/error_tracking_list.png
+++ b/doc/user/project/operations/img/error_tracking_list.png
Binary files differ
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 326a2d302d2..2f16606c5a8 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
@@ -169,6 +169,22 @@ from the GitLab project.
in place: your domain will be periodically reverified, and may be
disabled if the record is removed.
+##### Troubleshooting Pages domain verification
+
+To manually verify that you have properly configured the domain verification
+`TXT` DNS entry, you can run the following command in your terminal:
+
+```
+dig _gitlab-pages-verification-code.<YOUR-PAGES-DOMAIN> TXT
+```
+
+Expect the output:
+
+```
+;; ANSWER SECTION:
+_gitlab-pages-verification-code.<YOUR-PAGES-DOMAIN>. 300 IN TXT "gitlab-pages-verification-code=<YOUR-VERIFICATION-CODE>"
+```
+
### Adding more domain aliases
You can add more than one alias (custom domains and subdomains) to the same project.
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 c9b504dc6ee..1d64e843e46 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
@@ -5,7 +5,8 @@ description: "Automatic Let's Encrypt SSL certificates for GitLab Pages."
# GitLab Pages integration with Let's Encrypt
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) in GitLab 12.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) in GitLab 12.1. For versions earlier than GitLab 12.1, see the [manual Let's Encrypt instructions](../lets_encrypt_for_gitlab_pages.md).
+This feature is in **beta** and may still have bugs. See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996) for more information.
The GitLab Pages integration with Let's Encrypt (LE) allows you
to use LE certificates for your Pages website with custom domains
@@ -16,19 +17,11 @@ GitLab does it for you, out-of-the-box.
open source Certificate Authority.
CAUTION: **Caution:**
-This feature is in **beta** and might present bugs and UX issues
-such as [#64870](https://gitlab.com/gitlab-org/gitlab-foss/issues/64870).
-See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-foss/issues/28996)
-for more information.
-
-CAUTION: **Caution:**
-This feature covers only certificates for **custom domains**,
-not the wildcard certificate required to run [Pages daemon](../../../../administration/pages/index.md) **(CORE ONLY)**.
-Wildcard certificate generation is tracked in [this issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3342).
+This feature covers only certificates for **custom domains**, not the wildcard certificate required to run [Pages daemon](../../../../administration/pages/index.md) **(CORE ONLY)**. Wildcard certificate generation is tracked in [this issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3342).
## Requirements
-Before you can enable automatic provisioning of a SSL certificate for your domain, make sure you have:
+Before you can enable automatic provisioning of an SSL certificate for your domain, make sure you have:
- Created a [project](../getting_started_part_two.md) in GitLab
containing your website's source code.
@@ -36,7 +29,7 @@ Before you can enable automatic provisioning of a SSL certificate for your domai
pointing it to your Pages website.
- [Added your domain to your Pages project](index.md#1-add-a-custom-domain-to-pages)
and verified your ownership.
-- Have your website up and running, accessible through your custom domain.
+- Verified your website is up and running, accessible through your custom domain.
NOTE: **Note:**
GitLab's Let's Encrypt integration is enabled and available on GitLab.com.
@@ -45,7 +38,7 @@ For **self-managed** GitLab instances, make sure your administrator has
## Enabling Let's Encrypt integration for your custom domain
-Once you've met the requirements, to enable Let's Encrypt integration:
+Once you've met the requirements, enable Let's Encrypt integration:
1. Navigate to your project's **Settings > Pages**.
1. Find your domain and click **Details**.
diff --git a/doc/user/project/pages/getting_started/fork_sample_project.md b/doc/user/project/pages/getting_started/fork_sample_project.md
new file mode 100644
index 00000000000..ac1a29ca2a0
--- /dev/null
+++ b/doc/user/project/pages/getting_started/fork_sample_project.md
@@ -0,0 +1,61 @@
+---
+type: reference, howto
+---
+
+# New Pages website from a forked sample
+
+To get started with GitLab Pages from a sample website, the easiest
+way to do it is by using one of the [bundled templates](pages_bundled_template.md).
+If you don't find one that suits your needs, you can opt by
+forking (copying) a [sample project from the most popular Static Site Generators](https://gitlab.com/pages).
+
+<table class="borderless-table center fixed-table middle width-80">
+ <tr>
+ <td style="width: 30%"><img src="../img/icons/fork.png" alt="Fork" class="image-noshadow half-width"></td>
+ <td style="width: 10%">
+ <strong>
+ <i class="fa fa-angle-double-right" aria-hidden="true"></i>
+ </strong>
+ </td>
+ <td style="width: 30%"><img src="../img/icons/terminal.png" alt="Deploy" class="image-noshadow half-width"></td>
+ <td style="width: 10%">
+ <strong>
+ <i class="fa fa-angle-double-right" aria-hidden="true"></i>
+ </strong>
+ </td>
+ <td style="width: 30%"><img src="../img/icons/click.png" alt="Visit" class="image-noshadow half-width"></td>
+ </tr>
+ <tr>
+ <td><em>Fork an example project</em></td>
+ <td></td>
+ <td><em>Deploy your website</em></td>
+ <td></td>
+ <td><em>Visit your website's URL</em></td>
+ </tr>
+</table>
+
+**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=TWqh9MtT4Bg) with all the steps below.**
+
+1. [Fork](../../../../gitlab-basics/fork-project.md) a sample project from the [GitLab Pages examples](https://gitlab.com/pages) group.
+1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
+ and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
+ site to the server.
+1. Once the pipeline has finished successfully, find the link to visit your
+ website from your project's **Settings > Pages**. It can take aproximatelly
+ 30 minutes to be deployed.
+
+You can also take some **optional** further steps:
+
+- _Remove the fork relationship._ The fork relationship is necessary to contribute back to the project you originally forked from. If you don't have any intentions to do so, you can remove it. To do so, navigate to your project's **Settings**, expand **Advanced settings**, and scroll down to **Remove fork relationship**:
+
+ ![remove fork relationship](../img/remove_fork_relationship.png)
+
+- _Make it a user or group website._ To turn a **project website** forked
+ from the Pages group into a **user/group** website, you'll need to:
+ - Rename it to `namespace.gitlab.io`: go to your project's
+ **Settings > General** and expand **Advanced**. Scroll down to
+ **Rename repository** and change the path to `namespace.gitlab.io`.
+ - Adjust your SSG's [base URL](../getting_started_part_one.md#urls-and-baseurls) from `"project-name"` to
+ `""`. This setting will be at a different place for each SSG, as each of them
+ have their own structure and file tree. Most likely, it will be in the SSG's
+ config file.
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
new file mode 100644
index 00000000000..62b5fa33117
--- /dev/null
+++ b/doc/user/project/pages/getting_started/new_or_existing_website.md
@@ -0,0 +1,45 @@
+---
+type: reference, howto
+---
+
+# Start a new Pages website from scratch or deploy an existing website
+
+If you already have a website and want to deploy it with GitLab Pages,
+or, if you want to start a new site from scratch, you'll need to:
+
+- Create a new project in GitLab to hold your site content.
+- Set up GitLab CI/CD to deploy your website to Pages.
+
+To do so, follow the steps below.
+
+1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
+ click **New project**, and name it according to the
+ [Pages domain names](../getting_started_part_one.md#gitlab-pages-default-domain-names).
+1. Clone it to your local computer, add your website
+ files to your project, add, commit and push to GitLab.
+ Alternativelly, you can run `git init` in your local directory,
+ add the remote URL:
+ `git remote add origin git@gitlab.com:namespace/project-name.git`,
+ then add, commit, and push to GitLab.
+1. From the your **Project**'s page, click **Set up CI/CD**:
+
+ ![setup GitLab CI/CD](../img/setup_ci.png)
+
+1. Choose one of the templates from the dropbox menu.
+ Pick up the template corresponding to the SSG you're using (or plain HTML).
+
+ ![gitlab-ci templates](../img/choose_ci_template.png)
+
+ Note that, if you don't find a corresponding template, you can look into
+ [GitLab Pages group of sample projects](https://gitlab.com/pages),
+ you may find one among them that suits your needs, from which you
+ can copy `.gitlab-ci.yml`'s content and adjust for your case.
+ If you don't find it there either, [learn how to write a `.gitlab-ci.yml`
+ file for GitLab Pages](../getting_started_part_four.md).
+
+Once you have both site files and `.gitlab-ci.yml` in your project's
+root, GitLab CI/CD will build your site and deploy it with Pages.
+Once the first build passes, you access your site by
+navigating to your **Project**'s **Settings** > **Pages**,
+where you'll find its default URL. It can take approximately 30 min to be
+deployed.
diff --git a/doc/user/project/pages/getting_started/pages_bundled_template.md b/doc/user/project/pages/getting_started/pages_bundled_template.md
new file mode 100644
index 00000000000..802abeb3cc2
--- /dev/null
+++ b/doc/user/project/pages/getting_started/pages_bundled_template.md
@@ -0,0 +1,29 @@
+---
+type: reference, howto
+---
+
+# New Pages website from a bundled template
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47857)
+in GitLab 11.8.
+
+The simplest way to create a GitLab Pages site is to use one of the most
+popular templates, which come already bundled with GitLab and are ready to go.
+
+1. From the top navigation, click the **+** button and select **New project**.
+1. Select **Create from Template**.
+1. Choose one of the templates starting with **Pages**:
+
+ ![Project templates for Pages](../img/pages_project_templates_v11_8.png)
+
+1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
+ and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
+ site to the server.
+1. After the pipeline has finished successfully, wait approximately 30 minutes
+ for your website to be visible. After waiting 30 minutes, find the link to
+ visit your website from your project's **Settings > Pages**. If the link
+ leads to a 404 page, wait a few minutes and try again.
+
+Your website is then visible on your domain and you can modify your files
+as you wish. For every modification pushed to your repository, GitLab CI/CD
+will run a new pipeline to immediately publish your changes to the server.
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 0b1cae9ab4c..b876e547ba5 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -3,25 +3,12 @@ last_updated: 2018-06-04
type: concepts, reference
---
-# Static sites and GitLab Pages domains
+# GitLab Pages domain names, URLs, and baseurls
On this document, learn how to name your project for GitLab Pages
according to your intended website's URL.
-## Static sites
-
-GitLab Pages only supports static websites, meaning,
-your output files must be HTML, CSS, and JavaScript only.
-
-To create your static site, you can either hardcode in HTML,
-CSS, and JS, or use a [Static Site Generator (SSG)](https://www.staticgen.com/)
-to simplify your code and build the static site for you,
-which is highly recommendable and much faster than hardcoding.
-
-See the [further reading](#further-reading) section below for
-references on static site concepts.
-
-## GitLab Pages domain names
+## GitLab Pages default domain names
>**Note:**
If you use your own GitLab instance to deploy your
@@ -93,11 +80,35 @@ To understand Pages domains clearly, read the examples below.
- On your GitLab instance, replace `gitlab.io` above with your
Pages server domain. Ask your sysadmin for this information.
-_Read on about [Projects for GitLab Pages and URL structure](getting_started_part_two.md)._
+## URLs and baseurls
+
+Every Static Site Generator (SSG) default configuration expects
+to find your website under a (sub)domain (`example.com`), not
+in a subdirectory of that domain (`example.com/subdir`). Therefore,
+whenever you publish a project website (`namespace.gitlab.io/project-name`),
+you'll have to look for this configuration (base URL) on your SSG's
+documentation and set it up to reflect this pattern.
+
+For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
+configuration file, `_config.yml`. If your website URL is
+`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
+
+```yaml
+baseurl: "/blog"
+```
+
+On the contrary, if you deploy your website after forking one of
+our [default examples](https://gitlab.com/pages), the baseurl will
+already be configured this way, as all examples there are project
+websites. If you decide to make yours a user or group website, you'll
+have to remove this configuration from your project. For the Jekyll
+example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
+
+```yaml
+baseurl: ""
+```
-### Further reading
+## Custom domains
-- Read through this technical overview on [Static versus Dynamic Websites](https://about.gitlab.com/blog/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
-- Understand [how modern Static Site Generators work](https://about.gitlab.com/blog/2016/06/10/ssg-overview-gitlab-pages-part-2/) and what you can add to your static site
-- You can use [any SSG with GitLab Pages](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
-- Fork an [example project](https://gitlab.com/pages) to build your website based upon
+GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
+See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information.
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index ff752917087..70e84f5d486 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -1,172 +1,5 @@
---
-last_updated: 2019-06-04
-type: reference, howto
+redirect_to: 'index.md'
---
-# Projects for GitLab Pages and URL structure
-
-## What you need to get started
-
-To get started with GitLab Pages, you need:
-
-1. A project, thus a repository to hold your website's codebase.
-1. A configuration file (`.gitlab-ci.yml`) to deploy your site.
-1. A specific `job` called `pages` in the configuration file
- that will make GitLab aware that you are deploying a GitLab Pages website.
-1. A `public` directory with the static content of the website.
-
-Optional Features:
-
-1. A custom domain or subdomain.
-1. A DNS pointing your (sub)domain to your Pages site.
- 1. **Optional**: an SSL/TLS certificate so your custom
- domain is accessible under HTTPS.
-
-The optional settings, custom domain, DNS records, and SSL/TLS certificates, are described in [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md)).
-
-## Project
-
-Your GitLab Pages project is a regular project created the
-same way you do for the other ones. To get started with GitLab Pages, you have three ways:
-
-- [Use one of the popular project templates bundled with GitLab](#use-one-of-the-popular-pages-templates-bundled-with-gitlab).
-- [Fork one of the templates from Page Examples](#fork-a-project-to-get-started-from).
-- [Create a new project from scratch](#create-a-project-from-scratch).
-
-### Use one of the popular Pages templates bundled with GitLab
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47857)
-in GitLab 11.8.
-
-The simplest way to create a GitLab Pages site is to
-[use one of the most popular templates](index.md#getting-started),
-which come already bundled with GitLab and are ready to go.
-
-### Fork a project to get started from
-
-If you don't find an existing project template that suits you,
-we've created this [group](https://gitlab.com/pages) of default projects
-containing the most popular SSGs templates to get you started.
-
-<table class="borderless-table center fixed-table middle width-80">
- <tr>
- <td style="width: 30%"><img src="img/icons/fork.png" alt="Fork" class="image-noshadow half-width"></td>
- <td style="width: 10%">
- <strong>
- <i class="fa fa-angle-double-right" aria-hidden="true"></i>
- </strong>
- </td>
- <td style="width: 30%"><img src="img/icons/terminal.png" alt="Deploy" class="image-noshadow half-width"></td>
- <td style="width: 10%">
- <strong>
- <i class="fa fa-angle-double-right" aria-hidden="true"></i>
- </strong>
- </td>
- <td style="width: 30%"><img src="img/icons/click.png" alt="Visit" class="image-noshadow half-width"></td>
- </tr>
- <tr>
- <td><em>Fork an example project</em></td>
- <td></td>
- <td><em>Deploy your website</em></td>
- <td></td>
- <td><em>Visit your website's URL</em></td>
- </tr>
-</table>
-
-**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> Watch a [video tutorial](https://www.youtube.com/watch?v=TWqh9MtT4Bg) with all the steps below.**
-
-1. [Fork](../../../gitlab-basics/fork-project.md) a sample project from the [GitLab Pages examples](https://gitlab.com/pages) group.
-1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
- and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
- site to the server.
-1. Once the pipeline has finished successfully, find the link to visit your
- website from your project's **Settings > Pages**.
-
-You can also take some **optional** further steps:
-
-- _Remove the fork relationship._ The fork relationship is necessary to contribute back to the project you originally forked from. If you don't have any intentions to do so, you can remove it. To do so, navigate to your project's **Settings**, expand **Advanced settings**, and scroll down to **Remove fork relationship**:
-
- ![remove fork relationship](img/remove_fork_relationship.png)
-
-- _Make it a user or group website._ To turn a **project website** forked
- from the Pages group into a **user/group** website, you'll need to:
- - Rename it to `namespace.gitlab.io`: go to your project's
- **Settings > General** and expand **Advanced**. Scroll down to
- **Rename repository** and change the path to `namespace.gitlab.io`.
- - Adjust your SSG's [base URL](#urls-and-baseurls) from `"project-name"` to
- `""`. This setting will be at a different place for each SSG, as each of them
- have their own structure and file tree. Most likely, it will be in the SSG's
- config file.
-
-### Create a project from scratch
-
-1. From your **Project**'s **[Dashboard](https://gitlab.com/dashboard/projects)**,
- click **New project**, and name it according to the
- [Pages domain names](getting_started_part_one.md#gitlab-pages-domain-names).
-1. Clone it to your local computer, add your website
- files to your project, add, commit and push to GitLab.
-1. From the your **Project**'s page, click **Set up CI/CD**:
-
- ![setup GitLab CI/CD](img/setup_ci.png)
-
-1. Choose one of the templates from the dropbox menu.
- Pick up the template corresponding to the SSG you're using (or plain HTML).
-
- ![gitlab-ci templates](img/choose_ci_template.png)
-
-Once you have both site files and `.gitlab-ci.yml` in your project's
-root, GitLab CI/CD will build your site and deploy it with Pages.
-Once the first build passes, you see your site is live by
-navigating to your **Project**'s **Settings** > **Pages**,
-where you'll find its default URL.
-
-> **Notes:**
->
-> - GitLab Pages [supports any SSG](https://about.gitlab.com/blog/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/), but,
-> if you don't find yours among the templates, you'll need
-> to configure your own `.gitlab-ci.yml`. To do that, please
-> read through the article [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md). New SSGs are very welcome among
-> the [example projects](https://gitlab.com/pages). If you set
-> up a new one, please
-> [contribute](https://gitlab.com/pages/pages.gitlab.io/blob/master/CONTRIBUTING.md)
-> to our examples.
->
-> - The second step _"Clone it to your local computer"_, can be done
-> differently, achieving the same results: instead of cloning the bare
-> repository to you local computer and moving your site files into it,
-> you can run `git init` in your local website directory, add the
-> remote URL: `git remote add origin git@gitlab.com:namespace/project-name.git`,
-> then add, commit, and push to GitLab.
-
-## URLs and Baseurls
-
-Every Static Site Generator (SSG) default configuration expects
-to find your website under a (sub)domain (`example.com`), not
-in a subdirectory of that domain (`example.com/subdir`). Therefore,
-whenever you publish a project website (`namespace.gitlab.io/project-name`),
-you'll have to look for this configuration (base URL) on your SSG's
-documentation and set it up to reflect this pattern.
-
-For example, for a Jekyll site, the `baseurl` is defined in the Jekyll
-configuration file, `_config.yml`. If your website URL is
-`https://john.gitlab.io/blog/`, you need to add this line to `_config.yml`:
-
-```yaml
-baseurl: "/blog"
-```
-
-On the contrary, if you deploy your website after forking one of
-our [default examples](https://gitlab.com/pages), the baseurl will
-already be configured this way, as all examples there are project
-websites. If you decide to make yours a user or group website, you'll
-have to remove this configuration from your project. For the Jekyll
-example we've just mentioned, you'd have to change Jekyll's `_config.yml` to:
-
-```yaml
-baseurl: ""
-```
-
-## Custom Domains
-
-GitLab Pages supports custom domains and subdomains, served under HTTP or HTTPS.
-See [GitLab Pages custom domains and SSL/TLS Certificates](custom_domains_ssl_tls_certification/index.md) for more information.
+This document was moved to [another location](index.md#getting-started).
diff --git a/doc/user/project/pages/img/new_project_for_pages_v12_5.png b/doc/user/project/pages/img/new_project_for_pages_v12_5.png
new file mode 100644
index 00000000000..8d2dc0bf9f5
--- /dev/null
+++ b/doc/user/project/pages/img/new_project_for_pages_v12_5.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_workflow_v12_5.png b/doc/user/project/pages/img/pages_workflow_v12_5.png
new file mode 100644
index 00000000000..ca5190fca79
--- /dev/null
+++ b/doc/user/project/pages/img/pages_workflow_v12_5.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 7d533c6f9d1..abd67c90dd6 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -19,38 +19,7 @@ You can use it either for personal or business websites, such as
portfolios, documentation, manifestos, and business presentations.
You can also attribute any license to your content.
-<table class="borderless-table center fixed-table">
- <tr>
- <td style="width: 22%"><img src="img/icons/cogs.png" alt="SSGs" class="image-noshadow half-width"></td>
- <td style="width: 4%">
- <strong>
- <i class="fa fa-angle-double-right" aria-hidden="true"></i>
- </strong>
- </td>
- <td style="width: 22%"><img src="img/icons/monitor.png" alt="Websites" class="image-noshadow half-width"></td>
- <td style="width: 4%">
- <strong>
- <i class="fa fa-angle-double-right" aria-hidden="true"></i>
- </strong>
- </td>
- <td style="width: 22%"><img src="img/icons/free.png" alt="Pages is free" class="image-noshadow half-width"></td>
- <td style="width: 4%">
- <strong>
- <i class="fa fa-angle-double-right" aria-hidden="true"></i>
- </strong>
- </td>
- <td style="width: 22%"><img src="img/icons/lock.png" alt="Secure your website" class="image-noshadow half-width"></td>
- </tr>
- <tr>
- <td><em>Use any static website generator or plain HTML</em></td>
- <td></td>
- <td><em>Create websites for your projects, groups, or user account</em></td>
- <td></td>
- <td><em>Host on GitLab.com for free, or on your own GitLab instance</em></td>
- <td></td>
- <td><em>Connect your custom domain(s) and TLS certificates</em></td>
- </tr>
-</table>
+<img src="img/pages_workflow_v12_5.png" alt="Pages websites workflow" class="image-noshadow">
Pages is available for free for all GitLab.com users as well as for self-managed
instances (GitLab Core, Starter, Premium, and Ultimate).
@@ -73,6 +42,7 @@ publish any website written directly in plain HTML, CSS, and JavaScript.</p>
To use GitLab Pages, first you need to create a project in GitLab to upload your website's
files to. These projects can be either public, internal, or private, at your own choice.
+
GitLab will always deploy your website from a very specific folder called `public` in your
repository. Note that when you create a new project in GitLab, a [repository](../repository/index.md)
becomes available automatically.
@@ -80,67 +50,50 @@ becomes available automatically.
To deploy your site, GitLab will use its built-in tool called [GitLab CI/CD](../../../ci/README.md),
to build your site and publish it to the GitLab Pages server. The sequence of
scripts that GitLab CI/CD runs to accomplish this task is created from a file named
-`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will.
+`.gitlab-ci.yml`, which you can [create and modify](getting_started_part_four.md) at will. A specific `job` called `pages` in the configuration file will make GitLab aware that you are deploying a GitLab Pages website.
-You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-domain-names),
+You can either use GitLab's [default domain for GitLab Pages websites](getting_started_part_one.md#gitlab-pages-default-domain-names),
`*.gitlab.io`, or your own domain (`example.com`). In that case, you'll
need admin access to your domain's registrar (or control panel) to set it up with Pages.
-Optionally, when adding your own domain, you can add an SSL/TLS certificate to secure your
-site under the HTTPS protocol.
-
### Getting started
To get started with GitLab Pages, you can either:
-- [Create a project from scratch](getting_started_part_two.md#create-a-project-from-scratch).
-- [Copy an existing example project](getting_started_part_two.md#fork-a-project-to-get-started-from).
-- Use a bundled project template ready to go:
-
-1. From the top navigation, click the **+** button and select **New project**.
-1. Select **Create from Template**.
-1. Choose one of the templates starting with **Pages**:
-
- ![Project templates for Pages](img/pages_project_templates_v11_8.png)
+- [Use a bundled website template ready to go](getting_started/pages_bundled_template.md).
+- [Copy an existing sample](getting_started/fork_sample_project.md).
+- [Create a website from scratch or deploy an existing one](getting_started/new_or_existing_website.md).
-1. From the left sidebar, navigate to your project's **CI/CD > Pipelines**
- and click **Run pipeline** to trigger GitLab CI/CD to build and deploy your
- site to the server.
-1. After the pipeline has finished successfully, wait approximately 30 minutes
- for your website to be visible. After waiting 30 minutes, find the link to
- visit your website from your project's **Settings > Pages**. If the link
- leads to a 404 page, wait a few minutes and try again.
+<img src="img/new_project_for_pages_v12_5.png" alt="New projects for GitLab Pages" class="image-noshadow">
-Your website is then visible on your domain and you can modify your files
-as you wish. For every modification pushed to your repository, GitLab CI/CD
-will run a new pipeline to immediately publish your changes to the server.
+Optional features:
-_Advanced options:_
+- Use a [custom domain or subdomain](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain).
+- Add an [SSL/TLS certificate to secure your site under the HTTPS protocol](custom_domains_ssl_tls_certification/index.md#adding-an-ssltls-certificate-to-pages).
-- [Use a custom domain](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain)
-- Apply [SSL/TLS certification](custom_domains_ssl_tls_certification/index.md#adding-an-ssltls-certificate-to-pages) to your custom domain
+Note that, if you're using GitLab Pages default domain (`.gitlab.io`),
+your website will be automatically secure and available under
+HTTPS. If you're using your own custom domain, you can
+optionally secure it with SSL/TLS certificates.
## Availability
If you're using GitLab.com, your website will be publicly available to the internet.
+
+To restrict access to your website, enable [GitLab Pages Access Control](pages_access_control.md).
+
If you're using self-managed instances (Core, Starter, Premium, or Ultimate),
your websites will be published on your own server, according to the
[Pages admin settings](../../../administration/pages/index.md) chosen by your sysadmin,
who can opt for making them public or internal to your server.
-Note that, if you're using GitLab Pages default domain (`.gitlab.io`),
-your website will be automatically secure and available under
-HTTPS. If you're using your own custom domain, you can
-optionally secure it with SSL/TLS certificates.
-
## Explore GitLab Pages
To learn more about configuration options for GitLab Pages, read the following:
| Document | Description |
| --- | --- |
-| [Static websites and Pages domains](getting_started_part_one.md) | Understand what is a static website, and how GitLab Pages default domains work. |
-| [Projects and URL structure](getting_started_part_two.md) | Forking projects and creating new ones from scratch, understanding URLs structure and baseurls. |
+| [GitLab Pages domain names, URLs, and baseurls](getting_started_part_one.md) | Understand how GitLab Pages default domains work. |
| [GitLab CI/CD for GitLab Pages](getting_started_part_four.md) | Understand how to create your own `.gitlab-ci.yml` for your site. |
| [Exploring GitLab Pages](introduction.md) | Requirements, technical aspects, specific GitLab CI's configuration options, Access Control, custom 404 pages, limitations, FAQ. |
|---+---|
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 86257e2aa03..01e1909f6d6 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -69,40 +69,7 @@ don't have to create and edit HTML files manually. For example, Jekyll has the
## GitLab Pages Access Control **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33422) in GitLab 11.5.
-
-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:
-
-1. Navigate to your project's **Settings > General > Permissions**.
-1. Toggle the **Pages** button to enable the access control.
-
- NOTE: **Note:**
- If you don't see the toggle button, that means that it's not enabled.
- Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
-
-1. The Pages access control dropdown allows you to set who can view pages hosted
- with GitLab Pages, depending on your project's visibility:
-
- - If your project is private:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- - If your project is internal:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
- - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
- - If your project is public:
- - **Only project members**: Only project members will be able to browse the website.
- - **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
-
-1. Click **Save changes**.
-
----
-
-The next time someone tries to access your website and the access control is
-enabled, they will be presented with a page to sign into GitLab and verify they
-can access the website.
+To restrict access to your website, enable [GitLab Pages Access Control](pages_access_control.md).
## Unpublishing your Pages
diff --git a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
index 1338c7e58f5..c9bd3e35a5f 100644
--- a/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
+++ b/doc/user/project/pages/lets_encrypt_for_gitlab_pages.md
@@ -21,8 +21,8 @@ open source Certificate Authority.
To follow along with this tutorial, we assume you already have:
-- Created a [project](getting_started_part_two.md) in GitLab which
- contains your website's source code.
+- [Created a project](index.md#getting-started) in GitLab
+ containing your website's source code.
- Acquired a domain (`example.com`) and added a [DNS entry](custom_domains_ssl_tls_certification/index.md#set-up-pages-with-a-custom-domain)
pointing it to your Pages website.
- [Added your domain to your Pages project](custom_domains_ssl_tls_certification/index.md#steps)
diff --git a/doc/user/project/pages/pages_access_control.md b/doc/user/project/pages/pages_access_control.md
new file mode 100644
index 00000000000..cd715c6e3b9
--- /dev/null
+++ b/doc/user/project/pages/pages_access_control.md
@@ -0,0 +1,48 @@
+---
+type: reference, howto
+---
+
+# GitLab Pages Access Control
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/33422) in GitLab 11.5.
+> - Available on GitLab.com in GitLab 12.4.
+
+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:
+
+1. Navigate to your project's **Settings > General > Permissions**.
+1. Toggle the **Pages** button to enable the access control.
+
+ NOTE: **Note:**
+ If you don't see the toggle button, that means that it's not enabled.
+ Ask your administrator to [enable it](../../../administration/pages/index.md#access-control).
+
+1. The Pages access control dropdown allows you to set who can view pages hosted
+ with GitLab Pages, depending on your project's visibility:
+
+ - If your project is private:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+ - If your project is internal:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone with access**: Everyone logged into GitLab will be able to browse the website, no matter their project membership.
+ - **Everyone**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+ - If your project is public:
+ - **Only project members**: Only project members will be able to browse the website.
+ - **Everyone with access**: Everyone, both logged into and logged out of GitLab, will be able to browse the website, no matter their project membership.
+
+1. Click **Save changes**.
+
+The next time someone tries to access your website and the access control is
+enabled, they will be presented with a page to sign into GitLab and verify they
+can access the website.
+
+## Terminating a Pages session
+
+If you want to log out from your Pages website,
+you can do so by revoking application access token for GitLab Pages:
+
+1. Navigate to your profile's **Settings > Applications**.
+1. Find **Authorized applications** at the bottom of the page.
+1. Find **GitLab Pages** and press the **Revoke** button.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index b7c9faeb1df..8ce575222b9 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -55,7 +55,7 @@ the actions that different roles can perform with the protected branch.
For example, you could set "Allowed to push" to "No one", and "Allowed to merge"
to "Developers + Maintainers", to require _everyone_ to submit a merge request for
changes going into the protected branch. This is compatible with workflows like
-the [GitLab workflow](../../workflow/gitlab_flow.md).
+the [GitLab workflow](../../topics/gitlab_flow.md).
However, there are workflows where that is not needed, and only protecting from
force pushes and branch removal is useful. For those workflows, you can allow
@@ -118,10 +118,11 @@ all matching branches:
When a protected branch or wildcard protected branches are set to
[**No one** is **Allowed to push**](#using-the-allowed-to-merge-and-allowed-to-push-settings),
-Developers (and users with higher [permission levels](../permissions.md)) are allowed
-to create a new protected branch, but only via the UI or through the API (to avoid
-creating protected branches accidentally from the command line or from a Git
-client application).
+Developers (and users with higher [permission levels](../permissions.md)) are
+allowed to create a new protected branch as long as they are
+[**Allowed to merge**](#using-the-allowed-to-merge-and-allowed-to-push-settings).
+This can only be done via the UI or through the API (to avoid creating protected
+branches accidentally from the command line or from a Git client application).
To create a new branch through the user interface:
diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md
index 51c46dbd1d4..8952f845b96 100644
--- a/doc/user/project/push_options.md
+++ b/doc/user/project/push_options.md
@@ -75,3 +75,33 @@ merge request, and target a branch named `my-target-branch`:
```shell
git push -o merge_request.create -o merge_request.target=my-target-branch
```
+
+Additionally if you want the merge request to merge as soon as the pipeline succeeds you can do:
+
+```shell
+git push -o merge_request.create -o merge_request.target=my-target-branch -o merge_request.merge_when_pipeline_succeeds
+```
+
+## Useful Git aliases
+
+As shown above, Git push options can cause Git commands to grow very long. If
+you use the same push options frequently, it's useful to create [Git
+aliases](https://git-scm.com/book/en/v2/Git-Basics-Git-Aliases). Git aliases
+are command line shortcuts for Git which can significantly simplify the use of
+long Git commands.
+
+### Merge when pipeline succeeds alias
+
+To set up a Git alias for the [merge when pipeline succeeds Git push
+option](#push-options-for-merge-requests):
+
+```shell
+git config --global alias.mwps "push -o merge_request.create -o merge_request.target=master -o merge_request.merge_when_pipeline_succeeds"
+```
+
+Then to quickly push a local branch that will target master and merge when the
+pipeline succeeds:
+
+```shell
+git mwps origin <local-branch-name>
+```
diff --git a/doc/user/project/releases/img/edit_release_page_v12_5.png b/doc/user/project/releases/img/edit_release_page_v12_5.png
new file mode 100644
index 00000000000..8b9c502a2ef
--- /dev/null
+++ b/doc/user/project/releases/img/edit_release_page_v12_5.png
Binary files differ
diff --git a/doc/user/project/releases/img/milestone_list_with_releases_v12_5.png b/doc/user/project/releases/img/milestone_list_with_releases_v12_5.png
new file mode 100644
index 00000000000..2e3ec08ba87
--- /dev/null
+++ b/doc/user/project/releases/img/milestone_list_with_releases_v12_5.png
Binary files differ
diff --git a/doc/user/project/releases/img/milestone_with_releases_v12_5.png b/doc/user/project/releases/img/milestone_with_releases_v12_5.png
new file mode 100644
index 00000000000..8719a58ce4e
--- /dev/null
+++ b/doc/user/project/releases/img/milestone_with_releases_v12_5.png
Binary files differ
diff --git a/doc/workflow/releases/new_tag.png b/doc/user/project/releases/img/new_tag_12_5.png
index 6137ad2ee56..6137ad2ee56 100644
--- a/doc/workflow/releases/new_tag.png
+++ b/doc/user/project/releases/img/new_tag_12_5.png
Binary files differ
diff --git a/doc/user/project/releases/img/release_edit_button_v12_5.png b/doc/user/project/releases/img/release_edit_button_v12_5.png
new file mode 100644
index 00000000000..f60b0ecb1be
--- /dev/null
+++ b/doc/user/project/releases/img/release_edit_button_v12_5.png
Binary files differ
diff --git a/doc/user/project/releases/img/release_with_milestone_v12_5.png b/doc/user/project/releases/img/release_with_milestone_v12_5.png
new file mode 100644
index 00000000000..2a7a2ee9754
--- /dev/null
+++ b/doc/user/project/releases/img/release_with_milestone_v12_5.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/user/project/releases/img/tags_12_5.png
index 4c032f96125..4c032f96125 100644
--- a/doc/workflow/releases/tags.png
+++ b/doc/user/project/releases/img/tags_12_5.png
Binary files differ
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index ceb077ab8af..8372aefc94c 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -58,6 +58,31 @@ links from your GitLab instance.
NOTE: **NOTE**
You can manipulate links of each release entry with [Release Links API](../../../api/releases/links.md)
+#### Releases associated with milestones
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29020) in GitLab 12.5.
+
+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 display this association with the **Milestone** indicator near
+the top of the Release block on the **Project overview > Releases** page.
+
+![A Release with one associated milestone](img/release_with_milestone_v12_5.png)
+
+Below is an example of milestones with no Releases, one Release, and two
+Releases, respectively.
+
+![Milestones with and without Release associations](img/milestone_list_with_releases_v12_5.png)
+
+This relationship is also visible in the **Releases** section of the sidebar
+when viewing a specific milestone. Below is an example of a milestone
+associated with a large number of Releases.
+
+![Milestone with lots of associated Releases](img/milestone_with_releases_v12_5.png)
+
## Releases list
Navigate to **Project > Releases** in order to see the list of releases for a given
@@ -65,6 +90,27 @@ project.
![Releases list](img/releases.png)
+## Editing a release
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26016) in GitLab 12.5.
+
+To edit the details of a release, navigate to **Project overview > Releases** and click
+the edit button (pencil icon) in the top-right corner of the release you want to modify.
+
+![A release with an edit button](img/release_edit_button_v12_5.png)
+
+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_5.png)
+
+Currently, it is only possible to edit the release title and notes.
+To change other release information, such as its tag, associated
+milestones, 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.
+
## Notification for Releases
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26001) in GitLab 12.4.
@@ -77,6 +123,91 @@ following modal window will be then displayed, from which you can select **New r
![Custom notification - New release](img/custom_notifications_new_release_v12_4.png)
+## Add release notes to Git tags
+
+You can add release notes to any Git tag using the notes feature. Release notes
+behave like any other markdown form in GitLab so you can write text and
+drag and drop files to it. Release notes are stored in GitLab's database.
+
+There are several ways to add release notes:
+
+- In the interface, when you create a new Git tag.
+- In the interface, by adding a note to an existing Git tag.
+- Using the GitLab API.
+
+To create a new tag, navigate to your project's **Repository > Tags** and
+click **New tag**. From there, you can fill the form with all the information
+about the release:
+
+![new_tag](img/new_tag_12_5.png "Creation of a new tag.")
+
+You can also edit an existing tag to add release notes:
+
+![tags](img/tags_12_5.png "Addition of note to an existing tag")
+
+## Release Evidence
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26019) in GitLab 12.5.
+
+Each time a new release is created, specific related data is collected in
+parallel. This dataset will be a snapshot this new release (including linked
+milestones and issues) at moment of creation. Such collection of data will
+provide a chain of custody and facilitate processes like external audits, for example.
+
+The gathered Evidence data is stored in the database upon creation of a new
+release as a JSON object. In GitLab 12.5, a link to
+the Evidence data is provided for [each Release](#releases-list).
+
+Here's what this object can look like:
+
+```json
+{
+ "release": {
+ "id": 5,
+ "tag": "v4.0",
+ "name": "New release",
+ "project_id": 45,
+ "project_name": "Project name",
+ "released_at": "2019-06-28 13:23:40 UTC",
+ "milestones": [
+ {
+ "id": 11,
+ "title": "v4.0-rc1",
+ "state": "closed",
+ "due_date": "2019-05-12 12:00:00 UTC",
+ "created_at": "2019-04-17 15:45:12 UTC",
+ "issues": [
+ {
+ "id": 82,
+ "title": "The top-right popup is broken",
+ "author_name": "John Doe",
+ "author_email": "john@doe.com",
+ "state": "closed",
+ "due_date": "2019-05-10 12:00:00 UTC"
+ },
+ {
+ "id": 89,
+ "title": "The title of this page is misleading",
+ "author_name": "Jane Smith",
+ "author_email": "jane@smith.com",
+ "state": "closed",
+ "due_date": "nil"
+ }
+ ]
+ },
+ {
+ "id": 12,
+ "title": "v4.0-rc2",
+ "state": "closed",
+ "due_date": "2019-05-30 18:30:00 UTC",
+ "created_at": "2019-04-17 15:45:12 UTC",
+ "issues": []
+ }
+ ]
+ }
+}
+```
+
<!-- ## 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
new file mode 100644
index 00000000000..576001d4305
--- /dev/null
+++ b/doc/user/project/repository/file_finder.md
@@ -0,0 +1,45 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/file_finder.html'
+---
+
+# File finder
+
+> [Introduced][gh-9889] in GitLab 8.4.
+
+The file finder feature allows you to search for a file in a repository using the
+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)
+
+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_
+in a project.
+
+Press `t` to launch the File search function when in **Issues**,
+**Merge requests**, **Milestones**, even the project's settings.
+
+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**.
+
+## 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
+results by recognizing patterns that people use while searching.
+
+For example, consider the [GitLab CE repository][ce] 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.
+
+**Protip:** 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"
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
new file mode 100644
index 00000000000..8756760fe4b
--- /dev/null
+++ b/doc/user/project/repository/forking_workflow.md
@@ -0,0 +1,55 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/forking_workflow.html'
+---
+
+# Project forking workflow
+
+Forking a project to your own namespace is useful if you have no write
+access to the project you want to contribute to. If you do have write
+access or can request it, we recommend working together in the same
+repository since it is simpler. See our [GitLab Flow](../../../topics/gitlab_flow.md)
+document more information about using branches to work together.
+
+## Creating a fork
+
+Forking a project is in most cases a two-step process.
+
+1. Click on the fork button located located in between the star and clone buttons on the project's home page.
+
+ ![Fork button](img/forking_workflow_fork_button.png)
+
+1. Once you do that, you'll be presented with a screen where you can choose
+ the namespace to fork to. Only namespaces (groups and your own
+ namespace) where you have write access to, will be shown. Click on the
+ namespace to create your fork there.
+
+ ![Choose namespace](img/forking_workflow_choose_namespace.png)
+
+ **Note:**
+ If the namespace you chose to fork the project to has another project with
+ the same path name, you will be presented with a warning that the forking
+ could not be completed. Try to resolve the error before repeating the forking
+ process.
+
+ ![Path taken error](img/forking_workflow_path_taken_error.png)
+
+After the forking is done, you can start working on the newly created
+repository. There, you will have full [Owner](../../permissions.md)
+access, so you can set it up as you please.
+
+## Merging upstream
+
+Once you are ready to send your code back to the main project, you need
+to create a merge request. Choose your forked project's main branch as
+the source and the original project's main branch as the destination and
+create the [merge request](../merge_requests/index.md).
+
+![Selecting branches](img/forking_workflow_branch_select.png)
+
+You can then assign the merge request to someone to have them review
+your changes. Upon pressing the 'Submit Merge Request' button, your
+changes will be added to the repository and branch you're merging into.
+
+![New merge request](img/forking_workflow_merge_request.png)
+
+[gitlab flow]: https://about.gitlab.com/blog/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
diff --git a/doc/workflow/img/file_finder_find_button.png b/doc/user/project/repository/img/file_finder_find_button.png
index 0c2d7d7bc73..0c2d7d7bc73 100644
--- a/doc/workflow/img/file_finder_find_button.png
+++ b/doc/user/project/repository/img/file_finder_find_button.png
Binary files differ
diff --git a/doc/workflow/img/file_finder_find_file.png b/doc/user/project/repository/img/file_finder_find_file.png
index c2212c7cd9e..c2212c7cd9e 100644
--- a/doc/workflow/img/file_finder_find_file.png
+++ b/doc/user/project/repository/img/file_finder_find_file.png
Binary files differ
diff --git a/doc/workflow/forking/branch_select.png b/doc/user/project/repository/img/forking_workflow_branch_select.png
index 0ea5410f832..0ea5410f832 100644
--- a/doc/workflow/forking/branch_select.png
+++ b/doc/user/project/repository/img/forking_workflow_branch_select.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_choose_namespace.png b/doc/user/project/repository/img/forking_workflow_choose_namespace.png
index eb023ca85f2..eb023ca85f2 100644
--- a/doc/workflow/img/forking_workflow_choose_namespace.png
+++ b/doc/user/project/repository/img/forking_workflow_choose_namespace.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_fork_button.png b/doc/user/project/repository/img/forking_workflow_fork_button.png
index 7fb07529b6d..7fb07529b6d 100644
--- a/doc/workflow/img/forking_workflow_fork_button.png
+++ b/doc/user/project/repository/img/forking_workflow_fork_button.png
Binary files differ
diff --git a/doc/workflow/forking/merge_request.png b/doc/user/project/repository/img/forking_workflow_merge_request.png
index 43851203f3f..43851203f3f 100644
--- a/doc/workflow/forking/merge_request.png
+++ b/doc/user/project/repository/img/forking_workflow_merge_request.png
Binary files differ
diff --git a/doc/workflow/img/forking_workflow_path_taken_error.png b/doc/user/project/repository/img/forking_workflow_path_taken_error.png
index ef62d0ab6a9..ef62d0ab6a9 100644
--- a/doc/workflow/img/forking_workflow_path_taken_error.png
+++ b/doc/user/project/repository/img/forking_workflow_path_taken_error.png
Binary files differ
diff --git a/doc/workflow/img/copy_ssh_public_key_button.png b/doc/user/project/repository/img/repository_mirroring_copy_ssh_public_key_button.png
index e20dae09a4d..e20dae09a4d 100644
--- a/doc/workflow/img/copy_ssh_public_key_button.png
+++ b/doc/user/project/repository/img/repository_mirroring_copy_ssh_public_key_button.png
Binary files differ
diff --git a/doc/workflow/img/repository_mirroring_force_update.png b/doc/user/project/repository/img/repository_mirroring_force_update.png
index 1e6dcb9ea08..1e6dcb9ea08 100644
--- a/doc/workflow/img/repository_mirroring_force_update.png
+++ b/doc/user/project/repository/img/repository_mirroring_force_update.png
Binary files differ
diff --git a/doc/workflow/img/repository_mirroring_pull_settings_lower.png b/doc/user/project/repository/img/repository_mirroring_pull_settings_lower.png
index a3e0b74ddf8..a3e0b74ddf8 100644
--- a/doc/workflow/img/repository_mirroring_pull_settings_lower.png
+++ b/doc/user/project/repository/img/repository_mirroring_pull_settings_lower.png
Binary files differ
diff --git a/doc/workflow/img/repository_mirroring_pull_settings_upper.png b/doc/user/project/repository/img/repository_mirroring_pull_settings_upper.png
index 8e15b5a0784..8e15b5a0784 100644
--- a/doc/workflow/img/repository_mirroring_pull_settings_upper.png
+++ b/doc/user/project/repository/img/repository_mirroring_pull_settings_upper.png
Binary files differ
diff --git a/doc/workflow/img/repository_mirroring_push_settings.png b/doc/user/project/repository/img/repository_mirroring_push_settings.png
index 3c0eacaa2df..3c0eacaa2df 100644
--- a/doc/workflow/img/repository_mirroring_push_settings.png
+++ b/doc/user/project/repository/img/repository_mirroring_push_settings.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index c14783b72bd..5a6e011220c 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -11,7 +11,8 @@ A repository is part of a [project](../index.md), which has a lot of other featu
## Create a repository
To create a new repository, all you need to do is
-[create a new project](../../../gitlab-basics/create-project.md).
+[create a new project](../../../gitlab-basics/create-project.md) or
+[fork an existing project](forking_workflow.md).
Once you create a new project, you can add new files via UI
(read the section below) or via command line.
@@ -55,7 +56,7 @@ To get started with the command line, please read through the
### Find files
-Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
+Use GitLab's [file finder](file_finder.md) to search for files in a repository.
### Supported markup languages and extensions
diff --git a/doc/user/project/repository/repository_mirroring.md b/doc/user/project/repository/repository_mirroring.md
new file mode 100644
index 00000000000..a682983ab83
--- /dev/null
+++ b/doc/user/project/repository/repository_mirroring.md
@@ -0,0 +1,430 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/repository_mirroring.html'
+---
+
+# Repository mirroring
+
+Repository mirroring allows for mirroring of repositories to and from external sources. It can be
+used to mirror branches, tags, and commits between repositories.
+
+A repository mirror at GitLab will be updated automatically. You can also manually trigger an update
+at most once every 5 minutes.
+
+## Overview
+
+Repository mirroring is useful when you want to use a repository outside of GitLab.
+
+There are two kinds of repository mirroring supported by GitLab:
+
+- Push: for mirroring a GitLab repository to another location.
+- Pull: for mirroring a repository from another location to GitLab. **(STARTER)**
+
+When the mirror repository is updated, all new branches, tags, and commits will be visible in the
+project's activity feed.
+
+Users with at least [developer access](../../permissions.md) to the project can also force an
+immediate update, unless:
+
+- The mirror is already being updated.
+- 5 minutes haven't elapsed since its last update.
+
+## Use cases
+
+The following are some possible use cases for repository mirroring:
+
+- You migrated to GitLab but still need to keep your project in another source. In that case, you
+ can simply set it up to mirror to GitLab (pull) and all the essential history of commits, tags,
+ and branches will be available in your GitLab instance. **(STARTER)**
+- You have old projects in another source that you don't use actively anymore, but don't want to
+ remove for archiving purposes. In that case, you can create a push mirror so that your active
+ GitLab repository can push its changes to the old location.
+
+## Pushing to a remote repository **(CORE)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/249) in GitLab Enterprise Edition 8.7.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18715) in 10.8.
+
+For an existing project, you can set up push mirroring as follows:
+
+1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories** section.
+1. Enter a repository URL.
+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. Click the **Mirror repository** button to save the configuration.
+
+![Repository mirroring push settings screen](img/repository_mirroring_push_settings.png)
+
+When push mirroring is enabled, only push commits directly to the mirrored repository to prevent the
+mirror diverging. All changes will end up in the mirrored repository whenever:
+
+- Commits are pushed to GitLab.
+- A [forced update](#forcing-an-update-core) is initiated.
+
+Changes pushed to files in the repository are automatically pushed to the remote mirror at least:
+
+- Within five minutes of being received.
+- Within one minute if **Only mirror protected branches** is enabled.
+
+In the case of a diverged branch, you will see an error indicated at the **Mirroring repositories**
+section.
+
+### Push only protected branches **(CORE)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18715) in 10.8.
+
+You can choose to only push your protected branches from GitLab to your remote repository.
+
+To use this option, check the **Only mirror protected branches** box when creating a repository
+mirror.
+
+## 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:
+
+1. Create a [GitHub personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with the `public_repo` box checked.
+1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`.
+1. Fill in **Password** field with your GitHub personal access token.
+1. Click the **Mirror repository** button.
+
+The mirrored repository will be listed. For example, `https://*****:*****@github.com/<your_github_group>/<your_github_project>.git`.
+
+The repository will push soon. To force a push, click the appropriate button.
+
+## Setting up a push mirror to another GitLab instance with 2FA activated
+
+1. On the destination GitLab instance, create a [personal access token](../../profile/personal_access_tokens.md) with `API` scope.
+1. On the source GitLab instance:
+ 1. Fill in the **Git repository URL** field using this format: `https://oauth2@<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
+ 1. Fill in **Password** field with the GitLab personal access token created on the destination GitLab instance.
+ 1. Click the **Mirror repository** button.
+
+## Pulling from a remote repository **(STARTER)**
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/51) in GitLab Enterprise Edition 8.2.
+> - [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
+
+NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab/issues/10361) to
+GitLab.com users until March 22nd, 2020.
+
+You can set up a repository to automatically have its branches, tags, and commits updated from an
+upstream repository.
+
+This is useful when a repository you're interested in is located on a different server, and you want
+to be able to browse its content and its activity using the familiar GitLab interface.
+
+To configure mirror pulling for an existing project:
+
+1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories**
+ section.
+1. Enter a repository URL.
+1. Select **Pull** from the **Mirror direction** dropdown.
+1. Select an authentication method from the **Authentication method** dropdown, if necessary.
+1. If necessary, check the following boxes:
+ - **Overwrite diverged branches**.
+ - **Trigger pipelines for mirror updates**.
+ - **Only mirror protected branches**.
+1. Click the **Mirror repository** button to save the configuration.
+
+![Repository mirroring pull settings screen - upper part](img/repository_mirroring_pull_settings_upper.png)
+
+---
+
+![Repository mirroring pull settings screen - lower part](img/repository_mirroring_pull_settings_lower.png)
+
+Because GitLab is now set to pull changes from the upstream repository, you should not push commits
+directly to the repository on GitLab. Instead, any commits should be pushed to the upstream repository.
+Changes pushed to the upstream repository will be pulled into the GitLab repository, either:
+
+- Automatically within a certain period of time.
+- When a [forced update](#forcing-an-update-core) is initiated.
+
+CAUTION: **Caution:**
+If you do manually update a branch in the GitLab repository, the branch will become diverged from
+upstream and GitLab will no longer automatically update this branch to prevent any changes from being lost.
+
+### How it works
+
+Once the pull mirroring feature has been enabled for a repository, the repository is added to a queue.
+
+Once per minute, a Sidekiq cron job schedules repository mirrors to update, based on:
+
+- The capacity available. This is determined by Sidekiq settings. For GitLab.com, see [GitLab.com Sidekiq settings](../../gitlab_com/index.md#sidekiq).
+- The number of repository mirrors already in the queue that are due to be updated. Being due depends on when the repository mirror was last updated and how many times it's been retried.
+
+Repository mirrors are updated as Sidekiq becomes available to process them. If the process of updating the repository mirror:
+
+- Succeeds, an update will be enqueued again with at least a 30 minute wait.
+- Fails (for example, a branch diverged from upstream), it will be attempted again later. Mirrors can fail
+ up to 14 times before they will not be enqueued for update again.
+
+### SSH authentication
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
+
+SSH authentication is mutual:
+
+- You have to prove to the server that you're allowed to access the repository.
+- The server also has to prove to *you* that it's who it claims to be.
+
+You provide your credentials as a password or public key. The server that the
+other repository resides on provides its credentials as a "host key", the
+fingerprint of which needs to be verified manually.
+
+If you're mirroring over SSH (that is, using an `ssh://` URL), you can authenticate using:
+
+- Password-based authentication, just as over HTTPS.
+- Public key authentication. This is often more secure than password authentication,
+ especially when the other repository supports [Deploy Keys](../../../ssh/README.md#deploy-keys).
+
+To get started:
+
+1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories** section.
+1. Enter an `ssh://` URL for mirroring.
+
+NOTE: **Note:**
+SCP-style URLs (that is, `git@example.com:group/project.git`) are not supported at this time.
+
+Entering the URL adds two buttons to the page:
+
+- **Detect host keys**.
+- **Input host keys manually**.
+
+If you click the:
+
+- **Detect host keys** button, GitLab will fetch the host keys from the server and display the fingerprints.
+- **Input host keys manually** button, a field is displayed where you can paste in host keys.
+
+Assuming you used the former, you now need to verify that the fingerprints are
+those you expect. GitLab.com and other code hosting sites publish their
+fingerprints in the open for you to check:
+
+- [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
+- [Bitbucket](https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html)
+- [GitHub](https://help.github.com/en/articles/githubs-ssh-key-fingerprints)
+- [GitLab.com](../../gitlab_com/index.md#ssh-host-keys-fingerprints)
+- [Launchpad](https://help.launchpad.net/SSHFingerprints)
+- [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
+- [SourceForge](https://sourceforge.net/p/forge/documentation/SSH%20Key%20Fingerprints/)
+
+Other providers will vary. If you're running self-managed GitLab, or otherwise
+have access to the server for the other repository, you can securely gather the
+key fingerprints:
+
+```sh
+$ cat /etc/ssh/ssh_host*pub | ssh-keygen -E md5 -l -f -
+256 MD5:f4:28:9f:23:99:15:21:1b:bf:ed:1f:8e:a0:76:b2:9d root@example.com (ECDSA)
+256 MD5:e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73 root@example.com (ED25519)
+2048 MD5:3f:72:be:3d:62:03:5c:62:83:e8:6e:14:34:3a:85:1d root@example.com (RSA)
+```
+
+NOTE: **Note:**
+You may need to exclude `-E md5` for some older versions of SSH.
+
+When mirroring the repository, GitLab will now check that at least one of the
+stored host keys matches before connecting. This can prevent malicious code from
+being injected into your mirror, or your password being stolen.
+
+### SSH public key authentication
+
+To use SSH public key authentication, you'll also need to choose that option
+from the **Authentication method** dropdown. When the mirror is created,
+GitLab generates a 4096-bit RSA key that can be copied by clicking the **Copy SSH public key** button.
+
+![Repository mirroring copy SSH public key to clipboard button](img/repository_mirroring_copy_ssh_public_key_button.png)
+
+You then need to add the public SSH key to the other repository's configuration:
+
+- If the other repository is hosted on GitLab, you should add the public SSH key
+ as a [Deploy Key](../../../ssh/README.md#deploy-keys).
+- If the other repository is hosted elsewhere, you may need to add the key to
+ your user's `authorized_keys` file. Paste the entire public SSH key into the
+ file on its own line and save it.
+
+If you need to change the key at any time, you can remove and re-add the mirror
+to generate a new key. You'll have to update the other repository with the new
+key to keep the mirror running.
+
+### Overwrite diverged branches **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/4559) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
+
+You can choose to always update your local branches with remote versions, even if they have
+diverged from the remote.
+
+CAUTION: **Caution:**
+For mirrored branches, enabling this option results in the loss of local changes.
+
+To use this option, check the **Overwrite diverged branches** box when creating a repository mirror.
+
+### Only mirror protected branches **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3326) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+
+You can choose to pull mirror only the protected branches from your remote repository to GitLab.
+Non-protected branches are not mirrored and can diverge.
+
+To use this option, check the **Only mirror protected branches** box when creating a repository mirror.
+
+### Hard failure **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3117) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.2.
+
+Once the mirroring process is unsuccessfully retried 14 times in a row, it will get marked as hard
+failed. This will become visible in either the:
+
+- Project's main dashboard.
+- Pull mirror settings page.
+
+When a project is hard failed, it will no longer get picked up for mirroring. A user can resume the
+project mirroring again by [Forcing an update](#forcing-an-update-core).
+
+### Trigger update using API **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3453) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+
+Pull mirroring uses polling to detect new branches and commits added upstream, often minutes
+afterwards. If you notify GitLab by [API](../../../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter),
+updates will be pulled immediately.
+
+For more information, see [Start the pull mirroring process for a Project](../../../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter).
+
+## Forcing an update **(CORE)**
+
+While mirrors are scheduled to update automatically, you can always force an update by using the
+update button which is available on the **Mirroring repositories** section of the **Repository Settings** page.
+
+![Repository mirroring force update user interface](img/repository_mirroring_force_update.png)
+
+## Bidirectional mirroring **(STARTER)**
+
+CAUTION: **Caution:**
+Bidirectional mirroring may cause conflicts.
+
+If you configure a GitLab repository to both pull from, and push to, the same remote source, there
+is no guarantee that either repository will update correctly. If you set up a repository for
+bidirectional mirroring, you should prepare for the likely conflicts by deciding who will resolve
+them and how they will be resolved.
+
+Rewriting any mirrored commit on either remote will cause conflicts and mirroring to fail. This can
+be prevented by:
+
+- [Pulling only protected branches](#only-mirror-protected-branches-starter).
+- [Pushing only protected branches](#push-only-protected-branches-core).
+
+You should [protect the branches](../protected_branches.md) you wish to mirror on both
+remotes to prevent conflicts caused by rewriting history.
+
+Bidirectional mirroring also creates a race condition where commits made close together to the same
+branch causes conflicts. The race condition can be mitigated by reducing the mirroring delay by using
+a [Push event webhook](../integrations/webhooks.md#push-events) to trigger an immediate
+pull to GitLab. Push mirroring from GitLab is rate limited to once per minute when only push mirroring
+protected branches.
+
+### Preventing conflicts using a `pre-receive` hook
+
+CAUTION: **Warning:**
+The solution proposed will negatively impact the performance of
+Git push operations because they will be proxied to the upstream Git
+repository.
+
+A server-side `pre-receive` hook can be used to prevent the race condition
+described above by only accepting the push after first pushing the commit to
+the upstream Git repository. In this configuration one Git repository acts as
+the authoritative upstream, and the other as downstream. The `pre-receive` hook
+will be installed on the downstream repository.
+
+Read about [configuring custom Git hooks](../../../administration/custom_hooks.md) on the GitLab server.
+
+A sample `pre-receive` hook is provided below.
+
+```bash
+#!/usr/bin/env bash
+
+# --- Assume only one push mirror target
+# Push mirroring remotes are named `remote_mirror_<id>`, this finds the first remote and uses that.
+TARGET_REPO=$(git remote | grep -m 1 remote_mirror)
+
+proxy_push()
+{
+ # --- Arguments
+ OLDREV=$(git rev-parse $1)
+ NEWREV=$(git rev-parse $2)
+ REFNAME="$3"
+
+ # --- Pattern of branches to proxy pushes
+ whitelisted=$(expr "$branch" : "\(master\)")
+
+ case "$refname" in
+ refs/heads/*)
+ branch=$(expr "$refname" : "refs/heads/\(.*\)")
+
+ if [ "$whitelisted" = "$branch" ]; then
+ error="$(git push --quiet $TARGET_REPO $NEWREV:$REFNAME 2>&1)"
+ fail=$?
+
+ if [ "$fail" != "0" ]; then
+ echo >&2 ""
+ echo >&2 " Error: updates were rejected by upstream server"
+ echo >&2 " This is usually caused by another repository pushing changes"
+ echo >&2 " to the same ref. You may want to first integrate remote changes"
+ echo >&2 ""
+ return
+ fi
+ fi
+ ;;
+ esac
+}
+
+# Allow dual mode: run from the command line just like the update hook, or
+# if no arguments are given then run as a hook script
+if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
+ # Output to the terminal in command line mode - if someone wanted to
+ # resend an email; they could redirect the output to sendmail
+ # themselves
+ PAGER= proxy_push $2 $3 $1
+else
+ # Push is proxied upstream one ref at a time. Because of this it is possible
+ # for some refs to succeed, and others to fail. This will result in a failed
+ # push.
+ while read oldrev newrev refname
+ do
+ proxy_push $oldrev $newrev $refname
+ done
+fi
+```
+
+### Mirroring with Perforce Helix via Git Fusion **(STARTER)**
+
+CAUTION: **Warning:**
+Bidirectional mirroring should not be used as a permanent configuration. Refer to
+[Migrating from Perforce Helix](../import/perforce.md) for alternative migration approaches.
+
+[Git Fusion](https://www.perforce.com/manuals/git-fusion/#Git-Fusion/section_avy_hyc_gl.html) provides a Git interface
+to [Perforce Helix](https://www.perforce.com/products) which can be used by GitLab to bidirectionally
+mirror projects with GitLab. This may be useful in some situations when migrating from Perforce Helix
+to GitLab where overlapping Perforce Helix workspaces cannot be migrated simultaneously to GitLab.
+
+If using mirroring with Perforce Helix, you should only mirror protected branches. Perforce Helix
+will reject any pushes that rewrite history. Only the fewest number of branches should be mirrored
+due to the performance limitations of Git Fusion.
+
+When configuring mirroring with Perforce Helix via Git Fusion, the following Git Fusion
+settings are recommended:
+
+- `change-pusher` should be disabled. Otherwise, every commit will be rewritten as being committed
+ by the mirroring account, rather than being mapped to existing Perforce Helix users or the `unknown_git` user.
+- `unknown_git` user will be used as the commit author if the GitLab user does not exist in
+ Perforce Helix.
+
+Read about [Git Fusion settings on Perforce.com](https://www.perforce.com/manuals/git-fusion/Content/Git-Fusion/section_vss_bdw_w3.html#section_zdp_zz1_3l).
+
+## Troubleshooting
+
+Should an error occur during a push, GitLab will display an "Error" highlight for that repository. Details on the error can then be seen by hovering over the highlight text.
+
+### 13:Received RST_STREAM with error code 2 with GitHub
+
+If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](https://github.com/settings/emails) setting.
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 131999dbf60..2dc507901d0 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -49,10 +49,11 @@ Add an [issue description template](../description_templates.md#description-temp
Set up your project's merge request settings:
- Set up the merge request method (merge commit, [fast-forward merge](../merge_requests/fast_forward_merge.html)).
-- Merge request [description templates](../description_templates.md#description-templates).
+- Add merge request [description templates](../description_templates.md#description-templates).
- Enable [merge request approvals](../merge_requests/merge_request_approvals.md). **(STARTER)**
-- Enable [merge only of pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
-- Enable [merge only when all discussions are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved).
+- Enable [merge only if pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
+- Enable [merge only when all threads are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved).
+- Enable [`delete source branch after merge` option by default](../merge_requests/creating_merge_requests.md#deleting-the-source-branch)
![project's merge request settings](img/merge_requests_settings.png)
diff --git a/doc/user/project/time_tracking.md b/doc/user/project/time_tracking.md
new file mode 100644
index 00000000000..9cdee0f2b5a
--- /dev/null
+++ b/doc/user/project/time_tracking.md
@@ -0,0 +1,92 @@
+---
+type: reference
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/time_tracking.html'
+---
+
+# Time Tracking
+
+> Introduced in GitLab 8.14.
+
+Time Tracking allows you to track estimates and time spent on issues and merge
+requests within GitLab.
+
+## Overview
+
+Time Tracking allows you to:
+
+- Record the time spent working on an issue or a merge request.
+- Add an estimate of the amount of time needed to complete an issue or a merge
+ request.
+
+You don't have to indicate an estimate to enter the time spent, and vice versa.
+
+Data about time tracking is shown on the issue/merge request sidebar, as shown
+below.
+
+![Time tracking in the sidebar](img/time_tracking_sidebar_v8_16.png)
+
+## How to enter data
+
+Time Tracking uses two [quick actions](quick_actions.md)
+that GitLab introduced with this new feature: `/spend` and `/estimate`.
+
+Quick actions can be used in the body of an issue or a merge request, but also
+in a comment in both an issue or a merge request.
+
+Below is an example of how you can use those new quick actions inside a comment.
+
+![Time tracking example in a comment](img/time_tracking_example_v12_2.png)
+
+Adding time entries (time spent or estimates) is limited to project members.
+
+### Estimates
+
+To enter an estimate, write `/estimate`, followed by the time. For example, if
+you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
+`/estimate 3d 5h 10m`. Time units that we support are listed at the bottom of
+this help page.
+
+Every time you enter a new time estimate, any previous time estimates will be
+overridden by this new value. There should only be one valid estimate in an
+issue or a merge request.
+
+To remove an estimation entirely, use `/remove_estimate`.
+
+### Time spent
+
+To enter a time spent, use `/spend 3d 5h 10m`.
+
+Every new time spent entry will be added to the current total time spent for the
+issue or the merge request.
+
+You can remove time by entering a negative amount: `/spend -3d` will remove 3
+days from the total time spent. You can't go below 0 minutes of time spent,
+so GitLab will automatically reset the time spent if you remove a larger amount
+of time compared to the time that was entered already.
+
+To remove all the time spent at once, use `/remove_time_spent`.
+
+## Configuration
+
+The following time units are available:
+
+- Months (mo)
+- Weeks (w)
+- Days (d)
+- Hours (h)
+- Minutes (m)
+
+Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h.
+
+### Limit displayed units to hours **(CORE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/29469/) in GitLab 12.1.
+
+In GitLab self-managed instances, the display of time units can be limited to
+hours through the option in **Admin Area > Settings > Preferences** under **Localization**.
+
+With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
+
+## Other interesting links
+
+- [Time Tracking landing page in the GitLab handbook](https://about.gitlab.com/solutions/time-tracking/)
diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md
index d65dd32fe11..faa3a118137 100644
--- a/doc/user/search/advanced_search_syntax.md
+++ b/doc/user/search/advanced_search_syntax.md
@@ -17,6 +17,8 @@ The Advanced Syntax Search is a subset of the
[Advanced Global Search](advanced_global_search.md), which you can use if you
want to have more specific search results.
+Advanced Global Search only supports searching the [default branch](../project/repository/branches/index.md#default-branch).
+
## Use cases
Let's say for example that the product you develop relies on the code of another
diff --git a/doc/user/search/img/issue_search_filter_v12_5.png b/doc/user/search/img/issue_search_filter_v12_5.png
new file mode 100644
index 00000000000..1e2dd3d98a3
--- /dev/null
+++ b/doc/user/search/img/issue_search_filter_v12_5.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 8d7b4a429aa..bc31052b758 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -32,13 +32,14 @@ on the search field on the top-right of your screen:
If you want to search for issues present in a specific project, navigate to
a project's **Issues** tab, and click on the field **Search or filter results...**. It will
display a dropdown menu, from which you can add filters per author, assignee, milestone,
-label, weight, and 'my-reaction' (based on your emoji votes). When done, press **Enter** on your keyboard to filter the issues.
+release, label, weight, confidentiality, and "my-reaction" (based on your emoji votes).
+When done, press **Enter** on your keyboard to filter the issues.
-![filter issues in a project](img/issue_search_filter.png)
+![filter issues in a project](img/issue_search_filter_v12_5.png)
The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab,
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
-milestone, and label.
+approver, milestone, release, label, "my-reaction", "work in progess" status, and target branch.
### Filtering by **None** / **Any**
@@ -99,8 +100,8 @@ quickly access issues and merge requests created or assigned to you within that
## To-Do List
-Your [To-Do List](../../workflow/todos.md#gitlab-to-do-list) can be searched by "to do" and "done".
-You can [filter](../../workflow/todos.md#filtering-your-to-do-list) them per project,
+Your [To-Do List](../todos.md#gitlab-to-do-list) can be searched by "to do" and "done".
+You can [filter](../todos.md#filtering-your-to-do-list) them per project,
author, type, and action. Also, you can sort them by
[**Label priority**](../../user/project/labels.md#label-priority),
**Last created** and **Oldest created**.
diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md
new file mode 100644
index 00000000000..54e7938d8f7
--- /dev/null
+++ b/doc/user/shortcuts.md
@@ -0,0 +1,135 @@
+---
+type: reference
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/shortcuts.html'
+---
+
+# GitLab keyboard shortcuts
+
+GitLab has many useful keyboard shortcuts to make it easier to access different features.
+You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>.
+
+The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
+be in specific pages for the other shortcuts to be available, as explained in each
+section below.
+
+## Global Shortcuts
+
+These shortcuts are available in most areas of GitLab
+
+| Keyboard Shortcut | Description |
+| ------------------------------- | ----------- |
+| <kbd>?</kbd> | Show/hide shortcut reference sheet. |
+| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to your Projects page. |
+| <kbd>Shift</kbd> + <kbd>g</kbd> | Go to your Groups page. |
+| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to your Activity page. |
+| <kbd>Shift</kbd> + <kbd>l</kbd> | Go to your Milestones page. |
+| <kbd>Shift</kbd> + <kbd>s</kbd> | Go to your Snippets page. |
+| <kbd>s</kbd> | Put cursor in the issues/merge requests search. |
+| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
+| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.|
+| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
+| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. |
+
+Additionally, the following shortcuts are available when editing text in text fields,
+for example comments, replies, or issue and merge request descriptions:
+
+| Keyboard Shortcut | Description |
+| ---------------------------------------------------------------------- | ----------- |
+| <kbd>↑</kbd> | Edit your last comment. You must be in a blank text field below a thread, and you must already have at least one comment in the thread. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle Markdown preview, when editing text in a text field that has **Write** and **Preview** tabs at the top. |
+
+## Project
+
+These shortcuts are available from any page within a project. You must type them
+relatively quickly to work, and they will take you to another page in the project.
+
+| Keyboard Shortcut | Description |
+| --------------------------- | ----------- |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project home page (**Project > Details**). |
+| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project activity feed (**Project > Activity**). |
+| <kbd>g</kbd> + <kbd>r</kbd> | Go to the project releases list (**Project > Releases**). |
+| <kbd>g</kbd> + <kbd>f</kbd> | Go to the [project files](#project-files) list (**Repository > Files**). |
+| <kbd>t</kbd> | Go to the project file search page. (**Repository > Files**, click **Find Files**). |
+| <kbd>g</kbd> + <kbd>c</kbd> | Go to the project commits list (**Repository > Commits**). |
+| <kbd>g</kbd> + <kbd>n</kbd> | Go to the [repository graph](#repository-graph) page (**Repository > Graph**). |
+| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts (**Repository > Charts**). |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to the project issues list (**Issues > List**). |
+| <kbd>i</kbd> | Go to the New Issue page (**Issues**, click **New Issue** ). |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to the project issue boards list (**Issues > Boards**). |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to the project merge requests list (**Merge Requests**). |
+| <kbd>g</kbd> + <kbd>j</kbd> | Go to the CI/CD jobs list (**CI/CD > Jobs**). |
+| <kbd>g</kbd> + <kbd>l</kbd> | Go to the project metrics (**Operations > Metrics**). |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project environments (**Operations > Environments**). |
+| <kbd>g</kbd> + <kbd>k</kbd> | Go to the project Kubernetes cluster integration page (**Operations > Kubernetes**). Note that you must have at least [`maintainer` permissions](permissions.md) to access this page. |
+| <kbd>g</kbd> + <kbd>s</kbd> | Go to the project snippets list (**Snippets**). |
+| <kbd>g</kbd> + <kbd>w</kbd> | Go to the project wiki (**Wiki**), if enabled. |
+
+### Issues and Merge Requests
+
+These shortcuts are available when viewing issues and merge requests.
+
+| Keyboard Shortcut | Description |
+| ---------------------------- | ----------- |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>a</kbd> | Change assignee. |
+| <kbd>m</kbd> | Change milestone. |
+| <kbd>l</kbd> | Change label. |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). |
+| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). |
+| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). |
+| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). |
+
+### Project Files
+
+These shortcuts are available when browsing the files in a project (navigate to
+**Repository** > **Files**):
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>↑</kbd> | Move selection up. |
+| <kbd>↓</kbd> | Move selection down. |
+| <kbd>enter</kbd> | Open selection. |
+| <kbd>esc</kbd> | Go back to file list screen (only while searching for files, **Repository > Files** then click on **Find File**). |
+| <kbd>y</kbd> | Go to file permalink (only while viewing a file). |
+
+### Web IDE
+
+These shortcuts are available when editing a file with the [Web IDE](project/web_ide/index.md):
+
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------- | ----------- |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
+| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). |
+
+### Repository Graph
+
+These shortcuts are available when viewing the project [repository graph](project/repository/index.md#repository-graph)
+page (navigate to **Repository > Graph**):
+
+| Keyboard Shortcut | Description |
+| ------------------------------------------------------------------ | ----------- |
+| <kbd>â†</kbd> or <kbd>h</kbd> | Scroll left. |
+| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right. |
+| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up. |
+| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down. |
+| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top. |
+| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom. |
+
+### Wiki pages
+
+This shortcut is available when viewing a [wiki page](project/wiki/index.md):
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>e</kbd> | Edit wiki page. |
+
+## Epics **(ULTIMATE)**
+
+These shortcuts are available when viewing [Epics](group/epics/index.md):
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
+| <kbd>e</kbd> | Edit description. |
+| <kbd>l</kbd> | Change label. |
diff --git a/doc/user/todos.md b/doc/user/todos.md
new file mode 100644
index 00000000000..d53baa688a4
--- /dev/null
+++ b/doc/user/todos.md
@@ -0,0 +1,142 @@
+---
+disqus_identifier: 'https://docs.gitlab.com/ee/workflow/todos.html'
+---
+
+# GitLab To-Do List
+
+> [Introduced][ce-2817] in GitLab 8.5.
+
+When you log into GitLab, you normally want to see where you should spend your
+time, take some action, or know what you need to keep an eye on without
+a huge pile of e-mail notifications. GitLab is where you do your work,
+so being able to get started quickly is important.
+
+Your To-Do List offers a chronological list of items that are waiting for your input, all
+in a simple dashboard.
+
+![To Do screenshot showing a list of items to check on](img/todos_index.png)
+
+You can quickly access your To-Do List by clicking the checkmark icon next to the
+search bar in the top navigation. If the count is:
+
+- Less than 100, the number in blue is the number of To-Do items.
+- 100 or more, the number displays as 99+. The exact number displays
+ on the To-Do List.
+you still have open. Otherwise, the number displays as 99+. The exact number
+displays on the To-Do List.
+
+![To Do icon](img/todos_icon.png)
+
+## What triggers a To Do
+
+A To Do displays on your To-Do List when:
+
+- An issue or merge request is assigned to you
+- You are `@mentioned` in the description or comment of an:
+ - Issue
+ - Merge Request
+ - Epic **(ULTIMATE)**
+- You are `@mentioned` in a comment on a commit
+- 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:
+ - The author
+ - Have set it to automatically merge once the pipeline succeeds
+
+To-do triggers are not affected by [GitLab Notification Email settings](profile/notifications.md).
+
+NOTE: **Note:**
+When a user no longer has access to a resource related to a To Do (like an issue, merge request, project, or group) the related To-Do items are deleted within the next hour for security reasons. The delete is delayed to prevent data loss, in case the user's access was revoked by mistake.
+
+### Directly addressing a To Do
+
+> [Introduced][ce-7926] in GitLab 9.0.
+
+If you are mentioned at the start of a line, the To Do you receive will be listed
+as 'directly addressed'. For example, in this comment:
+
+```markdown
+@alice What do you think? cc: @bob
+
+- @carol can you please have a look?
+
+>>>
+@dan what do you think?
+>>>
+
+@erin @frank thank you!
+```
+
+The people receiving directly addressed To-Do items are `@alice`, `@erin`, and
+`@frank`. Directly addressed To-Do items only differ from mentions in their type
+for filtering purposes; otherwise, they appear as normal.
+
+### Manually creating a To Do
+
+You can also add the following to your To-Do List by clicking the **Add a To Do** button on an:
+
+- Issue
+- Merge Request
+- Epic **(ULTIMATE)**
+
+![Adding a To Do from the issuable sidebar](img/todos_add_todo_sidebar.png)
+
+## Marking a To Do as done
+
+Any action to the following will mark the corresponding To Do as done:
+
+- Issue
+- Merge Request
+- Epic **(ULTIMATE)**
+
+Actions that dismiss To-Do items include:
+
+- Changing the assignee
+- Changing the milestone
+- Adding/removing a label
+- Commenting on the issue
+
+Your To-Do List is personal, and items are only marked as done if the action comes from
+you. If you close the issue or merge request, your To Do is automatically
+marked as done.
+
+To prevent other users from closing issues without you being notified, if someone else closes, merges, or takes action on the any of the following, your To Do will remain pending:
+
+- Issue
+- Merge request
+- Epic **(ULTIMATE)**
+
+There is just one To Do for each of these, so mentioning a user a hundred times in an issue will only trigger one To Do.
+
+If no action is needed, you can manually mark the To Do as done by clicking the
+corresponding **Done** button, and it will disappear from your To-Do List.
+
+![A To Do in the To-Do List](img/todos_todo_list_item.png)
+
+You can also mark a To Do as done by clicking the **Mark as done** button in the sidebar of the following:
+
+- Issue
+- Merge Request
+- Epic **(ULTIMATE)**
+
+![Mark as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
+
+You can mark all your To-Do items as done at once by clicking the **Mark all as
+done** button.
+
+## Filtering your To-Do List
+
+There are four kinds of filters you can use on your To-Do List.
+
+| Filter | Description |
+| ------- | ----------- |
+| 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)** |
+| 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 `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-to-do).
+
+[ce-2817]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/2817
+[ce-7926]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/7926
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index c6396672e59..9836255932a 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -2,53 +2,10 @@
comments: false
---
-# Workflow
+# Workflow (Deprecated)
-- [Automatic issue closing](../user/project/issues/managing_issues.md#closing-issues-automatically)
-- [Change your time zone](timezone.md)
-- [Cycle Analytics](../user/project/cycle_analytics.md)
-- [Description templates](../user/project/description_templates.md)
-- [Feature branch workflow](workflow.md)
-- [GitLab Flow](gitlab_flow.md)
-- [Groups](../user/group/index.md)
-- Issues - The GitLab Issue Tracker is an advanced and complete tool for
- tracking the evolution of a new idea or the process of solving a problem.
- - [Exporting Issues](../user/project/issues/csv_export.md) **(STARTER)** Export issues as a CSV, emailed as an attachment.
- - [Confidential issues](../user/project/issues/confidential_issues.md)
- - [Due date for issues](../user/project/issues/due_dates.md)
-- [Issue Board](../user/project/issue_board.md)
-- [Keyboard shortcuts](shortcuts.md)
-- [File finder](file_finder.md)
-- [File lock](../user/project/file_lock.md) **(PREMIUM)**
-- [Labels](../user/project/labels.md)
-- [Issue weight](issue_weight.md) **(STARTER)**
-- [Notification emails](notifications.md)
-- [Projects](../user/project/index.md)
-- [Project forking workflow](forking_workflow.md)
-- [Project users](../user/project/members/index.md)
-- [Protected branches](../user/project/protected_branches.md)
-- [Protected tags](../user/project/protected_tags.md)
-- [Quick Actions](../user/project/quick_actions.md)
-- [Sharing projects with groups](../user/project/members/share_project_with_groups.md)
-- [Time tracking](time_tracking.md)
-- [Web Editor](../user/project/repository/web_editor.md)
-- [Releases](releases.md)
-- [Milestones](../user/project/milestones/index.md)
-- [Merge Requests](../user/project/merge_requests/index.md)
- - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
- - [Resolve threads in merge requests reviews](../user/discussions/index.md)
- - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
- - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
- - [Merge requests versions](../user/project/merge_requests/versions.md)
- - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- - [Fast-forward merge requests](../user/project/merge_requests/fast_forward_merge.md)
- - [Merge request approvals](../user/project/merge_requests/merge_request_approvals.md) **(STARTER)**
-- [Repository mirroring](repository_mirroring.md) **(STARTER)**
-- [Service Desk](../user/project/service_desk.md) **(PREMIUM)**
-- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
-- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
-- [Todos](todos.md)
-- [Snippets](../user/snippets.md)
-- [Subgroups](../user/group/subgroups/index.md)
+This page was deprecated, with all content previously stored under the `/workflow` path moved
+to other locations in the documentation site, organized by topic. You can use the search
+box to find the content you are looking for, browse the main [GitLab Documentation page](../README.md),
+or view the [issue that deprecated this page](https://gitlab.com/gitlab-org/gitlab/issues/32940)
+for more details.
diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md
index 8eb705b5363..f7098c88fd1 100644
--- a/doc/workflow/file_finder.md
+++ b/doc/workflow/file_finder.md
@@ -1,41 +1,5 @@
-# File finder
+---
+redirect_to: '../user/project/repository/file_finder.md'
+---
-> [Introduced][gh-9889] in GitLab 8.4.
-
-The file finder feature allows you to quickly shortcut your way when you are
-searching for a file in a repository using the 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)
-
-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_
-in a project.
-
-Press `t` to launch the File search function when in **Issues**,
-**Merge requests**, **Milestones**, even the project's settings.
-
-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**.
-
-## 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
-results by recognizing patterns that people use while searching.
-
-For example, consider the [GitLab CE repository][ce] 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.
-
-**Protip:** 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"
+This document was moved to [another location](../user/project/repository/file_finder.md).
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
index 48be38b2eca..fa617d859a5 100644
--- a/doc/workflow/forking_workflow.md
+++ b/doc/workflow/forking_workflow.md
@@ -1,51 +1,5 @@
-# Project forking workflow
+---
+redirect_to: '../user/project/repository/forking_workflow.md'
+---
-Forking a project to your own namespace is useful if you have no write
-access to the project you want to contribute to. If you do have write
-access or can request it, we recommend working together in the same
-repository since it is simpler. See our [GitLab Flow](gitlab_flow.md)
-document more information about using branches to work together.
-
-## Creating a fork
-
-Forking a project is in most cases a two-step process.
-
-1. Click on the fork button located located in between the star and clone buttons on the project's home page.
-
- ![Fork button](img/forking_workflow_fork_button.png)
-
-1. Once you do that, you'll be presented with a screen where you can choose
- the namespace to fork to. Only namespaces (groups and your own
- namespace) where you have write access to, will be shown. Click on the
- namespace to create your fork there.
-
- ![Choose namespace](img/forking_workflow_choose_namespace.png)
-
- **Note:**
- If the namespace you chose to fork the project to has another project with
- the same path name, you will be presented with a warning that the forking
- could not be completed. Try to resolve the error before repeating the forking
- process.
-
- ![Path taken error](img/forking_workflow_path_taken_error.png)
-
-After the forking is done, you can start working on the newly created
-repository. There, you will have full [Owner](../user/permissions.md)
-access, so you can set it up as you please.
-
-## Merging upstream
-
-Once you are ready to send your code back to the main project, you need
-to create a merge request. Choose your forked project's main branch as
-the source and the original project's main branch as the destination and
-create the [merge request](merge_requests.md).
-
-![Selecting branches](forking/branch_select.png)
-
-You can then assign the merge request to someone to have them review
-your changes. Upon pressing the 'Submit Merge Request' button, your
-changes will be added to the repository and branch you're merging into.
-
-![New merge request](forking/merge_request.png)
-
-[gitlab flow]: https://about.gitlab.com/blog/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
+This document was moved to [another location](../user/project/repository/forking_workflow.md).
diff --git a/doc/workflow/git_annex.md b/doc/workflow/git_annex.md
index 84d49569a95..e54d52ea70d 100644
--- a/doc/workflow/git_annex.md
+++ b/doc/workflow/git_annex.md
@@ -1,238 +1,5 @@
-# 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].
-
-The biggest limitation of Git, compared to some older centralized version
-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
-are over 50GB!), we subscribe to the advice to keep repositories as small as
-you can.
-
-Not being able to version control large binaries is a big problem for many
-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.
-
-This problem is solved in GitLab Enterprise Edition by integrating the
-[git-annex] application.
-
-`git-annex` allows managing large binaries with Git without checking the
-contents into Git.
-You check-in only a symlink that contains the SHA-1 of the large binary. If you
-need the large binary, you can sync it from the GitLab server over `rsync`, a
-very fast file copying tool.
-
-## GitLab git-annex Configuration
-
-`git-annex` is disabled by default in GitLab. Below you will find the
-configuration options required to enable it.
-
-### Requirements
-
-`git-annex` needs to be installed both on the server and the client side.
-
-For Debian-like systems (e.g., Debian, Ubuntu) this can be achieved by running:
-
-```
-sudo apt-get update && sudo apt-get install git-annex
-```
-
-For RedHat-like systems (e.g., CentOS, RHEL) this can be achieved by running:
-
-```
-sudo yum install epel-release && sudo yum install git-annex
-```
-
-### Configuration for Omnibus packages
-
-For Omnibus GitLab packages, only one configuration setting is needed.
-The Omnibus package will internally set the correct options in all locations.
-
-1. In `/etc/gitlab/gitlab.rb` add the following line:
-
- ```ruby
- gitlab_shell['git_annex_enabled'] = true
- ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
-### Configuration for installations from source
-
-There are 2 settings to enable git-annex on your GitLab server.
-
-One is located in `config/gitlab.yml` of the GitLab repository and the other
-one is located in `config.yml` of GitLab Shell.
-
-1. In `config/gitlab.yml` add or edit the following lines:
-
- ```yaml
- gitlab_shell:
- git_annex_enabled: true
- ```
-
-1. In `config.yml` of GitLab Shell add or edit the following lines:
-
- ```yaml
- git_annex_enabled: true
- ```
-
-1. Save the files and [restart GitLab][] for the changes to take effect.
-
-## Using GitLab git-annex
-
-> **Note:**
-> Your Git remotes must be using the SSH protocol, not HTTP(S).
-
-Here is an example workflow of uploading a very large file and then checking it
-into your Git repository:
-
-```bash
-git clone git@example.com:group/project.git
-
-git annex init 'My Laptop' # initialize the annex project and give an optional description
-cp ~/tmp/debian.iso ./ # copy a large file into the current directory
-git annex add debian.iso # add the large file to git annex
-git commit -am "Add Debian iso" # commit the file metadata
-git annex sync --content # sync the Git repo and large file to the GitLab server
-```
-
-The output should look like this:
-
-```
-commit
-On branch master
-Your branch is ahead of 'origin/master' by 1 commit.
- (use "git push" to publish your local commits)
-nothing to commit, working tree clean
-ok
-pull origin
-remote: Counting objects: 5, done.
-remote: Compressing objects: 100% (4/4), done.
-remote: Total 5 (delta 2), reused 0 (delta 0)
-Unpacking objects: 100% (5/5), done.
-From example.com:group/project
- 497842b..5162f80 git-annex -> origin/git-annex
-ok
-(merging origin/git-annex into git-annex...)
-(recording state in git...)
-copy debian.iso (checking origin...) (to origin...)
-SHA256E-s26214400--8092b3d482fb1b7a5cf28c43bc1425c8f2d380e86869c0686c49aa7b0f086ab2.iso
- 26,214,400 100% 638.88kB/s 0:00:40 (xfr#1, to-chk=0/1)
-ok
-pull origin
-ok
-(recording state in git...)
-push origin
-Counting objects: 15, done.
-Delta compression using up to 4 threads.
-Compressing objects: 100% (13/13), done.
-Writing objects: 100% (15/15), 1.64 KiB | 0 bytes/s, done.
-Total 15 (delta 1), reused 0 (delta 0)
-To example.com:group/project.git
- * [new branch] git-annex -> synced/git-annex
- * [new branch] master -> synced/master
-ok
-```
-
-Your files can be found in the `master` branch, but you'll notice that there
-are more branches created by the `annex sync` command.
-
-Git Annex will also create a new directory at `.git/annex/` and will record the
-tracked files in the `.git/config` file. The files you assign to be tracked
-with `git-annex` will not affect the existing `.git/config` records. The files
-are turned into symbolic links that point to data in `.git/annex/objects/`.
-
-The `debian.iso` file in the example will contain the symbolic link:
-
-```
-.git/annex/objects/ZW/1k/SHA256E-s82701--6384039733b5035b559efd5a2e25a493ab6e09aabfd5162cc03f6f0ec238429d.png/SHA256E-s82701--6384039733b5035b559efd5a2e25a493ab6e09aabfd5162cc03f6f0ec238429d.iso
-```
-
-Use `git annex info` to retrieve the information about the local copy of your
-repository.
-
+---
+redirect_to: '../administration/git_annex.md'
---
-Downloading a single large file is also very simple:
-
-```bash
-git clone git@gitlab.example.com:group/project.git
-
-git annex sync # sync Git branches but not the large file
-git annex get debian.iso # download the large file
-```
-
-To download all files:
-
-```bash
-git clone git@gitlab.example.com:group/project.git
-
-git annex sync --content # sync Git branches and download all the large files
-```
-
-By using `git-annex` without GitLab, anyone that can access the server can also
-access the files of all projects, but GitLab Annex ensures that you can only
-access files of projects you have access to (developer, maintainer, or owner role).
-
-## How it works
-
-Internally GitLab uses [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
-can cause `git-annex` to raise unpredicted warnings and errors.
-
-Consult the [Annex upgrade page][annex-upgrade] 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.
-
-Although there is no general guide for `git-annex` errors, there are a few tips
-on how to go around the warnings.
-
-### `git-annex-shell: Not a git-annex or gcrypt repository`
-
-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].
-
-One important thing to note is that despite the warning, the `sync` succeeds
-and the files are pushed to the GitLab repository.
-
-If you get hit by this, you can run the following command inside the repository
-that the warning was raised:
-
-```
-git config remote.origin.annex-ignore false
-```
-
-Consecutive runs of `git annex sync --content` **should not** produce this
-warning and the output should look like this:
-
-```
-commit ok
-pull origin
-ok
-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]: lfs/migrate_from_git_annex_to_git_lfs.html
-[issue]: https://git-annex.branchable.com/forum/Error_from_git-annex-shell_on_creation_of_gcrypt_special_remote/ "git-annex issue"
-[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+This document was moved to [another location](../administration/git_annex.md).
diff --git a/doc/workflow/git_lfs.md b/doc/workflow/git_lfs.md
index da217b0a5da..0a8c33c264c 100644
--- a/doc/workflow/git_lfs.md
+++ b/doc/workflow/git_lfs.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'lfs/manage_large_binaries_with_git_lfs.md'
+redirect_to: '../administration/lfs/manage_large_binaries_with_git_lfs.md'
---
-This document was moved to [another location](lfs/manage_large_binaries_with_git_lfs.md).
+This document was moved to [another location](../administration/lfs/manage_large_binaries_with_git_lfs.md).
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index e3568d6489d..e03281c0ffc 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -1,326 +1,5 @@
-# Introduction to GitLab Flow
+---
+redirect_to: '../topics/gitlab_flow.md'
+---
-![GitLab Flow](img/gitlab_flow.png)
-
-Git allows a wide variety of branching strategies and workflows.
-Because of this, many organizations end up with workflows that are too complicated, not clearly defined, or not integrated with issue tracking systems.
-Therefore, we propose GitLab flow as a clearly defined set of best practices.
-It combines [feature-driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](https://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
-
-Organizations coming to Git from other version control systems frequently find it hard to develop a productive workflow.
-This article describes GitLab flow, which integrates the Git workflow with an issue tracking system.
-It offers a simple, transparent, and effective way to work with Git.
-
-![Four stages (working copy, index, local repo, remote repo) and three steps between them](img/four_stages.png)
-
-When converting to Git, you have to get used to the fact that it takes three steps to share a commit with colleagues.
-Most version control systems have only one step: committing from the working copy to a shared server.
-In Git, you add files from the working copy to the staging area. After that, you commit them to your local repo.
-The third step is pushing to a shared remote repository.
-After getting used to these three steps, the next challenge is the branching model.
-
-![Multiple long-running branches and merging in all directions](img/messy_flow.png)
-
-Since many organizations new to Git have no conventions for how to work with it, their repositories can quickly become messy.
-The biggest problem is that many long-running branches emerge that all contain part of the changes.
-People have a hard time figuring out which branch has the latest code, or which branch to deploy to production.
-Frequently, the reaction to this problem is to adopt a standardized pattern such as [Git flow](https://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html).
-We think there is still room for improvement. In this document, we describe a set of practices we call GitLab flow.
-
-For a video introduction of how this works in GitLab, see [GitLab Flow](https://youtu.be/InKNIvky2KE).
-
-## Git flow and its problems
-
-![Git Flow timeline by Vincent Driessen, used with permission](img/gitdashflow.png)
-
-Git flow was one of the first proposals to use Git branches, and it has received a lot of attention.
-It suggests a `master` branch and a separate `develop` branch, as well as supporting branches for features, releases, and hotfixes.
-The development happens on the `develop` branch, moves to a release branch, and is finally merged into the `master` branch.
-
-Git flow is a well-defined standard, but its complexity introduces two problems.
-The first problem is that developers must use the `develop` branch and not `master`. `master` is reserved for code that is released to production.
-It is a convention to call your default branch `master` and to mostly branch from and merge to this.
-Since most tools automatically use the `master` branch as the default, it is annoying to have to switch to another branch.
-
-The second problem of Git flow is the complexity introduced by the hotfix and release branches.
-These branches can be a good idea for some organizations but are overkill for the vast majority of them.
-Nowadays, most organizations practice continuous delivery, which means that your default branch can be deployed.
-Continuous delivery removes the need for hotfix and release branches, including all the ceremony they introduce.
-An example of this ceremony is the merging back of release branches.
-Though specialized tools do exist to solve this, they require documentation and add complexity.
-Frequently, developers make mistakes such as merging changes only into `master` and not into the `develop` branch.
-The reason for these errors is that Git flow is too complicated for most use cases.
-For example, many projects do releases but don't need to do hotfixes.
-
-## GitHub flow as a simpler alternative
-
-![Master branch with feature branches merged in](img/github_flow.png)
-
-In reaction to Git flow, GitHub created a simpler alternative.
-[GitHub flow](https://guides.github.com/introduction/flow/index.html) has only feature branches and a `master` branch.
-This flow is clean and straightforward, and many organizations have adopted it with great success.
-Atlassian recommends [a similar strategy](https://www.atlassian.com/blog/git/simple-git-workflow-is-simple), although they rebase feature branches.
-Merging everything into the `master` branch and frequently deploying means you minimize the amount of unreleased code, which is in line with lean and continuous delivery best practices.
-However, this flow still leaves a lot of questions unanswered regarding deployments, environments, releases, and integrations with issues.
-With GitLab flow, we offer additional guidance for these questions.
-
-## Production branch with GitLab flow
-
-![Master branch and production branch with an arrow that indicates a deployment](img/production_branch.png)
-
-GitHub flow assumes you can deploy to production every time you merge a feature branch.
-While this is possible in some cases, such as SaaS applications, there are many cases where this is not possible.
-One case is where you don't control the timing of a release, for example, an iOS application that is released when it passes App Store validation.
-Another case is when you have deployment windows &mdash; for example, workdays from 10&nbsp;AM to 4&nbsp;PM when the operations team is at full capacity &mdash; but you also merge code at other times.
-In these cases, you can make a production branch that reflects the deployed code.
-You can deploy a new version by merging `master` into the production branch.
-If you need to know what code is in production, you can just checkout the production branch to see.
-The approximate time of deployment is easily visible as the merge commit in the version control system.
-This time is pretty accurate if you automatically deploy your production branch.
-If you need a more exact time, you can have your deployment script create a tag on each deployment.
-This flow prevents the overhead of releasing, tagging, and merging that happens with Git flow.
-
-## Environment branches with GitLab flow
-
-![Multiple branches with the code cascading from one to another](img/environment_branches.png)
-
-It might be a good idea to have an environment that is automatically updated to the `master` branch.
-Only, in this case, the name of this environment might differ from the branch name.
-Suppose you have a staging environment, a pre-production environment, and a production environment.
-In this case, deploy the `master` branch to staging.
-To deploy to pre-production, create a merge request from the `master` branch to the pre-production branch.
-Go live by merging the pre-production branch into the production branch.
-This workflow, where commits only flow downstream, ensures that everything is tested in all environments.
-If you need to cherry-pick a commit with a hotfix, it is common to develop it on a feature branch and merge it into `master` with a merge request.
-In this case, do not delete the feature branch yet.
-If `master` passes automatic testing, you then merge the feature branch into the other branches.
-If this is not possible because more manual testing is required, you can send merge requests from the feature branch to the downstream branches.
-
-## Release branches with GitLab flow
-
-![Master and multiple release branches that vary in length with cherry-picks from master](img/release_branches.png)
-
-You only need to work with release branches if you need to release software to the outside world.
-In this case, each branch contains a minor version, for example, 2-3-stable, 2-4-stable, etc.
-Create stable branches using `master` as a starting point, and branch as late as possible.
-By doing this, you minimize the length of time during which you have to apply bug fixes to multiple branches.
-After announcing a release branch, only add serious bug fixes to the branch.
-If possible, first merge these bug fixes into `master`, and then cherry-pick them into the release branch.
-If you start by merging into the release branch, you might forget to cherry-pick them into `master`, and then you'd encounter the same bug in subsequent releases.
-Merging into `master` and then cherry-picking into release is called an "upstream first" policy, which is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/en/blog/a-community-for-using-openstack-with-red-hat-rdo).
-Every time you include a bug fix in a release branch, increase the patch version (to comply with [Semantic Versioning](https://semver.org/)) by setting a new tag.
-Some projects also have a stable branch that points to the same commit as the latest released branch.
-In this flow, it is not common to have a production branch (or Git flow `master` branch).
-
-## Merge/pull requests with GitLab flow
-
-![Merge request with inline comments](img/mr_inline_comments.png)
-
-Merge or pull requests are created in a Git management application. They ask an assigned person to merge two branches.
-Tools such as GitHub and Bitbucket choose the name "pull request" since the first manual action is to pull the feature branch.
-Tools such as GitLab and others choose the name "merge request" since the final action is to merge the feature branch.
-In this article, we'll refer to them as merge requests.
-
-If you work on a feature branch for more than a few hours, it is good to share the intermediate result with the rest of the team.
-To do this, create a merge request without assigning it to anyone.
-Instead, mention people in the description or a comment, for example, "/cc @mark @susan."
-This indicates that the merge request is not ready to be merged yet, but feedback is welcome.
-Your team members can comment on the merge request in general or on specific lines with line comments.
-The merge request serves as a code review tool, and no separate code review tools should be needed.
-If the review reveals shortcomings, anyone can commit and push a fix.
-Usually, the person to do this is the creator of the merge request.
-The diff in the merge request automatically updates when new commits are pushed to the branch.
-
-When you are ready for your feature branch to be merged, assign the merge request to the person who knows most about the codebase you are changing.
-Also, mention any other people from whom you would like feedback.
-After the assigned person feels comfortable with the result, they can merge the branch.
-If the assigned person does not feel comfortable, they can request more changes or close the merge request without merging.
-
-In GitLab, it is common to protect the long-lived branches, e.g., the `master` branch, so that [most developers can't modify them](../permissions/permissions.md).
-So, if you want to merge into a protected branch, assign your merge request to someone with maintainer permissions.
-
-After you merge a feature branch, you should remove it from the source control software.
-In GitLab, you can do this when merging.
-Removing finished branches ensures that the list of branches shows only work in progress.
-It also ensures that if someone reopens the issue, they can use the same branch name without causing problems.
-
-NOTE: **Note:**
-When you reopen an issue you need to create a new merge request.
-
-![Remove checkbox for branch in merge requests](img/remove_checkbox.png)
-
-## Issue tracking with GitLab flow
-
-![Merge request with the branch name "15-require-a-password-to-change-it" and assignee field shown](img/merge_request.png)
-
-GitLab flow is a way to make the relation between the code and the issue tracker more transparent.
-
-Any significant change to the code should start with an issue that describes the goal.
-Having a reason for every code change helps to inform the rest of the team and to keep the scope of a feature branch small.
-In GitLab, each change to the codebase starts with an issue in the issue tracking system.
-If there is no issue yet, create the issue, as long as the change will take a significant amount of work, i.e., more than 1 hour.
-In many organizations, raising an issue is part of the development process because they are used in sprint planning.
-The issue title should describe the desired state of the system.
-For example, the issue title "As an administrator, I want to remove users without receiving an error" is better than "Admin can't remove users."
-
-When you are ready to code, create a branch for the issue from the `master` branch.
-This branch is the place for any work related to this change.
-
-NOTE: **Note:**
-The name of a branch might be dictated by organizational standards.
-
-When you are done or want to discuss the code, open a merge request.
-A merge request is an online place to discuss the change and review the code.
-
-If you open the merge request but do not assign it to anyone, it is a "Work In Progress" merge request.
-These are used to discuss the proposed implementation but are not ready for inclusion in the `master` branch yet.
-Start the title of the merge request with `[WIP]` or `WIP:` to prevent it from being merged before it's ready.
-
-When you think the code is ready, assign the merge request to a reviewer.
-The reviewer can merge the changes when they think the code is ready for inclusion in the `master` branch.
-When they press the merge button, GitLab merges the code and creates a merge commit that makes this event easily visible later on.
-Merge requests always create a merge commit, even when the branch could be merged without one.
-This merge strategy is called "no fast-forward" in Git.
-After the merge, delete the feature branch since it is no longer needed.
-In GitLab, this deletion is an option when merging.
-
-Suppose that a branch is merged but a problem occurs and the issue is reopened.
-In this case, it is no problem to reuse the same branch name since the first branch was deleted when it was merged.
-At any time, there is at most one branch for every issue.
-It is possible that one feature branch solves more than one issue.
-
-## Linking and closing issues from merge requests
-
-![Merge request showing the linked issues that will be closed](img/close_issue_mr.png)
-
-Link to issues by mentioning them in commit messages or the description of a merge request, for example, "Fixes #16" or "Duck typing is preferred. See #12."
-GitLab then creates links to the mentioned issues and creates comments in the issues linking back to the merge request.
-
-To automatically close linked issues, mention them with the words "fixes" or "closes," for example, "fixes #14" or "closes #67." GitLab closes these issues when the code is merged into the default branch.
-
-If you have an issue that spans across multiple repositories, create an issue for each repository and link all issues to a parent issue.
-
-## Squashing commits with rebase
-
-![Vim screen showing the rebase view](img/rebase.png)
-
-With Git, you can use an interactive rebase (`rebase -i`) to squash multiple commits into one or reorder them.
-This functionality is useful if you want to replace a couple of small commits with a single commit, or if you want to make the order more logical.
-
-However, you should never rebase commits you have pushed to a remote server.
-Rebasing creates new commits for all your changes, which can cause confusion because the same change would have multiple identifiers.
-It also causes merge errors for anyone working on the same branch because their history would not match with yours.
-Also, if someone has already reviewed your code, rebasing makes it hard to tell what changed since the last review.
-
-You should also never rebase commits authored by other people.
-Not only does this rewrite history, but it also loses authorship information.
-Rebasing prevents the other authors from being attributed and sharing part of the [`git blame`](https://git-scm.com/docs/git-blame).
-
-If a merge involves many commits, it may seem more difficult to undo.
-You might think to solve this by squashing all the changes into one commit before merging, but as discussed earlier, it is a bad idea to rebase commits that you have already pushed.
-Fortunately, there is an easy way to undo a merge with all its commits.
-The way to do this is by reverting the merge commit.
-Preserving this ability to revert a merge is a good reason to always use the "no fast-forward" (`--no-ff`) strategy when you merge manually.
-
-NOTE: **Note:**
-If you revert a merge commit and then change your mind, revert the revert commit to redo the merge.
-Git does not allow you to merge the code again otherwise.
-
-## Reducing merge commits in feature branches
-
-![List of sequential merge commits](img/merge_commits.png)
-
-Having lots of merge commits can make your repository history messy.
-Therefore, you should try to avoid merge commits in feature branches.
-Often, people avoid merge commits by just using rebase to reorder their commits after the commits on the `master` branch.
-Using rebase prevents a merge commit when merging `master` into your feature branch, and it creates a neat linear history.
-However, as discussed in [the section about rebasing](#squashing-commits-with-rebase), you should never rebase commits you have pushed to a remote server.
-This restriction makes it impossible to rebase work in progress that you already shared with your team, which is something we recommend.
-
-Rebasing also creates more work, since every time you rebase, you have to resolve similar conflicts.
-Sometimes you can reuse recorded resolutions (`rerere`), but merging is better since you only have to resolve conflicts once.
-Atlassian has a more thorough explanation of the tradeoffs between merging and rebasing [on their blog](https://www.atlassian.com/blog/git/git-team-workflows-merge-or-rebase).
-
-A good way to prevent creating many merge commits is to not frequently merge `master` into the feature branch.
-There are three reasons to merge in `master`: utilizing new code, resolving merge conflicts, and updating long-running branches.
-
-If you need to utilize some code that was introduced in `master` after you created the feature branch, you can often solve this by just cherry-picking a commit.
-
-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.
-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.
-The solution here is to keep your feature branches short-lived.
-Most feature branches should take less than one day of work.
-If your feature branches often take more than a day of work, try to split your features into smaller units of work.
-
-If you need to keep a feature branch open for more than a day, there are a few strategies to keep it up-to-date.
-One option is to use continuous integration (CI) to merge in `master` at the start of the day.
-Another option is to only merge in from well-defined points in time, for example, a tagged release.
-You could also use [feature toggles](https://martinfowler.com/bliki/FeatureToggle.html) to hide incomplete features so you can still merge back into `master` every day.
-
-> **Note:** Don't confuse automatic branch testing with continuous integration.
-> Martin Fowler makes this distinction in [his article about feature branches](https://martinfowler.com/bliki/FeatureBranch.html):
->
-> "I've heard people say they are doing CI because they are running builds, perhaps using a CI server, on every branch with every commit.
-> That's continuous building, and a Good Thing, but there's no *integration*, so it's not CI."
-
-In conclusion, you should try to prevent merge commits, but not eliminate them.
-Your codebase should be clean, but your history should represent what actually happened.
-Developing software happens in small, messy steps, and it is OK to have your history reflect this.
-You can use tools to view the network graphs of commits and understand the messy history that created your code.
-If you rebase code, the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers.
-
-## Commit often and push frequently
-
-Another way to make your development work easier is to commit often.
-Every time you have a working set of tests and code, you should make a commit.
-Splitting up work into individual commits provides context for developers looking at your code later.
-Smaller commits make it clear how a feature was developed, and they make it easy to roll back to a specific good point in time or to revert one code change without reverting several unrelated changes.
-
-Committing often also makes it easy to share your work, which is important so that everyone is aware of what you are working on.
-You should push your feature branch frequently, even when it is not yet ready for review.
-By sharing your work in a feature branch or [a merge request](#mergepull-requests-with-gitlab-flow), you prevent your team members from duplicating work.
-Sharing your work before it's complete also allows for discussion and feedback about the changes, which can help improve the code before it gets to review.
-
-## How to write a good commit message
-
-![Good and bad commit message](img/good_commit.png)
-
-A commit message should reflect your intention, not just the contents of the commit.
-It is easy to see the changes in a commit, so the commit message should explain why you made those changes.
-An example of a good commit message is: "Combine templates to reduce duplicate code in the user views."
-The words "change," "improve," "fix," and "refactor" don't add much information to a commit message.
-For example, "Improve XML generation" could be better written as "Properly escape special characters in XML generation."
-For more information about formatting commit messages, please see this excellent [blog post by Tim Pope](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
-
-## Testing before merging
-
-![Merge requests showing the test states: red, yellow, and green](img/ci_mr.png)
-
-In old workflows, the continuous integration (CI) server commonly ran tests on the `master` branch only.
-Developers had to ensure their code did not break the `master` branch.
-When using GitLab flow, developers create their branches from this `master` branch, so it is essential that it never breaks.
-Therefore, each merge request must be tested before it is accepted.
-CI software like Travis CI and GitLab CI show the build results right in the merge request itself to make this easy.
-
-There is one drawback to testing merge requests: the CI server only tests the feature branch itself, not the merged result.
-Ideally, the server could also test the `master` branch after each change.
-However, retesting on every commit to `master` is computationally expensive and means you are more frequently waiting for test results.
-Since feature branches should be short-lived, testing just the branch is an acceptable risk.
-If new commits in `master` cause merge conflicts with the feature branch, merge `master` back into the branch to make the CI server re-run the tests.
-As said before, if you often have feature branches that last for more than a few days, you should make your issues smaller.
-
-## Working with feature branches
-
-![Shell output showing git pull output](img/git_pull.png)
-
-When creating a feature branch, always branch from an up-to-date `master`.
-If you know before you start that your work depends on another branch, you can also branch from there.
-If you need to merge in another branch after starting, explain the reason in the merge commit.
-If you have not pushed your commits to a shared location yet, you can also incorporate changes by rebasing on `master` or another feature branch.
-Do not merge from upstream again if your code can work and merge cleanly without doing so.
-Merging only when needed prevents creating merge commits in your feature branch that later end up littering the `master` history.
+This document was moved to [another location](../topics/gitlab_flow.md).
diff --git a/doc/workflow/issue_weight.md b/doc/workflow/issue_weight.md
index 79b8e5f5164..94eb38356e8 100644
--- a/doc/workflow/issue_weight.md
+++ b/doc/workflow/issue_weight.md
@@ -1,21 +1,5 @@
-# Issue weight **(STARTER)**
+---
+redirect_to: '../user/project/issues/issue_weight.md'
+---
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/76) in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
-
-When you have a lot of issues, it can be hard to get an overview.
-By adding a weight to each issue, you can get a better idea of how much time,
-value or complexity a given issue has or will cost.
-
-You can set the weight of an issue during its creation, by simply changing the
-value in the dropdown menu. You can set it to a non-negative integer
-value from 0, 1, 2, and so on. (The database stores a 4-byte value, so the
-upper bound is essentially limitless).
-You can remove weight from an issue
-as well.
-
-This value will appear on the right sidebar of an individual issue, as well as
-in the issues page next to a distinctive balance scale icon.
-
-As an added bonus, you can see the total sum of all issues on the milestone page.
-
-![issue page](issue_weight/issue.png)
+This document was moved to [another location](../user/project/issues/issue_weight.md).
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 7ad87982501..58c48b4f6e6 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -1,269 +1,5 @@
-# GitLab Git LFS Administration
+---
+redirect_to: '../../administration/lfs/lfs_administration.md'
+---
-Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](manage_large_binaries_with_git_lfs.md).
-
-## Requirements
-
-- Git LFS is supported in GitLab starting with version 8.2.
-- Support for object storage, such as AWS S3, was introduced in 10.0.
-- Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up.
-
-## Configuration
-
-Git LFS objects can be large in size. By default, they are stored on the server
-GitLab is installed on.
-
-There are various configuration options to help GitLab server administrators:
-
-- Enabling/disabling Git LFS support
-- Changing the location of LFS object storage
-- Setting up object storage supported by [Fog](http://fog.io/about/provider_documentation.html)
-
-### Configuration for Omnibus installations
-
-In `/etc/gitlab/gitlab.rb`:
-
-```ruby
-# Change to true to enable lfs - enabled by default if not defined
-gitlab_rails['lfs_enabled'] = false
-
-# Optionally, change the storage path location. Defaults to
-# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
-# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
-gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
-```
-
-### Configuration for installations from source
-
-In `config/gitlab.yml`:
-
-```yaml
-# Change to true to enable lfs
- lfs:
- enabled: false
- storage_path: /mnt/storage/lfs-objects
-```
-
-## Storing LFS objects in remote object storage
-
-> [Introduced][ee-2760] in [GitLab Premium][eep] 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.
-GitLab is tightly integrated with `Fog`, so you can refer to its [documentation](http://fog.io/about/provider_documentation.html)
-to check which storage services can be integrated with GitLab.
-You can also use external object storage in a private local network. For example,
-[MinIO](https://min.io/) is a standalone object storage service, is easy to set up, and works well with GitLab instances.
-
-GitLab provides two different options for the uploading mechanism: "Direct upload" and "Background upload".
-
-**Option 1. Direct upload**
-
-1. User pushes an lfs file to the GitLab instance
-1. GitLab-workhorse uploads the file directly to the external object storage
-1. GitLab-workhorse notifies GitLab-rails that the upload process is complete
-
-**Option 2. Background upload**
-
-1. User pushes an lfs file to the GitLab instance
-1. GitLab-rails stores the file in the local file storage
-1. GitLab-rails then uploads the file to the external object storage asynchronously
-
-The following general settings are supported.
-
-| Setting | Description | Default |
-|---------|-------------|---------|
-| `enabled` | Enable/disable object storage | `false` |
-| `remote_directory` | The bucket name where LFS objects will be stored| |
-| `direct_upload` | Set to true to enable direct upload of LFS without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
-| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `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 | |
-
-The `connection` settings match those provided by [Fog](https://github.com/fog).
-
-Here is a configuration example with S3.
-
-| Setting | Description | example |
-|---------|-------------|---------|
-| `provider` | The provider name | AWS |
-| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
-| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
-| `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 |
-| `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
-
-Here is a configuration example with GCS.
-
-| Setting | Description | example |
-|---------|-------------|---------|
-| `provider` | The provider name | `Google` |
-| `google_project` | GCP project name | `gcp-project-12345` |
-| `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` |
-| `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` |
-
-NOTE: **Note:**
-The service account must have permission to access the bucket.
-[See more](https://cloud.google.com/storage/docs/authentication)
-
-Here is a configuration example with Rackspace Cloud Files.
-
-| Setting | Description | example |
-|---------|-------------|---------|
-| `provider` | The provider name | `Rackspace` |
-| `rackspace_username` | The username of the Rackspace account with access to the container | `joe.smith` |
-| `rackspace_api_key` | The API key of the Rackspace account with access to the container | `ABC123DEF456ABC123DEF456ABC123DE` |
-| `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` |
-| `rackspace_temp_url_key` | The private key you have set in the Rackspace API for temporary URLs. Read more [here](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl) | `ABC123DEF456ABC123DEF456ABC123DE` |
-
-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 they 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.
-
-### Manual uploading to an object storage
-
-There are two ways to manually do the same thing as automatic uploading (described above).
-
-**Option 1: rake task**
-
-```sh
-rake gitlab:lfs:migrate
-```
-
-**Option 2: rails console**
-
-```sh
-$ sudo gitlab-rails console # Login to 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
-```
-
-### S3 for Omnibus installations
-
-On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
- the values you want:
-
- ```ruby
- gitlab_rails['lfs_object_store_enabled'] = true
- gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
- gitlab_rails['lfs_object_store_connection'] = {
- 'provider' => 'AWS',
- 'region' => 'eu-central-1',
- 'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
- 'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
- # The below options configure an S3 compatible host instead of AWS
- 'host' => 'localhost',
- 'endpoint' => 'http://127.0.0.1:9000',
- 'path_style' => true
- }
- ```
-
-1. Save the file and [reconfigure GitLab]s for the changes to take effect.
-1. Migrate any existing local LFS objects to the object storage:
-
- ```bash
- gitlab-rake gitlab:lfs:migrate
- ```
-
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless
- `gitlab_rails['lfs_object_store_background_upload']` is set to false.
-
-### S3 for installations from source
-
-For source installations the settings are nested under `lfs:` and then
-`object_store:`:
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
- lines:
-
- ```yaml
- lfs:
- enabled: true
- object_store:
- enabled: false
- remote_directory: lfs-objects # Bucket name
- connection:
- provider: AWS
- aws_access_key_id: 1ABCD2EFGHI34JKLM567N
- aws_secret_access_key: abcdefhijklmnopQRSTUVwxyz0123456789ABCDE
- region: eu-central-1
- # Use the following options to configure an AWS compatible host such as Minio
- host: 'localhost'
- endpoint: 'http://127.0.0.1:9000'
- path_style: true
- ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-1. Migrate any existing local LFS objects to the object storage:
-
- ```bash
- sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
- ```
-
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless `background_upload` is set to
- false.
-
-### Migrating back to local storage
-
-In order to migrate back to local storage:
-
-1. Set both `direct_upload` and `background_upload` to false under the LFS object storage settings. Don't forget to restart GitLab.
-1. Run `rake gitlab:lfs:migrate_to_local` on your console.
-1. Disable `object_storage` for LFS objects in `gitlab.rb`. Remember to restart GitLab afterwards.
-
-## Storage statistics
-
-You can see the total storage used for LFS objects on groups and projects
-in the administration area, as well as through the [groups](../../api/groups.md)
-and [projects APIs](../../api/projects.md).
-
-## Troubleshooting: `Google::Apis::TransmissionError: execution expired`
-
-If LFS integration is configred with Google Cloud Storage and background uploads (`background_upload: true` and `direct_upload: false`),
-Sidekiq workers may encouter 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.
-
-```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
-```
-
-See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/19581)
-
-## Known limitations
-
-- Support for removing unreferenced LFS objects was added in 8.14 onwards.
-- LFS authentications via SSH was added with GitLab 8.12.
-- 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]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: ../../administration/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
+This document was moved to [another location](../../administration/lfs/lfs_administration.md).
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index f747a7b5196..56e2f72284a 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -1,262 +1,5 @@
-# Git LFS
+---
+redirect_to: '../../administration/lfs/manage_large_binaries_with_git_lfs.md'
+---
-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
-larger than 1GB to preserve performance.
-
-![Git LFS tracking status](img/lfs-icon.png)
-
-An LFS icon is shown on files tracked by Git LFS to denote if a file is stored
-as a blob or as an LFS pointer.
-
-## How it works
-
-Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication
-to authorize client requests. Once the request is authorized, Git LFS client receives
-instructions from where to fetch or where to push the large file.
-
-## GitLab server configuration
-
-Documentation for GitLab instance administrators is under [LFS administration doc](lfs_administration.md).
-
-## Requirements
-
-- Git LFS is supported in GitLab starting with version 8.2
-- Git LFS must be enabled under project settings
-- [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up
-
-## Known limitations
-
-- Git LFS v1 original API is not supported since it was deprecated early in LFS
- development
-- When SSH is set as a remote, Git LFS objects still go through HTTPS
-- Any Git LFS request will ask for HTTPS credentials to be provided so a good Git
- credentials store is recommended
-- Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
- to add the URL to Git config manually (see [troubleshooting](#troubleshooting))
-
-NOTE: **Note:**
-With 8.12 GitLab added LFS support to SSH. The Git LFS communication
-still goes over HTTP, but now the SSH client passes the correct credentials
-to the Git LFS client, so no action is required by the user.
-
-## Using Git LFS
-
-Lets take a look at the workflow when you need to check large files into your Git
-repository with Git LFS. For example, if you want to upload a very large file and
-check it into your Git repository:
-
-```bash
-git clone git@gitlab.example.com:group/project.git
-git lfs install # initialize the Git LFS project
-git lfs track "*.iso" # select the file extensions that you want to treat as large files
-```
-
-Once a certain file extension is marked for tracking as a LFS object you can use
-Git as usual without having to redo the command to track a file with the same extension:
-
-```bash
-cp ~/tmp/debian.iso ./ # copy a large file into the current directory
-git add . # add the large file to the project
-git commit -am "Added Debian iso" # commit the file meta data
-git push origin master # sync the git repo and large file to the GitLab server
-```
-
-**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git
-LFS will not be working properly for people cloning the project:
-
-```bash
-git add .gitattributes
-```
-
-Cloning the repository works the same as before. Git automatically detects the
-LFS-tracked files and clones them via HTTP. If you performed the `git clone`
-command with a SSH URL, you have to enter your GitLab credentials for HTTP
-authentication.
-
-```bash
-git clone git@gitlab.example.com:group/project.git
-```
-
-If you already cloned the repository and you want to get the latest LFS object
-that are on the remote repository, eg. for a branch from origin:
-
-```bash
-git lfs fetch origin master
-```
-
-### Migrate an existing repo to Git LFS
-
-Read the documentation on how to [migrate an existing Git repo with Git LFS](../../topics/git/migrate_to_git_lfs/index.md).
-
-## File Locking
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/35856) in GitLab 10.5.
-
-The first thing to do before using File Locking is to tell Git LFS which
-kind of files are lockable. The following command will store PNG files
-in LFS and flag them as lockable:
-
-```bash
-git lfs track "*.png" --lockable
-```
-
-After executing the above command a file named `.gitattributes` will be
-created or updated with the following content:
-
-```bash
-*.png filter=lfs diff=lfs merge=lfs -text lockable
-```
-
-You can also register a file type as lockable without using LFS
-(In order to be able to lock/unlock a file you need a remote server that implements the LFS File Locking API),
-in order to do that you can edit the `.gitattributes` file manually:
-
-```bash
-*.pdf lockable
-```
-
-After a file type has been registered as lockable, Git LFS will make
-them readonly on the file system automatically. This means you will
-need to lock the file before editing it.
-
-### Managing Locked Files
-
-Once you're ready to edit your file you need to lock it first:
-
-```bash
-git lfs lock images/banner.png
-Locked images/banner.png
-```
-
-This will register the file as locked in your name on the server:
-
-```bash
-git lfs locks
-images/banner.png joe ID:123
-```
-
-Once you have pushed your changes, you can unlock the file so others can
-also edit it:
-
-```bash
-git lfs unlock images/banner.png
-```
-
-You can also unlock by id:
-
-```bash
-git lfs unlock --id=123
-```
-
-If for some reason you need to unlock a file that was not locked by you,
-you can use the `--force` flag as long as you have a `maintainer` access on
-the project:
-
-```bash
-git lfs unlock --id=123 --force
-```
-
-## Troubleshooting
-
-### error: Repository or object not found
-
-There are a couple of reasons why this error can occur:
-
-- You don't have permissions to access certain LFS object
-
-Check if you have permissions to push to the project or fetch from the project.
-
-- Project is not allowed to access the LFS object
-
-LFS object you are trying to push to the project or fetch from the project is not
-available to the project anymore. Probably the object was removed from the server.
-
-- Local Git repository is using deprecated LFS API
-
-### Invalid status for `<url>` : 501
-
-Git LFS will log the failures into a log file.
-To view this log file, while in project directory:
-
-```bash
-git lfs logs last
-```
-
-If the status `error 501` is shown, it is because:
-
-- Git LFS is not enabled in project settings. Check your project settings and
- enable Git LFS.
-
-- Git LFS support is not enabled on the GitLab server. Check with your GitLab
- administrator why Git LFS is not enabled on the server. See
- [LFS administration documentation](lfs_administration.md) for instructions
- on how to enable LFS support.
-
-- Git LFS client version is not supported by GitLab server. Check your Git LFS
- version with `git lfs version`. Check the Git config of the project for traces
- of deprecated API with `git lfs -l`. If `batch = false` is set in the config,
- remove the line and try to update your Git LFS client. Only version 1.0.1 and
- newer are supported.
-
-### getsockopt: connection refused
-
-If you push a LFS object to a project and you receive an error similar to:
-`Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
-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
-`lfsurl` is not set in the Git config.
-
-To prevent this from happening, set the lfs url in project Git config:
-
-```bash
-git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
-```
-
-### Credentials are always required when pushing an object
-
-NOTE: **Note:**
-With 8.12 GitLab added LFS support to SSH. The Git LFS communication
-still goes over HTTP, but now the SSH client passes the correct credentials
-to the Git LFS client, so no action is required by the user.
-
-Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
-the LFS object on every push for every object, user HTTPS credentials are required.
-
-By default, Git has support for remembering the credentials for each repository
-you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
-
-For example, you can tell Git to remember the password for a period of time in
-which you expect to push the objects:
-
-```bash
-git config --global credential.helper 'cache --timeout=3600'
-```
-
-This will remember the credentials for an hour after which Git operations will
-require re-authentication.
-
-If you are using OS X you can use `osxkeychain` to store and encrypt your credentials.
-For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
-
-More details about various methods of storing the user credentials can be found
-on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
-
-### LFS objects are missing on push
-
-GitLab checks files to detect LFS pointers on push. If LFS pointers are detected, GitLab tries to verify that those files already exist in LFS on GitLab.
-
-Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
-
-If you are storing LFS files outside of GitLab you can disable LFS on the project by setting `lfs_enabled: false` with the [projects API](../../api/projects.md#edit-project).
-
-### 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`.
-
-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.
-
-To stop push failure, LFS support can be disabled in the [Project settings](../../user/project/settings/index.md). This means you will lose GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS).
+This document was moved to [another location](../../administration/lfs/manage_large_binaries_with_git_lfs.md).
diff --git a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
index 8f24929c9dc..997ef8938a6 100644
--- a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
+++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -1,254 +1,5 @@
-# Migration guide from Git Annex to Git LFS
-
->**Note:**
-Git Annex support [has been removed][issue-remove-annex] in GitLab Enterprise
-Edition 9.0 (2017/03/22).
-
-Both [Git Annex][] and [Git LFS][] are tools to manage large files in Git.
-
-## History
-
-Git Annex [was introduced in GitLab Enterprise Edition 7.8][post-3], 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
-Enterprise editions.
-
-## Differences between Git Annex and Git LFS
-
-Some items below are general differences between the two protocols and some are
-ones that GitLab developed.
-
-- Git Annex works only through SSH, whereas Git LFS works both with SSH and HTTPS
- (SSH support was added in GitLab 8.12).
-- Annex files are stored in a sub-directory of the normal repositories, whereas
- LFS files are stored outside of the repositories in a place you can define.
-- Git Annex requires a more complex setup, but has much more options than Git
- LFS. You can compare the commands each one offers by running `man git-annex`
- and `man git-lfs`.
-- Annex files cannot be browsed directly in GitLab's interface, whereas LFS
- files can.
-
-## Migration steps
-
->**Note:**
-Since Git Annex files are stored in a sub-directory of the normal repositories
-(`.git/annex/objects`) and LFS files are stored outside of the repositories,
-they are not compatible as they are using a different scheme. Therefore, the
-migration has to be done manually per repository.
-
-There are basically two steps you need to take in order to migrate from Git
-Annex to Git LFS.
-
-### TL; DR
-
-If you know what you are doing and want to skip the reading, this is what you
-need to do (we assume you have [git-annex enabled](../git_annex.md#using-gitlab-git-annex) in your
-repository and that you have made backups in case something goes wrong).
-Fire up a terminal, navigate to your Git repository and:
-
-1. Disable `git-annex`:
-
- ```bash
- git annex sync --content
- git annex direct
- git annex uninit
- git annex indirect
- ```
-
-1. Enable `git-lfs`:
-
- ```
- git lfs install
- git lfs track <files>
- git add .
- git commit -m "commit message"
- git push
- ```
-
-### Disabling Git Annex in your repo
-
-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].
-
-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
-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
-[Git Annex troubleshooting tips](../git_annex.md#troubleshooting-tips) section.
-
-1. Backup your repository
-
- ```bash
- cd repository
- git annex sync --content
- cd ..
- git clone repository repository-backup
- cd repository-backup
- git annex get
- cd ..
- ```
-
-1. Use `annex direct`:
-
- ```bash
- cd repository
- git annex direct
- ```
-
- The output should be similar to this:
-
- ```bash
- commit
- On branch master
- Your branch is up-to-date with 'origin/master'.
- nothing to commit, working tree clean
- ok
- direct debian.iso ok
- direct ok
- ```
-
-1. Disable Git Annex with [`annex uninit`][uninit]:
-
- ```bash
- git annex uninit
- ```
-
- The output should be similar to this:
-
- ```bash
- unannex debian.iso ok
- Deleted branch git-annex (was 2534d2c).
- ```
-
- This will `unannex` every file in the repository, leaving the original files.
-
-1. Switch back to `indirect` mode:
-
- ```bash
- git annex indirect
- ```
-
- The output should be similar to this:
-
- ```bash
- (merging origin/git-annex into git-annex...)
- (recording state in git...)
- commit (recording state in git...)
-
- ok
- (recording state in git...)
- [master fac3194] commit before switching to indirect mode
- 1 file changed, 1 deletion(-)
- delete mode 120000 alpine-virt-3.4.4-x86_64.iso
- ok
- indirect ok
- ok
- ```
-
+---
+redirect_to: '../../administration/lfs/migrate_from_git_annex_to_git_lfs.md'
---
-At this point, you have two options. Either add, commit and push the files
-directly back to GitLab or switch to Git LFS. We will tackle the LFS switch in
-the next section.
-
-### Enabling Git LFS in your repo
-
-Git LFS is enabled by default on all GitLab products (GitLab CE, GitLab EE,
-GitLab.com), therefore, you don't need to do anything server-side.
-
-1. First, make sure you have `git-lfs` installed locally:
-
- ```bash
- git lfs help
- ```
-
- If the terminal doesn't prompt you with a full response on `git-lfs` commands,
- [install the Git LFS client][install-lfs] first.
-
-1. Inside the repo, run the following command to initiate LFS:
-
- ```bash
- git lfs install
- ```
-
-1. Enable `git-lfs` for the group of files you want to track. You
- can track specific files, all files containing the same extension, or an
- entire directory:
-
- ```bash
- git lfs track images/01.png # per file
- git lfs track **/*.png # per extension
- git lfs track images/ # per directory
- ```
-
- Once you do that, run `git status` and you'll see `.gitattributes` added
- to your repo. It collects all file patterns that you chose to track via
- `git-lfs`.
-
-1. Add the files, commit and push them to GitLab:
-
- ```bash
- git add .
- git commit -m "commit message"
- git push
- ```
-
- If your remote is set up with HTTP, you will be asked to enter your login
- credentials. If you have [2FA enabled](../../user/profile/account/two_factor_authentication.md), make sure to use a
- [personal access token](../../user/profile/account/two_factor_authentication.md#personal-access-tokens)
- instead of your password.
-
-## Removing the Git Annex branches
-
-After the migration finishes successfully, you can remove all `git-annex`
-related branches from your repository.
-
-On GitLab, navigate to your project's **Repository âž” Branches** and delete all
-branches created by Git Annex: `git-annex`, and all under `synced/`.
-
-![repository branches](images/git-annex-branches.png)
-
-You can also do this on the command line with:
-
-```bash
-git branch -d synced/master
-git branch -d synced/git-annex
-git push origin :synced/master
-git push origin :synced/git-annex
-git push origin :git-annex
-git remote prune origin
-```
-
-If there are still some Annex objects inside your repository (`.git/annex/`)
-or references inside `.git/config`, run `annex uninit` again:
-
-```bash
-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]
-- (GitLab Docs) [Git Annex](../git_annex.md)
-- (GitLab Docs) [Git LFS](manage_large_binaries_with_git_lfs.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
-[lfs-track]: https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/#tracking-files-with-lfs
-[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/
+This document was moved to [another location](../../administration/lfs/migrate_from_git_annex_to_git_lfs.md).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index d619c870c5e..23f96360484 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -1,172 +1,5 @@
-# GitLab Notification Emails
+---
+redirect_to: '../user/profile/notifications.md'
+---
-GitLab has a notification system in place to notify a user of events that are important for the workflow.
-
-## Notification settings
-
-You can find notification settings under the user profile.
-
-![notification settings](img/notification_global_settings.png)
-
-Notification settings are divided into three groups:
-
-- Global settings
-- Group settings
-- Project settings
-
-Each of these settings have levels of notification:
-
-- Global: For groups and projects, notifications as per global settings.
-- Watch: Receive notifications for any activity.
-- Participate: Receive notifications for threads you have participated in.
-- On Mention: Receive notifications when `@mentioned` in comments.
-- Disabled: Turns off notifications.
-- Custom: Receive notifications for custom selected events.
-
-> Introduced in GitLab 12.0
-
-You can also select an email address to receive notifications for each group you belong to.
-
-### Global Settings
-
-Global settings are at the bottom of the hierarchy.
-Any setting set here will be overridden by a setting at the group or a project level.
-
-Group or Project settings can use `global` notification setting which will then use
-anything that is set at Global Settings.
-
-### Group Settings
-
-![notification settings](img/notification_group_settings.png)
-
-Group settings are taking precedence over Global Settings but are on a level below Project or Subgroup settings:
-
-```
-Group < Subgroup < Project
-```
-
-This means that you can set a different level of notifications per group while still being able
-to have a finer level setting per project or subgroup.
-Organization like this is suitable for users that belong to different groups but don't have the
-same need for being notified for every group they are member of.
-These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
-
-The group owner can disable email notifications for a group, which also includes
-it's subgroups and projects. If this is the case, you will not receive any corresponding notifications,
-and the notification button will be disabled with an explanatory tooltip.
-
-### Project Settings
-
-![notification settings](img/notification_project_settings.png)
-
-Project settings are at the top level and any setting placed at this level will take precedence of any
-other setting.
-This is suitable for users that have different needs for notifications per project basis.
-These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
-
-The project owner (or it's group owner) can disable email notifications for the project.
-If this is the case, you will not receive any corresponding notifications, and the notification
-button will be disabled with an explanatory tooltip.
-
-## Notification events
-
-Below is the table of events users can be notified of:
-
-| Event | Sent to | Settings level |
-|------------------------------|---------------------|------------------------------|
-| New SSH key added | User | Security email, always sent. |
-| New email added | User | Security email, always sent. |
-| Email changed | User | Security email, always sent. |
-| Password changed | User | Security email, always sent. |
-| New user created | User | Sent on user creation, except for OmniAuth (LDAP)|
-| User added to project | User | Sent when user is added to project |
-| Project access level changed | User | Sent when user project access level is changed |
-| User added to group | User | Sent when user is added to group |
-| Group access level changed | User | Sent when user group access level is changed |
-| Project moved | Project members (1) | (1) not disabled |
-| New release | Project members | Custom notification |
-
-### Issue / Epics / Merge request events
-
-In most of the below cases, the notification will be sent to:
-
-- Participants:
- - the author and assignee of the issue/merge request
- - authors of comments on the issue/merge request
- - anyone mentioned by `@username` in the title or description of the issue, merge request or epic **(ULTIMATE)**
- - anyone with notification level "Participating" or higher that is mentioned by `@username`
- in any of the comments on the issue, merge request, or epic **(ULTIMATE)**
-- Watchers: users with notification level "Watch"
-- Subscribers: anyone who manually subscribed to the issue, merge request, or epic **(ULTIMATE)**
-- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
-
-| Event | Sent to |
-|------------------------|---------|
-| New issue | |
-| Close issue | |
-| Reassign issue | The above, plus the old assignee |
-| Reopen issue | |
-| Due issue | Participants and Custom notification level with this event selected |
-| Change milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
-| Remove milestone issue | Subscribers, participants mentioned, and Custom notification level with this event selected |
-| New merge request | |
-| Push to merge request | Participants and Custom notification level with this event selected |
-| Reassign merge request | The above, plus the old assignee |
-| Close merge request | |
-| Reopen merge request | |
-| Merge merge request | |
-| Change milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
-| Remove milestone merge request | Subscribers, participants mentioned, and Custom notification level with this event selected |
-| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
-| Failed pipeline | The author of the pipeline |
-| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
-| New epic **(ULTIMATE)** | |
-| Close epic **(ULTIMATE)** | |
-| Reopen epic **(ULTIMATE)** | |
-
-In addition, if the title or description of an Issue or Merge Request is
-changed, notifications will be sent to any **new** mentions by `@username` as
-if they had been mentioned in the original text.
-
-You won't receive notifications for Issues, Merge Requests or Milestones created
-by yourself (except when an issue is due). You will only receive automatic
-notifications when somebody else comments or adds changes to the ones that
-you've created or mentions you.
-
-If an open merge request becomes unmergeable due to conflict, its author will be notified about the cause.
-If a user has also set the merge request to automatically merge once pipeline succeeds,
-then that user will also be notified.
-
-### Email Headers
-
-Notification emails include headers that provide extra content about the notification received:
-
-| Header | Description |
-|-----------------------------|-------------------------------------------------------------------------|
-| X-GitLab-Project | The name of the project the notification belongs to |
-| X-GitLab-Project-Id | The ID of the project |
-| X-GitLab-Project-Path | The path of the project |
-| X-GitLab-(Resource)-ID | The ID of the resource the notification is for, where resource is `Issue`, `MergeRequest`, `Commit`, etc|
-| X-GitLab-Discussion-ID | Only in comment emails, the ID of the thread the comment is from |
-| X-GitLab-Pipeline-Id | Only in pipeline emails, the ID of the pipeline the notification is for |
-| X-GitLab-Reply-Key | A unique token to support reply by email |
-| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
-| List-Id | The path of the project in a RFC 2919 mailing list identifier useful for email organization, for example, with Gmail filters |
-
-#### X-GitLab-NotificationReason
-
-This header holds the reason for the notification to have been sent out,
-where reason can be `mentioned`, `assigned`, `own_activity`, etc.
-Only one reason is sent out according to its priority:
-
-- `own_activity`
-- `assigned`
-- `mentioned`
-
-The reason in this header will also be shown in the footer of the notification email. For example an email with the
-reason `assigned` will have this sentence in the footer:
-`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
-
-NOTE: **Note:**
-Only reasons listed above have been implemented so far.
-Further implementation is [being discussed](https://gitlab.com/gitlab-org/gitlab-foss/issues/42062).
+This document was moved to [another location](../user/profile/notifications.md).
diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md
index 1fd63a556c6..f3ba61f6a5c 100644
--- a/doc/workflow/releases.md
+++ b/doc/workflow/releases.md
@@ -1,22 +1,5 @@
-# Releases
+---
+redirect_to: '../user/project/releases/index.md#add-release-notes-to-git-tags'
+---
-NOTE: In GitLab 11.7, we introduced the full fledged [Releases](../user/project/releases/index.md)
-feature. You can still create release notes on this page, but the new method is preferred.
-
-You can add release notes to any Git tag using the notes feature. Release notes
-behave like any other markdown form in GitLab so you can write text and
-drag-n-drop files to it. Release notes are stored in GitLab's database.
-
-There are several ways to add release notes:
-
-- In the interface, when you create a new Git tag
-- In the interface, by adding a note to an existing Git tag
-- Using the GitLab API
-
-## New tag page with release notes text area
-
-![new_tag](releases/new_tag.png)
-
-## Tags page with button to add or edit release notes for existing Git tag
-
-![tags](releases/tags.png)
+This document was moved to [another location](../user/project/releases/index.md#add-release-notes-to-git-tags).
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 6d1a5913789..dc77f4f47af 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -1,426 +1,5 @@
-# Repository mirroring
-
-Repository mirroring allows for mirroring of repositories to and from external sources. It can be
-used to mirror branches, tags, and commits between repositories.
-
-A repository mirror at GitLab will be updated automatically. You can also manually trigger an update
-at most once every 5 minutes.
-
-## Overview
-
-Repository mirroring is useful when you want to use a repository outside of GitLab.
-
-There are two kinds of repository mirroring supported by GitLab:
-
-- Push: for mirroring a GitLab repository to another location.
-- Pull: for mirroring a repository from another location to GitLab. **(STARTER)**
-
-When the mirror repository is updated, all new branches, tags, and commits will be visible in the
-project's activity feed.
-
-Users with at least [developer access](../user/permissions.md) to the project can also force an
-immediate update, unless:
-
-- The mirror is already being updated.
-- 5 minutes haven't elapsed since its last update.
-
-## Use cases
-
-The following are some possible use cases for repository mirroring:
-
-- You migrated to GitLab but still need to keep your project in another source. In that case, you
- can simply set it up to mirror to GitLab (pull) and all the essential history of commits, tags,
- and branches will be available in your GitLab instance. **(STARTER)**
-- You have old projects in another source that you don't use actively anymore, but don't want to
- remove for archiving purposes. In that case, you can create a push mirror so that your active
- GitLab repository can push its changes to the old location.
-
-## Pushing to a remote repository **(CORE)**
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/249) in GitLab Enterprise Edition 8.7.
-> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18715) in 10.8.
-
-For an existing project, you can set up push mirroring as follows:
-
-1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories** section.
-1. Enter a repository URL.
-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. Click the **Mirror repository** button to save the configuration.
-
-![Repository mirroring push settings screen](img/repository_mirroring_push_settings.png)
-
-When push mirroring is enabled, only push commits directly to the mirrored repository to prevent the
-mirror diverging. All changes will end up in the mirrored repository whenever:
-
-- Commits are pushed to GitLab.
-- A [forced update](#forcing-an-update-core) is initiated.
-
-Changes pushed to files in the repository are automatically pushed to the remote mirror at least:
-
-- Within five minutes of being received.
-- Within one minute if **Only mirror protected branches** is enabled.
-
-In the case of a diverged branch, you will see an error indicated at the **Mirroring repositories**
-section.
-
-### Push only protected branches **(CORE)**
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
-> - [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18715) in 10.8.
-
-You can choose to only push your protected branches from GitLab to your remote repository.
-
-To use this option, check the **Only mirror protected branches** box when creating a repository
-mirror.
-
-## 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:
-
-1. Create a [GitHub personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with the `public_repo` box checked.
-1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`.
-1. Fill in **Password** field with your GitHub personal access token.
-1. Click the **Mirror repository** button.
-
-The mirrored repository will be listed. For example, `https://*****:*****@github.com/<your_github_group>/<your_github_project>.git`.
-
-The repository will push soon. To force a push, click the appropriate button.
-
-## Setting up a push mirror to another GitLab instance with 2FA activated
-
-1. On the destination GitLab instance, create a [personal access token](../user/profile/personal_access_tokens.md) with `API` scope.
-1. On the source GitLab instance:
- 1. Fill in the **Git repository URL** field using this format: `https://oauth2@<destination host>/<your_gitlab_group_or_name>/<your_gitlab_project>.git`.
- 1. Fill in **Password** field with the GitLab personal access token created on the destination GitLab instance.
- 1. Click the **Mirror repository** button.
-
-## Pulling from a remote repository **(STARTER)**
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/51) in GitLab Enterprise Edition 8.2.
-> - [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
-
-NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab/issues/10361) to
-GitLab.com users until March 22nd, 2020.
-
-You can set up a repository to automatically have its branches, tags, and commits updated from an
-upstream repository.
-
-This is useful when a repository you're interested in is located on a different server, and you want
-to be able to browse its content and its activity using the familiar GitLab interface.
-
-To configure mirror pulling for an existing project:
-
-1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories**
- section.
-1. Enter a repository URL.
-1. Select **Pull** from the **Mirror direction** dropdown.
-1. Select an authentication method from the **Authentication method** dropdown, if necessary.
-1. If necessary, check the following boxes:
- - **Overwrite diverged branches**.
- - **Trigger pipelines for mirror updates**.
- - **Only mirror protected branches**.
-1. Click the **Mirror repository** button to save the configuration.
-
-![Repository mirroring pull settings screen - upper part](img/repository_mirroring_pull_settings_upper.png)
-
+---
+redirect_to: '../user/project/repository/repository_mirroring.md'
---
-![Repository mirroring pull settings screen - lower part](img/repository_mirroring_pull_settings_lower.png)
-
-Because GitLab is now set to pull changes from the upstream repository, you should not push commits
-directly to the repository on GitLab. Instead, any commits should be pushed to the upstream repository.
-Changes pushed to the upstream repository will be pulled into the GitLab repository, either:
-
-- Automatically within a certain period of time.
-- When a [forced update](#forcing-an-update-core) is initiated.
-
-CAUTION: **Caution:**
-If you do manually update a branch in the GitLab repository, the branch will become diverged from
-upstream and GitLab will no longer automatically update this branch to prevent any changes from being lost.
-
-### How it works
-
-Once the pull mirroring feature has been enabled for a repository, the repository is added to a queue.
-
-Once per minute, a Sidekiq cron job schedules repository mirrors to update, based on:
-
-- The capacity available. This is determined by Sidekiq settings. For GitLab.com, see [GitLab.com Sidekiq settings](../user/gitlab_com/index.md#sidekiq).
-- The number of repository mirrors already in the queue that are due to be updated. Being due depends on when the repository mirror was last updated and how many times it's been retried.
-
-Repository mirrors are updated as Sidekiq becomes available to process them. If the process of updating the repository mirror:
-
-- Succeeds, an update will be enqueued again with at least a 30 minute wait.
-- Fails (for example, a branch diverged from upstream), it will be attempted again later. Mirrors can fail
- up to 14 times before they will not be enqueued for update again.
-
-### SSH authentication
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
-
-SSH authentication is mutual:
-
-- You have to prove to the server that you're allowed to access the repository.
-- The server also has to prove to *you* that it's who it claims to be.
-
-You provide your credentials as a password or public key. The server that the
-other repository resides on provides its credentials as a "host key", the
-fingerprint of which needs to be verified manually.
-
-If you're mirroring over SSH (that is, using an `ssh://` URL), you can authenticate using:
-
-- Password-based authentication, just as over HTTPS.
-- Public key authentication. This is often more secure than password authentication,
- especially when the other repository supports [Deploy Keys](../ssh/README.md#deploy-keys).
-
-To get started:
-
-1. Navigate to your project's **Settings > Repository** and expand the **Mirroring repositories** section.
-1. Enter an `ssh://` URL for mirroring.
-
-NOTE: **Note:**
-SCP-style URLs (that is, `git@example.com:group/project.git`) are not supported at this time.
-
-Entering the URL adds two buttons to the page:
-
-- **Detect host keys**.
-- **Input host keys manually**.
-
-If you click the:
-
-- **Detect host keys** button, GitLab will fetch the host keys from the server and display the fingerprints.
-- **Input host keys manually** button, a field is displayed where you can paste in host keys.
-
-Assuming you used the former, you now need to verify that the fingerprints are
-those you expect. GitLab.com and other code hosting sites publish their
-fingerprints in the open for you to check:
-
-- [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
-- [Bitbucket](https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html)
-- [GitHub](https://help.github.com/en/articles/githubs-ssh-key-fingerprints)
-- [GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints)
-- [Launchpad](https://help.launchpad.net/SSHFingerprints)
-- [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
-- [SourceForge](https://sourceforge.net/p/forge/documentation/SSH%20Key%20Fingerprints/)
-
-Other providers will vary. If you're running self-managed GitLab, or otherwise
-have access to the server for the other repository, you can securely gather the
-key fingerprints:
-
-```sh
-$ cat /etc/ssh/ssh_host*pub | ssh-keygen -E md5 -l -f -
-256 MD5:f4:28:9f:23:99:15:21:1b:bf:ed:1f:8e:a0:76:b2:9d root@example.com (ECDSA)
-256 MD5:e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73 root@example.com (ED25519)
-2048 MD5:3f:72:be:3d:62:03:5c:62:83:e8:6e:14:34:3a:85:1d root@example.com (RSA)
-```
-
-NOTE: **Note:**
-You may need to exclude `-E md5` for some older versions of SSH.
-
-When mirroring the repository, GitLab will now check that at least one of the
-stored host keys matches before connecting. This can prevent malicious code from
-being injected into your mirror, or your password being stolen.
-
-### SSH public key authentication
-
-To use SSH public key authentication, you'll also need to choose that option
-from the **Authentication method** dropdown. When the mirror is created,
-GitLab generates a 4096-bit RSA key that can be copied by clicking the **Copy SSH public key** button.
-
-![Repository mirroring copy SSH public key to clipboard button](img/copy_ssh_public_key_button.png)
-
-You then need to add the public SSH key to the other repository's configuration:
-
-- If the other repository is hosted on GitLab, you should add the public SSH key
- as a [Deploy Key](../ssh/README.md#deploy-keys).
-- If the other repository is hosted elsewhere, you may need to add the key to
- your user's `authorized_keys` file. Paste the entire public SSH key into the
- file on its own line and save it.
-
-If you need to change the key at any time, you can remove and re-add the mirror
-to generate a new key. You'll have to update the other repository with the new
-key to keep the mirror running.
-
-### Overwrite diverged branches **(STARTER)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/4559) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
-
-You can choose to always update your local branches with remote versions, even if they have
-diverged from the remote.
-
-CAUTION: **Caution:**
-For mirrored branches, enabling this option results in the loss of local changes.
-
-To use this option, check the **Overwrite diverged branches** box when creating a repository mirror.
-
-### Only mirror protected branches **(STARTER)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3326) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
-
-You can choose to pull mirror only the protected branches from your remote repository to GitLab.
-Non-protected branches are not mirrored and can diverge.
-
-To use this option, check the **Only mirror protected branches** box when creating a repository mirror.
-
-### Hard failure **(STARTER)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3117) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.2.
-
-Once the mirroring process is unsuccessfully retried 14 times in a row, it will get marked as hard
-failed. This will become visible in either the:
-
-- Project's main dashboard.
-- Pull mirror settings page.
-
-When a project is hard failed, it will no longer get picked up for mirroring. A user can resume the
-project mirroring again by [Forcing an update](#forcing-an-update-core).
-
-### Trigger update using API **(STARTER)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/3453) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
-
-Pull mirroring uses polling to detect new branches and commits added upstream, often minutes
-afterwards. If you notify GitLab by [API](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter),
-updates will be pulled immediately.
-
-For more information, see [Start the pull mirroring process for a Project](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter).
-
-## Forcing an update **(CORE)**
-
-While mirrors are scheduled to update automatically, you can always force an update by using the
-update button which is available on the **Mirroring repositories** section of the **Repository Settings** page.
-
-![Repository mirroring force update user interface](img/repository_mirroring_force_update.png)
-
-## Bidirectional mirroring **(STARTER)**
-
-CAUTION: **Caution:**
-Bidirectional mirroring may cause conflicts.
-
-If you configure a GitLab repository to both pull from, and push to, the same remote source, there
-is no guarantee that either repository will update correctly. If you set up a repository for
-bidirectional mirroring, you should prepare for the likely conflicts by deciding who will resolve
-them and how they will be resolved.
-
-Rewriting any mirrored commit on either remote will cause conflicts and mirroring to fail. This can
-be prevented by:
-
-- [Pulling only protected branches](#only-mirror-protected-branches-starter).
-- [Pushing only protected branches](#push-only-protected-branches-core).
-
-You should [protect the branches](../user/project/protected_branches.md) you wish to mirror on both
-remotes to prevent conflicts caused by rewriting history.
-
-Bidirectional mirroring also creates a race condition where commits made close together to the same
-branch causes conflicts. The race condition can be mitigated by reducing the mirroring delay by using
-a [Push event webhook](../user/project/integrations/webhooks.md#push-events) to trigger an immediate
-pull to GitLab. Push mirroring from GitLab is rate limited to once per minute when only push mirroring
-protected branches.
-
-### Preventing conflicts using a `pre-receive` hook
-
-CAUTION: **Warning:**
-The solution proposed will negatively impact the performance of
-Git push operations because they will be proxied to the upstream Git
-repository.
-
-A server-side `pre-receive` hook can be used to prevent the race condition
-described above by only accepting the push after first pushing the commit to
-the upstream Git repository. In this configuration one Git repository acts as
-the authoritative upstream, and the other as downstream. The `pre-receive` hook
-will be installed on the downstream repository.
-
-Read about [configuring custom Git hooks](../administration/custom_hooks.md) on the GitLab server.
-
-A sample `pre-receive` hook is provided below.
-
-```bash
-#!/usr/bin/env bash
-
-# --- Assume only one push mirror target
-# Push mirroring remotes are named `remote_mirror_<id>`, this finds the first remote and uses that.
-TARGET_REPO=$(git remote | grep -m 1 remote_mirror)
-
-proxy_push()
-{
- # --- Arguments
- OLDREV=$(git rev-parse $1)
- NEWREV=$(git rev-parse $2)
- REFNAME="$3"
-
- # --- Pattern of branches to proxy pushes
- whitelisted=$(expr "$branch" : "\(master\)")
-
- case "$refname" in
- refs/heads/*)
- branch=$(expr "$refname" : "refs/heads/\(.*\)")
-
- if [ "$whitelisted" = "$branch" ]; then
- error="$(git push --quiet $TARGET_REPO $NEWREV:$REFNAME 2>&1)"
- fail=$?
-
- if [ "$fail" != "0" ]; then
- echo >&2 ""
- echo >&2 " Error: updates were rejected by upstream server"
- echo >&2 " This is usually caused by another repository pushing changes"
- echo >&2 " to the same ref. You may want to first integrate remote changes"
- echo >&2 ""
- return
- fi
- fi
- ;;
- esac
-}
-
-# Allow dual mode: run from the command line just like the update hook, or
-# if no arguments are given then run as a hook script
-if [ -n "$1" -a -n "$2" -a -n "$3" ]; then
- # Output to the terminal in command line mode - if someone wanted to
- # resend an email; they could redirect the output to sendmail
- # themselves
- PAGER= proxy_push $2 $3 $1
-else
- # Push is proxied upstream one ref at a time. Because of this it is possible
- # for some refs to succeed, and others to fail. This will result in a failed
- # push.
- while read oldrev newrev refname
- do
- proxy_push $oldrev $newrev $refname
- done
-fi
-```
-
-### Mirroring with Perforce Helix via Git Fusion **(STARTER)**
-
-CAUTION: **Warning:**
-Bidirectional mirroring should not be used as a permanent configuration. Refer to
-[Migrating from Perforce Helix](../user/project/import/perforce.md) for alternative migration approaches.
-
-[Git Fusion](https://www.perforce.com/manuals/git-fusion/#Git-Fusion/section_avy_hyc_gl.html) provides a Git interface
-to [Perforce Helix](https://www.perforce.com/products) which can be used by GitLab to bidirectionally
-mirror projects with GitLab. This may be useful in some situations when migrating from Perforce Helix
-to GitLab where overlapping Perforce Helix workspaces cannot be migrated simultaneously to GitLab.
-
-If using mirroring with Perforce Helix, you should only mirror protected branches. Perforce Helix
-will reject any pushes that rewrite history. Only the fewest number of branches should be mirrored
-due to the performance limitations of Git Fusion.
-
-When configuring mirroring with Perforce Helix via Git Fusion, the following Git Fusion
-settings are recommended:
-
-- `change-pusher` should be disabled. Otherwise, every commit will be rewritten as being committed
- by the mirroring account, rather than being mapped to existing Perforce Helix users or the `unknown_git` user.
-- `unknown_git` user will be used as the commit author if the GitLab user does not exist in
- Perforce Helix.
-
-Read about [Git Fusion settings on Perforce.com](https://www.perforce.com/manuals/git-fusion/Content/Git-Fusion/section_vss_bdw_w3.html#section_zdp_zz1_3l).
-
-## Troubleshooting
-
-Should an error occur during a push, GitLab will display an "Error" highlight for that repository. Details on the error can then be seen by hovering over the highlight text.
-
-### 13:Received RST_STREAM with error code 2 with GitHub
-
-If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](https://github.com/settings/emails) setting.
+This document was moved to [another location](../user/project/repository/repository_mirroring.md).
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 2ec733182f8..4b35c61ec5e 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -1,134 +1,5 @@
---
-type: reference
+redirect_to: '../user/shortcuts.md'
---
-# GitLab keyboard shortcuts
-
-GitLab has many useful keyboard shortcuts to make it easier to access different features.
-You can see the quick reference sheet within GitLab itself with <kbd>Shift</kbd> + <kbd>?</kbd>.
-
-The [Global Shortcuts](#global-shortcuts) work from any area of GitLab, but you must
-be in specific pages for the other shortcuts to be available, as explained in each
-section below.
-
-## Global Shortcuts
-
-These shortcuts are available in most areas of GitLab
-
-| Keyboard Shortcut | Description |
-| ------------------------------- | ----------- |
-| <kbd>?</kbd> | Show/hide shortcut reference sheet. |
-| <kbd>Shift</kbd> + <kbd>p</kbd> | Go to your Projects page. |
-| <kbd>Shift</kbd> + <kbd>g</kbd> | Go to your Groups page. |
-| <kbd>Shift</kbd> + <kbd>a</kbd> | Go to your Activity page. |
-| <kbd>Shift</kbd> + <kbd>l</kbd> | Go to your Milestones page. |
-| <kbd>Shift</kbd> + <kbd>s</kbd> | Go to your Snippets page. |
-| <kbd>s</kbd> | Put cursor in the issues/merge requests search. |
-| <kbd>Shift</kbd> + <kbd>i</kbd> | Go to your Issues page. |
-| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your Merge requests page.|
-| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
-| <kbd>p</kbd> + <kbd>b</kbd> | Show/hide the Performance Bar. |
-
-Additionally, the following shortcuts are available when editing text in text fields,
-for example comments, replies, or issue and merge request descriptions:
-
-| Keyboard Shortcut | Description |
-| ---------------------------------------------------------------------- | ----------- |
-| <kbd>↑</kbd> | Edit your last comment. You must be in a blank text field below a thread, and you must already have at least one comment in the thread. |
-| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>p</kbd> | Toggle Markdown preview, when editing text in a text field that has **Write** and **Preview** tabs at the top. |
-
-## Project
-
-These shortcuts are available from any page within a project. You must type them
-relatively quickly to work, and they will take you to another page in the project.
-
-| Keyboard Shortcut | Description |
-| --------------------------- | ----------- |
-| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project home page (**Project > Details**). |
-| <kbd>g</kbd> + <kbd>v</kbd> | Go to the project activity feed (**Project > Activity**). |
-| <kbd>g</kbd> + <kbd>r</kbd> | Go to the project releases list (**Project > Releases**). |
-| <kbd>g</kbd> + <kbd>f</kbd> | Go to the [project files](#project-files) list (**Repository > Files**). |
-| <kbd>t</kbd> | Go to the project file search page. (**Repository > Files**, click **Find Files**). |
-| <kbd>g</kbd> + <kbd>c</kbd> | Go to the project commits list (**Repository > Commits**). |
-| <kbd>g</kbd> + <kbd>n</kbd> | Go to the [repository graph](#repository-graph) page (**Repository > Graph**). |
-| <kbd>g</kbd> + <kbd>d</kbd> | Go to repository charts (**Repository > Charts**). |
-| <kbd>g</kbd> + <kbd>i</kbd> | Go to the project issues list (**Issues > List**). |
-| <kbd>i</kbd> | Go to the New Issue page (**Issues**, click **New Issue** ). |
-| <kbd>g</kbd> + <kbd>b</kbd> | Go to the project issue boards list (**Issues > Boards**). |
-| <kbd>g</kbd> + <kbd>m</kbd> | Go to the project merge requests list (**Merge Requests**). |
-| <kbd>g</kbd> + <kbd>j</kbd> | Go to the CI/CD jobs list (**CI/CD > Jobs**). |
-| <kbd>g</kbd> + <kbd>l</kbd> | Go to the project metrics (**Operations > Metrics**). |
-| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project environments (**Operations > Environments**). |
-| <kbd>g</kbd> + <kbd>k</kbd> | Go to the project Kubernetes cluster integration page (**Operations > Kubernetes**). Note that you must have at least [`maintainer` permissions](../user/permissions.md) to access this page. |
-| <kbd>g</kbd> + <kbd>s</kbd> | Go to the project snippets list (**Snippets**). |
-| <kbd>g</kbd> + <kbd>w</kbd> | Go to the project wiki (**Wiki**), if enabled. |
-
-### Issues and Merge Requests
-
-These shortcuts are available when viewing issues and merge requests.
-
-| Keyboard Shortcut | Description |
-| ---------------------------- | ----------- |
-| <kbd>e</kbd> | Edit description. |
-| <kbd>a</kbd> | Change assignee. |
-| <kbd>m</kbd> | Change milestone. |
-| <kbd>l</kbd> | Change label. |
-| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
-| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). |
-| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). |
-| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). |
-| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). |
-
-### Project Files
-
-These shortcuts are available when browsing the files in a project (navigate to
-**Repository** > **Files**):
-
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>↑</kbd> | Move selection up. |
-| <kbd>↓</kbd> | Move selection down. |
-| <kbd>enter</kbd> | Open selection. |
-| <kbd>esc</kbd> | Go back to file list screen (only while searching for files, **Repository > Files** then click on **Find File**). |
-| <kbd>y</kbd> | Go to file permalink (only while viewing a file). |
-
-### Web IDE
-
-These shortcuts are available when editing a file with the [Web IDE](../user/project/web_ide/index.md):
-
-| Keyboard Shortcut | Description |
-| ------------------------------------------------------- | ----------- |
-| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>p</kbd> | Search for, and then open another file for editing. |
-| <kbd>⌘</kbd> (Mac) / <kbd>Ctrl</kbd> + <kbd>Enter</kbd> | Commit (when editing the commit message). |
-
-### Repository Graph
-
-These shortcuts are available when viewing the project [repository graph](../user/project/repository/index.md#repository-graph)
-page (navigate to **Repository > Graph**):
-
-| Keyboard Shortcut | Description |
-| ------------------------------------------------------------------ | ----------- |
-| <kbd>â†</kbd> or <kbd>h</kbd> | Scroll left. |
-| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right. |
-| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up. |
-| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down. |
-| <kbd>Shift</kbd> + <kbd>↑</kbd> or <kbd>Shift</kbd> + <kbd>k</kbd> | Scroll to top. |
-| <kbd>Shift</kbd> + <kbd>↓</kbd> or <kbd>Shift</kbd> + <kbd>j</kbd> | Scroll to bottom. |
-
-### Wiki pages
-
-This shortcut is available when viewing a [wiki page](../user/project/wiki/index.md):
-
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>e</kbd> | Edit wiki page. |
-
-## Epics **(ULTIMATE)**
-
-These shortcuts are available when viewing [Epics](../user/group/epics/index.md):
-
-| Keyboard Shortcut | Description |
-| ----------------- | ----------- |
-| <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. |
-| <kbd>e</kbd> | Edit description. |
-| <kbd>l</kbd> | Change label. |
+This document was moved to [another location](../user/shortcuts.md).
diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md
index 3d2e1de24da..e109410e22d 100644
--- a/doc/workflow/time_tracking.md
+++ b/doc/workflow/time_tracking.md
@@ -1,91 +1,5 @@
---
-type: reference
+redirect_to: '../user/project/time_tracking.md'
---
-# Time Tracking
-
-> Introduced in GitLab 8.14.
-
-Time Tracking allows you to track estimates and time spent on issues and merge
-requests within GitLab.
-
-## Overview
-
-Time Tracking allows you to:
-
-- Record the time spent working on an issue or a merge request.
-- Add an estimate of the amount of time needed to complete an issue or a merge
- request.
-
-You don't have to indicate an estimate to enter the time spent, and vice versa.
-
-Data about time tracking is shown on the issue/merge request sidebar, as shown
-below.
-
-![Time tracking in the sidebar](time_tracking/img/time_tracking_sidebar_v8_16.png)
-
-## How to enter data
-
-Time Tracking uses two [quick actions](../user/project/quick_actions.md)
-that GitLab introduced with this new feature: `/spend` and `/estimate`.
-
-Quick actions can be used in the body of an issue or a merge request, but also
-in a comment in both an issue or a merge request.
-
-Below is an example of how you can use those new quick actions inside a comment.
-
-![Time tracking example in a comment](time_tracking/img/time_tracking_example_v12_2.png)
-
-Adding time entries (time spent or estimates) is limited to project members.
-
-### Estimates
-
-To enter an estimate, write `/estimate`, followed by the time. For example, if
-you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write
-`/estimate 3d 5h 10m`. Time units that we support are listed at the bottom of
-this help page.
-
-Every time you enter a new time estimate, any previous time estimates will be
-overridden by this new value. There should only be one valid estimate in an
-issue or a merge request.
-
-To remove an estimation entirely, use `/remove_estimate`.
-
-### Time spent
-
-To enter a time spent, use `/spend 3d 5h 10m`.
-
-Every new time spent entry will be added to the current total time spent for the
-issue or the merge request.
-
-You can remove time by entering a negative amount: `/spend -3d` will remove 3
-days from the total time spent. You can't go below 0 minutes of time spent,
-so GitLab will automatically reset the time spent if you remove a larger amount
-of time compared to the time that was entered already.
-
-To remove all the time spent at once, use `/remove_time_spent`.
-
-## Configuration
-
-The following time units are available:
-
-- Months (mo)
-- Weeks (w)
-- Days (d)
-- Hours (h)
-- Minutes (m)
-
-Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h.
-
-### Limit displayed units to hours **(CORE ONLY)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/29469/) in GitLab 12.1.
-
-In GitLab self-managed instances, the display of time units can be limited to
-hours through the option in **Admin Area > Settings > Preferences** under **Localization**.
-
-With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
-
-## Other interesting links
-
-- [Time Tracking landing page in the GitLab handbook](https://about.gitlab.com/solutions/time-tracking/)
+This document was moved to [another location](../user/project/time_tracking.md).
diff --git a/doc/workflow/timezone.md b/doc/workflow/timezone.md
index 3594ba19181..f1a2e1af66a 100644
--- a/doc/workflow/timezone.md
+++ b/doc/workflow/timezone.md
@@ -1,37 +1,5 @@
-# Changing your time zone
+---
+redirect_to: '../administration/timezone.md'
+---
-The global time zone configuration parameter can be changed in `config/gitlab.yml`:
-
-```text
-# time_zone: 'UTC'
-```
-
-Uncomment and customize if you want to change the default time zone of the GitLab application.
-
-## Viewing available timezones
-
-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).
-
-## Changing time zone in Omnibus installations
-
-GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`.
-
-To obtain a list of timezones, log in to your GitLab application server and run a command that generates a list of timezones in TZInfo format for the server. For example, install `timedatectl` and run `timedatectl list-timezones`.
-
-To update, add the timezone that best applies to your location. For example:
-
-```ruby
-gitlab_rails['time_zone'] = 'America/New_York'
-```
-
-After adding the configuration parameter, reconfigure and restart your GitLab instance:
-
-```sh
-gitlab-ctl reconfigure
-gitlab-ctl restart
-```
+This document was moved to [another location](../administration/timezone.md).
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 5d576d8ff35..48c9a3faf1d 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -1,138 +1,5 @@
-# GitLab To-Do List
+---
+redirect_to: '../user/todos.md'
+---
-> [Introduced][ce-2817] in GitLab 8.5.
-
-When you log into GitLab, you normally want to see where you should spend your
-time, take some action, or know what you need to keep an eye on without
-a huge pile of e-mail notifications. GitLab is where you do your work,
-so being able to get started quickly is important.
-
-Your To-Do List offers a chronological list of items that are waiting for your input, all
-in a simple dashboard.
-
-![To Do screenshot showing a list of items to check on](img/todos_index.png)
-
-You can quickly access your To-Do List by clicking the checkmark icon next to the
-search bar in the top navigation. If the count is:
-
-- Less than 100, the number in blue is the number of To-Do items.
-- 100 or more, the number displays as 99+. The exact number displays
- on the To-Do List.
-you still have open. Otherwise, the number displays as 99+. The exact number
-displays on the To-Do List.
-
-![To Do icon](img/todos_icon.png)
-
-## What triggers a To Do
-
-A To Do displays on your To-Do List when:
-
-- An issue or merge request is assigned to you
-- You are `@mentioned` in the description or comment of an:
- - Issue
- - Merge Request
- - Epic **(ULTIMATE)**
-- You are `@mentioned` in a comment on a commit
-- 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:
- - The author
- - Have set it to automatically merge once the pipeline succeeds
-
-To-do triggers are not affected by [GitLab Notification Email settings](notifications.md).
-
-NOTE: **Note:**
-When a user no longer has access to a resource related to a To Do (like an issue, merge request, project, or group) the related To-Do items are deleted within the next hour for security reasons. The delete is delayed to prevent data loss, in case the user's access was revoked by mistake.
-
-### Directly addressing a To Do
-
-> [Introduced][ce-7926] in GitLab 9.0.
-
-If you are mentioned at the start of a line, the To Do you receive will be listed
-as 'directly addressed'. For example, in this comment:
-
-```markdown
-@alice What do you think? cc: @bob
-
-- @carol can you please have a look?
-
->>>
-@dan what do you think?
->>>
-
-@erin @frank thank you!
-```
-
-The people receiving directly addressed To-Do items are `@alice`, `@erin`, and
-`@frank`. Directly addressed To-Do items only differ from mentions in their type
-for filtering purposes; otherwise, they appear as normal.
-
-### Manually creating a To Do
-
-You can also add the following to your To-Do List by clicking the **Add a To Do** button on an:
-
-- Issue
-- Merge Request
-- Epic **(ULTIMATE)**
-
-![Adding a To Do from the issuable sidebar](img/todos_add_todo_sidebar.png)
-
-## Marking a To Do as done
-
-Any action to the following will mark the corresponding To Do as done:
-
-- Issue
-- Merge Request
-- Epic **(ULTIMATE)**
-
-Actions that dismiss To-Do items include:
-
-- Changing the assignee
-- Changing the milestone
-- Adding/removing a label
-- Commenting on the issue
-
-Your To-Do List is personal, and items are only marked as done if the action comes from
-you. If you close the issue or merge request, your To Do is automatically
-marked as done.
-
-To prevent other users from closing issues without you being notified, if someone else closes, merges, or takes action on the any of the following, your To Do will remain pending:
-
-- Issue
-- Merge request
-- Epic **(ULTIMATE)**
-
-There is just one To Do for each of these, so mentioning a user a hundred times in an issue will only trigger one To Do.
-
-If no action is needed, you can manually mark the To Do as done by clicking the
-corresponding **Done** button, and it will disappear from your To-Do List.
-
-![A To Do in the To-Do List](img/todo_list_item.png)
-
-You can also mark a To Do as done by clicking the **Mark as done** button in the sidebar of the following:
-
-- Issue
-- Merge Request
-- Epic **(ULTIMATE)**
-
-![Mark as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
-
-You can mark all your To-Do items as done at once by clicking the **Mark all as
-done** button.
-
-## Filtering your To-Do List
-
-There are four kinds of filters you can use on your To-Do List.
-
-| Filter | Description |
-| ------- | ----------- |
-| 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)** |
-| 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 `Any Action`, `Assigned`, `Mentioned`, `Added`, `Pipelines`, and `Directly Addressed`, [as described above](#what-triggers-a-to-do).
-
-[ce-2817]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/2817
-[ce-7926]: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/7926
+This document was moved to [another location](../user/todos.md).
diff --git a/doc/workflow/workflow.md b/doc/workflow/workflow.md
index 7fac41c3b6f..c77d95cd326 100644
--- a/doc/workflow/workflow.md
+++ b/doc/workflow/workflow.md
@@ -1,31 +1,5 @@
-# Feature branch workflow
+---
+redirect_to: '../gitlab-basics/feature_branch_workflow.md'
+---
-1. Clone project:
-
- ```bash
- git clone git@example.com:project-name.git
- ```
-
-1. Create branch with your feature:
-
- ```bash
- git checkout -b $feature_name
- ```
-
-1. Write code. Commit changes:
-
- ```bash
- git commit -am "My feature is ready"
- ```
-
-1. Push your branch to GitLab:
-
- ```bash
- git push origin $feature_name
- ```
-
-1. Review your code on commits page.
-
-1. Create a merge request.
-
-1. Your team lead will review the code &amp; merge it to the main branch.
+This document was moved to [another location](../gitlab-basics/feature_branch_workflow.md).
diff --git a/jest.config.js b/jest.config.js
index c2a512e8afa..3f9dc3fe213 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -28,32 +28,39 @@ 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)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
+ 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
+ '^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
+};
+
+if (IS_EE) {
+ const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1';
+ Object.assign(moduleNameMapper, {
+ '^ee(/.*)$': rootDirEE,
+ '^ee_component(/.*)$': rootDirEE,
+ '^ee_else_ce(/.*)$': rootDirEE,
+ });
+}
+
// eslint-disable-next-line import/no-commonjs
module.exports = {
testMatch,
moduleFileExtensions: ['js', 'json', 'vue'],
- moduleNameMapper: {
- '^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
- '^ee(/.*)$': '<rootDir>/ee/app/assets/javascripts$1',
- '^ee_component(/.*)$': IS_EE
- ? '<rootDir>/ee/app/assets/javascripts$1'
- : '<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
- '^ee_else_ce(/.*)$': IS_EE
- ? '<rootDir>/ee/app/assets/javascripts$1'
- : '<rootDir>/app/assets/javascripts$1',
- '^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
- '^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
- '\\.(jpg|jpeg|png|svg)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
- 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
- '^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
- },
+ moduleNameMapper,
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
cacheDirectory: '<rootDir>/tmp/cache/jest',
modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
reporters,
- setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js'],
+ setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js', 'jest-canvas-mock'],
restoreMocks: true,
transform: {
'^.+\\.(gql|graphql)$': 'jest-transform-graphql',
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d71f0c38ce6..a2bdb76b834 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -21,6 +21,7 @@ module API
Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
+ Gitlab::GrapeLogging::Loggers::ExceptionLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
Gitlab::GrapeLogging::Loggers::PerfLogger.new,
Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
@@ -112,6 +113,7 @@ module API
mount ::API::Files
mount ::API::GroupBoards
mount ::API::GroupClusters
+ mount ::API::GroupExport
mount ::API::GroupLabels
mount ::API::GroupMilestones
mount ::API::Groups
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index f8f79ab6f5a..054242dca4c 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -32,7 +32,7 @@ module API
use :filter_params
end
get ':id/repository/branches' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42329')
+ user_project.preload_protected_branches
repository = user_project.repository
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index ffff40141de..63a7fdfa3ab 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -169,7 +169,7 @@ module API
not_found! 'Commit' unless commit
- raw_diffs = ::Kaminari.paginate_array(commit.raw_diffs.to_a)
+ raw_diffs = ::Kaminari.paginate_array(commit.diffs(expanded: true).diffs.to_a)
present paginate(raw_diffs), with: Entities::Diff
end
@@ -223,7 +223,7 @@ module API
present user_project.repository.commit(result[:result]),
with: Entities::Commit
else
- render_api_error!(result[:message], 400)
+ error!(result.slice(:message, :error_code), 400, header)
end
end
@@ -257,7 +257,7 @@ module API
present user_project.repository.commit(result[:result]),
with: Entities::Commit
else
- render_api_error!(result[:message], 400)
+ error!(result.slice(:message, :error_code), 400, header)
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index da882547071..f97200f20b9 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -17,7 +17,7 @@ module API
end
params do
use :pagination
- optional :order_by, type: String, values: %w[id iid created_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `ref`'
+ optional :order_by, type: String, values: %w[id iid created_at updated_at ref], default: 'id', desc: 'Return deployments ordered by `id` or `iid` or `created_at` or `updated_at` or `ref`'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 91811efacd7..9617f1a8acf 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -307,6 +307,7 @@ module API
expose :only_allow_merge_if_pipeline_succeeds
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
+ expose :remove_source_branch_after_merge
expose :printing_merge_request_link_enabled
expose :merge_method
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
@@ -488,11 +489,11 @@ module API
end
expose :developers_can_push do |repo_branch, options|
- options[:project].protected_branches.developers_can?(:push, repo_branch.name)
+ ::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :developers_can_merge do |repo_branch, options|
- options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
+ ::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
end
expose :can_push do |repo_branch, options|
@@ -754,6 +755,7 @@ module API
end
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
+ expose :squash_commit_sha
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
@@ -776,6 +778,10 @@ module API
expose :squash
expose :task_completion_status
+
+ expose :cannot_be_merged?, as: :has_conflicts
+
+ expose :mergeable_discussions_state?, as: :blocking_discussions_resolved
end
class MergeRequest < MergeRequestBasic
@@ -1248,6 +1254,7 @@ module API
# let's not expose the secret key in a response
attributes.delete(:asset_proxy_secret_key)
+ attributes.delete(:eks_secret_access_key)
attributes
end
@@ -1290,7 +1297,11 @@ module API
end
class Release < Grape::Entity
- expose :name
+ include ::API::Helpers::Presentable
+
+ expose :name do |release, _|
+ can_download_code? ? release.name : "Release-#{release.id}"
+ end
expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
expose :description
expose :description_html do |entity|
@@ -1302,8 +1313,8 @@ module API
expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
expose :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
- expose :commit_path, if: ->(_, _) { can_download_code? }
- expose :tag_path, if: ->(_, _) { can_download_code? }
+ expose :commit_path, expose_nil: false
+ expose :tag_path, expose_nil: false
expose :assets do
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
@@ -1315,8 +1326,9 @@ module API
end
end
expose :_links do
- expose :merge_requests_url, if: -> (_) { release_mr_issue_urls_available? }
- expose :issues_url, if: -> (_) { release_mr_issue_urls_available? }
+ expose :merge_requests_url, expose_nil: false
+ expose :issues_url, expose_nil: false
+ expose :edit_url, expose_nil: false
end
private
@@ -1324,36 +1336,6 @@ module API
def can_download_code?
Ability.allowed?(options[:current_user], :download_code, object.project)
end
-
- def commit_path
- return unless object.commit
-
- Gitlab::Routing.url_helpers.project_commit_path(project, object.commit.id)
- end
-
- def tag_path
- Gitlab::Routing.url_helpers.project_tag_path(project, object.tag)
- end
-
- def merge_requests_url
- Gitlab::Routing.url_helpers.project_merge_requests_url(project, params_for_issues_and_mrs)
- end
-
- def issues_url
- Gitlab::Routing.url_helpers.project_issues_url(project, params_for_issues_and_mrs)
- end
-
- def params_for_issues_and_mrs
- { scope: 'all', state: 'opened', release_tag: object.tag }
- end
-
- def release_mr_issue_urls_available?
- ::Feature.enabled?(:release_mr_issue_urls, project)
- end
-
- def project
- @project ||= object.project
- end
end
class Tag < Grape::Entity
@@ -1699,6 +1681,7 @@ module API
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
+ expose :auto_ssl_enabled
expose :certificate,
as: :certificate_expiration,
@@ -1714,6 +1697,7 @@ module API
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
+ expose :auto_ssl_enabled
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
@@ -1737,7 +1721,12 @@ module API
class Blob < Grape::Entity
expose :basename
expose :data
- expose :filename
+ expose :path
+ # TODO: :filename was renamed to :path but both still return the full path,
+ # in the future we can only return the filename here without the leading
+ # directory path.
+ # https://gitlab.com/gitlab-org/gitlab/issues/34521
+ expose :filename, &:path
expose :id
expose :ref
expose :startline
@@ -1813,6 +1802,7 @@ module API
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
expose :provider_gcp, using: Entities::Provider::Gcp
+ expose :management_project, using: Entities::ProjectIdentity
end
class ClusterProject < Cluster
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index a70ac63cc6e..abfe10b7fa1 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -84,6 +84,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb
index fd24662cc9a..7f95b411b36 100644
--- a/lib/api/group_container_repositories.rb
+++ b/lib/api/group_container_repositories.rb
@@ -23,9 +23,11 @@ module API
end
get ':id/registry/repositories' do
repositories = ContainerRepositoriesFinder.new(
- id: user_group.id, container_type: :group
+ user: current_user, subject: user_group
).execute
+ track_event('list_repositories')
+
present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags]
end
end
diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb
new file mode 100644
index 00000000000..8025a16e191
--- /dev/null
+++ b/lib/api/group_export.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module API
+ class GroupExport < Grape::API
+ before do
+ authorize! :admin_group, user_group
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: { id: %r{[^/]+} } do
+ desc 'Download export' do
+ detail 'This feature was introduced in GitLab 12.5.'
+ end
+ get ':id/export/download' do
+ if user_group.export_file_exists?
+ present_carrierwave_file!(user_group.export_file)
+ else
+ render_api_error!('404 Not found or has expired', 404)
+ end
+ end
+
+ desc 'Start export' do
+ detail 'This feature was introduced in GitLab 12.5.'
+ end
+ post ':id/export' do
+ GroupExportWorker.perform_async(current_user.id, user_group.id, params)
+
+ accepted!
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 19c29847ce3..49b86489a8b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -9,6 +9,7 @@ module API
GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret"
SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user'
+ API_EXCEPTION_ENV = 'gitlab.api.exception'
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
@@ -387,6 +388,9 @@ module API
Gitlab::Sentry.track_acceptable_exception(exception, extra: params)
end
+ # This is used with GrapeLogging::Loggers::ExceptionLogger
+ env[API_EXCEPTION_ENV] = exception
+
# lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
trace = exception.backtrace
@@ -451,6 +455,17 @@ module API
end
end
+ def track_event(action = action_name, **args)
+ category = args.delete(:category) || self.options[:for].name
+ raise "invalid category" unless category
+
+ ::Gitlab::Tracking.event(category, action.to_s, **args)
+ rescue => error
+ Rails.logger.warn( # rubocop:disable Gitlab/RailsLogger
+ "Tracking event failed for action: #{action}, category: #{category}, message: #{error.message}"
+ )
+ end
+
protected
def project_finder_params_ce
@@ -464,6 +479,8 @@ module API
finder_params[:user] = params.delete(:user) if params[:user]
finder_params[:custom_attributes] = params[:custom_attributes] if params[:custom_attributes]
finder_params[:min_access_level] = params[:min_access_level] if params[:min_access_level]
+ finder_params[:id_after] = params[:id_after] if params[:id_after]
+ finder_params[:id_before] = params[:id_before] if params[:id_before]
finder_params
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4c575381d30..dfac777e4a1 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -140,7 +140,8 @@ module API
{
repository: repository.gitaly_repository,
address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
+ token: Gitlab::GitalyClient.token(project.repository_storage),
+ features: Feature::Gitaly.server_feature_flags
}
end
end
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 71bbc218f94..9c5b355e823 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -4,254 +4,7 @@ module API
module Helpers
module Pagination
def paginate(relation)
- strategy = if params[:pagination] == 'keyset' && Feature.enabled?('api_keyset_pagination')
- KeysetPaginationStrategy
- else
- DefaultPaginationStrategy
- end
-
- strategy.new(self).paginate(relation)
- end
-
- class Base
- private
-
- def per_page
- @per_page ||= params[:per_page]
- end
-
- def base_request_uri
- @base_request_uri ||= URI.parse(request.url).tap do |uri|
- uri.host = Gitlab.config.gitlab.host
- uri.port = Gitlab.config.gitlab.port
- end
- end
-
- def build_page_url(query_params:)
- base_request_uri.tap do |uri|
- uri.query = query_params
- end.to_s
- end
-
- def page_href(next_page_params = {})
- query_params = params.merge(**next_page_params, per_page: per_page).to_query
-
- build_page_url(query_params: query_params)
- end
- end
-
- class KeysetPaginationInfo
- attr_reader :relation, :request_context
-
- def initialize(relation, request_context)
- # This is because it's rather complex to support multiple values with possibly different sort directions
- # (and we don't need this in the API)
- if relation.order_values.size > 1
- raise "Pagination only supports ordering by a single column." \
- "The following columns were given: #{relation.order_values.map { |v| v.expr.name }}"
- end
-
- @relation = relation
- @request_context = request_context
- end
-
- def fields
- keys.zip(values).reject { |_, v| v.nil? }.to_h
- end
-
- def column_for_order_by(relation)
- relation.order_values.first&.expr&.name
- end
-
- # Sort direction (`:asc` or `:desc`)
- def sort
- @sort ||= if order_by_primary_key?
- # Default order is by id DESC
- :desc
- else
- # API defaults to DESC order if param `sort` not present
- request_context.params[:sort]&.to_sym || :desc
- end
- end
-
- # Do we only sort by primary key?
- def order_by_primary_key?
- keys.size == 1 && keys.first == primary_key
- end
-
- def primary_key
- relation.model.primary_key.to_sym
- end
-
- def sort_ascending?
- sort == :asc
- end
-
- # Build hash of request parameters for a given record (relevant to pagination)
- def params_for(record)
- return {} unless record
-
- keys.each_with_object({}) do |key, h|
- h["ks_prev_#{key}".to_sym] = record.attributes[key.to_s]
- end
- end
-
- private
-
- # All values present in request parameters that correspond to #keys.
- def values
- @values ||= keys.map do |key|
- request_context.params["ks_prev_#{key}".to_sym]
- end
- end
-
- # All keys relevant to pagination.
- # This always includes the primary key. Optionally, the `order_by` key is prepended.
- def keys
- @keys ||= [column_for_order_by(relation), primary_key].compact.uniq
- end
- end
-
- class KeysetPaginationStrategy < Base
- attr_reader :request_context
- delegate :params, :header, :request, to: :request_context
-
- def initialize(request_context)
- @request_context = request_context
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def paginate(relation)
- pagination = KeysetPaginationInfo.new(relation, request_context)
-
- paged_relation = relation.limit(per_page)
-
- if conds = conditions(pagination)
- paged_relation = paged_relation.where(*conds)
- end
-
- # In all cases: sort by primary key (possibly in addition to another sort column)
- paged_relation = paged_relation.order(pagination.primary_key => pagination.sort)
-
- add_default_pagination_headers
-
- if last_record = paged_relation.last
- next_page_params = pagination.params_for(last_record)
- add_navigation_links(next_page_params)
- end
-
- paged_relation
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def conditions(pagination)
- fields = pagination.fields
-
- return if fields.empty?
-
- placeholder = fields.map { '?' }
-
- comp = if pagination.sort_ascending?
- '>'
- else
- '<'
- end
-
- [
- # Row value comparison:
- # (A, B) < (a, b) <=> (A < a) OR (A = a AND B < b)
- # <=> A <= a AND ((A < a) OR (A = a AND B < b))
- "(#{fields.keys.join(',')}) #{comp} (#{placeholder.join(',')})",
- *fields.values
- ]
- end
-
- def add_default_pagination_headers
- header 'X-Per-Page', per_page.to_s
- end
-
- def add_navigation_links(next_page_params)
- header 'X-Next-Page', page_href(next_page_params)
- header 'Link', link_for('next', next_page_params)
- end
-
- def link_for(rel, next_page_params)
- %(<#{page_href(next_page_params)}>; rel="#{rel}")
- end
- end
-
- class DefaultPaginationStrategy < Base
- attr_reader :request_context
- delegate :params, :header, :request, to: :request_context
-
- def initialize(request_context)
- @request_context = request_context
- end
-
- def paginate(relation)
- paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
- add_pagination_headers(data)
- end
- end
-
- private
-
- def paginate_with_limit_optimization(relation)
- pagination_data = relation.page(params[:page]).per(params[:per_page])
- return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
- return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
-
- limited_total_count = pagination_data.total_count_with_limit
- if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
- # The call to `total_count_with_limit` memoizes `@arel` because of a call to `references_eager_loaded_tables?`
- # We need to call `reset` because `without_count` relies on `@arel` being unmemoized
- pagination_data.reset.without_count
- else
- pagination_data
- end
- end
-
- def add_default_order(relation)
- if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
- relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord
- end
-
- relation
- end
-
- def add_pagination_headers(paginated_data)
- header 'X-Per-Page', paginated_data.limit_value.to_s
- header 'X-Page', paginated_data.current_page.to_s
- header 'X-Next-Page', paginated_data.next_page.to_s
- header 'X-Prev-Page', paginated_data.prev_page.to_s
- header 'Link', pagination_links(paginated_data)
-
- return if data_without_counts?(paginated_data)
-
- header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', total_pages(paginated_data).to_s
- end
-
- def pagination_links(paginated_data)
- [].tap do |links|
- links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page
- links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page
- links << %(<#{page_href(page: 1)}>; rel="first")
-
- links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data)
- end.join(', ')
- end
-
- def total_pages(paginated_data)
- # Ensure there is in total at least 1 page
- [paginated_data.total_pages, 1].max
- end
-
- def data_without_counts?(paginated_data)
- paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
- end
+ ::Gitlab::Pagination::OffsetPagination.new(self).paginate(relation)
end
end
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 94619204274..47b1f037eb8 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -30,6 +30,7 @@ module API
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'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
@@ -94,6 +95,7 @@ module API
:path,
:printing_merge_request_link_enabled,
:public_builds,
+ :remove_source_branch_after_merge,
:repository_access_level,
:request_access_enabled,
:resolve_outdated_diff_discussions,
@@ -109,7 +111,6 @@ module API
:jobs_enabled,
:merge_requests_enabled,
:wiki_enabled,
- :jobs_enabled,
:snippets_enabled
]
end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index d9a22484c1f..c70f2f3e2c8 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -77,7 +77,7 @@ module API
response_with_status(**payload)
when ::Gitlab::GitAccessResult::CustomAction
- response_with_status(code: 300, message: check_result.message, payload: check_result.payload)
+ response_with_status(code: 300, payload: check_result.payload, gl_console_messages: check_result.console_messages)
else
response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR)
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 1436238c5cf..6e10414def4 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -296,9 +296,12 @@ module API
end
get ':id/merge_requests/:merge_request_iid/commits' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- commits = ::Kaminari.paginate_array(merge_request.commits)
- present paginate(commits), with: Entities::Commit
+ commits =
+ paginate(merge_request.merge_request_diff.merge_request_diff_commits)
+ .map { |commit| Commit.from_hash(commit.to_hash, merge_request.project) }
+
+ present commits, with: Entities::Commit
end
desc 'Show the merge request changes' do
@@ -404,7 +407,8 @@ module API
merge_params = HashWithIndifferentAccess.new(
commit_message: params[:merge_commit_message],
squash_commit_message: params[:squash_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
+ should_remove_source_branch: params[:should_remove_source_branch],
+ sha: params[:sha] || merge_request.diff_head_sha
)
if merge_when_pipeline_succeeds && merge_request.head_pipeline && merge_request.head_pipeline.active?
@@ -455,6 +459,8 @@ module API
status :accepted
present rebase_in_progress: merge_request.rebase_in_progress?
+ rescue ::MergeRequest::RebaseLockTimeout => e
+ render_api_error!(e.message, 409)
end
desc 'List issues that will be closed on merge' do
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index ec2fe8270b7..2d02a4e624c 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -92,8 +92,10 @@ module API
requires :domain, type: String, desc: 'The domain'
# rubocop:disable Scalability/FileUploads
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
- optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
+ optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false,
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
all_or_none_of :user_provided_certificate, :user_provided_key
end
@@ -116,14 +118,16 @@ module API
requires :domain, type: String, desc: 'The domain'
# rubocop:disable Scalability/FileUploads
# TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960
- optional :certificate, allow_blank: false, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
- optional :key, allow_blank: false, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
+ optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
+ optional :auto_ssl_enabled, allow_blank: true, type: Boolean,
+ desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
end
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do
authorize! :update_pages, user_project
- pages_domain_params = declared(params, include_parent_namespaces: false)
+ pages_domain_params = declared(params, include_parent_namespaces: false, include_missing: false)
# Remove empty private key if certificate is not empty.
if pages_domain_params[:user_provided_certificate] && !pages_domain_params[:user_provided_key]
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index 45c800d7d1e..8e35914f48a 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -88,6 +88,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 2a05974509a..2b33069e324 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -24,9 +24,11 @@ module API
end
get ':id/registry/repositories' do
repositories = ContainerRepositoriesFinder.new(
- id: user_project.id, container_type: :project
+ user: current_user, subject: user_project
).execute
+ track_event( 'list_repositories')
+
present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags]
end
@@ -40,6 +42,7 @@ module API
authorize_admin_container_image!
DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id)
+ track_event('delete_repository')
status :accepted
end
@@ -56,6 +59,8 @@ module API
authorize_read_container_image!
tags = Kaminari.paginate_array(repository.tags)
+ track_event('list_tags')
+
present paginate(tags), with: Entities::ContainerRegistry::Tag
end
@@ -77,6 +82,8 @@ module API
CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id,
declared_params.except(:repository_id))
+ track_event('delete_tag_bulk')
+
status :accepted
end
@@ -111,6 +118,8 @@ module API
.execute(repository)
if result[:status] == :success
+ track_event('delete_tag')
+
status :ok
else
status :bad_request
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d2dacafe7f9..669def2b63c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -61,6 +61,8 @@ module API
optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
optional :with_programming_language, type: String, desc: 'Limit to repositories which use the given programming language'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user'
+ optional :id_after, type: Integer, desc: 'Limit results to projects with IDs greater than the specified ID'
+ optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID'
use :optional_filter_params_ee
end
@@ -69,7 +71,8 @@ module API
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported'
optional :template_name, type: String, desc: "Name of template from which to create project"
- mutually_exclusive :import_url, :template_name
+ optional :template_project_id, type: Integer, desc: "Project ID of template from which to create project"
+ mutually_exclusive :import_url, :template_name, :template_project_id
end
def load_projects
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index 4238529142c..3f600ef4a04 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -45,7 +45,7 @@ module API
end
params do
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
- requires :name, type: String, desc: 'The name of the release'
+ optional :name, type: String, desc: 'The name of the release'
requires :description, type: String, desc: 'The release notes'
optional :ref, type: String, desc: 'The commit sha or branch name'
optional :assets, type: Hash do
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c90ba0c9b5d..5362b3060c1 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -42,6 +42,7 @@ module API
optional :asset_proxy_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Assets that match these domain(s) will NOT be proxied. Wildcards allowed. Your GitLab installation URL is automatically whitelisted.'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
+ optional :default_ci_config_path, type: String, desc: 'The instance default CI configuration path for new projects'
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
@@ -52,6 +53,12 @@ module API
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: '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'
optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
+ optional :eks_integration_enabled, type: Boolean, desc: 'Enable integration with Amazon EKS'
+ given eks_integration_enabled: -> (val) { val } do
+ requires :eks_account_id, type: String, desc: 'Amazon account ID for EKS integration'
+ requires :eks_access_key_id, type: String, desc: 'Access key ID for the EKS integration IAM user'
+ requires :eks_secret_access_key, type: String, desc: 'Secret access key for the EKS integration IAM user'
+ end
optional :email_author_in_body, type: Boolean, desc: '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.'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
optional :gitaly_timeout_default, type: Integer, desc: 'Default Gitaly timeout, in seconds. Set to 0 to disable timeouts.'
@@ -129,16 +136,22 @@ module API
optional :sign_in_text, type: String, desc: 'The sign in text of the GitLab application'
optional :signin_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
optional :signup_enabled, type: Boolean, desc: 'Flag indicating if sign up is enabled'
+ optional :sourcegraph_enabled, type: Boolean, desc: 'Enable Sourcegraph'
+ optional :sourcegraph_public_only, type: Boolean, desc: 'Only allow public projects to communicate with Sourcegraph'
+ given sourcegraph_enabled: ->(val) { val } do
+ requires :sourcegraph_url, type: String, desc: 'The configured Sourcegraph instance URL'
+ end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.'
optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins'
optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated'
optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5
optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking'
+ optional :snowplow_iglu_registry_url, type: String, desc: 'The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'
given snowplow_enabled: ->(val) { val } do
requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname'
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
- optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic'
+ optional :snowplow_app_id, type: String, desc: 'The Snowplow site name / application id'
end
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index daa9598a204..693c20cb73a 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -36,7 +36,8 @@ module API
{
processed: stats.processed,
failed: stats.failed,
- enqueued: stats.enqueued
+ enqueued: stats.enqueued,
+ dead: stats.dead_size
}
end
end
diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb
new file mode 100644
index 00000000000..321580b532f
--- /dev/null
+++ b/lib/banzai/filter/inline_grafana_metrics_filter.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a placeholder element for each
+ # reference to a grafana dashboard.
+ class InlineGrafanaMetricsFilter < Banzai::Filter::InlineEmbedsFilter
+ # Placeholder element for the frontend to use as an
+ # injection point for charts.
+ def create_element(params)
+ begin_loading_dashboard(params[:url])
+
+ doc.document.create_element(
+ 'div',
+ class: 'js-render-metrics',
+ 'data-dashboard-url': metrics_dashboard_url(params)
+ )
+ end
+
+ def embed_params(node)
+ query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href'])
+ return unless [:panelId, :from, :to].all? do |param|
+ query_params.include?(param)
+ end
+
+ { url: node['href'], start: query_params[:from], end: query_params[:to] }
+ end
+
+ # Selects any links with an href contains the configured
+ # grafana domain for the project
+ def xpath_search
+ return unless grafana_url.present?
+
+ %(descendant-or-self::a[starts-with(@href, '#{grafana_url}')])
+ end
+
+ private
+
+ def project
+ context[:project]
+ end
+
+ def grafana_url
+ project&.grafana_integration&.grafana_url
+ end
+
+ def metrics_dashboard_url(params)
+ Gitlab::Routing.url_helpers.project_grafana_api_metrics_dashboard_url(
+ project,
+ embedded: true,
+ grafana_url: params[:url],
+ start: format_time(params[:start]),
+ end: format_time(params[:end])
+ )
+ end
+
+ # Formats a timestamp from Grafana for compatibility with
+ # parsing in JS via `new Date(timestamp)`
+ #
+ # @param time [String] Represents miliseconds since epoch
+ def format_time(time)
+ Time.at(time.to_i / 1000).utc.strftime('%FT%TZ')
+ end
+
+ # Fetches a dashboard and caches the result for the
+ # FE to fetch quickly while rendering charts
+ def begin_loading_dashboard(url)
+ ::Gitlab::Metrics::Dashboard::Finder.find(
+ project,
+ embedded: true,
+ grafana_url: url
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
index 4d8a5028898..e84ba83e03e 100644
--- a/lib/banzai/filter/inline_metrics_redactor_filter.rb
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -8,14 +8,17 @@ module Banzai
include Gitlab::Utils::StrongMemoize
METRICS_CSS_CLASS = '.js-render-metrics'
+ URL = Gitlab::Metrics::Dashboard::Url
+
+ Embed = Struct.new(:project_path, :permission)
# Finds all embeds based on the css class the FE
# uses to identify the embedded content, removing
# only unnecessary nodes.
def call
nodes.each do |node|
- path = paths_by_node[node]
- user_has_access = user_access_by_path[path]
+ embed = embeds_by_node[node]
+ user_has_access = user_access_by_embed[embed]
node.remove unless user_has_access
end
@@ -30,40 +33,69 @@ module Banzai
end
# Returns all nodes which the FE will identify as
- # a metrics dashboard placeholder element
+ # a metrics embed placeholder element
#
# @return [Nokogiri::XML::NodeSet]
def nodes
@nodes ||= doc.css(METRICS_CSS_CLASS)
end
- # Maps a node to the full path of a project.
+ # Maps a node to key properties of an embed.
# Memoized so we only need to run the regex to get
# the project full path from the url once per node.
#
- # @return [Hash<Nokogiri::XML::Node, String>]
- def paths_by_node
- strong_memoize(:paths_by_node) do
- nodes.each_with_object({}) do |node, paths|
- paths[node] = path_for_node(node)
+ # @return [Hash<Nokogiri::XML::Node, Embed>]
+ def embeds_by_node
+ strong_memoize(:embeds_by_node) do
+ nodes.each_with_object({}) do |node, embeds|
+ embed = Embed.new
+ url = node.attribute('data-dashboard-url').to_s
+
+ set_path_and_permission(embed, url, URL.regex, :read_environment)
+ set_path_and_permission(embed, url, URL.grafana_regex, :read_project) unless embed.permission
+
+ embeds[node] = embed if embed.permission
end
end
end
- # Gets a project's full_path from the dashboard url
- # in the placeholder node. The FE will use the attr
- # `data-dashboard-url`, so we want to check against that
- # attribute directly in case a user has manually
- # created a metrics element (rather than supporting
- # an alternate attr in InlineMetricsFilter).
+ # Attempts to determine the path and permission attributes
+ # of a url based on expected dashboard url formats and
+ # sets the attributes on an Embed object
#
- # @return [String]
- def path_for_node(node)
- url = node.attribute('data-dashboard-url').to_s
-
- Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
+ # @param embed [Embed]
+ # @param url [String]
+ # @param regex [RegExp]
+ # @param permission [Symbol]
+ def set_path_and_permission(embed, url, regex, permission)
+ return unless path = regex.match(url) do |m|
"#{$~[:namespace]}/#{$~[:project]}"
end
+
+ embed.project_path = path
+ embed.permission = permission
+ end
+
+ # Returns a mapping representing whether the current user
+ # has permission to view the embed for the project.
+ # Determined in a batch
+ #
+ # @return [Hash<Embed, Boolean>]
+ def user_access_by_embed
+ strong_memoize(:user_access_by_embed) do
+ unique_embeds.each_with_object({}) do |embed, access|
+ project = projects_by_path[embed.project_path]
+
+ access[embed] = Ability.allowed?(user, embed.permission, project)
+ end
+ end
+ end
+
+ # Returns a unique list of embeds
+ #
+ # @return [Array<Embed>]
+ def unique_embeds
+ embeds_by_node.values.uniq
end
# Maps a project's full path to a Project object.
@@ -74,22 +106,17 @@ module Banzai
def projects_by_path
strong_memoize(:projects_by_path) do
Project.eager_load(:route, namespace: [:route])
- .where_full_path_in(paths_by_node.values.uniq)
+ .where_full_path_in(unique_project_paths)
.index_by(&:full_path)
end
end
- # Returns a mapping representing whether the current user
- # has permission to view the metrics for the project.
- # Determined in a batch
+ # Returns a list of the full_paths of every project which
+ # has an embed in the doc
#
- # @return [Hash<Project, Boolean>]
- def user_access_by_path
- strong_memoize(:user_access_by_path) do
- projects_by_path.each_with_object({}) do |(path, project), access|
- access[path] = Ability.allowed?(user, :read_environment, project)
- end
- end
+ # @return [Array<String>]
+ def unique_project_paths
+ embeds_by_node.values.map(&:project_path).uniq
end
end
end
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index ed82fbc1f94..98987ee2019 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -15,7 +15,7 @@ module Banzai
end
def extra_element_attrs
- { width: "100%" }
+ { width: "400" }
end
end
end
diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb
index 82b99d3de4a..90edc7010f4 100644
--- a/lib/banzai/pipeline/ascii_doc_pipeline.rb
+++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb
@@ -10,6 +10,9 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter,
Filter::PlantumlFilter,
+ Filter::ColorFilter,
+ Filter::ImageLazyLoadFilter,
+ Filter::ImageLinkFilter,
Filter::AsciiDocPostProcessingFilter
]
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 08e27257fdf..f6c12cdb53b 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -30,6 +30,7 @@ module Banzai
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::InlineMetricsFilter,
+ Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
index a498c9bc213..8d9de2dbc7d 100644
--- a/lib/bitbucket/representation/pull_request.rb
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -16,9 +16,10 @@ module Bitbucket
end
def state
- if raw['state'] == 'MERGED'
+ case raw['state']
+ when 'MERGED'
'merged'
- elsif raw['state'] == 'DECLINED'
+ when 'DECLINED', 'SUPERSEDED'
'closed'
else
'opened'
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 92861c567a8..bc0347f6ea1 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -51,7 +51,7 @@ module ContainerRegistry
def upload_blob(name, content, digest)
upload = faraday.post("/v2/#{name}/blobs/uploads/")
- return unless upload.success?
+ return upload unless upload.success?
location = URI(upload.headers['location'])
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index d99a209dc87..9e9df88373a 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -74,7 +74,14 @@ module DeclarativePolicy
next unless klass.name
begin
- policy_class = "#{klass.name}Policy".constantize
+ 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
# NOTE: the < operator here tests whether policy_class
# inherits from Base. We can't use #is_a? because that
diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb
index 81f8ba5c8c3..0ac2d017e1a 100644
--- a/lib/feature/gitaly.rb
+++ b/lib/feature/gitaly.rb
@@ -7,7 +7,6 @@ class Feature
# Server feature flags should use '_' to separate words.
SERVER_FEATURE_FLAGS =
%w[
- cache_invalidator
inforef_uploadpack_cache
get_all_lfs_pointers_go
].freeze
@@ -20,7 +19,7 @@ class Feature
default_on = DEFAULT_ON_FLAGS.include?(feature_flag)
Feature.enabled?("gitaly_#{feature_flag}", default_enabled: default_on)
- rescue ActiveRecord::NoDatabaseError
+ rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
false
end
diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
index 15cdd25e711..568104cb30b 100644
--- a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
+++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
@@ -5,7 +5,7 @@ require 'rails/generators'
module Rails
class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
def create_migration_file
- timestamp = Time.now.strftime('%Y%m%d%H%M%S')
+ timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index ad8e693ccbc..0e6db54eb46 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -47,6 +47,18 @@ module Gitlab
Gitlab.config.gitlab.url == COM_URL || gl_subdomain?
end
+ def self.canary?
+ Gitlab::Utils.to_boolean(ENV['CANARY'])
+ end
+
+ def self.com_and_canary?
+ com? && canary?
+ end
+
+ def self.com_but_not_canary?
+ com? && !canary?
+ end
+
def self.org?
Gitlab.config.gitlab.url == 'https://dev.gitlab.org'
end
diff --git a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
index 33cbe1a62ef..9ea20a4d6a4 100644
--- a/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
+++ b/lib/gitlab/analytics/cycle_analytics/base_query_builder.rb
@@ -68,3 +68,5 @@ module Gitlab
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::BaseQueryBuilder')
diff --git a/lib/gitlab/analytics/cycle_analytics/data_collector.rb b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
index 0c0f737f2c9..05b16672912 100644
--- a/lib/gitlab/analytics/cycle_analytics/data_collector.rb
+++ b/lib/gitlab/analytics/cycle_analytics/data_collector.rb
@@ -12,6 +12,8 @@ module Gitlab
class DataCollector
include Gitlab::Utils::StrongMemoize
+ delegate :serialized_records, to: :records_fetcher
+
def initialize(stage:, params: {})
@stage = stage
@params = params
diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
index 90d03142b2a..2662aa38d6b 100644
--- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
@@ -130,3 +130,5 @@ module Gitlab
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::RecordsFetcher.prepend_if_ee('EE::Gitlab::Analytics::CycleAnalytics::RecordsFetcher')
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
index 58572446de6..f6e22044142 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb
@@ -47,27 +47,29 @@ module Gitlab
]
}.freeze
- def [](identifier)
+ def self.[](identifier)
events.find { |e| e.identifier.to_s.eql?(identifier.to_s) } || raise(KeyError)
end
# hash for defining ActiveRecord enum: identifier => number
- def to_enum
- ENUM_MAPPING.each_with_object({}) { |(k, v), hash| hash[k.identifier] = v }
+ def self.to_enum
+ enum_mapping.each_with_object({}) { |(k, v), hash| hash[k.identifier] = v }
end
- # will be overridden in EE with custom events
- def pairing_rules
+ def self.pairing_rules
PAIRING_RULES
end
- # will be overridden in EE with custom events
- def events
+ def self.events
EVENTS
end
- module_function :[], :to_enum, :pairing_rules, :events
+ def self.enum_mapping
+ ENUM_MAPPING
+ end
end
end
end
end
+
+Gitlab::Analytics::CycleAnalytics::StageEvents.prepend_if_ee('::EE::Gitlab::Analytics::CycleAnalytics::StageEvents')
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
index 6af1b90bccc..9f0ca80ba50 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class CodeStageStart < SimpleStageEvent
+ class CodeStageStart < StageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first mentioned in a commit")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
index 8c9a80740a9..a159580b7bd 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueCreated < SimpleStageEvent
+ class IssueCreated < StageEvent
def self.name
s_("CycleAnalyticsEvent|Issue created")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
index fe7f2d85f8b..a3b7fa16daf 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueFirstMentionedInCommit < SimpleStageEvent
+ class IssueFirstMentionedInCommit < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first mentioned in a commit")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
issue_metrics_table[:first_mentioned_in_commit_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
index 77e4092b9ab..0ea98e82ecc 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class IssueStageEnd < SimpleStageEvent
+ class IssueStageEnd < MetricsBasedStageEvent
def self.name
PlanStageStart.name
end
@@ -26,7 +26,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query.joins(:metrics).where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
+ super.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
index 7059c425b8f..013e068e479 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestCreated < SimpleStageEvent
+ class MergeRequestCreated < StageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request created")
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
index 3d7482eaaf0..654d0befbc3 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestFirstDeployedToProduction < SimpleStageEvent
+ class MergeRequestFirstDeployedToProduction < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request first deployed to production")
end
@@ -23,7 +23,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query.joins(:metrics).where(timestamp_projection.gteq(mr_table[:created_at]))
+ super.where(timestamp_projection.gteq(mr_table[:created_at]))
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
index 36bb4d6fc8d..a0b1c12756f 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestLastBuildFinished < SimpleStageEvent
+ class MergeRequestLastBuildFinished < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request last build finish time")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:latest_build_finished_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
index 468d9899cc7..da3b5cdfaa4 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestLastBuildStarted < SimpleStageEvent
+ class MergeRequestLastBuildStarted < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request last build start time")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:latest_build_started_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
index 82ecaf1cd6b..e67a6f7eea6 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class MergeRequestMerged < SimpleStageEvent
+ class MergeRequestMerged < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Merge request merged")
end
@@ -20,12 +20,6 @@ module Gitlab
def timestamp_projection
mr_metrics_table[:merged_at]
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def apply_query_customization(query)
- query.joins(:metrics)
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb
new file mode 100644
index 00000000000..4ca8745abe4
--- /dev/null
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/metrics_based_stage_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Analytics
+ module CycleAnalytics
+ module StageEvents
+ class MetricsBasedStageEvent < StageEvent
+ # rubocop: disable CodeReuse/ActiveRecord
+ def apply_query_customization(query)
+ query.joins(:metrics)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
index 7ece7d62faa..37168a1fb0f 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class PlanStageStart < SimpleStageEvent
+ class PlanStageStart < MetricsBasedStageEvent
def self.name
s_("CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board")
end
@@ -26,8 +26,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def apply_query_customization(query)
- query
- .joins(:metrics)
+ super
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
index 607371a32e8..b249f6874e7 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/production_stage_end.rb
@@ -4,7 +4,7 @@ module Gitlab
module Analytics
module CycleAnalytics
module StageEvents
- class ProductionStageEnd < SimpleStageEvent
+ class ProductionStageEnd < StageEvent
def self.name
PlanStageStart.name
end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb
deleted file mode 100644
index 253c489d822..00000000000
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Analytics
- module CycleAnalytics
- module StageEvents
- # Represents a simple event that usually refers to one database column and does not require additional user input
- class SimpleStageEvent < StageEvent
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
index aa392140eb5..667d6def414 100644
--- a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
+++ b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb
@@ -35,6 +35,10 @@ module Gitlab
query
end
+ def label_based?
+ false
+ end
+
private
attr_reader :params
diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb
index 74d359bcd28..acb46abb6f3 100644
--- a/lib/gitlab/auth/ip_rate_limiter.rb
+++ b/lib/gitlab/auth/ip_rate_limiter.rb
@@ -21,11 +21,12 @@ module Gitlab
end
def register_fail!
+ return false if trusted_ip?
+
# Allow2Ban.filter will return false if this IP has not failed too often yet
@banned = Rack::Attack::Allow2Ban.filter(ip, config) do
- # If we return false here, the failure for this IP is ignored by Allow2Ban
- # If we return true here, the count for the IP is incremented.
- ip_can_be_banned?
+ # We return true to increment the count for this IP
+ true
end
end
@@ -33,20 +34,16 @@ module Gitlab
@banned
end
+ def trusted_ip?
+ trusted_ips.any? { |netmask| netmask.include?(ip) }
+ end
+
private
def config
Gitlab.config.rack_attack.git_basic_auth
end
- def ip_can_be_banned?
- !trusted_ip?
- end
-
- def trusted_ip?
- trusted_ips.any? { |netmask| netmask.include?(ip) }
- end
-
def trusted_ips
strong_memoize(:trusted_ips) do
config.ip_whitelist.map do |proxy|
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index eb1d0925c55..4bc0ceedae7 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -21,6 +21,14 @@ module Gitlab
Gitlab.config.ldap.enabled
end
+ def self.sign_in_enabled?
+ enabled? && !prevent_ldap_sign_in?
+ end
+
+ def self.prevent_ldap_sign_in?
+ Gitlab.config.ldap.prevent_ldap_sign_in
+ end
+
def self.servers
Gitlab.config.ldap['servers']&.values || []
end
diff --git a/lib/gitlab/background_migration/legacy_upload_mover.rb b/lib/gitlab/background_migration/legacy_upload_mover.rb
index c9e47f210be..1879a6c5427 100644
--- a/lib/gitlab/background_migration/legacy_upload_mover.rb
+++ b/lib/gitlab/background_migration/legacy_upload_mover.rb
@@ -18,6 +18,7 @@ module Gitlab
def execute
return unless upload
+ return unless upload.model_type == 'Note'
if !project
# if we don't have models associated with the upload we can not move it
diff --git a/lib/gitlab/background_migration/legacy_uploads_migrator.rb b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
index a9d38a27e0c..f7cadb9b00d 100644
--- a/lib/gitlab/background_migration/legacy_uploads_migrator.rb
+++ b/lib/gitlab/background_migration/legacy_uploads_migrator.rb
@@ -14,7 +14,7 @@ module Gitlab
include Database::MigrationHelpers
def perform(start_id, end_id)
- Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader').find_each do |upload|
+ Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader', model_type: 'Note').find_each do |upload|
LegacyUploadMover.new(upload).execute
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
index f5fb33f1660..23e8be4a9ab 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -176,7 +176,7 @@ module Gitlab
self.table_name = 'projects'
def self.find_by_full_path(path)
- order_sql = "(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
+ order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)")
where_full_path_in(path).reorder(order_sql).take
end
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index 8d25b66af9c..cbda3808b86 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -22,11 +22,11 @@ module Gitlab
start_offset = @state.offset
- @state.set_current_line!(style: Style.new(@state.inherited_style))
+ @state.new_line!(
+ style: Style.new(@state.inherited_style))
stream.each_line do |line|
- s = StringScanner.new(line)
- convert_line(s)
+ consume_line(line)
end
# This must be assigned before flushing the current line
@@ -52,26 +52,41 @@ module Gitlab
private
- def convert_line(scanner)
- until scanner.eos?
-
- if scanner.scan(Gitlab::Regex.build_trace_section_regex)
- handle_section(scanner)
- elsif scanner.scan(/\e([@-_])(.*?)([@-~])/)
- handle_sequence(scanner)
- elsif scanner.scan(/\e(([@-_])(.*?)?)?$/)
- break
- elsif scanner.scan(/</)
- @state.current_line << '&lt;'
- elsif scanner.scan(/\r?\n/)
- # we advance the offset of the next current line
- # so it does not start from \n
- flush_current_line(advance_offset: scanner.matched_size)
- else
- @state.current_line << scanner.scan(/./m)
- end
-
- @state.offset += scanner.matched_size
+ def consume_line(line)
+ scanner = StringScanner.new(line)
+
+ consume_token(scanner) until scanner.eos?
+ end
+
+ def consume_token(scanner)
+ if scan_token(scanner, Gitlab::Regex.build_trace_section_regex, consume: false)
+ handle_section(scanner)
+ elsif scan_token(scanner, /\e([@-_])(.*?)([@-~])/)
+ handle_sequence(scanner)
+ elsif scan_token(scanner, /\e(([@-_])(.*?)?)?$/)
+ # stop scanning
+ scanner.terminate
+ elsif scan_token(scanner, /\r?\n/)
+ flush_current_line
+ elsif scan_token(scanner, /\r/)
+ # drop last line
+ @state.current_line.clear!
+ elsif scan_token(scanner, /.[^\e\r\ns]*/m)
+ # this is a join from all previous tokens and first letters
+ # it always matches at least one character `.`
+ # it matches everything that is not start of:
+ # `\e`, `<`, `\r`, `\n`, `s` (for section_start)
+ @state.current_line << scanner[0]
+ else
+ raise 'invalid parser state'
+ end
+ end
+
+ def scan_token(scanner, match, consume: true)
+ scanner.scan(match).tap do |result|
+ # we need to move offset as soon
+ # as we match the token
+ @state.offset += scanner.matched_size if consume && result
end
end
@@ -96,32 +111,50 @@ module Gitlab
section_name = sanitize_section_name(section)
if action == "start"
- handle_section_start(section_name, timestamp)
+ handle_section_start(scanner, section_name, timestamp)
elsif action == "end"
- handle_section_end(section_name, timestamp)
+ handle_section_end(scanner, section_name, timestamp)
+ else
+ raise 'unsupported action'
end
end
- def handle_section_start(section, timestamp)
- flush_current_line unless @state.current_line.empty?
+ def handle_section_start(scanner, section, timestamp)
+ # We make a new line for new section
+ flush_current_line
+
@state.open_section(section, timestamp)
+
+ # we need to consume match after handling
+ # the open of section, as we want the section
+ # marker to be refresh on incremental update
+ @state.offset += scanner.matched_size
end
- def handle_section_end(section, timestamp)
+ def handle_section_end(scanner, section, timestamp)
return unless @state.section_open?(section)
- flush_current_line unless @state.current_line.empty?
+ # We flush the content to make the end
+ # of section to be a new line
+ flush_current_line
+
@state.close_section(section, timestamp)
- # ensure that section end is detached from the last
- # line in the section
+ # we need to consume match before handling
+ # as we want the section close marker
+ # not to be refreshed on incremental update
+ @state.offset += scanner.matched_size
+
+ # this flushes an empty line with `section_duration`
flush_current_line
end
- def flush_current_line(advance_offset: 0)
- @lines << @state.current_line.to_h
+ def flush_current_line
+ unless @state.current_line.empty?
+ @lines << @state.current_line.to_h
+ end
- @state.set_current_line!(advance_offset: advance_offset)
+ @state.new_line!
end
def sanitize_section_name(section)
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index 173fb1df88e..21aa1f84353 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -47,12 +47,17 @@ module Gitlab
@current_segment.text << data
end
+ def clear!
+ @segments.clear
+ @current_segment = Segment.new(style: style)
+ end
+
def style
@current_segment.style
end
def empty?
- @segments.empty? && @current_segment.empty?
+ @segments.empty? && @current_segment.empty? && @section_duration.nil?
end
def update_style(ansi_commands)
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index db7a9035b8b..7e1a8102a35 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -46,9 +46,9 @@ module Gitlab
@open_sections.key?(section)
end
- def set_current_line!(style: nil, advance_offset: 0)
+ def new_line!(style: nil)
new_line = Line.new(
- offset: @offset + advance_offset,
+ offset: @offset,
style: style || @current_line.style,
sections: @open_sections.keys
)
diff --git a/lib/gitlab/ci/ansi2json/style.rb b/lib/gitlab/ci/ansi2json/style.rb
index 2739ffdfa5d..77f61178b37 100644
--- a/lib/gitlab/ci/ansi2json/style.rb
+++ b/lib/gitlab/ci/ansi2json/style.rb
@@ -15,14 +15,10 @@ module Gitlab
end
def update(ansi_commands)
- command = ansi_commands.shift
- return unless command
-
- if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
- apply_changes(changes)
- end
+ # treat e\[m as \e[0m
+ ansi_commands = ['0'] if ansi_commands.empty?
- update(ansi_commands)
+ evaluate_stack_command(ansi_commands)
end
def set?
@@ -50,6 +46,17 @@ module Gitlab
private
+ def evaluate_stack_command(ansi_commands)
+ command = ansi_commands.shift
+ return unless command
+
+ if changes = Gitlab::Ci::Ansi2json::Parser.new(command, ansi_commands).changes
+ apply_changes(changes)
+ end
+
+ evaluate_stack_command(ansi_commands)
+ end
+
def apply_changes(changes)
case
when changes[:reset]
diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb
new file mode 100644
index 00000000000..02b97ea76e9
--- /dev/null
+++ b/lib/gitlab/ci/build/context/base.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Base
+ attr_reader :pipeline
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def variables
+ raise NotImplementedError
+ end
+
+ protected
+
+ def pipeline_attributes
+ {
+ pipeline: pipeline,
+ project: pipeline.project,
+ user: pipeline.user,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ trigger_request: pipeline.legacy_trigger,
+ protected: pipeline.protected_ref?
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
new file mode 100644
index 00000000000..dfd86d3ad72
--- /dev/null
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Build < Base
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :attributes
+
+ def initialize(pipeline, attributes = {})
+ super(pipeline)
+
+ @attributes = attributes
+ end
+
+ def variables
+ strong_memoize(:variables) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ stub_build.scoped_variables_hash
+ end
+ end
+
+ private
+
+ def stub_build
+ ::Ci::Build.new(build_attributes)
+ end
+
+ def build_attributes
+ attributes.merge(pipeline_attributes)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/context/global.rb b/lib/gitlab/ci/build/context/global.rb
new file mode 100644
index 00000000000..fdd3ac358d5
--- /dev/null
+++ b/lib/gitlab/ci/build/context/global.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ module Context
+ class Global < Base
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(pipeline, yaml_variables:)
+ super(pipeline)
+
+ @yaml_variables = yaml_variables.to_a
+ end
+
+ def variables
+ strong_memoize(:variables) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate workflow:rules
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ stub_build.scoped_variables_hash
+ .reject { |key, _value| key =~ /\ACI_(JOB|BUILD)/ }
+ end
+ end
+
+ private
+
+ def stub_build
+ ::Ci::Build.new(build_attributes)
+ end
+
+ def build_attributes
+ pipeline_attributes.merge(
+ yaml_variables: @yaml_variables)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/changes.rb b/lib/gitlab/ci/build/policy/changes.rb
index 9c705a1cd3e..9ae4198bbf7 100644
--- a/lib/gitlab/ci/build/policy/changes.rb
+++ b/lib/gitlab/ci/build/policy/changes.rb
@@ -9,7 +9,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index 4c7dc947cd0..4e8693724e5 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -11,7 +11,7 @@ module Gitlab
end
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
pipeline.has_kubernetes_active?
end
end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index c3005303fd8..afe0ccb361e 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -9,7 +9,7 @@ module Gitlab
@patterns = Array(refs)
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index ceb5210cfb5..1394340ce1f 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -17,7 +17,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index e9c8864123f..7b1ce6330f0 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -9,8 +9,8 @@ module Gitlab
@expressions = Array(expressions)
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
+ def satisfied_by?(pipeline, context)
+ variables = context.variables
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index 43399c74457..c705b6f86c7 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -13,17 +13,21 @@ module Gitlab
options: { start_in: start_in }.compact
}.compact
end
+
+ def pass?
+ self.when != 'never'
+ end
end
- def initialize(rule_hashes, default_when = 'on_success')
+ def initialize(rule_hashes, default_when:)
@rule_list = Rule.fabricate_list(rule_hashes)
@default_when = default_when
end
- def evaluate(pipeline, build)
+ def evaluate(pipeline, context)
if @rule_list.nil?
Result.new(@default_when)
- elsif matched_rule = match_rule(pipeline, build)
+ elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in]
@@ -35,8 +39,8 @@ module Gitlab
private
- def match_rule(pipeline, build)
- @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ def match_rule(pipeline, context)
+ @rule_list.find { |rule| rule.matches?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
index 8d52158c8d2..077e4d150fb 100644
--- a/lib/gitlab/ci/build/rules/rule.rb
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -23,8 +23,8 @@ module Gitlab
end
end
- def matches?(pipeline, build)
- @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ def matches?(pipeline, context)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
index bf787fe95a6..6d4bbbb8c21 100644
--- a/lib/gitlab/ci/build/rules/rule/clause.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -20,7 +20,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
index 81d2ee6c24c..728a66ca87f 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -8,7 +8,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
index 62f8371283f..85e77438f51 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
@@ -15,7 +15,7 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline)
exact_matches?(paths) || pattern_matches?(paths)
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
index 18c3b450f95..6143a736ca6 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/if.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -8,10 +8,9 @@ module Gitlab
@expression = expression
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
-
- ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ def satisfied_by?(pipeline, context)
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(
+ @expression, context.variables).truthful?
end
end
end
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index 41613369ca2..9d8d7675234 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -12,7 +12,9 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
+ ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as].freeze
+ EXPOSE_AS_REGEX = /\A\w[-\w ]*\z/.freeze
+ EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
attributes ALLOWED_KEYS
@@ -21,11 +23,18 @@ module Gitlab
validations do
validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :paths, presence: true, if: :expose_as_present?
with_options allow_nil: true do
validates :name, type: String
validates :untracked, boolean: true
validates :paths, array_of_strings: true
+ validates :paths, array_of_strings: {
+ with: /\A[^*]*\z/,
+ message: "can't contain '*' when used with 'expose_as'"
+ }, 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 :reports, type: Hash
validates :when,
inclusion: { in: %w[on_success on_failure always],
@@ -41,6 +50,12 @@ module Gitlab
@config[:reports] = reports_value if @config.key?(:reports)
@config
end
+
+ def expose_as_present?
+ return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+
+ !@config[:expose_as].nil?
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
new file mode 100644
index 00000000000..10619ef9f8d
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents the interrutible value.
+ #
+ class Boolean < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index 02e368c1813..7a86fca3056 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -11,11 +11,11 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, array_of_strings_or_string: true
+ validates :config, string_or_nested_array_of_strings: true
end
def value
- Array(@config)
+ Array(@config).flatten(1)
end
end
end
diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb
index 6200d7c7f87..83127bde6e4 100644
--- a/lib/gitlab/ci/config/entry/default.rb
+++ b/lib/gitlab/ci/config/entry/default.rb
@@ -11,11 +11,10 @@ module Gitlab
#
class Default < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
-
- DuplicateError = Class.new(Gitlab::Config::Loader::FormatError)
+ include ::Gitlab::Config::Entry::Inheritable
ALLOWED_KEYS = %i[before_script image services
- after_script cache].freeze
+ after_script cache interruptible].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -41,31 +40,22 @@ module Gitlab
description: 'Configure caching between build jobs.',
inherit: true
- helpers :before_script, :image, :services, :after_script, :cache
-
- def compose!(deps = nil)
- super(self)
+ entry :interruptible, Entry::Boolean,
+ description: 'Set jobs interruptible default value.',
+ inherit: false
- inherit!(deps)
- end
+ helpers :before_script, :image, :services, :after_script, :cache, :interruptible
private
- def inherit!(deps)
- return unless deps
-
- self.class.nodes.each do |key, factory|
- next unless factory.inheritable?
+ def overwrite_entry(deps, key, current_entry)
+ inherited_entry = deps[key]
- root_entry = deps[key]
- next unless root_entry.specified?
-
- if self[key].specified?
- raise DuplicateError, "#{key} is defined in top-level and `default:` entry"
- end
-
- @entries[key] = root_entry
+ if inherited_entry.specified? && current_entry.specified?
+ raise InheritError, "#{key} is defined in top-level and `default:` entry"
end
+
+ inherited_entry unless current_entry.specified?
end
end
end
diff --git a/lib/gitlab/ci/config/entry/files.rb b/lib/gitlab/ci/config/entry/files.rb
new file mode 100644
index 00000000000..d0d6a36d754
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/files.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents an array of file paths.
+ #
+ class Files < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ validates :config, length: {
+ minimum: 1,
+ maximum: 2,
+ too_short: 'requires at least %{count} item',
+ too_long: 'has too many items (maximum is %{count})'
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 07d5be86b1e..c75ae87a985 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -10,6 +10,7 @@ module Gitlab
class Job < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Inheritable
ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
ALLOWED_KEYS = %i[tags script only except rules type image services
@@ -37,7 +38,6 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
- validates :interruptible, boolean: true
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
@@ -49,7 +49,6 @@ module Gitlab
validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
validates :dependencies, array_of_strings: true
- validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true
end
@@ -73,13 +72,16 @@ module Gitlab
inherit: true
entry :script, Entry::Commands,
- description: 'Commands that will be executed in this job.'
+ description: 'Commands that will be executed in this job.',
+ inherit: false
entry :stage, Entry::Stage,
- description: 'Pipeline stage this job will be executed into.'
+ description: 'Pipeline stage this job will be executed into.',
+ inherit: false
entry :type, Entry::Stage,
- description: 'Deprecated: stage this job will be executed into.'
+ description: 'Deprecated: stage this job will be executed into.',
+ inherit: false
entry :after_script, Entry::Script,
description: 'Commands that will be executed when finishing job.',
@@ -97,30 +99,50 @@ module Gitlab
description: 'Services that will be used to execute this job.',
inherit: true
+ entry :interruptible, Entry::Boolean,
+ description: 'Set jobs interruptible value.',
+ inherit: true
+
entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.',
- default: Entry::Policy::DEFAULT_ONLY
+ default: Entry::Policy::DEFAULT_ONLY,
+ inherit: false
entry :except, Entry::Policy,
- description: 'Refs policy this job will be executed for.'
+ description: 'Refs policy this job will be executed for.',
+ inherit: false
entry :rules, Entry::Rules,
- description: 'List of evaluable Rules to determine job inclusion.'
+ description: 'List of evaluable Rules to determine job inclusion.',
+ inherit: false,
+ metadata: {
+ allowed_when: %w[on_success on_failure always never manual delayed].freeze
+ }
+
+ entry :needs, Entry::Needs,
+ description: 'Needs configuration for this job.',
+ metadata: { allowed_needs: %i[job] },
+ inherit: false
entry :variables, Entry::Variables,
- description: 'Environment variables available for this job.'
+ description: 'Environment variables available for this job.',
+ inherit: false
entry :artifacts, Entry::Artifacts,
- description: 'Artifacts configuration for this job.'
+ description: 'Artifacts configuration for this job.',
+ inherit: false
entry :environment, Entry::Environment,
- description: 'Environment configuration for this job.'
+ description: 'Environment configuration for this job.',
+ inherit: false
entry :coverage, Entry::Coverage,
- description: 'Coverage configuration for this job.'
+ description: 'Coverage configuration for this job.',
+ inherit: false
entry :retry, Entry::Retry,
- description: 'Retry configuration for this job.'
+ description: 'Retry configuration for this job.',
+ inherit: false
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
@@ -155,8 +177,6 @@ module Gitlab
@entries.delete(:except)
end
end
-
- inherit!(deps)
end
def name
@@ -185,21 +205,8 @@ module Gitlab
private
- # We inherit config entries from `default:`
- # if the entry has the `inherit: true` flag set
- def inherit!(deps)
- return unless deps
-
- self.class.nodes.each do |key, factory|
- next unless factory.inheritable?
-
- default_entry = deps.default[key]
- job_entry = self[key]
-
- if default_entry.specified? && !job_entry.specified?
- @entries[key] = default_entry
- end
- end
+ def overwrite_entry(deps, key, current_entry)
+ deps.default[key] unless current_entry.specified?
end
def to_hash
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 0c10967e629..f12f0919348 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -7,11 +7,48 @@ module Gitlab
##
# Entry that represents a key.
#
- class Key < ::Gitlab::Config::Entry::Node
- include ::Gitlab::Config::Entry::Validatable
+ class Key < ::Gitlab::Config::Entry::Simplifiable
+ strategy :SimpleKey, if: -> (config) { config.is_a?(String) || config.is_a?(Symbol) }
+ strategy :ComplexKey, if: -> (config) { config.is_a?(Hash) }
- validations do
- validates :config, key: true
+ class SimpleKey < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, key: true
+ end
+
+ def self.default
+ 'default'
+ end
+
+ def value
+ super.to_s
+ end
+ end
+
+ class ComplexKey < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Attributable
+ include ::Gitlab::Config::Entry::Configurable
+
+ ALLOWED_KEYS = %i[files prefix].freeze
+ REQUIRED_KEYS = %i[files].freeze
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, required_keys: REQUIRED_KEYS
+ end
+
+ entry :files, Entry::Files,
+ description: 'Files that should be used to build the key'
+ entry :prefix, Entry::Prefix,
+ description: 'Prefix that is added to the final cache key'
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def errors
+ ["#{location} should be a hash, a string or a symbol"]
+ end
end
def self.default
diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb
new file mode 100644
index 00000000000..b6db546d8ff
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/need.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Need < ::Gitlab::Config::Entry::Simplifiable
+ strategy :Job, if: -> (config) { config.is_a?(String) }
+
+ class Job < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: String
+ end
+
+ def type
+ :job
+ end
+
+ def value
+ { name: @config }
+ end
+ end
+
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
+ def type
+ end
+
+ def value
+ end
+
+ def errors
+ ["#{location} has an unsupported type"]
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+::Gitlab::Ci::Config::Entry::Need.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Need')
diff --git a/lib/gitlab/ci/config/entry/needs.rb b/lib/gitlab/ci/config/entry/needs.rb
new file mode 100644
index 00000000000..28452aaaa16
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/needs.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a set of needs dependencies.
+ #
+ class Needs < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+
+ validate do
+ unless config.is_a?(Hash) || config.is_a?(Array)
+ errors.add(:config, 'can only be a Hash or an Array')
+ end
+ end
+
+ validate on: :composed do
+ extra_keys = value.keys - opt(:allowed_needs)
+ if extra_keys.any?
+ errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}")
+ end
+ end
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ [@config].flatten.each_with_index do |need, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
+ .value(need)
+ .with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+
+ def value
+ values = @entries.values.select(&:type)
+ values.group_by(&:type).transform_values do |values|
+ values.map(&:value)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/prefix.rb b/lib/gitlab/ci/config/entry/prefix.rb
new file mode 100644
index 00000000000..3244ad6d611
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/prefix.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a key prefix.
+ #
+ class Prefix < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, key: true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index 07022ff7b54..25fb278d9b8 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[default include before_script image services
- after_script variables stages types cache].freeze
+ after_script variables stages types cache workflow].freeze
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -64,6 +64,9 @@ module Gitlab
description: 'Configure caching between build jobs.',
reserved: true
+ entry :workflow, Entry::Workflow,
+ description: 'List of evaluable rules to determine Pipeline status'
+
helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value,
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
index 5d6d1c026e3..59e0ef583ae 100644
--- a/lib/gitlab/ci/config/entry/rules/rule.rb
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -8,9 +8,9 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- CLAUSES = %i[if changes exists].freeze
- ALLOWED_KEYS = %i[if changes exists when start_in].freeze
- ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+ CLAUSES = %i[if changes exists].freeze
+ ALLOWED_KEYS = %i[if changes exists when start_in].freeze
+ ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
attributes :if, :changes, :exists, :when, :start_in
@@ -25,7 +25,14 @@ module Gitlab
with_options allow_nil: true do
validates :if, expression: true
validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
- validates :when, allowed_values: { in: ALLOWED_WHEN }
+ validates :when, allowed_values: { in: ALLOWABLE_WHEN }
+ end
+
+ validate do
+ validates_with Gitlab::Config::Entry::Validators::AllowedValuesValidator,
+ attributes: %i[when],
+ allow_nil: true,
+ in: opt(:allowed_when)
end
end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index 9d25a82b521..285e18218b3 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -11,7 +11,11 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
- validates :config, array_of_strings: true
+ validates :config, nested_array_of_strings: true
+ end
+
+ def value
+ config.flatten(1)
end
end
end
diff --git a/lib/gitlab/ci/config/entry/workflow.rb b/lib/gitlab/ci/config/entry/workflow.rb
new file mode 100644
index 00000000000..a51a3fbdcd2
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/workflow.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Workflow < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+
+ ALLOWED_KEYS = %i[rules].freeze
+
+ validations do
+ validates :config, type: Hash
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, presence: true
+ end
+
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine Pipeline status.',
+ metadata: { allowed_when: %w[always never] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
index 09f9bf5f69f..e714ef225f5 100644
--- a/lib/gitlab/ci/config/normalizer.rb
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -18,8 +18,8 @@ module Gitlab
config[:dependencies] = expand_names(config[:dependencies])
end
- if config[:needs]
- config[:needs] = expand_names(config[:needs])
+ if job_needs = config.dig(:needs, :job)
+ config[:needs][:job] = expand_needs(job_needs)
end
config
@@ -36,6 +36,22 @@ module Gitlab
end
end
+ def expand_needs(job_needs)
+ return unless job_needs
+
+ job_needs.flat_map do |job_need|
+ job_need_name = job_need[:name].to_sym
+
+ if all_job_names = parallelized_jobs[job_need_name]
+ all_job_names.map do |job_name|
+ { name: job_name }
+ end
+ else
+ job_need
+ end
+ end
+ end
+
def parallelized_jobs
strong_memoize(:parallelized_jobs) do
@jobs_config.each_with_object({}) do |(job_name, config), hash|
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index bab1c73e2f1..aabdf7ce47d 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -5,7 +5,7 @@ module Gitlab
module Pipeline
module Chain
class Base
- attr_reader :pipeline, :command
+ attr_reader :pipeline, :command, :config
delegate :project, :current_user, to: :command
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index 899df81ea5c..9662209f88e 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -22,8 +22,6 @@ module Gitlab
external_pull_request: @command.external_pull_request,
variables_attributes: Array(@command.variables_attributes)
)
-
- @pipeline.set_config_source
end
def break?
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 58f89a6be5e..c2df419cca0 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -10,7 +10,9 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
- :chat_data, :allow_mirror_update
+ :chat_data, :allow_mirror_update,
+ # These attributes are set by Chains during processing:
+ :config_content, :config_processor, :stage_seeds
) do
include Gitlab::Utils::StrongMemoize
diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb
new file mode 100644
index 00000000000..a8cd99b8e92
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ return if @command.config_content
+
+ if content = content_from_repo
+ @command.config_content = content
+ @pipeline.config_source = :repository_source
+ # TODO: we should persist ci_config_path
+ # @pipeline.config_path = ci_config_path
+ elsif content = content_from_auto_devops
+ @command.config_content = content
+ @pipeline.config_source = :auto_devops_source
+ end
+
+ unless @command.config_content
+ return error("Missing #{ci_config_path} file")
+ end
+ end
+
+ def break?
+ @pipeline.errors.any? || @pipeline.persisted?
+ end
+
+ private
+
+ def content_from_repo
+ return unless project
+ return unless @pipeline.sha
+ return unless ci_config_path
+
+ project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path)
+ rescue GRPC::NotFound, GRPC::Internal
+ nil
+ end
+
+ def content_from_auto_devops
+ return unless project&.auto_devops_enabled?
+
+ Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
+ end
+
+ def ci_config_path
+ project.ci_config_path.presence || '.gitlab-ci.yml'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/process.rb b/lib/gitlab/ci/pipeline/chain/config/process.rb
new file mode 100644
index 00000000000..731b0fdb286
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/process.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Process < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ raise ArgumentError, 'missing config content' unless @command.config_content
+
+ @command.config_processor = ::Gitlab::Ci::YamlProcessor.new(
+ @command.config_content, {
+ project: project,
+ sha: @pipeline.sha,
+ user: current_user
+ }
+ )
+ rescue Gitlab::Ci::YamlProcessor::ValidationError => ex
+ error(ex.message, config_error: true)
+ rescue => ex
+ Gitlab::Sentry.track_acceptable_exception(ex, extra: {
+ project_id: project.id,
+ sha: @pipeline.sha
+ })
+
+ error("Undefined error (#{Labkit::Correlation::CorrelationId.current_id})",
+ config_error: true)
+ end
+
+ def break?
+ @pipeline.errors.any? || @pipeline.persisted?
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
new file mode 100644
index 00000000000..0ee9485eebc
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class EvaluateWorkflowRules < Chain::Base
+ include ::Gitlab::Utils::StrongMemoize
+ include Chain::Helpers
+
+ def perform!
+ return unless Feature.enabled?(:workflow_rules, @pipeline.project)
+
+ unless workflow_passed?
+ error('Pipeline filtered out by workflow rules.')
+ end
+ end
+
+ def break?
+ return false unless Feature.enabled?(:workflow_rules, @pipeline.project)
+
+ !workflow_passed?
+ end
+
+ private
+
+ def workflow_passed?
+ strong_memoize(:workflow_passed) do
+ workflow_rules.evaluate(@pipeline, global_context).pass?
+ end
+ end
+
+ def workflow_rules
+ Gitlab::Ci::Build::Rules.new(
+ workflow_config[:rules], default_when: 'always')
+ end
+
+ def global_context
+ Gitlab::Ci::Build::Context::Global.new(
+ @pipeline, yaml_variables: workflow_config[:yaml_variables])
+ end
+
+ def workflow_config
+ @command.config_processor.workflow_attributes || {}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 13eca5a9d28..3a40c7b167c 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -10,29 +10,12 @@ module Gitlab
PopulateError = Class.new(StandardError)
def perform!
- # Allocate next IID. This operation must be outside of transactions of pipeline creations.
- pipeline.ensure_project_iid!
-
- # Protect the pipeline. This is assigned in Populate instead of
- # Build to prevent erroring out on ambiguous refs.
- pipeline.protected = @command.protected_ref?
-
- ##
- # Populate pipeline with block argument of CreatePipelineService#execute.
- #
- @command.seeds_block&.call(pipeline)
-
- ##
- # Gather all runtime build/stage errors
- #
- if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence
- return error(seeds_errors.join("\n"), config_error: true)
- end
+ raise ArgumentError, 'missing stage seeds' unless @command.stage_seeds
##
# Populate pipeline with all stages, and stages with builds.
#
- pipeline.stages = pipeline.stage_seeds.map(&:to_resource)
+ pipeline.stages = @command.stage_seeds.map(&:to_resource)
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
diff --git a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
index 1e09b417311..9267c72efa4 100644
--- a/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
+++ b/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs.rb
@@ -6,11 +6,13 @@ module Gitlab
module Chain
class RemoveUnwantedChatJobs < Chain::Base
def perform!
- return unless pipeline.config_processor && pipeline.chat?
+ raise ArgumentError, 'missing config processor' unless @command.config_processor
+
+ return unless pipeline.chat?
# When scheduling a chat pipeline we only want to run the build
# that matches the chat command.
- pipeline.config_processor.jobs.select! do |name, _|
+ @command.config_processor.jobs.select! do |name, _|
name.to_s == command.chat_data[:command].to_s
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/seed.rb b/lib/gitlab/ci/pipeline/chain/seed.rb
new file mode 100644
index 00000000000..2e177cfec7e
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/seed.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Seed < Chain::Base
+ include Chain::Helpers
+ include Gitlab::Utils::StrongMemoize
+
+ def perform!
+ raise ArgumentError, 'missing config processor' unless @command.config_processor
+
+ # Allocate next IID. This operation must be outside of transactions of pipeline creations.
+ pipeline.ensure_project_iid!
+
+ # Protect the pipeline. This is assigned in Populate instead of
+ # Build to prevent erroring out on ambiguous refs.
+ pipeline.protected = @command.protected_ref?
+
+ ##
+ # Populate pipeline with block argument of CreatePipelineService#execute.
+ #
+ @command.seeds_block&.call(pipeline)
+
+ ##
+ # Gather all runtime build/stage errors
+ #
+ if stage_seeds_errors
+ return error(stage_seeds_errors.join("\n"), config_error: true)
+ end
+
+ @command.stage_seeds = stage_seeds
+ end
+
+ def break?
+ pipeline.errors.any?
+ end
+
+ private
+
+ def stage_seeds_errors
+ stage_seeds.flat_map(&:errors).compact.presence
+ end
+
+ def stage_seeds
+ strong_memoize(:stage_seeds) do
+ seeds = stages_attributes.inject([]) do |previous_stages, attributes|
+ seed = Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, previous_stages)
+ previous_stages + [seed]
+ end
+
+ seeds.select(&:included?)
+ end
+ end
+
+ def stages_attributes
+ @command.config_processor.stages_attributes
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
deleted file mode 100644
index 28c38cc3d18..00000000000
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Pipeline
- module Chain
- module Validate
- class Config < Chain::Base
- include Chain::Helpers
-
- def perform!
- unless @pipeline.config_processor
- unless @pipeline.ci_yaml_file
- return error("Missing #{@pipeline.ci_yaml_file_path} file")
- end
-
- if @command.save_incompleted && @pipeline.has_yaml_errors?
- @pipeline.drop!(:config_error)
- end
-
- error(@pipeline.yaml_errors)
- end
- end
-
- def break?
- @pipeline.errors.any? || @pipeline.persisted?
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index fc9c540088b..dce56b22666 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -28,7 +28,9 @@ module Gitlab
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules
- .new(attributes.delete(:rules))
+ .new(attributes.delete(:rules), default_when: 'on_success')
+ @cache = Seed::Build::Cache
+ .new(pipeline, attributes.delete(:cache))
end
def name
@@ -38,7 +40,7 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
if @using_rules
- included_by_rules?
+ rules_result.pass?
elsif @using_only || @using_except
all_of_only? && none_of_except?
else
@@ -59,6 +61,7 @@ module Gitlab
@seed_attributes
.deep_merge(pipeline_attributes)
.deep_merge(rules_attributes)
+ .deep_merge(cache_attributes)
end
def bridge?
@@ -80,26 +83,14 @@ module Gitlab
end
end
- def scoped_variables_hash
- strong_memoize(:scoped_variables_hash) do
- # This is a temporary piece of technical debt to allow us access
- # to the CI variables to evaluate rules before we persist a Build
- # with the result. We should refactor away the extra Build.new,
- # but be able to get CI Variables directly from the Seed::Build.
- ::Ci::Build.new(
- @seed_attributes.merge(pipeline_attributes)
- ).scoped_variables_hash
- end
- end
-
private
def all_of_only?
- @only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def none_of_except?
- @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def needs_errors
@@ -141,13 +132,27 @@ module Gitlab
}
end
- def included_by_rules?
- rules_attributes[:when] != 'never'
+ def rules_attributes
+ return {} unless @using_rules
+
+ rules_result.build_attributes
end
- def rules_attributes
- strong_memoize(:rules_attributes) do
- @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
+ def rules_result
+ strong_memoize(:rules_result) do
+ @rules.evaluate(@pipeline, evaluate_context)
+ end
+ end
+
+ def evaluate_context
+ strong_memoize(:evaluate_context) do
+ Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes)
+ end
+ end
+
+ def cache_attributes
+ strong_memoize(:cache_attributes) do
+ @cache.build_attributes
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb
new file mode 100644
index 00000000000..7671035b896
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Build
+ class Cache
+ def initialize(pipeline, cache)
+ @pipeline = pipeline
+ local_cache = cache.to_h.deep_dup
+ @key = local_cache.delete(:key)
+ @paths = local_cache.delete(:paths)
+ @policy = local_cache.delete(:policy)
+ @untracked = local_cache.delete(:untracked)
+
+ raise ArgumentError, "unknown cache keys: #{local_cache.keys}" if local_cache.any?
+ end
+
+ def build_attributes
+ {
+ options: {
+ cache: {
+ key: key_string,
+ paths: @paths,
+ policy: @policy,
+ untracked: @untracked
+ }.compact.presence
+ }.compact
+ }
+ end
+
+ private
+
+ def key_string
+ key_from_string || key_from_files
+ end
+
+ def key_from_string
+ @key.to_s if @key.is_a?(String) || @key.is_a?(Symbol)
+ end
+
+ def key_from_files
+ return unless @key.is_a?(Hash)
+
+ [@key[:prefix], files_digest].select(&:present?).join('-')
+ end
+
+ def files_digest
+ hash_of_the_latest_changes || 'default'
+ end
+
+ def hash_of_the_latest_changes
+ return unless Feature.enabled?(:ci_file_based_cache, @pipeline.project, default_enabled: true)
+
+ ids = files.map { |path| last_commit_id_for_path(path) }
+ ids = ids.compact.sort.uniq
+
+ Digest::SHA1.hexdigest(ids.join('-')) if ids.any?
+ end
+
+ def files
+ @key[:files]
+ .to_a
+ .select(&:present?)
+ .uniq
+ end
+
+ def last_commit_id_for_path(path)
+ @pipeline.project.repository.last_commit_id_for_path(@pipeline.sha, path)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index 961012c2cee..910d93f54ce 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -16,7 +16,9 @@ module Gitlab
stale_schedule: 'stale schedule',
job_execution_timeout: 'job execution timeout',
archived_failure: 'archived failure',
- unmet_prerequisites: 'unmet prerequisites'
+ unmet_prerequisites: 'unmet prerequisites',
+ scheduler_failure: 'scheduler failure',
+ data_integrity_failure: 'data integrity failure'
}.freeze
private_constant :REASONS
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 3cdb7b5420c..a60b00b2ee8 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -18,7 +18,7 @@ code_quality:
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:12-0-stable" /code
+ "registry.gitlab.com/gitlab-org/security-products/codequality:12-5-stable" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
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 ae2ff9992f9..7a672f910dd 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,8 +1,8 @@
-.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+.dast-auto-deploy:
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.6.0"
dast_environment_deploy:
- extends: .auto-deploy
+ extends: .dast-auto-deploy
stage: review
script:
- auto-deploy check_kube_domain
@@ -28,10 +28,10 @@ dast_environment_deploy:
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
+ - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
stop_dast_environment:
- extends: .auto-deploy
+ extends: .dast-auto-deploy
stage: cleanup
variables:
GIT_STRATEGY: none
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index a8ec2d4781d..738be44d5f4 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.7.0"
review:
extends: .auto-deploy
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 f058468ed8e..ef2fc561201 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -9,16 +9,17 @@ container_scanning:
name: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CS_MAJOR_VERSION
entrypoint: []
variables:
- # By default, use the latest clair vulnerabilities database, however, allow it to be overridden here
- # with a specific version to provide consistency for integration testing purposes
- CLAIR_DB_IMAGE_TAG: latest
- # Override this variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yaml` file.
- # See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
+ # 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"
+ # 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
GIT_STRATEGY: none
allow_failure: true
services:
- - name: arminc/clair-db:$CLAIR_DB_IMAGE_TAG
+ - name: $CLAIR_DB_IMAGE
alias: clair-vulnerabilities-db
script:
# the kubernetes executor currently ignores the Docker image entrypoint value, so the start.sh script must
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 c8930bc6263..4993d22d400 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -4,6 +4,12 @@
# List of the variables: https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
+variables:
+ DS_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+ DS_DEFAULT_ANALYZERS: "gemnasium, retire.js, gemnasium-python, gemnasium-maven, bundler-audit"
+ DS_MAJOR_VERSION: 2
+ DS_DISABLE_DIND: "false"
+
dependency_scanning:
stage: test
image: docker:stable
@@ -45,6 +51,7 @@ dependency_scanning:
DS_PIP_DEPENDENCY_PATH \
PIP_INDEX_URL \
PIP_EXTRA_INDEX_URL \
+ MAVEN_CLI_OPTS \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
@@ -61,3 +68,63 @@ dependency_scanning:
except:
variables:
- $DEPENDENCY_SCANNING_DISABLED
+ - $DS_DISABLE_DIND == 'true'
+
+.analyzer:
+ extends: dependency_scanning
+ services: []
+ except:
+ variables:
+ - $DS_DISABLE_DIND == 'false'
+ script:
+ - /analyzer run
+
+gemnasium-dependency_scanning:
+ extends: .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/
+
+gemnasium-maven-dependency_scanning:
+ extends: .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 =~ /\bjava\b/
+
+gemnasium-python-dependency_scanning:
+ extends: .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/
+
+bundler-audit-dependency_scanning:
+ extends: .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/
+
+retire-js-dependency_scanning:
+ extends: .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/
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index a0c2ab3aa26..c81b4efddbc 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -7,7 +7,7 @@
variables:
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex"
- SAST_MAJOR_VERSION: 2
+ SAST_ANALYZER_IMAGE_TAG: 2
SAST_DISABLE_DIND: "false"
sast:
@@ -35,45 +35,12 @@ sast:
export DOCKER_HOST='tcp://localhost:2375'
fi
fi
- - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
- function propagate_env_vars() {
- CURRENT_ENV=$(printenv)
-
- for VAR_NAME; do
- echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
- done
- }
+ - |
+ printenv | grep -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | cut -d'=' -f1 | \
+ (while IFS='\\n' read -r VAR; do unset -v "$VAR"; done; /bin/printenv > .env)
- |
docker run \
- $(propagate_env_vars \
- SAST_BANDIT_EXCLUDED_PATHS \
- SAST_ANALYZER_IMAGES \
- SAST_ANALYZER_IMAGE_PREFIX \
- SAST_ANALYZER_IMAGE_TAG \
- SAST_DEFAULT_ANALYZERS \
- SAST_PULL_ANALYZER_IMAGES \
- SAST_BRAKEMAN_LEVEL \
- SAST_FLAWFINDER_LEVEL \
- SAST_GITLEAKS_ENTROPY_LEVEL \
- SAST_GOSEC_LEVEL \
- SAST_EXCLUDED_PATHS \
- SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- SAST_RUN_ANALYZER_TIMEOUT \
- SAST_JAVA_VERSION \
- ANT_HOME \
- ANT_PATH \
- GRADLE_PATH \
- JAVA_OPTS \
- JAVA_PATH \
- JAVA_8_VERSION \
- JAVA_11_VERSION \
- MAVEN_CLI_OPTS \
- MAVEN_PATH \
- MAVEN_REPO_PATH \
- SBT_PATH \
- FAIL_NEVER \
- ) \
+ --env-file .env \
--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
@@ -94,7 +61,7 @@ sast:
bandit-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/bandit:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -104,7 +71,7 @@ bandit-sast:
brakeman-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -114,7 +81,7 @@ brakeman-sast:
eslint-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -124,7 +91,7 @@ eslint-sast:
flawfinder-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/flawfinder:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -134,7 +101,7 @@ flawfinder-sast:
gosec-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/gosec:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -144,7 +111,7 @@ gosec-sast:
nodejs-scan-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -154,7 +121,7 @@ nodejs-scan-sast:
phpcs-security-audit-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/phpcs-security-audit:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -164,7 +131,7 @@ phpcs-security-audit-sast:
pmd-apex-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/pmd-apex:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -174,7 +141,7 @@ pmd-apex-sast:
secrets-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -183,7 +150,7 @@ secrets-sast:
security-code-scan-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/security-code-scan:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -193,7 +160,7 @@ security-code-scan-sast:
sobelow-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/sobelow:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -203,7 +170,7 @@ sobelow-sast:
spotbugs-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/spotbugs:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
@@ -213,7 +180,7 @@ spotbugs-sast:
tslint-sast:
extends: .analyzer
image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/tslint:$SAST_MAJOR_VERSION"
+ name: "$SAST_ANALYZER_IMAGE_PREFIX/tslint:$SAST_ANALYZER_IMAGE_TAG"
only:
variables:
- $GITLAB_FEATURES =~ /\bsast\b/ &&
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index f6a3abefcfb..833c545fc5b 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -39,15 +39,15 @@ module Gitlab
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
- yaml_variables: yaml_variables(name),
- needs_attributes: job[:needs]&.map { |need| { name: need } },
+ yaml_variables: transform_to_yaml_variables(job_variables(name)),
+ needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
rules: job[:rules],
+ cache: job[:cache],
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
- cache: job[:cache],
dependencies: job[:dependencies],
job_timeout: job[:timeout],
before_script: job[:before_script],
@@ -59,7 +59,7 @@ module Gitlab
instance: job[:instance],
start_in: job[:start_in],
trigger: job[:trigger],
- bridge_needs: job[:needs]
+ bridge_needs: job.dig(:needs, :bridge)&.first
}.compact }.compact
end
@@ -83,6 +83,13 @@ module Gitlab
end
end
+ def workflow_attributes
+ {
+ rules: @config.dig(:workflow, :rules),
+ yaml_variables: transform_to_yaml_variables(@variables)
+ }
+ end
+
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
@@ -118,20 +125,17 @@ module Gitlab
end
end
- def yaml_variables(name)
- variables = (@variables || {})
- .merge(job_variables(name))
+ def job_variables(name)
+ job_variables = @jobs.dig(name.to_sym, :variables)
- variables.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
+ @variables.to_h
+ .merge(job_variables.to_h)
end
- def job_variables(name)
- job = @jobs[name.to_sym]
- return {} unless job
-
- job[:variables] || {}
+ def transform_to_yaml_variables(variables)
+ variables.to_h.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
end
def validate_job_stage!(name, job)
@@ -159,17 +163,19 @@ module Gitlab
end
def validate_job_needs!(name, job)
- return unless job[:needs]
+ return unless job.dig(:needs, :job)
stage_index = @stages.index(job[:stage])
- job[:needs].each do |need|
- raise ValidationError, "#{name} job: undefined need: #{need}" unless @jobs[need.to_sym]
+ job.dig(:needs, :job).each do |need|
+ need_job_name = need[:name]
+
+ raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]
- needs_stage_index = @stages.index(@jobs[need.to_sym][:stage])
+ needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage])
unless needs_stage_index.present? && needs_stage_index < stage_index
- raise ValidationError, "#{name} job: need #{need} is not defined in prior stages"
+ raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages"
end
end
end
diff --git a/lib/gitlab/cleanup/orphan_job_artifact_files.rb b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
index 1b01ca25559..020de45e5bf 100644
--- a/lib/gitlab/cleanup/orphan_job_artifact_files.rb
+++ b/lib/gitlab/cleanup/orphan_job_artifact_files.rb
@@ -8,7 +8,8 @@ module Gitlab
ABSOLUTE_ARTIFACT_DIR = ::JobArtifactUploader.root.freeze
LOST_AND_FOUND = File.join(ABSOLUTE_ARTIFACT_DIR, '-', 'lost+found').freeze
BATCH_SIZE = 500
- DEFAULT_NICENESS = 'Best-effort'
+ DEFAULT_NICENESS = 'best-effort'
+ VALID_NICENESS_LEVELS = %w{none realtime best-effort idle}.freeze
attr_accessor :batch, :total_found, :total_cleaned
attr_reader :limit, :dry_run, :niceness, :logger
@@ -16,7 +17,7 @@ module Gitlab
def initialize(limit: nil, dry_run: true, niceness: nil, logger: nil)
@limit = limit
@dry_run = dry_run
- @niceness = niceness || DEFAULT_NICENESS
+ @niceness = (niceness || DEFAULT_NICENESS).downcase
@logger = logger || Rails.logger # rubocop:disable Gitlab/RailsLogger
@total_found = @total_cleaned = 0
@@ -35,7 +36,7 @@ module Gitlab
clean_batch!
- log_info("Processed #{total_found} job artifacts to find and clean #{total_cleaned} orphans.")
+ log_info("Processed #{total_found} job artifact(s) to find and cleaned #{total_cleaned} orphan(s).")
end
private
@@ -75,7 +76,7 @@ module Gitlab
def find_artifacts
Open3.popen3(*find_command) do |stdin, stdout, stderr, status_thread|
stdout.each_line do |line|
- yield line
+ yield line.chomp
end
log_error(stderr.read.color(:red)) unless status_thread.value.success?
@@ -99,7 +100,7 @@ module Gitlab
cmd += %w[-type d]
if ionice
- raise ArgumentError, 'Invalid niceness' unless niceness.match?(/^\w[\w\-]*$/)
+ raise ArgumentError, 'Invalid niceness' unless VALID_NICENESS_LEVELS.include?(niceness)
cmd.unshift(*%W[#{ionice} --class #{niceness}])
end
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index 294ffad02ce..2b3dc94fc5e 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -10,38 +10,39 @@ module Gitlab
#
# We have the following lifecycle events.
#
- # - on_master_start:
+ # - on_before_fork (on master process):
#
# Unicorn/Puma Cluster: This will be called exactly once,
# on startup, before the workers are forked. This is
# called in the PARENT/MASTER process.
#
- # Sidekiq/Puma Single: This is called immediately.
+ # Sidekiq/Puma Single: This is not called.
#
- # - on_before_fork:
+ # - on_master_start (on master process):
#
# Unicorn/Puma Cluster: This will be called exactly once,
# on startup, before the workers are forked. This is
# called in the PARENT/MASTER process.
#
- # Sidekiq/Puma Single: This is not called.
+ # Sidekiq/Puma Single: This is called immediately.
#
- # - on_worker_start:
+ # - on_before_blackout_period (on master process):
#
- # Unicorn/Puma Cluster: This is called in the worker process
- # exactly once before processing requests.
+ # Unicorn/Puma Cluster: This will be called before a blackout
+ # period when performing graceful shutdown of master.
+ # This is called on `master` process.
#
- # Sidekiq/Puma Single: This is called immediately.
+ # Sidekiq/Puma Single: This is not called.
#
- # - on_before_phased_restart:
+ # - on_before_graceful_shutdown (on master process):
#
# Unicorn/Puma Cluster: This will be called before a graceful
- # shutdown of workers starts happening.
+ # shutdown of workers starts happening, but after blackout period.
# This is called on `master` process.
#
# Sidekiq/Puma Single: This is not called.
#
- # - on_before_master_restart:
+ # - on_before_master_restart (on master process):
#
# Unicorn: This will be called before a new master is spun up.
# This is called on forked master before `execve` to become
@@ -53,6 +54,13 @@ module Gitlab
#
# Sidekiq/Puma Single: This is not called.
#
+ # - on_worker_start (on worker process):
+ #
+ # Unicorn/Puma Cluster: This is called in the worker process
+ # exactly once before processing requests.
+ #
+ # Sidekiq/Puma Single: This is called immediately.
+ #
# Blocks will be executed in the order in which they are registered.
#
class LifecycleEvents
@@ -75,9 +83,15 @@ module Gitlab
end
# Read the config/initializers/cluster_events_before_phased_restart.rb
- def on_before_phased_restart(&block)
+ def on_before_blackout_period(&block)
# Defer block execution
- (@master_phased_restart ||= []) << block
+ (@master_blackout_period ||= []) << block
+ end
+
+ # Read the config/initializers/cluster_events_before_phased_restart.rb
+ def on_before_graceful_shutdown(&block)
+ # Defer block execution
+ (@master_graceful_shutdown ||= []) << block
end
def on_before_master_restart(&block)
@@ -97,27 +111,24 @@ module Gitlab
# Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
#
def do_worker_start
- @worker_start_hooks&.each do |block|
- block.call
- end
+ call(@worker_start_hooks)
end
def do_before_fork
- @before_fork_hooks&.each do |block|
- block.call
- end
+ call(@before_fork_hooks)
end
- def do_before_phased_restart
- @master_phased_restart&.each do |block|
- block.call
- end
+ def do_before_graceful_shutdown
+ call(@master_blackout_period)
+
+ blackout_seconds = ::Settings.shutdown.blackout_seconds.to_i
+ sleep(blackout_seconds) if blackout_seconds > 0
+
+ call(@master_graceful_shutdown)
end
def do_before_master_restart
- @master_restart_hooks&.each do |block|
- block.call
- end
+ call(@master_restart_hooks)
end
# DEPRECATED
@@ -132,6 +143,10 @@ module Gitlab
private
+ def call(hooks)
+ hooks&.each(&:call)
+ end
+
def in_clustered_environment?
# Sidekiq doesn't fork
return false if Sidekiq.server?
diff --git a/lib/gitlab/cluster/mixins/puma_cluster.rb b/lib/gitlab/cluster/mixins/puma_cluster.rb
index e9157d9f1e4..106c2731c07 100644
--- a/lib/gitlab/cluster/mixins/puma_cluster.rb
+++ b/lib/gitlab/cluster/mixins/puma_cluster.rb
@@ -8,8 +8,12 @@ module Gitlab
raise 'missing method Puma::Cluster#stop_workers' unless base.method_defined?(:stop_workers)
end
+ # This looks at internal status of `Puma::Cluster`
+ # https://github.com/puma/puma/blob/v3.12.1/lib/puma/cluster.rb#L333
def stop_workers
- Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+ if @status == :stop # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+ end
super
end
diff --git a/lib/gitlab/cluster/mixins/unicorn_http_server.rb b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
index 765fd0c2baa..440ed02a355 100644
--- a/lib/gitlab/cluster/mixins/unicorn_http_server.rb
+++ b/lib/gitlab/cluster/mixins/unicorn_http_server.rb
@@ -5,11 +5,26 @@ module Gitlab
module Mixins
module UnicornHttpServer
def self.prepended(base)
- raise 'missing method Unicorn::HttpServer#reexec' unless base.method_defined?(:reexec)
+ unless base.method_defined?(:reexec) && base.method_defined?(:stop)
+ raise 'missing method Unicorn::HttpServer#reexec or Unicorn::HttpServer#stop'
+ end
end
def reexec
- Gitlab::Cluster::LifecycleEvents.do_before_phased_restart
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+
+ super
+ end
+
+ # The stop on non-graceful shutdown is executed twice:
+ # `#stop(false)` and `#stop`.
+ #
+ # The first stop will wipe-out all workers, so we need to check
+ # the flag and a list of workers
+ def stop(graceful = true)
+ if graceful && @workers.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ Gitlab::Cluster::LifecycleEvents.do_before_graceful_shutdown
+ end
super
end
diff --git a/lib/gitlab/cluster/puma_worker_killer_initializer.rb b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
index a8440b63baa..92c799875b5 100644
--- a/lib/gitlab/cluster/puma_worker_killer_initializer.rb
+++ b/lib/gitlab/cluster/puma_worker_killer_initializer.rb
@@ -3,7 +3,12 @@
module Gitlab
module Cluster
class PumaWorkerKillerInitializer
- def self.start(puma_options, puma_per_worker_max_memory_mb: 850, puma_master_max_memory_mb: 550)
+ def self.start(
+ puma_options,
+ puma_per_worker_max_memory_mb: 850,
+ puma_master_max_memory_mb: 550,
+ additional_puma_dev_max_memory_mb: 200
+ )
require 'puma_worker_killer'
PumaWorkerKiller.config do |config|
@@ -14,7 +19,11 @@ module Gitlab
# The Puma Worker Killer checks the total RAM used by both the master
# and worker processes.
# https://github.com/schneems/puma_worker_killer/blob/v0.1.0/lib/puma_worker_killer/puma_memory.rb#L57
- config.ram = puma_master_max_memory_mb + (worker_count * puma_per_worker_max_memory_mb)
+ #
+ # Additional memory is added when running in `development`
+ config.ram = puma_master_max_memory_mb +
+ (worker_count * puma_per_worker_max_memory_mb) +
+ (Rails.env.development? ? (1 + worker_count) * additional_puma_dev_max_memory_mb : 0)
config.frequency = 20 # seconds
diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb
index b7ec4b7c4f8..bda84dc2cff 100644
--- a/lib/gitlab/config/entry/configurable.rb
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -29,22 +29,24 @@ module Gitlab
def compose!(deps = nil)
return unless valid?
- self.class.nodes.each do |key, factory|
- # If we override the config type validation
- # we can end with different config types like String
- next unless config.is_a?(Hash)
+ super do
+ self.class.nodes.each do |key, factory|
+ # If we override the config type validation
+ # we can end with different config types like String
+ next unless config.is_a?(Hash)
- factory
- .value(config[key])
- .with(key: key, parent: self)
+ factory
+ .value(config[key])
+ .with(key: key, parent: self)
- entries[key] = factory.create!
- end
+ entries[key] = factory.create!
+ end
- yield if block_given?
+ yield if block_given?
- entries.each_value do |entry|
- entry.compose!(deps)
+ entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -67,12 +69,13 @@ module Gitlab
private
# rubocop: disable CodeReuse/ActiveRecord
- def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil)
+ def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: description)
.with(default: default)
.with(inherit: inherit)
.with(reserved: reserved)
+ .metadata(metadata)
(@nodes ||= {}).merge!(key.to_sym => factory)
end
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
index 8f1f4a81bb5..7c5ffaa7621 100644
--- a/lib/gitlab/config/entry/factory.rb
+++ b/lib/gitlab/config/entry/factory.rb
@@ -9,10 +9,12 @@ module Gitlab
class Factory
InvalidFactory = Class.new(StandardError)
- def initialize(entry)
- @entry = entry
+ attr_reader :entry_class
+
+ def initialize(entry_class)
+ @entry_class = entry_class
@metadata = {}
- @attributes = { default: entry.default }
+ @attributes = { default: entry_class.default }
end
def value(value)
@@ -34,6 +36,10 @@ module Gitlab
@attributes[:description]
end
+ def inherit
+ @attributes[:inherit]
+ end
+
def inheritable?
@attributes[:inherit]
end
@@ -52,7 +58,7 @@ module Gitlab
if @value.nil?
Entry::Unspecified.new(fabricate_unspecified)
else
- fabricate(@entry, @value)
+ fabricate(entry_class, @value)
end
end
@@ -68,12 +74,12 @@ module Gitlab
if default.nil?
fabricate(Entry::Undefined)
else
- fabricate(@entry, default)
+ fabricate(entry_class, default)
end
end
- def fabricate(entry, value = nil)
- entry.new(value, @metadata) do |node|
+ def fabricate(entry_class, value = nil)
+ entry_class.new(value, @metadata) do |node|
node.key = @attributes[:key]
node.parent = @attributes[:parent]
node.default = @attributes[:default]
diff --git a/lib/gitlab/config/entry/inheritable.rb b/lib/gitlab/config/entry/inheritable.rb
new file mode 100644
index 00000000000..91ca82e6338
--- /dev/null
+++ b/lib/gitlab/config/entry/inheritable.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents an inheritable configs.
+ #
+ module Inheritable
+ InheritError = Class.new(Gitlab::Config::Loader::FormatError)
+
+ def compose!(deps = nil, &blk)
+ super(deps, &blk)
+
+ inherit!(deps)
+ end
+
+ private
+
+ # We inherit config entries from `default:`
+ # if the entry has the `inherit: true` flag set
+ def inherit!(deps)
+ return unless deps
+
+ self.class.nodes.each do |key, factory|
+ next unless factory.inheritable?
+
+ new_entry = overwrite_entry(deps, key, self[key])
+
+ entries[key] = new_entry if new_entry&.specified?
+ end
+ end
+
+ def overwrite_entry(deps, key, current_entry)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
index e014f15fbd8..84d3409ed91 100644
--- a/lib/gitlab/config/entry/node.rb
+++ b/lib/gitlab/config/entry/node.rb
@@ -112,6 +112,10 @@ module Gitlab
@aspects ||= []
end
+ def self.with_aspect(blk)
+ self.aspects.append(blk)
+ end
+
private
attr_reader :entries
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
index d58aba07d15..315f1947e2c 100644
--- a/lib/gitlab/config/entry/simplifiable.rb
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -4,11 +4,11 @@ module Gitlab
module Config
module Entry
class Simplifiable < SimpleDelegator
- EntryStrategy = Struct.new(:name, :condition)
+ EntryStrategy = Struct.new(:name, :klass, :condition)
attr_reader :subject
- def initialize(config, **metadata)
+ def initialize(config, **metadata, &blk)
unless self.class.const_defined?(:UnknownStrategy)
raise ArgumentError, 'UndefinedStrategy not available!'
end
@@ -19,14 +19,13 @@ module Gitlab
entry = self.class.entry_class(strategy)
- @subject = entry.new(config, metadata)
+ @subject = entry.new(config, metadata, &blk)
- yield(@subject) if block_given?
super(@subject)
end
def self.strategy(name, **opts)
- EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
+ EntryStrategy.new(name, opts.dig(:class), opts.fetch(:if)).tap do |strategy|
strategies.append(strategy)
end
end
@@ -37,7 +36,7 @@ module Gitlab
def self.entry_class(strategy)
if strategy.present?
- self.const_get(strategy.name, false)
+ strategy.klass || self.const_get(strategy.name, false)
else
self::UnknownStrategy
end
diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb
index 1c88c68c11c..45b852dc2e0 100644
--- a/lib/gitlab/config/entry/validatable.rb
+++ b/lib/gitlab/config/entry/validatable.rb
@@ -7,14 +7,27 @@ module Gitlab
extend ActiveSupport::Concern
def self.included(node)
- node.aspects.append -> do
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
+ node.with_aspect -> do
+ validate(:new)
end
end
+ def validator
+ @validator ||= self.class.validator.new(self)
+ end
+
+ def validate(context = nil)
+ validator.validate(context)
+ end
+
+ def compose!(deps = nil, &blk)
+ super(deps, &blk)
+
+ validate(:composed)
+ end
+
def errors
- @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ validator.messages + descendants.flat_map(&:errors)
end
class_methods do
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 374f929878e..d1c23c41d35 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -61,8 +61,15 @@ module Gitlab
include LegacyValidationHelpers
def validate_each(record, attribute, value)
- unless validate_array_of_strings(value)
- record.errors.add(attribute, 'should be an array of strings')
+ valid = validate_array_of_strings(value)
+
+ record.errors.add(attribute, 'should be an array of strings') unless valid
+
+ if valid && options[:with]
+ unless value.all? { |v| v =~ options[:with] }
+ message = options[:message] || 'contains elements that do not match the format'
+ record.errors.add(attribute, message)
+ end
end
end
end
@@ -221,6 +228,34 @@ module Gitlab
end
end
+ class NestedArrayOfStringsValidator < ArrayOfStringsOrStringValidator
+ def validate_each(record, attribute, value)
+ unless validate_nested_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array containing strings and arrays of strings')
+ end
+ end
+
+ private
+
+ def validate_nested_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |element| validate_array_of_strings_or_string(element) }
+ end
+ end
+
+ class StringOrNestedArrayOfStringsValidator < NestedArrayOfStringsValidator
+ def validate_each(record, attribute, value)
+ unless validate_string_or_nested_array_of_strings(value)
+ record.errors.add(attribute, 'should be a string or an array containing strings and arrays of strings')
+ end
+ end
+
+ private
+
+ def validate_string_or_nested_array_of_strings(values)
+ validate_string(values) || validate_nested_array_of_strings(values)
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/cycle_analytics/group_stage_summary.rb b/lib/gitlab/cycle_analytics/group_stage_summary.rb
index a1fc941495d..26eaaf7df83 100644
--- a/lib/gitlab/cycle_analytics/group_stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/group_stage_summary.rb
@@ -3,18 +3,17 @@
module Gitlab
module CycleAnalytics
class GroupStageSummary
- attr_reader :group, :from, :current_user, :options
+ attr_reader :group, :current_user, :options
def initialize(group, options:)
@group = group
- @from = options[:from]
@current_user = options[:current_user]
@options = options
end
def data
- [serialize(Summary::Group::Issue.new(group: group, from: from, current_user: current_user, options: options)),
- serialize(Summary::Group::Deploy.new(group: group, from: from, options: options))]
+ [serialize(Summary::Group::Issue.new(group: group, current_user: current_user, options: options)),
+ serialize(Summary::Group::Deploy.new(group: group, options: options))]
end
private
diff --git a/lib/gitlab/cycle_analytics/summary/group/base.rb b/lib/gitlab/cycle_analytics/summary/group/base.rb
index 48d8164bde1..f1d20d5aefa 100644
--- a/lib/gitlab/cycle_analytics/summary/group/base.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/base.rb
@@ -5,11 +5,10 @@ module Gitlab
module Summary
module Group
class Base
- attr_reader :group, :from, :options
+ attr_reader :group, :options
- def initialize(group:, from:, options:)
+ def initialize(group:, options:)
@group = group
- @from = from
@options = options
end
diff --git a/lib/gitlab/cycle_analytics/summary/group/deploy.rb b/lib/gitlab/cycle_analytics/summary/group/deploy.rb
index 78d677cf558..11a9152cf0c 100644
--- a/lib/gitlab/cycle_analytics/summary/group/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/deploy.rb
@@ -20,7 +20,8 @@ module Gitlab
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 > ?", from)
+ deployments = deployments.where("deployments.created_at > ?", options[:from])
+ deployments = deployments.where("deployments.created_at < ?", options[:to]) if options[:to]
deployments.success.count
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/group/issue.rb b/lib/gitlab/cycle_analytics/summary/group/issue.rb
index 9daae8531d8..4d5ee1d43ca 100644
--- a/lib/gitlab/cycle_analytics/summary/group/issue.rb
+++ b/lib/gitlab/cycle_analytics/summary/group/issue.rb
@@ -5,11 +5,10 @@ module Gitlab
module Summary
module Group
class Issue < Group::Base
- attr_reader :group, :from, :current_user, :options
+ attr_reader :group, :current_user, :options
- def initialize(group:, from:, current_user:, options:)
+ def initialize(group:, current_user:, options:)
@group = group
- @from = from
@current_user = current_user
@options = options
end
@@ -25,10 +24,19 @@ module Gitlab
private
def find_issues
- issues = IssuesFinder.new(current_user, group_id: group.id, include_subgroups: true, created_after: from).execute
+ 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
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 8a253893892..ddb9d907640 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -28,6 +28,10 @@ module Gitlab
true
end
+ def thread_name
+ self.class.name.demodulize.underscore
+ end
+
def start
return unless enabled?
@@ -35,7 +39,10 @@ module Gitlab
break thread if thread?
if start_working
- @thread = Thread.new { run_thread }
+ @thread = Thread.new do
+ Thread.current.name = thread_name
+ run_thread
+ end
end
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index f22fc41a6d8..0e7e0c40a8a 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -93,8 +93,8 @@ module Gitlab
docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
qa: "~QA",
- test: "~test for `spec/features/*`",
- engineering_productivity: "Engineering Productivity for CI config review"
+ test: "~test ~Quality for `spec/features/*`",
+ engineering_productivity: '~"Engineering Productivity" for CI, Danger'
}.freeze
CATEGORIES = {
%r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
@@ -104,7 +104,7 @@ module Gitlab
%r{\A(ee/)?public/} => :frontend,
%r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend,
%r{\A(ee/)?vendor/assets/} => :frontend,
- %r{\Ascripts/frontend/} => :frontend,
+ %r{\A(ee/)?scripts/frontend/} => :frontend,
%r{(\A|/)(
\.babelrc |
\.eslintignore |
@@ -130,14 +130,18 @@ module Gitlab
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
%r{\Arubocop/cop/migration(/|\.rb)} => :database,
+ %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
+ %r{Dangerfile\z} => :engineering_productivity,
+ %r{\A(ee/)?(danger/|lib/gitlab/danger/)} => :engineering_productivity,
+ %r{\A(ee/)?scripts/} => :engineering_productivity,
+
%r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
- %r{\A(ee/)?(bin|config|danger|generator_templates|lib|rubocop|scripts)/} => :backend,
+ %r{\A(ee/)?(bin|config|generator_templates|lib|rubocop)/} => :backend,
%r{\A(ee/)?spec/features/} => :test,
%r{\A(ee/)?spec/(?!javascripts|frontend)[^/]+} => :backend,
%r{\A(ee/)?vendor/(?!assets)[^/]+} => :backend,
%r{\A(ee/)?vendor/(languages\.yml|licenses\.csv)\z} => :backend,
- %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
- %r{\A(Dangerfile|Gemfile|Gemfile.lock|Procfile|Rakefile)\z} => :backend,
+ %r{\A(Gemfile|Gemfile.lock|Procfile|Rakefile)\z} => :backend,
%r{\A[A-Z_]+_VERSION\z} => :backend,
%r{\A\.rubocop(_todo)?\.yml\z} => :backend,
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 5c2324836d7..e96f5177195 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -67,7 +67,10 @@ module Gitlab
area && labels.any?("devops::#{area.downcase}") if kind == :reviewer
when :engineering_productivity
- role[/Engineering Productivity/] if kind == :reviewer
+ return false unless role[/Engineering Productivity/]
+ return true if kind == :reviewer
+
+ capabilities(project).include?("#{kind} backend")
else
capabilities(project).include?("#{kind} #{category}")
end
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
index f11e032ab84..70587b3132a 100644
--- a/lib/gitlab/data_builder/deployment.rb
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -6,11 +6,17 @@ module Gitlab
extend self
def build(deployment)
+ # Deployments will not have a deployable when created using the API.
+ deployable_url =
+ if deployment.deployable
+ Gitlab::UrlBuilder.build(deployment.deployable)
+ end
+
{
object_kind: 'deployment',
status: deployment.status,
deployable_id: deployment.deployable_id,
- deployable_url: Gitlab::UrlBuilder.build(deployment.deployable),
+ deployable_url: deployable_url,
environment: deployment.environment.name,
project: deployment.project.hook_attrs,
short_sha: deployment.short_sha,
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index a83b03f540c..65cfd47e1e8 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -19,12 +19,25 @@ module Gitlab
user_email: "john@example.com",
user_avatar: "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
project_id: 15,
+ project: {
+ id: 15,
+ name: "gitlab",
+ description: "",
+ web_url: "http://test.example.com/gitlab/gitlab",
+ avatar_url: "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ git_ssh_url: "git@test.example.com:gitlab/gitlab.git",
+ git_http_url: "http://test.example.com/gitlab/gitlab.git",
+ namespace: "gitlab",
+ visibility_level: 0,
+ path_with_namespace: "gitlab/gitlab",
+ default_branch: "master"
+ },
commits: [
{
id: "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
message: "Add simple search to projects in public area",
timestamp: "2013-05-13T18:18:08+00:00",
- url: "https://test.example.com/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ url: "https://test.example.com/gitlab/gitlab/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
author: {
name: "Test User",
email: "test@example.com"
@@ -45,7 +58,20 @@ module Gitlab
# user_name: String,
# user_username: String,
# user_email: String
- # project_id: String,
+ # project_id: Fixnum,
+ # project: {
+ # id: Fixnum,
+ # name: String,
+ # description: String,
+ # web_url: String,
+ # avatar_url: String,
+ # git_ssh_url: String,
+ # git_http_url: String,
+ # namespace: String,
+ # visibility_level: Fixnum,
+ # path_with_namespace: String,
+ # default_branch: String
+ # }
# repository: {
# name: String,
# url: String,
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index ae29546cdac..7ea7565f758 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -108,9 +108,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists?(table_name, column_name, options)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}" # rubocop:disable Gitlab/RailsLogger
@@ -136,9 +134,7 @@ module Gitlab
'in the body of your migration class'
end
- if supports_drop_index_concurrently?
- options = options.merge({ algorithm: :concurrently })
- end
+ options = options.merge({ algorithm: :concurrently })
unless index_exists_by_name?(table_name, index_name)
Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}" # rubocop:disable Gitlab/RailsLogger
@@ -150,13 +146,6 @@ module Gitlab
end
end
- # Only available on Postgresql >= 9.2
- def supports_drop_index_concurrently?
- version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
-
- version >= 90200
- end
-
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking
@@ -966,7 +955,7 @@ into similar problems in the future (e.g. when new tables are created).
table_name = model_class.quoted_table_name
model_class.each_batch(of: batch_size) do |relation|
- start_id, end_id = relation.pluck("MIN(#{table_name}.id), MAX(#{table_name}.id)").first
+ start_id, end_id = relation.pluck("MIN(#{table_name}.id)", "MAX(#{table_name}.id)").first
if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
# Note: This code path generally only helps with many millions of rows
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
index 3e8a9b89998..cea25967801 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -66,11 +66,13 @@ module Gitlab
def move_repositories(namespace, old_full_path, new_full_path)
repo_shards_for_namespace(namespace).each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage, old_full_path)
+ Gitlab::GitalyClient::NamespaceService.allow do
+ gitlab_shell.add_namespace(repository_storage, old_full_path)
- unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
- message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
- Rails.logger.error message # rubocop:disable Gitlab/RailsLogger
+ unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
+ message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
+ Rails.logger.error message # rubocop:disable Gitlab/RailsLogger
+ end
end
end
end
diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
index dfef158cc1d..8cd9694b741 100644
--- a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
+++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb
@@ -21,7 +21,6 @@ module Gitlab
:create_project,
:save_project_id,
:add_group_members,
- :add_to_whitelist,
:add_prometheus_manual_configuration
def initialize
@@ -126,28 +125,6 @@ module Gitlab
end
end
- def add_to_whitelist(result)
- return success(result) unless prometheus_enabled?
- return success(result) unless prometheus_listen_address.present?
-
- uri = parse_url(internal_prometheus_listen_address_uri)
- return error(_('Prometheus listen_address in config/gitlab.yml is not a valid URI')) unless uri
-
- application_settings.add_to_outbound_local_requests_whitelist([uri.normalized_host])
- response = application_settings.save
-
- if response
- # Expire the Gitlab::CurrentSettings cache after updating the whitelist.
- # This happens automatically in an after_commit hook, but in migrations,
- # the after_commit hook only runs at the end of the migration.
- Gitlab::CurrentSettings.expire_current_application_settings
- success(result)
- else
- log_error("Could not add prometheus URL to whitelist, errors: %{errors}" % { errors: application_settings.errors.full_messages })
- error(_('Could not add prometheus URL to whitelist'))
- end
- end
-
def add_prometheus_manual_configuration(result)
return success(result) unless prometheus_enabled?
return success(result) unless prometheus_listen_address.present?
@@ -176,19 +153,11 @@ module Gitlab
end
def prometheus_enabled?
- Gitlab.config.prometheus.enable if Gitlab.config.prometheus
- rescue Settingslogic::MissingSetting
- log_error('prometheus.enable is not present in config/gitlab.yml')
-
- false
+ ::Gitlab::Prometheus::Internal.prometheus_enabled?
end
def prometheus_listen_address
- Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
- rescue Settingslogic::MissingSetting
- log_error('Prometheus listen_address is not present in config/gitlab.yml')
-
- nil
+ ::Gitlab::Prometheus::Internal.listen_address
end
def instance_admins
@@ -231,23 +200,7 @@ module Gitlab
end
def internal_prometheus_listen_address_uri
- if prometheus_listen_address.starts_with?('0.0.0.0:')
- # 0.0.0.0:9090
- port = ':' + prometheus_listen_address.split(':').second
- 'http://localhost' + port
-
- elsif prometheus_listen_address.starts_with?(':')
- # :9090
- 'http://localhost' + prometheus_listen_address
-
- elsif prometheus_listen_address.starts_with?('http')
- # https://localhost:9090
- prometheus_listen_address
-
- else
- # localhost:9090
- 'http://' + prometheus_listen_address
- end
+ ::Gitlab::Prometheus::Internal.uri
end
def prometheus_service_attributes
diff --git a/lib/gitlab/devise_failure.rb b/lib/gitlab/devise_failure.rb
index 4d27b706e1e..59a7c4a6660 100644
--- a/lib/gitlab/devise_failure.rb
+++ b/lib/gitlab/devise_failure.rb
@@ -2,6 +2,8 @@
module Gitlab
class DeviseFailure < Devise::FailureApp
+ include ::SessionsHelper
+
# If the request format is not known, send a redirect instead of a 401
# response, since this is the outcome we're most likely to want
def http_auth?
@@ -9,5 +11,11 @@ module Gitlab
request_format && super
end
+
+ def respond
+ limit_session_time
+
+ super
+ end
end
end
diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb
new file mode 100644
index 00000000000..225280a42f4
--- /dev/null
+++ b/lib/gitlab/error_tracking/detailed_error.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class DetailedError
+ include ActiveModel::Model
+
+ attr_accessor :count,
+ :culprit,
+ :external_base_url,
+ :external_url,
+ :first_release_last_commit,
+ :first_release_short_version,
+ :first_seen,
+ :frequency,
+ :id,
+ :last_release_last_commit,
+ :last_release_short_version,
+ :last_seen,
+ :message,
+ :project_id,
+ :project_name,
+ :project_slug,
+ :short_id,
+ :status,
+ :title,
+ :type,
+ :user_count
+ end
+ end
+end
diff --git a/lib/gitlab/error_tracking/error_event.rb b/lib/gitlab/error_tracking/error_event.rb
new file mode 100644
index 00000000000..c6e0d82f868
--- /dev/null
+++ b/lib/gitlab/error_tracking/error_event.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ErrorTracking
+ class ErrorEvent
+ include ActiveModel::Model
+
+ attr_accessor :issue_id, :date_received, :stack_trace_entries
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 3d14a8dde8d..efddda0ec65 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -3,8 +3,6 @@
module Gitlab
module EtagCaching
class Router
- prepend_if_ee('EE::Gitlab::EtagCaching::Router') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
Route = Struct.new(:regexp, :name)
# We enable an ETag for every request matching the regex.
# To match a regex the path needs to match the following:
@@ -80,3 +78,5 @@ module Gitlab
end
end
end
+
+Gitlab::EtagCaching::Router.prepend_if_ee('EE::Gitlab::EtagCaching::Router')
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 895755376ee..948f720b01b 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -14,13 +14,15 @@ module Gitlab
signup_flow: {
feature_toggle: :experimental_separate_sign_up_flow,
environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.1
+ enabled_ratio: 0.1,
+ tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow'
}
}.freeze
# 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.
+ # to controllers and views. It returns true when the experiment is enabled and the user is selected as part
+ # of the experimental group.
#
module ControllerConcern
extend ActiveSupport::Concern
@@ -36,22 +38,67 @@ module Gitlab
cookies.permanent.signed[:experimentation_subject_id] = {
value: SecureRandom.uuid,
domain: :all,
- secure: ::Gitlab.config.gitlab.https
+ secure: ::Gitlab.config.gitlab.https,
+ httponly: true
}
end
def experiment_enabled?(experiment_key)
- Experimentation.enabled?(experiment_key, experimentation_subject_index)
+ Experimentation.enabled_for_user?(experiment_key, experimentation_subject_index) || forced_enabled?(experiment_key)
+ end
+
+ def track_experiment_event(experiment_key, action)
+ track_experiment_event_for(experiment_key, action) do |tracking_data|
+ ::Gitlab::Tracking.event(tracking_data.delete(:category), tracking_data.delete(:action), tracking_data)
+ end
+ end
+
+ def frontend_experimentation_tracking_data(experiment_key, action)
+ track_experiment_event_for(experiment_key, action) do |tracking_data|
+ gon.push(tracking_data: tracking_data)
+ end
end
private
+ def experimentation_subject_id
+ cookies.signed[:experimentation_subject_id]
+ end
+
def experimentation_subject_index
- experimentation_subject_id = cookies.signed[:experimentation_subject_id]
return if experimentation_subject_id.blank?
experimentation_subject_id.delete('-').hex % 100
end
+
+ def track_experiment_event_for(experiment_key, action)
+ return unless Experimentation.enabled?(experiment_key)
+
+ yield experimentation_tracking_data(experiment_key, action)
+ end
+
+ def experimentation_tracking_data(experiment_key, action)
+ {
+ category: tracking_category(experiment_key),
+ action: action,
+ property: tracking_group(experiment_key),
+ label: experimentation_subject_id
+ }
+ end
+
+ def tracking_category(experiment_key)
+ Experimentation.experiment(experiment_key).tracking_category
+ end
+
+ def tracking_group(experiment_key)
+ return unless Experimentation.enabled?(experiment_key)
+
+ experiment_enabled?(experiment_key) ? 'experimental_group' : 'control_group'
+ end
+
+ def forced_enabled?(experiment_key)
+ params.has_key?(:force_experiment) && params[:force_experiment] == experiment_key.to_s
+ end
end
class << self
@@ -59,18 +106,20 @@ module Gitlab
Experiment.new(EXPERIMENTS[key].merge(key: key))
end
- def enabled?(experiment_key, experimentation_subject_index)
+ def enabled?(experiment_key)
return false unless EXPERIMENTS.key?(experiment_key)
experiment = experiment(experiment_key)
+ experiment.feature_toggle_enabled? && experiment.enabled_for_environment?
+ end
- experiment.feature_toggle_enabled? &&
- experiment.enabled_for_environment? &&
- experiment.enabled_for_experimentation_subject?(experimentation_subject_index)
+ def enabled_for_user?(experiment_key, experimentation_subject_index)
+ enabled?(experiment_key) &&
+ experiment(experiment_key).enabled_for_experimentation_subject?(experimentation_subject_index)
end
end
- Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, keyword_init: true) do
+ 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?
diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb
index b5d308e462c..ce1370bab0f 100644
--- a/lib/gitlab/favicon.rb
+++ b/lib/gitlab/favicon.rb
@@ -7,7 +7,7 @@ module Gitlab
image_name =
if appearance.favicon.exists?
appearance.favicon_path
- elsif Gitlab::Utils.to_boolean(ENV['CANARY'])
+ elsif Gitlab.canary?
'favicon-yellow.png'
elsif Rails.env.development?
development_favicon
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 3958814208c..ec9d2df613b 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -15,12 +15,12 @@ module Gitlab
def find(query)
query = Gitlab::Search::Query.new(query, encode_binary: true) do
- filter :filename, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}$/i }
- filter :path, matcher: ->(filter, blob) { blob.binary_filename =~ /#{filter[:regex_value]}/i }
- filter :extension, matcher: ->(filter, blob) { blob.binary_filename =~ /\.#{filter[:regex_value]}$/i }
+ filter :filename, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}$/i }
+ filter :path, matcher: ->(filter, blob) { blob.binary_path =~ /#{filter[:regex_value]}/i }
+ filter :extension, matcher: ->(filter, blob) { blob.binary_path =~ /\.#{filter[:regex_value]}$/i }
end
- files = find_by_filename(query.term) + find_by_content(query.term)
+ files = find_by_path(query.term) + find_by_content(query.term)
files = query.filter_results(files) if query.filters.any?
@@ -35,13 +35,14 @@ module Gitlab
end
end
- def find_by_filename(query)
- search_filenames(query).map do |filename|
- Gitlab::Search::FoundBlob.new(blob_filename: filename, project: project, ref: ref, repository: repository)
+ def find_by_path(query)
+ search_paths(query).map do |path|
+ Gitlab::Search::FoundBlob.new(blob_path: path, project: project, ref: ref, repository: repository)
end
end
- def search_filenames(query)
+ # Overriden in Gitlab::WikiFileFinder
+ def search_paths(query)
repository.search_files_by_name(query, ref)
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 8fac3621df9..6210223917b 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -155,10 +155,6 @@ module Gitlab
end
end
- def extract_signature(repository, commit_id)
- repository.gitaly_commit_client.extract_signature(commit_id)
- end
-
def extract_signature_lazily(repository, commit_id)
BatchLoader.for(commit_id).batch(key: repository) do |commit_ids, loader, args|
batch_signature_extraction(args[:key], commit_ids).each do |commit_id, signature_data|
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index b2c22898079..4971a18e270 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -25,9 +25,18 @@ module Gitlab
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
- CreateTreeError = Class.new(StandardError)
TagExistsError = Class.new(StandardError)
ChecksumError = Class.new(StandardError)
+ class CreateTreeError < StandardError
+ attr_reader :error_code
+
+ def initialize(error_code)
+ super(self.class.name)
+
+ # The value coming from Gitaly is an uppercase String (e.g., "EMPTY")
+ @error_code = error_code.downcase.to_sym
+ end
+ end
# Directory name of repo
attr_reader :name
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index c1bcd8e934a..3025fc6bfdb 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -133,14 +133,6 @@ module Gitlab
GollumSlug.generate(title, format)
end
- def page_formatted_data(title:, dir: nil, version: nil)
- version = version&.id
-
- wrapped_gitaly_errors do
- gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version)
- end
- end
-
private
def gitaly_wiki_client
diff --git a/lib/gitlab/git_access_result/custom_action.rb b/lib/gitlab/git_access_result/custom_action.rb
index a05a4baed82..336f3405f72 100644
--- a/lib/gitlab/git_access_result/custom_action.rb
+++ b/lib/gitlab/git_access_result/custom_action.rb
@@ -3,7 +3,7 @@
module Gitlab
module GitAccessResult
class CustomAction
- attr_reader :payload, :message
+ attr_reader :payload, :console_messages
# Example of payload:
#
@@ -16,9 +16,9 @@ module Gitlab
# }
# }
#
- def initialize(payload, message)
+ def initialize(payload, console_messages)
@payload = payload
- @message = message
+ @console_messages = console_messages
end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index be695e7e91a..5b47853b9c1 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -142,18 +142,39 @@ module Gitlab
# kwargs.merge(deadline: Time.now + 10)
# end
#
- def self.call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout)
- start = Gitlab::Metrics::System.monotonic_time
- request_hash = request.is_a?(Google::Protobuf::MessageExts) ? request.to_h : {}
+ def self.call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout, &block)
+ self.measure_timings(service, rpc, request) do
+ self.execute(storage, service, rpc, request, remote_storage: remote_storage, timeout: timeout, &block)
+ end
+ end
+ # This method is like GitalyClient.call but should be used with
+ # Gitaly streaming RPCs. It measures how long the the RPC took to
+ # produce the full response, not just the initial response.
+ def self.streaming_call(storage, service, rpc, request, remote_storage: nil, timeout: default_timeout)
+ self.measure_timings(service, rpc, request) do
+ response = self.execute(storage, service, rpc, request, remote_storage: remote_storage, timeout: timeout)
+
+ yield(response)
+ end
+ end
+
+ def self.execute(storage, service, rpc, request, remote_storage:, timeout:)
enforce_gitaly_request_limits(:call)
kwargs = request_kwargs(storage, timeout: timeout.to_f, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def self.measure_timings(service, rpc, request)
+ start = Gitlab::Metrics::System.monotonic_time
+
+ yield
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
+ request_hash = request.is_a?(Google::Protobuf::MessageExts) ? request.to_h : {}
# Keep track, separately, for the performance bar
self.query_time += duration
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index b0559729ff3..15318bc817a 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -200,8 +200,9 @@ module Gitlab
to: to
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :commits_between, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def diff_stats(left_commit_sha, right_commit_sha)
@@ -224,8 +225,9 @@ module Gitlab
)
request.order = opts[:order].upcase if opts[:order].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :find_all_commits, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def list_commits_by_oid(oids)
@@ -233,8 +235,9 @@ module Gitlab
request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids)
- response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
rescue GRPC::NotFound # If no repository is found, happens mainly during testing
[]
end
@@ -249,8 +252,9 @@ module Gitlab
offset: offset.to_i
)
- response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout)
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :commits_by_message, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def languages(ref = nil)
@@ -323,9 +327,9 @@ module Gitlab
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?
- response = GitalyClient.call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout)
-
- consume_commits_response(response)
+ GitalyClient.streaming_call(@repository.storage, :commit_service, :find_commits, request, timeout: GitalyClient.medium_timeout) do |response|
+ consume_commits_response(response)
+ end
end
def filter_shas_with_signatures(shas)
@@ -348,25 +352,6 @@ module Gitlab
end
end
- def extract_signature(commit_id)
- request = Gitaly::ExtractCommitSignatureRequest.new(repository: @gitaly_repo, commit_id: commit_id)
- response = GitalyClient.call(@repository.storage, :commit_service, :extract_commit_signature, request, timeout: GitalyClient.fast_timeout)
-
- signature = +''.b
- signed_text = +''.b
-
- response.each do |message|
- signature << message.signature
- signed_text << message.signed_text
- end
-
- return if signature.blank? && signed_text.blank?
-
- [signature, signed_text]
- rescue GRPC::InvalidArgument => ex
- raise ArgumentError, ex
- end
-
def get_commit_signatures(commit_ids)
request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request, timeout: GitalyClient.fast_timeout)
diff --git a/lib/gitlab/gitaly_client/namespace_service.rb b/lib/gitlab/gitaly_client/namespace_service.rb
index 0be214f3035..dbcebec3aa2 100644
--- a/lib/gitlab/gitaly_client/namespace_service.rb
+++ b/lib/gitlab/gitaly_client/namespace_service.rb
@@ -3,14 +3,23 @@
module Gitlab
module GitalyClient
class NamespaceService
- def initialize(storage)
- @storage = storage
+ extend Gitlab::TemporarilyAllow
+
+ NamespaceServiceAccessError = Class.new(StandardError)
+ ALLOW_KEY = :allow_namespace
+
+ def self.allow
+ temporarily_allow(ALLOW_KEY) { yield }
end
- def exists?(name)
- request = Gitaly::NamespaceExistsRequest.new(storage_name: @storage, name: name)
+ def self.denied?
+ !temporarily_allowed?(ALLOW_KEY)
+ end
+
+ def initialize(storage)
+ raise NamespaceServiceAccessError if self.class.denied?
- gitaly_client_call(:namespace_exists, request, timeout: GitalyClient.fast_timeout).exists
+ @storage = storage
end
def add(name)
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 6e486c763da..61c5db4c4df 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -447,7 +447,7 @@ module Gitlab
elsif response.commit_error.presence
raise Gitlab::Git::CommitError, response.commit_error
elsif response.create_tree_error.presence
- raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error
+ raise Gitlab::Git::Repository::CreateTreeError, response.create_tree_error_code
end
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index 15e0d7349dd..9034edb6263 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -179,18 +179,6 @@ module Gitlab
wiki_file
end
- def get_formatted_data(title:, dir: nil, version: nil)
- request = Gitaly::WikiGetFormattedDataRequest.new(
- repository: @gitaly_repo,
- title: encode_binary(title),
- revision: encode_binary(version),
- directory: encode_binary(dir)
- )
-
- response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_formatted_data, request, timeout: GitalyClient.medium_timeout)
- response.reduce([]) { |memo, msg| memo << msg.data }.join
- end
-
private
# If a block is given and the yielded value is truthy, iteration will be
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index f1e31a615a4..2616a19fdaa 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -42,9 +42,6 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:suppress_ajax_navigation_errors, default_enabled: true)
-
- # Flag controls a GFM feature used across many routes.
- push_frontend_feature_flag(:gfm_grafana_integration)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index 32f61b1d65c..1dce26efc65 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -4,6 +4,10 @@ module Gitlab
module Gpg
extend self
+ CleanupError = Class.new(StandardError)
+ BG_CLEANUP_RUNTIME_S = 2
+ FG_CLEANUP_RUNTIME_S = 0.5
+
MUTEX = Mutex.new
module CurrentKeyChain
@@ -94,16 +98,55 @@ module Gitlab
previous_dir = current_home_dir
tmp_dir = Dir.mktmpdir
GPGME::Engine.home_dir = tmp_dir
+ tmp_keychains_created.increment
+
yield
ensure
- # Ignore any errors when removing the tmp directory, as we may run into a
+ GPGME::Engine.home_dir = previous_dir
+
+ begin
+ cleanup_tmp_dir(tmp_dir)
+ rescue CleanupError => e
+ # This means we left a GPG-agent process hanging. Logging the problem in
+ # sentry will make this more visible.
+ Gitlab::Sentry.track_exception(e,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab/issues/20918',
+ extra: { tmp_dir: tmp_dir })
+ end
+
+ tmp_keychains_removed.increment unless File.exist?(tmp_dir)
+ end
+
+ def cleanup_tmp_dir(tmp_dir)
+ return FileUtils.remove_entry(tmp_dir, true) if Feature.disabled?(:gpg_cleanup_retries)
+
+ # Retry when removing the tmp directory failed, as we may run into a
# race condition:
# The `gpg-agent` agent process may clean up some files as well while
# `FileUtils.remove_entry` is iterating the directory and removing all
# its contained files and directories recursively, which could raise an
# error.
- FileUtils.remove_entry(tmp_dir, true)
- GPGME::Engine.home_dir = previous_dir
+ # Failing to remove the tmp directory could leave the `gpg-agent` process
+ # running forever.
+ Retriable.retriable(max_elapsed_time: cleanup_time, base_interval: 0.1) do
+ FileUtils.remove_entry(tmp_dir) if File.exist?(tmp_dir)
+ end
+ rescue => e
+ raise CleanupError, e
+ end
+
+ def cleanup_time
+ Sidekiq.server? ? BG_CLEANUP_RUNTIME_S : FG_CLEANUP_RUNTIME_S
+ end
+
+ def tmp_keychains_created
+ @tmp_keychains_created ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total,
+ 'The number of temporary GPG keychains created')
+ end
+
+ def tmp_keychains_removed
+ @tmp_keychains_removed ||= Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total,
+ 'The number of temporary GPG keychains removed')
end
end
end
diff --git a/lib/gitlab/grape_logging/loggers/exception_logger.rb b/lib/gitlab/grape_logging/loggers/exception_logger.rb
new file mode 100644
index 00000000000..022eb15d28d
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/exception_logger.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ExceptionLogger < ::GrapeLogging::Loggers::Base
+ def parameters(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]
+
+ return {} unless exception.is_a?(Exception)
+
+ data = {
+ exception: {
+ class: exception.class.to_s,
+ message: exception.message
+ }
+ }
+
+ if exception.backtrace
+ data[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(exception.backtrace)
+ end
+
+ data
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb
index 15ecc3b04f0..f9ff2b30eae 100644
--- a/lib/gitlab/graphql/authorize/instrumentation.rb
+++ b/lib/gitlab/graphql/authorize/instrumentation.rb
@@ -9,12 +9,16 @@ module Gitlab
def instrument(_type, field)
service = AuthorizeFieldService.new(field)
- if service.authorizations?
+ if service.authorizations? && !resolver_skips_authorizations?(field)
field.redefine { resolve(service.authorized_resolve) }
else
field
end
end
+
+ def resolver_skips_authorizations?(field)
+ field.metadata[:resolver].try(:skip_authorizations?)
+ end
end
end
end
diff --git a/lib/gitlab/graphql/connections.rb b/lib/gitlab/graphql/connections.rb
index fbccdfa7b08..38c7d98f37c 100644
--- a/lib/gitlab/graphql/connections.rb
+++ b/lib/gitlab/graphql/connections.rb
@@ -6,7 +6,11 @@ module Gitlab
def self.use(_schema)
GraphQL::Relay::BaseConnection.register_connection_implementation(
ActiveRecord::Relation,
- Gitlab::Graphql::Connections::KeysetConnection
+ Gitlab::Graphql::Connections::Keyset::Connection
+ )
+ GraphQL::Relay::BaseConnection.register_connection_implementation(
+ Gitlab::Graphql::FilterableArray,
+ Gitlab::Graphql::Connections::FilterableArrayConnection
)
end
end
diff --git a/lib/gitlab/graphql/connections/filterable_array_connection.rb b/lib/gitlab/graphql/connections/filterable_array_connection.rb
new file mode 100644
index 00000000000..800f2c949c6
--- /dev/null
+++ b/lib/gitlab/graphql/connections/filterable_array_connection.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ # FilterableArrayConnection is useful especially for lazy-loaded values.
+ # It allows us to call a callback only on the slice of array being
+ # rendered in the "after loaded" phase. For example we can check
+ # permissions only on a small subset of items.
+ class FilterableArrayConnection < GraphQL::Relay::ArrayConnection
+ def paged_nodes
+ @filtered_nodes ||= nodes.filter_callback.call(super)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
new file mode 100644
index 00000000000..22728cc0b65
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/base_condition.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class BaseCondition
+ def initialize(arel_table, names, values, operator, before_or_after)
+ @arel_table, @names, @values, @operator, @before_or_after = arel_table, names, values, operator, before_or_after
+ end
+
+ def build
+ raise NotImplementedError
+ end
+
+ private
+
+ attr_reader :arel_table, :names, :values, :operator, :before_or_after
+
+ def table_condition(attribute, value, operator)
+ case operator
+ when '>'
+ arel_table[attribute].gt(value)
+ when '<'
+ arel_table[attribute].lt(value)
+ when '='
+ arel_table[attribute].eq(value)
+ when 'is_null'
+ arel_table[attribute].eq(nil)
+ when 'is_not_null'
+ arel_table[attribute].not_eq(nil)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
new file mode 100644
index 00000000000..3b56ddb996d
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class NotNullCondition < BaseCondition
+ def build
+ conditions = [first_attribute_condition]
+
+ # If there is only one order field, we can assume it
+ # does not contain NULLs, and don't need additional
+ # conditions
+ unless names.count == 1
+ conditions << [second_attribute_condition, final_condition]
+ end
+
+ conditions.join
+ end
+
+ private
+
+ # ex: "(relative_position > 23)"
+ def first_attribute_condition
+ <<~SQL
+ (#{table_condition(names.first, values.first, operator.first).to_sql})
+ SQL
+ end
+
+ # ex: " OR (relative_position = 23 AND id > 500)"
+ def second_attribute_condition
+ condition = <<~SQL
+ OR (
+ #{table_condition(names.first, values.first, '=').to_sql}
+ AND
+ #{table_condition(names[1], values[1], operator[1]).to_sql}
+ )
+ SQL
+
+ condition
+ end
+
+ # ex: " OR (relative_position IS NULL)"
+ def final_condition
+ if before_or_after == :after
+ <<~SQL
+ OR (#{table_condition(names.first, nil, 'is_null').to_sql})
+ SQL
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
new file mode 100644
index 00000000000..71a74936d5d
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/conditions/null_condition.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module Conditions
+ class NullCondition < BaseCondition
+ def build
+ [first_attribute_condition, final_condition].join
+ end
+
+ private
+
+ # ex: "(relative_position IS NULL AND id > 500)"
+ def first_attribute_condition
+ condition = <<~SQL
+ (
+ #{table_condition(names.first, nil, 'is_null').to_sql}
+ AND
+ #{table_condition(names[1], values[1], operator[1]).to_sql}
+ )
+ SQL
+
+ condition
+ end
+
+ # ex: " OR (relative_position IS NOT NULL)"
+ def final_condition
+ if before_or_after == :before
+ <<~SQL
+ OR (#{table_condition(names.first, nil, 'is_not_null').to_sql})
+ SQL
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/connection.rb b/lib/gitlab/graphql/connections/keyset/connection.rb
new file mode 100644
index 00000000000..c75ea206edb
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/connection.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+# Keyset::Connection provides cursor based pagination, to avoid using OFFSET.
+# It basically sorts / filters using WHERE sorting_value > cursor.
+# We do this for performance reasons (https://gitlab.com/gitlab-org/gitlab-foss/issues/45756),
+# as well as for having stable pagination
+# https://graphql-ruby.org/pro/cursors.html#whats-the-difference
+# https://coderwall.com/p/lkcaag/pagination-you-re-probably-doing-it-wrong
+#
+# It currently supports sorting on two columns, but the last column must
+# be the primary key. If it's not already included, an order on the
+# primary key will be added automatically, like `order(id: :desc)`
+#
+# Issue.order(created_at: :asc).order(:id)
+# Issue.order(due_date: :asc)
+#
+# You can also use `Gitlab::Database.nulls_last_order`:
+#
+# Issue.reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC'))
+#
+# It will tolerate non-attribute ordering, but only attributes determine the cursor.
+# For example, this is legitimate:
+#
+# Issue.order('issues.due_date IS NULL').order(due_date: :asc).order(:id)
+#
+# but anything more complex has a chance of not working.
+#
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class Connection < GraphQL::Relay::BaseConnection
+ include Gitlab::Utils::StrongMemoize
+
+ # TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
+ include Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection
+
+ def cursor_from_node(node)
+ return legacy_cursor_from_node(node) if use_legacy_pagination?
+
+ encoded_json_from_ordering(node)
+ end
+
+ def sliced_nodes
+ return legacy_sliced_nodes if use_legacy_pagination?
+
+ @sliced_nodes ||=
+ begin
+ OrderInfo.validate_ordering(ordered_nodes, order_list)
+
+ sliced = ordered_nodes
+ sliced = slice_nodes(sliced, before, :before) if before.present?
+ sliced = slice_nodes(sliced, after, :after) if after.present?
+
+ sliced
+ end
+ end
+
+ def paged_nodes
+ # These are the nodes that will be loaded into memory for rendering
+ # So we're ok loading them into memory here as that's bound to happen
+ # anyway. Having them ready means we can modify the result while
+ # rendering the fields.
+ @paged_nodes ||= load_paged_nodes.to_a
+ end
+
+ private
+
+ def load_paged_nodes
+ if first && last
+ raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
+ end
+
+ if last
+ sliced_nodes.last(limit_value)
+ else
+ sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def slice_nodes(sliced, encoded_cursor, before_or_after)
+ decoded_cursor = ordering_from_encoded_json(encoded_cursor)
+ builder = QueryBuilder.new(arel_table, order_list, decoded_cursor, before_or_after)
+ ordering = builder.conditions
+
+ sliced.where(*ordering).where.not(id: decoded_cursor['id'])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def limit_value
+ @limit_value ||= [first, last, max_page_size].compact.min
+ end
+
+ def ordered_nodes
+ strong_memoize(:order_nodes) do
+ unless nodes.primary_key.present?
+ raise ArgumentError.new('Relation must have a primary key')
+ end
+
+ list = OrderInfo.build_order_list(nodes)
+
+ # ensure there is a primary key ordering
+ if list&.last&.attribute_name != nodes.primary_key
+ nodes.order(arel_table[nodes.primary_key].desc) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ nodes
+ end
+ end
+ end
+
+ def order_list
+ strong_memoize(:order_list) do
+ OrderInfo.build_order_list(ordered_nodes)
+ end
+ end
+
+ def arel_table
+ nodes.arel_table
+ end
+
+ # Storing the current order values in the cursor allows us to
+ # make an intelligent decision on handling NULL values.
+ # Otherwise we would either need to fetch the record first,
+ # or fetch it in the SQL, significantly complicating it.
+ def encoded_json_from_ordering(node)
+ ordering = { 'id' => node[:id].to_s }
+
+ order_list.each do |field|
+ field_name = field.attribute_name
+ ordering[field_name] = node[field_name].to_s
+ end
+
+ encode(ordering.to_json)
+ end
+
+ def ordering_from_encoded_json(cursor)
+ JSON.parse(decode(cursor))
+ rescue JSON::ParserError
+ # for the transition period where a client might request using an
+ # old style cursor. Once removed, make it an error:
+ # raise Gitlab::Graphql::Errors::ArgumentError, "Please provide a valid cursor"
+ # TODO can be removed in next release
+ # https://gitlab.com/gitlab-org/gitlab/issues/32933
+ field_name = order_list.first.attribute_name
+
+ { field_name => decode(cursor) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb b/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb
new file mode 100644
index 00000000000..baf900d1048
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ module LegacyKeysetConnection
+ def legacy_cursor_from_node(node)
+ encode(node[legacy_order_field].to_s)
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def legacy_sliced_nodes
+ @sliced_nodes ||=
+ begin
+ sliced = nodes
+
+ sliced = sliced.where(legacy_before_slice) if before.present?
+ sliced = sliced.where(legacy_after_slice) if after.present?
+
+ sliced
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def use_legacy_pagination?
+ strong_memoize(:feature_disabled) do
+ Feature.disabled?(:graphql_keyset_pagination, default_enabled: true)
+ end
+ end
+
+ def legacy_before_slice
+ if legacy_sort_direction == :asc
+ arel_table[legacy_order_field].lt(decode(before))
+ else
+ arel_table[legacy_order_field].gt(decode(before))
+ end
+ end
+
+ def legacy_after_slice
+ if legacy_sort_direction == :asc
+ arel_table[legacy_order_field].gt(decode(after))
+ else
+ arel_table[legacy_order_field].lt(decode(after))
+ end
+ end
+
+ def legacy_order_info
+ @legacy_order_info ||= nodes.order_values.first
+ end
+
+ def legacy_order_field
+ @legacy_order_field ||= legacy_order_info&.expr&.name || nodes.primary_key
+ end
+
+ def legacy_sort_direction
+ @legacy_order_direction ||= legacy_order_info&.direction || :desc
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/order_info.rb b/lib/gitlab/graphql/connections/keyset/order_info.rb
new file mode 100644
index 00000000000..4d85e8f79b7
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/order_info.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class OrderInfo
+ attr_reader :attribute_name, :sort_direction
+
+ def initialize(order_value)
+ if order_value.is_a?(String)
+ @attribute_name, @sort_direction = extract_nulls_last_order(order_value)
+ else
+ @attribute_name = order_value.expr.name
+ @sort_direction = order_value.direction
+ end
+ end
+
+ def operator_for(before_or_after)
+ case before_or_after
+ when :before
+ sort_direction == :asc ? '<' : '>'
+ when :after
+ sort_direction == :asc ? '>' : '<'
+ end
+ end
+
+ # Only allow specific node types
+ def self.build_order_list(relation)
+ order_list = relation.order_values.select do |value|
+ supported_order_value?(value)
+ end
+
+ order_list.map { |info| OrderInfo.new(info) }
+ end
+
+ def self.validate_ordering(relation, order_list)
+ if order_list.empty?
+ raise ArgumentError.new('A minimum of 1 ordering field is required')
+ end
+
+ if order_list.count > 2
+ raise ArgumentError.new('A maximum of 2 ordering fields are allowed')
+ end
+
+ # make sure the last ordering field is non-nullable
+ attribute_name = order_list.last&.attribute_name
+
+ if relation.columns_hash[attribute_name].null
+ raise ArgumentError.new("Column `#{attribute_name}` must not allow NULL")
+ end
+
+ if order_list.last.attribute_name != relation.primary_key
+ raise ArgumentError.new("Last ordering field must be the primary key, `#{relation.primary_key}`")
+ end
+ end
+
+ def self.supported_order_value?(order_value)
+ return true if order_value.is_a?(Arel::Nodes::Ascending) || order_value.is_a?(Arel::Nodes::Descending)
+ return false unless order_value.is_a?(String)
+
+ tokens = order_value.downcase.split
+
+ tokens.last(2) == %w(nulls last) && tokens.count == 4
+ end
+
+ private
+
+ def extract_nulls_last_order(order_value)
+ tokens = order_value.downcase.split
+
+ [tokens.first, (tokens[1] == 'asc' ? :asc : :desc)]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset/query_builder.rb b/lib/gitlab/graphql/connections/keyset/query_builder.rb
new file mode 100644
index 00000000000..e93c25d85fc
--- /dev/null
+++ b/lib/gitlab/graphql/connections/keyset/query_builder.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ module Connections
+ module Keyset
+ class QueryBuilder
+ def initialize(arel_table, order_list, decoded_cursor, before_or_after)
+ @arel_table, @order_list, @decoded_cursor, @before_or_after = arel_table, order_list, decoded_cursor, before_or_after
+
+ if order_list.empty?
+ raise ArgumentError.new('No ordering scopes have been supplied')
+ end
+ end
+
+ # Based on whether the main field we're ordering on is NULL in the
+ # cursor, we can more easily target our query condition.
+ # We assume that the last ordering field is unique, meaning
+ # it will not contain NULLs.
+ # We currently only support two ordering fields.
+ #
+ # Example of the conditions for
+ # relation: Issue.order(relative_position: :asc).order(id: :asc)
+ # after cursor: relative_position: 1500, id: 500
+ #
+ # when cursor[relative_position] is not NULL
+ #
+ # ("issues"."relative_position" > 1500)
+ # OR (
+ # "issues"."relative_position" = 1500
+ # AND
+ # "issues"."id" > 500
+ # )
+ # OR ("issues"."relative_position" IS NULL)
+ #
+ # when cursor[relative_position] is NULL
+ #
+ # "issues"."relative_position" IS NULL
+ # AND
+ # "issues"."id" > 500
+ #
+ def conditions
+ attr_names = order_list.map { |field| field.attribute_name }
+ attr_values = attr_names.map { |name| decoded_cursor[name] }
+
+ if attr_names.count == 1 && attr_values.first.nil?
+ raise Gitlab::Graphql::Errors::ArgumentError.new('Before/after cursor invalid: `nil` was provided as only sortable value')
+ end
+
+ if attr_names.count == 1 || attr_values.first.present?
+ Keyset::Conditions::NotNullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ else
+ Keyset::Conditions::NullCondition.new(arel_table, attr_names, attr_values, operators, before_or_after).build
+ end
+ end
+
+ private
+
+ attr_reader :arel_table, :order_list, :decoded_cursor, :before_or_after
+
+ def operators
+ order_list.map { |field| field.operator_for(before_or_after) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
deleted file mode 100644
index 715963a44c1..00000000000
--- a/lib/gitlab/graphql/connections/keyset_connection.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Connections
- class KeysetConnection < GraphQL::Relay::BaseConnection
- def cursor_from_node(node)
- encode(node[order_field].to_s)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def sliced_nodes
- @sliced_nodes ||=
- begin
- sliced = nodes
-
- sliced = sliced.where(before_slice) if before.present?
- sliced = sliced.where(after_slice) if after.present?
-
- sliced
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def paged_nodes
- # These are the nodes that will be loaded into memory for rendering
- # So we're ok loading them into memory here as that's bound to happen
- # anyway. Having them ready means we can modify the result while
- # rendering the fields.
- @paged_nodes ||= load_paged_nodes.to_a
- end
-
- private
-
- def load_paged_nodes
- if first && last
- raise Gitlab::Graphql::Errors::ArgumentError.new("Can only provide either `first` or `last`, not both")
- end
-
- if last
- sliced_nodes.last(limit_value)
- else
- sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
- end
- end
-
- def before_slice
- if sort_direction == :asc
- table[order_field].lt(decode(before))
- else
- table[order_field].gt(decode(before))
- end
- end
-
- def after_slice
- if sort_direction == :asc
- table[order_field].gt(decode(after))
- else
- table[order_field].lt(decode(after))
- end
- end
-
- def limit_value
- @limit_value ||= [first, last, max_page_size].compact.min
- end
-
- def table
- nodes.arel_table
- end
-
- def order_info
- @order_info ||= nodes.order_values.first
- end
-
- def order_field
- @order_field ||= order_info&.expr&.name || nodes.primary_key
- end
-
- def sort_direction
- @order_direction ||= order_info&.direction || :desc
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/graphql/filterable_array.rb b/lib/gitlab/graphql/filterable_array.rb
new file mode 100644
index 00000000000..4909d291fd6
--- /dev/null
+++ b/lib/gitlab/graphql/filterable_array.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Graphql
+ class FilterableArray < Array
+ attr_reader :filter_callback
+
+ def initialize(filter_callback, *args)
+ super(args)
+ @filter_callback = filter_callback
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb b/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb
deleted file mode 100644
index 70344392138..00000000000
--- a/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- module Loaders
- class PipelineForShaLoader
- attr_accessor :project, :sha
-
- def initialize(project, sha)
- @project, @sha = project, sha
- end
-
- def find_last
- BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
- pipelines = args[:key].ci_pipelines.latest_for_shas(shas)
-
- pipelines.each do |pipeline|
- loader.call(pipeline.sha, pipeline)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/health_checks/master_check.rb b/lib/gitlab/health_checks/master_check.rb
new file mode 100644
index 00000000000..057bce84ddd
--- /dev/null
+++ b/lib/gitlab/health_checks/master_check.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module HealthChecks
+ # This check is registered on master,
+ # and validated by worker
+ class MasterCheck
+ extend SimpleAbstractCheck
+
+ class << self
+ def register_master
+ # when we fork, we pass the read pipe to child
+ # child can then react on whether the other end
+ # of pipe is still available
+ @pipe_read, @pipe_write = IO.pipe
+ end
+
+ def finish_master
+ close_read
+ close_write
+ end
+
+ def register_worker
+ # fork needs to close the pipe
+ close_write
+ end
+
+ private
+
+ def close_read
+ @pipe_read&.close
+ @pipe_read = nil
+ end
+
+ def close_write
+ @pipe_write&.close
+ @pipe_write = nil
+ end
+
+ def metric_prefix
+ 'master_check'
+ end
+
+ def successful?(result)
+ result
+ end
+
+ def check
+ # the lack of pipe is a legitimate failure of check
+ return false unless @pipe_read
+
+ @pipe_read.read_nonblock(1)
+
+ true
+ rescue IO::EAGAINWaitReadable
+ # if it is blocked, it means that the pipe is still open
+ # and there's no data waiting on it
+ true
+ rescue EOFError
+ # the pipe is closed
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index b2ac60fe825..516e7f54a6e 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def storage_path
- File.join(Settings.shared['path'], 'tmp/project_exports')
+ File.join(Settings.shared['path'], 'tmp/gitlab_exports')
end
def import_upload_path(filename:)
@@ -50,8 +50,8 @@ module Gitlab
'VERSION'
end
- def export_filename(project:)
- basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.full_path.tr('/', '_')}"
+ def export_filename(exportable:)
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{exportable.full_path.tr('/', '_')}"
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
@@ -63,6 +63,14 @@ module Gitlab
def reset_tokens?
true
end
+
+ def group_filename
+ 'group.json'
+ end
+
+ def group_config_file
+ Rails.root.join('lib/gitlab/import_export/group_import_export.yml')
+ end
end
end
diff --git a/lib/gitlab/import_export/config.rb b/lib/gitlab/import_export/config.rb
index 6f4919ead4e..83c4bc47349 100644
--- a/lib/gitlab/import_export/config.rb
+++ b/lib/gitlab/import_export/config.rb
@@ -3,7 +3,8 @@
module Gitlab
module ImportExport
class Config
- def initialize
+ def initialize(config: Gitlab::ImportExport.config_file)
+ @config = config
@hash = parse_yaml
@hash.deep_symbolize_keys!
@ee_hash = @hash.delete(:ee) || {}
@@ -50,7 +51,7 @@ module Gitlab
end
def parse_yaml
- YAML.load_file(Gitlab::ImportExport.config_file)
+ YAML.load_file(@config)
end
end
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 05432f433e7..2fd12e3aa78 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -60,7 +60,7 @@ module Gitlab
def copy_archive
return if @archive_file
- @archive_file = File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
+ @archive_file = File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(exportable: @project))
download_or_copy_upload(@project.import_export_upload.import_file, @archive_file)
end
diff --git a/lib/gitlab/import_export/group_import_export.yml b/lib/gitlab/import_export/group_import_export.yml
new file mode 100644
index 00000000000..c1900350c86
--- /dev/null
+++ b/lib/gitlab/import_export/group_import_export.yml
@@ -0,0 +1,36 @@
+# 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
+ - members:
+ - :user
+
+included_attributes:
+
+excluded_attributes:
+ group:
+ - :runners_token
+ - :runners_token_encrypted
+
+methods:
+ labels:
+ - :type
+ badges:
+ - :type
+
+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
diff --git a/lib/gitlab/import_export/group_project_object_builder.rb b/lib/gitlab/import_export/group_project_object_builder.rb
index de1629d0e28..b94839363df 100644
--- a/lib/gitlab/import_export/group_project_object_builder.rb
+++ b/lib/gitlab/import_export/group_project_object_builder.rb
@@ -49,11 +49,12 @@ module Gitlab
].compact
end
- # Returns Arel clause `"{table_name}"."project_id" = {project.id}`
+ # Returns Arel clause `"{table_name}"."project_id" = {project.id}` if project is present
+ # For example: merge_request has :target_project_id, and we are searching by :iid
# or, if group is present:
# `"{table_name}"."project_id" = {project.id} OR "{table_name}"."group_id" = {group.id}`
def where_clause_base
- clause = table[:project_id].eq(project.id)
+ clause = table[:project_id].eq(project.id) if project
clause = clause.or(table[:group_id].eq(group.id)) if group
clause
@@ -103,6 +104,10 @@ module Gitlab
klass == Milestone
end
+ def merge_request?
+ klass == MergeRequest
+ 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:
@@ -124,7 +129,7 @@ module Gitlab
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
- # no-op
+ return attrs_to_arel(attributes.slice('iid')) if merge_request?
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..8d2fb881cc0
--- /dev/null
+++ b/lib/gitlab/import_export/group_tree_saver.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class GroupTreeSaver
+ attr_reader :full_path
+
+ def initialize(group:, current_user:, shared:, params: {})
+ @params = params
+ @current_user = current_user
+ @shared = shared
+ @group = group
+ @full_path = File.join(@shared.export_path, ImportExport.group_filename)
+ end
+
+ def save
+ group_tree = serialize(@group, reader.group_tree)
+ tree_saver.save(group_tree, @shared.export_path, ImportExport.group_filename)
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def serialize(group, relations_tree)
+ group_tree = tree_saver.serialize(group, relations_tree)
+
+ group.children.each do |child|
+ group_tree['children'] ||= []
+ group_tree['children'] << serialize(child, relations_tree)
+ end
+
+ group_tree
+ rescue => e
+ @shared.error(e)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ )
+ end
+
+ def tree_saver
+ @tree_saver ||= RelationTreeSaver.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 141e73e6a47..1aafe5804c0 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -28,6 +28,7 @@ tree:
- label:
- :priorities
- :issue_assignees
+ - :zoom_meetings
- snippets:
- :award_emoji
- notes:
@@ -147,6 +148,8 @@ excluded_attributes:
- :emails_disabled
- :max_pages_size
- :max_artifacts_size
+ - :marked_for_deletion_at
+ - :marked_for_deletion_by_user_id
namespaces:
- :runners_token
- :runners_token_encrypted
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 3fa5765fd4a..c401f96b5c1 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -15,7 +15,6 @@ module Gitlab
@user = user
@shared = shared
@project = project
- @saved = true
end
def restore
@@ -33,7 +32,8 @@ module Gitlab
ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do
update_project_params!
- create_relations
+ create_project_relations!
+ post_import!
end
end
@@ -69,81 +69,75 @@ module Gitlab
# in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
- def create_relations
- project_relations.each do |relation_key, relation_definition|
- relation_key_s = relation_key.to_s
-
- if relation_definition.present?
- create_sub_relations(relation_key_s, relation_definition, @tree_hash)
- elsif @tree_hash[relation_key_s].present?
- save_relation_hash(relation_key_s, @tree_hash[relation_key_s])
- end
- end
+ def create_project_relations!
+ project_relations.each(&method(
+ :process_project_relation!))
+ end
+ def post_import!
@project.merge_requests.set_latest_merge_request_diff_ids!
-
- @saved
end
- def save_relation_hash(relation_key, relation_hash_batch)
- relation_hash = create_relation(relation_key, relation_hash_batch)
+ def process_project_relation!(relation_key, relation_definition)
+ data_hashes = @tree_hash.delete(relation_key)
+ return unless data_hashes
- remove_group_models(relation_hash) if relation_hash.is_a?(Array)
+ # we do not care if we process array or hash
+ data_hashes = [data_hashes] unless data_hashes.is_a?(Array)
- @saved = false unless @project.append_or_update_attribute(relation_key, relation_hash)
+ # consume and remove objects from memory
+ while data_hash = data_hashes.shift
+ process_project_relation_item!(relation_key, relation_definition, data_hash)
+ end
+ end
- save_id_mappings(relation_key, relation_hash_batch, relation_hash)
+ def process_project_relation_item!(relation_key, relation_definition, data_hash)
+ relation_object = build_relation(relation_key, relation_definition, data_hash)
+ return unless relation_object
+ return if group_model?(relation_object)
- @project.reset
+ relation_object.project = @project
+ relation_object.save!
+
+ save_id_mapping(relation_key, data_hash, relation_object)
end
# Older, serialized CI pipeline exports may only have a
# merge_request_id and not the full hash of the merge request. To
# import these pipelines, we need to preserve the mapping between
# the old and new the merge request ID.
- def save_id_mappings(relation_key, relation_hash_batch, relation_hash)
+ def save_id_mapping(relation_key, data_hash, relation_object)
return unless relation_key == 'merge_requests'
- relation_hash = Array(relation_hash)
-
- Array(relation_hash_batch).each_with_index do |raw_data, index|
- merge_requests_mapping[raw_data['id']] = relation_hash[index]['id']
- end
- end
-
- # Remove project models that became group models as we found them at group level.
- # This no longer required saving them at the root project level.
- # For example, in the case of an existing group label that matched the title.
- def remove_group_models(relation_hash)
- relation_hash.reject! do |value|
- GROUP_MODELS.include?(value.class) && value.group_id
- end
- end
-
- def remove_feature_dependent_sub_relations!(_relation_item)
- # no-op
+ merge_requests_mapping[data_hash['id']] = relation_object.id
end
def project_relations
- @project_relations ||= reader.attributes_finder.find_relations_tree(:project)
+ @project_relations ||=
+ reader
+ .attributes_finder
+ .find_relations_tree(:project)
+ .deep_stringify_keys
end
def update_project_params!
- Gitlab::Timeless.timeless(@project) do
- project_params = @tree_hash.reject do |key, value|
- project_relations.include?(key.to_sym)
- end
+ project_params = @tree_hash.reject do |key, value|
+ project_relations.include?(key)
+ end
- project_params = project_params.merge(present_project_override_params)
+ project_params = project_params.merge(
+ present_project_override_params)
- # Cleaning all imported and overridden params
- project_params = Gitlab::ImportExport::AttributeCleaner.clean(
- relation_hash: project_params,
- relation_class: Project,
- excluded_keys: excluded_keys_for_relation(:project))
+ # Cleaning all imported and overridden params
+ project_params = Gitlab::ImportExport::AttributeCleaner.clean(
+ relation_hash: project_params,
+ relation_class: Project,
+ excluded_keys: excluded_keys_for_relation(:project))
- @project.assign_attributes(project_params)
- @project.drop_visibility_level!
+ @project.assign_attributes(project_params)
+ @project.drop_visibility_level!
+
+ Gitlab::Timeless.timeless(@project) do
@project.save!
end
end
@@ -160,75 +154,61 @@ module Gitlab
@project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
end
- # Given a relation hash containing one or more models and its relationships,
- # loops through each model and each object from a model type and
- # and assigns its correspondent attributes hash from +tree_hash+
- # Example:
- # +relation_key+ issues, loops through the list of *issues* and for each individual
- # issue, finds any subrelations such as notes, creates them and assign them back to the hash
- #
- # Recursively calls this method if the sub-relation is a hash containing more sub-relations
- def create_sub_relations(relation_key, relation_definition, tree_hash, save: true)
- return if tree_hash[relation_key].blank?
-
- tree_array = [tree_hash[relation_key]].flatten
-
- # Avoid keeping a possible heavy object in memory once we are done with it
- while relation_item = tree_array.shift
- remove_feature_dependent_sub_relations!(relation_item)
-
- # The transaction at this level is less speedy than one single transaction
- # But we can't have it in the upper level or GC won't get rid of the AR objects
- # after we save the batch.
- Project.transaction do
- process_sub_relation(relation_key, relation_definition, relation_item)
-
- # For every subrelation that hangs from Project, save the associated records altogether
- # This effectively batches all records per subrelation item, only keeping those in memory
- # We have to keep in mind that more batch granularity << Memory, but >> Slowness
- if save
- save_relation_hash(relation_key, [relation_item])
- tree_hash[relation_key].delete(relation_item)
- end
- end
- end
-
- tree_hash.delete(relation_key) if save
+ def build_relations(relation_key, relation_definition, data_hashes)
+ data_hashes.map do |data_hash|
+ build_relation(relation_key, relation_definition, data_hash)
+ end.compact
end
- def process_sub_relation(relation_key, relation_definition, relation_item)
- relation_definition.each do |sub_relation_key, sub_relation_definition|
- # We just use author to get the user ID, do not attempt to create an instance.
- next if sub_relation_key == :author
+ def build_relation(relation_key, relation_definition, data_hash)
+ # TODO: This is hack to not create relation for the author
+ # Rather make `RelationFactory#set_note_author` to take care of that
+ return data_hash if relation_key == 'author'
- sub_relation_key_s = sub_relation_key.to_s
+ # create relation objects recursively for all sub-objects
+ relation_definition.each do |sub_relation_key, sub_relation_definition|
+ transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ end
- # create dependent relations if present
- if sub_relation_definition.present?
- create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false)
+ Gitlab::ImportExport::RelationFactory.create(
+ relation_sym: relation_key.to_sym,
+ relation_hash: data_hash,
+ members_mapper: members_mapper,
+ merge_requests_mapping: merge_requests_mapping,
+ user: @user,
+ project: @project,
+ excluded_keys: excluded_keys_for_relation(relation_key))
+ end
+
+ def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
+ sub_data_hash = data_hash[sub_relation_key]
+ return unless sub_data_hash
+
+ # if object is a hash we can create simple object
+ # as it means that this is 1-to-1 vs 1-to-many
+ sub_data_hash =
+ if sub_data_hash.is_a?(Array)
+ build_relations(
+ sub_relation_key,
+ sub_relation_definition,
+ sub_data_hash).presence
+ else
+ build_relation(
+ sub_relation_key,
+ sub_relation_definition,
+ sub_data_hash)
end
- # transform relation hash to actual object
- sub_relation_hash = relation_item[sub_relation_key_s]
- if sub_relation_hash.present?
- relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash)
- end
+ # persist object(s) or delete from relation
+ if sub_data_hash
+ data_hash[sub_relation_key] = sub_data_hash
+ else
+ data_hash.delete(sub_relation_key)
end
end
- def create_relation(relation_key, relation_hash_list)
- relation_array = [relation_hash_list].flatten.map do |relation_hash|
- Gitlab::ImportExport::RelationFactory.create(
- relation_sym: relation_key.to_sym,
- relation_hash: relation_hash,
- members_mapper: members_mapper,
- merge_requests_mapping: merge_requests_mapping,
- user: @user,
- project: @project,
- excluded_keys: excluded_keys_for_relation(relation_key))
- end.compact
-
- relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
+ def group_model?(relation_object)
+ GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
end
def reader
@@ -241,5 +221,3 @@ module Gitlab
end
end
end
-
-Gitlab::ImportExport::ProjectTreeRestorer.prepend_if_ee('::EE::Gitlab::ImportExport::ProjectTreeRestorer')
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 63c71105efe..386a4cfdfc6 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -3,25 +3,20 @@
module Gitlab
module ImportExport
class ProjectTreeSaver
- include Gitlab::ImportExport::CommandLineUtil
-
attr_reader :full_path
def initialize(project:, current_user:, shared:, params: {})
- @params = params
- @project = project
+ @params = params
+ @project = project
@current_user = current_user
- @shared = shared
- @full_path = File.join(@shared.export_path, ImportExport.project_filename)
+ @shared = shared
+ @full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
def save
- mkdir_p(@shared.export_path)
-
- project_tree = serialize_project_tree
+ project_tree = tree_saver.serialize(@project, reader.project_tree)
fix_project_tree(project_tree)
- project_tree_json = JSON.generate(project_tree)
- File.write(full_path, project_tree_json)
+ tree_saver.save(project_tree, @shared.export_path, ImportExport.project_filename)
true
rescue => e
@@ -43,16 +38,6 @@ module Gitlab
RelationRenameService.add_new_associations(project_tree)
end
- def serialize_project_tree
- if Feature.enabled?(:export_fast_serialize, default_enabled: true)
- Gitlab::ImportExport::FastHashSerializer
- .new(@project, reader.project_tree)
- .execute
- else
- @project.as_json(reader.project_tree)
- end
- end
-
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
@@ -74,6 +59,10 @@ module Gitlab
GroupMembersFinder.new(@project.group).execute.where.not(user_id: non_null_user_ids)
end
+
+ def tree_saver
+ @tree_saver ||= RelationTreeSaver.new
+ end
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 9e81c6a3d07..1390770acef 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -5,24 +5,31 @@ module Gitlab
class Reader
attr_reader :tree, :attributes_finder
- def initialize(shared:)
- @shared = shared
-
- @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(
- config: ImportExport::Config.new.to_h)
+ def initialize(shared:, config: ImportExport::Config.new.to_h)
+ @shared = shared
+ @config = config
+ @attributes_finder = Gitlab::ImportExport::AttributesFinder.new(config: @config)
end
# Outputs a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
# for outputting a project in JSON format, including its relations and sub relations.
def project_tree
- attributes_finder.find_root(:project)
- rescue => e
- @shared.error(e)
- false
+ tree_by_key(:project)
+ end
+
+ def group_tree
+ tree_by_key(:group)
end
def group_members_tree
- attributes_finder.find_root(:group_members)
+ tree_by_key(:group_members)
+ end
+
+ def tree_by_key(key)
+ attributes_finder.find_root(key)
+ rescue => e
+ @shared.error(e)
+ false
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index ae8025c52ef..ae6b3c161ce 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -38,10 +38,13 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
- EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request].freeze
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting].freeze
TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze
+ # This represents all relations that have unique key on `project_id`
+ UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting].freeze
+
def self.create(*args)
new(*args).create
end
@@ -274,7 +277,7 @@ module Gitlab
end
def setup_pipeline
- @relation_hash.fetch('stages').each do |stage|
+ @relation_hash.fetch('stages', []).each do |stage|
stage.statuses.each do |status|
status.pipeline = imported_object
end
@@ -324,8 +327,7 @@ module Gitlab
end
def find_or_create_object!
- return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature
- return find_or_create_merge_request! if @relation_name == :merge_request
+ return relation_class.find_or_create_by(project_id: @project.id) if UNIQUE_RELATIONS.include?(@relation_name)
# Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash|
@@ -336,11 +338,6 @@ module Gitlab
GroupProjectObjectBuilder.build(relation_class, finder_hash)
end
-
- def find_or_create_merge_request!
- @project.merge_requests.find_by(iid: parsed_relation_hash['iid']) ||
- relation_class.new(parsed_relation_hash)
- end
end
end
end
diff --git a/lib/gitlab/import_export/relation_rename_service.rb b/lib/gitlab/import_export/relation_rename_service.rb
index 179bde5e21e..03aaa6aefc3 100644
--- a/lib/gitlab/import_export/relation_rename_service.rb
+++ b/lib/gitlab/import_export/relation_rename_service.rb
@@ -8,7 +8,7 @@
# The behavior of these renamed relationships should be transient and it should
# only last one release until you completely remove the renaming from the list.
#
-# When importing, this class will check the project hash and:
+# When importing, this class will check the hash and:
# - if only the old relationship name is found, it will rename it with the new one
# - if only the new relationship name is found, it will do nothing
# - if it finds both, it will use the new relationship data
diff --git a/lib/gitlab/import_export/relation_tree_saver.rb b/lib/gitlab/import_export/relation_tree_saver.rb
new file mode 100644
index 00000000000..a0452071ccf
--- /dev/null
+++ b/lib/gitlab/import_export/relation_tree_saver.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class RelationTreeSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def serialize(exportable, relations_tree)
+ if Feature.enabled?(:export_fast_serialize, default_enabled: true)
+ Gitlab::ImportExport::FastHashSerializer
+ .new(exportable, relations_tree)
+ .execute
+ else
+ exportable.as_json(relations_tree)
+ end
+ end
+
+ def save(tree, dir_path, filename)
+ mkdir_p(dir_path)
+
+ tree_json = JSON.generate(tree)
+
+ File.write(File.join(dir_path, filename), tree_json)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index bea7a7cce65..ae82c380755 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -9,16 +9,16 @@ module Gitlab
new(*args).save
end
- def initialize(project:, shared:)
- @project = project
- @shared = shared
+ def initialize(exportable:, shared:)
+ @exportable = exportable
+ @shared = shared
end
def save
if compress_and_save
remove_export_path
- Rails.logger.info("Saved project export #{archive_file}") # rubocop:disable Gitlab/RailsLogger
+ Rails.logger.info("Saved #{@exportable.class} export #{archive_file}") # rubocop:disable Gitlab/RailsLogger
save_upload
else
@@ -48,11 +48,11 @@ module Gitlab
end
def archive_file
- @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
+ @archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(exportable: @exportable))
end
def save_upload
- upload = ImportExportUpload.find_or_initialize_by(project: @project)
+ upload = initialize_upload
File.open(archive_file) { |file| upload.export_file = file }
@@ -62,6 +62,12 @@ module Gitlab
def error_message
"Unable to save #{archive_file} into #{@shared.export_path}."
end
+
+ def initialize_upload
+ exportable_kind = @exportable.class.name.downcase
+
+ ImportExportUpload.find_or_initialize_by(Hash[exportable_kind, @exportable])
+ end
end
end
end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 02d46a1f498..2539a6828c3 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -23,21 +23,21 @@
module Gitlab
module ImportExport
class Shared
- attr_reader :errors, :project
+ attr_reader :errors, :exportable, :logger
LOCKS_DIRECTORY = 'locks'
- def initialize(project)
- @project = project
- @errors = []
- @logger = Gitlab::Import::Logger.build
+ def initialize(exportable)
+ @exportable = exportable
+ @errors = []
+ @logger = Gitlab::Import::Logger.build
end
def active_export_count
Dir[File.join(base_path, '*')].count { |name| File.basename(name) != LOCKS_DIRECTORY && File.directory?(name) }
end
- # The path where the project metadata and repository bundle is saved
+ # The path where the exportable metadata and repository bundle (in case of project) is saved
def export_path
@export_path ||= Gitlab::ImportExport.export_path(relative_path: relative_path)
end
@@ -84,11 +84,18 @@ module Gitlab
end
def relative_archive_path
- @relative_archive_path ||= File.join(@project.disk_path, SecureRandom.hex)
+ @relative_archive_path ||= File.join(relative_base_path, SecureRandom.hex)
end
def relative_base_path
- @project.disk_path
+ case exportable_type
+ when 'Project'
+ @exportable.disk_path
+ when 'Group'
+ @exportable.full_path
+ else
+ raise Gitlab::ImportExport::Error.new("Unsupported Exportable Type #{@exportable&.class}")
+ end
end
def log_error(details)
@@ -100,17 +107,24 @@ module Gitlab
end
def log_base_data
- {
- importer: 'Import/Export',
- import_jid: @project&.import_state&.jid,
- project_id: @project&.id,
- project_path: @project&.full_path
+ log = {
+ importer: 'Import/Export',
+ exportable_id: @exportable&.id,
+ exportable_path: @exportable&.full_path
}
+
+ log[:import_jid] = @exportable&.import_state&.jid if exportable_type == 'Project'
+
+ log
end
def filtered_error_message(message)
Projects::ImportErrorFilter.filter_message(message)
end
+
+ def exportable_type
+ @exportable.class.name
+ end
end
end
end
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index e6a5facb2a5..edaa9c645b4 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -21,5 +21,49 @@ module Gitlab
payload[:rugged_duration_ms] = Gitlab::RuggedInstrumentation.query_time_ms
end
end
+
+ # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
+ # `enqueued_at` field or `created_at` field is available.
+ #
+ # * If the job doesn't contain sufficient information, returns nil
+ # * If the job has a start time in the future, returns 0
+ # * If the job contains an invalid start time value, returns nil
+ # @param [Hash] job a Sidekiq job, represented as a hash
+ def self.queue_duration_for_job(job)
+ # Old gitlab-shell messages don't provide enqueued_at/created_at attributes
+ enqueued_at = job['enqueued_at'] || job['created_at']
+ return unless enqueued_at
+
+ enqueued_at_time = convert_to_time(enqueued_at)
+ return unless enqueued_at_time
+
+ # 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
+ end
+
+ # Calculates the time in seconds, as a float, from
+ # the provided start time until now
+ #
+ # @param [Time] start
+ def self.elapsed_by_absolute_time(start)
+ (Time.now - start).to_f.round(6)
+ end
+ private_class_method :elapsed_by_absolute_time
+
+ # Convert a representation of a time into a `Time` value
+ #
+ # @param time_value String, Float time representation, or nil
+ def self.convert_to_time(time_value)
+ return time_value if time_value.is_a?(Time)
+ return Time.iso8601(time_value) if time_value.is_a?(String)
+ return Time.at(time_value) if time_value.is_a?(Numeric) && time_value > 0
+ rescue ArgumentError
+ # Swallow invalid dates. Better to loose some observability
+ # than bring all background processing down because of a date
+ # formatting bug in a client
+ end
+ private_class_method :convert_to_time
end
end
diff --git a/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb b/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb
new file mode 100644
index 00000000000..ef51cee09ca
--- /dev/null
+++ b/lib/gitlab/kubernetes/config_maps/aws_node_auth.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module ConfigMaps
+ class AwsNodeAuth
+ attr_reader :node_role
+
+ def initialize(node_role)
+ @node_role = node_role
+ end
+
+ def generate
+ Kubeclient::Resource.new(
+ metadata: metadata,
+ data: data
+ )
+ end
+
+ private
+
+ def metadata
+ {
+ 'name' => 'aws-auth',
+ 'namespace' => 'kube-system'
+ }
+ end
+
+ def data
+ { 'mapRoles' => instance_role_config(node_role) }
+ end
+
+ def instance_role_config(role)
+ [{
+ 'rolearn' => role,
+ 'username' => 'system:node:{{EC2PrivateDNSName}}',
+ 'groups' => [
+ 'system:bootstrappers',
+ 'system:nodes'
+ ]
+ }].to_yaml
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 16ed0cb0f8e..b5181670b93 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -3,8 +3,8 @@
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.14.3'
- KUBECTL_VERSION = '1.11.10'
+ HELM_VERSION = '2.16.1'
+ KUBECTL_VERSION = '1.13.12'
NAMESPACE = 'gitlab-managed-apps'
SERVICE_ACCOUNT = 'tiller'
CLUSTER_ROLE_BINDING = 'tiller-admin'
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index f572bc43533..ccb053f507d 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -40,7 +40,7 @@ module Gitlab
private
def repository_update_command
- 'helm repo update' if repository
+ 'helm repo update'
end
# Uses `helm upgrade --install` which means we can use this for both
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index d41bd2c43c7..264ea0488e7 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -9,6 +9,7 @@ module Gitlab
module Errors
DashboardProcessingError = Class.new(StandardError)
PanelNotFoundError = Class.new(StandardError)
+ MissingIntegrationError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError)
MissingQueryError = Class.new(DashboardProcessingError)
@@ -22,6 +23,10 @@ module Gitlab
error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError
error(error.message, :not_found)
+ when ::Grafana::Client::Error
+ error(error.message, :service_unavailable)
+ when MissingIntegrationError
+ error('Proxy support for this API is not available currently', :bad_request)
else
raise error
end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
index 297f109ff81..268112f33a9 100644
--- a/lib/gitlab/metrics/dashboard/finder.rb
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -12,6 +12,7 @@ module Gitlab
# @param project [Project]
# @param user [User]
# @param environment [Environment]
+ # @param options [Hash<Symbol,Any>]
# @param options - embedded [Boolean] Determines whether the
# dashboard is to be rendered as part of an
# issue or location other than the primary
@@ -31,6 +32,8 @@ module Gitlab
# @param options - cluster [Cluster]
# @param options - cluster_type [Symbol] The level of
# cluster, one of [:admin, :project, :group]
+ # @param options - grafana_url [String] URL pointing
+ # to a grafana dashboard panel
# @return [Hash]
def find(project, user, options = {})
service_for(options)
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index bfdee76a818..9566e5afb9a 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -17,7 +17,10 @@ module Gitlab
# Returns a new dashboard hash with the results of
# running transforms on the dashboard.
+ # @return [Hash, nil]
def process
+ return unless @dashboard
+
@dashboard.deep_symbolize_keys.tap do |dashboard|
@sequence.each do |stage|
stage.new(@project, dashboard, @params).transform!
diff --git a/lib/gitlab/metrics/dashboard/service_selector.rb b/lib/gitlab/metrics/dashboard/service_selector.rb
index 10b686fbb81..aee7f6685ad 100644
--- a/lib/gitlab/metrics/dashboard/service_selector.rb
+++ b/lib/gitlab/metrics/dashboard/service_selector.rb
@@ -18,6 +18,7 @@ module Gitlab
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
def call(params)
return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
+ return SERVICES::GrafanaMetricEmbedService if grafana_metric_embed?(params)
return SERVICES::DynamicEmbedService if dynamic_embed?(params)
return SERVICES::DefaultEmbedService if params[:embedded]
return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
@@ -40,6 +41,10 @@ module Gitlab
SERVICES::CustomMetricEmbedService.valid_params?(params)
end
+ def grafana_metric_embed?(params)
+ SERVICES::GrafanaMetricEmbedService.valid_params?(params)
+ end
+
def dynamic_embed?(params)
SERVICES::DynamicEmbedService.valid_params?(params)
end
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
index 188912bedb4..62479ed6de4 100644
--- a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -9,7 +9,7 @@ module Gitlab
# find a corresponding database record. If found,
# includes the record's id in the dashboard config.
def transform!
- common_metrics = ::PrometheusMetric.common
+ common_metrics = ::PrometheusMetricsFinder.new(common: true).execute
for_metrics do |metric|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
new file mode 100644
index 00000000000..ce75c54d014
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class GrafanaFormatter < BaseStage
+ include Gitlab::Utils::StrongMemoize
+
+ CHART_TYPE = 'area-chart'
+ PROXY_PATH = 'api/v1/query_range'
+
+ # Reformats the specified panel in the Gitlab
+ # dashboard-yml format
+ def transform!
+ InputFormatValidator.new(
+ grafana_dashboard,
+ datasource,
+ panel,
+ query_params
+ ).validate!
+
+ new_dashboard = formatted_dashboard
+
+ dashboard.clear
+ dashboard.merge!(new_dashboard)
+ end
+
+ private
+
+ def formatted_dashboard
+ { panel_groups: [{ panels: [formatted_panel] }] }
+ end
+
+ def formatted_panel
+ {
+ title: panel[:title],
+ type: CHART_TYPE,
+ y_label: '', # Grafana panels do not include a Y-Axis label
+ metrics: panel[:targets].map.with_index do |target, idx|
+ formatted_metric(target, idx)
+ end
+ }
+ end
+
+ def formatted_metric(metric, idx)
+ {
+ id: "#{metric[:legendFormat]}_#{idx}",
+ query_range: format_query(metric),
+ label: replace_variables(metric[:legendFormat]),
+ prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
+ }.compact
+ end
+
+ # Panel specified by the url from the Grafana dashboard
+ def panel
+ strong_memoize(:panel) do
+ grafana_dashboard[:dashboard][:panels].find do |panel|
+ panel[:id].to_s == query_params[:panelId]
+ end
+ end
+ end
+
+ # Grafana url query parameters. Includes information
+ # on which panel to select and time range.
+ def query_params
+ strong_memoize(:query_params) do
+ Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
+ end
+ end
+
+ # Endpoint which will return prometheus metric data
+ # for the metric
+ def prometheus_endpoint_for_metric(metric)
+ Gitlab::Routing.url_helpers.project_grafana_api_path(
+ project,
+ datasource_id: datasource[:id],
+ proxy_path: PROXY_PATH,
+ query: format_query(metric)
+ )
+ end
+
+ # Reformats query for compatibility with prometheus api.
+ def format_query(metric)
+ expression = remove_new_lines(metric[:expr])
+ expression = replace_variables(expression)
+ expression = replace_global_variables(expression, metric)
+
+ expression
+ end
+
+ # Accomodates instance-defined Grafana variables.
+ # These are variables defined by users, and values
+ # must be provided in the query parameters.
+ def replace_variables(expression)
+ return expression unless grafana_dashboard[:dashboard][:templating]
+
+ grafana_dashboard[:dashboard][:templating][:list]
+ .sort_by { |variable| variable[:name].length }
+ .each do |variable|
+ variable_value = query_params[:"var-#{variable[:name]}"]
+
+ expression = expression.gsub("$#{variable[:name]}", variable_value)
+ expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
+ expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
+ end
+
+ expression
+ end
+
+ # Replaces Grafana global built-in variables with values.
+ # Only $__interval and $__from and $__to are supported.
+ #
+ # See https://grafana.com/docs/reference/templating/#global-built-in-variables
+ def replace_global_variables(expression, metric)
+ expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
+ expression = expression.gsub('$__from', query_params[:from])
+ expression = expression.gsub('$__to', query_params[:to])
+
+ expression
+ end
+
+ # Removes new lines from expression.
+ def remove_new_lines(expression)
+ expression.gsub(/\R+/, '')
+ end
+
+ # Grafana datasource object corresponding to the
+ # specified dashboard
+ def datasource
+ params[:datasource]
+ end
+
+ # The specified Grafana dashboard
+ def grafana_dashboard
+ params[:grafana_dashboard]
+ end
+
+ # The URL specifying which Grafana panel to embed
+ def grafana_url
+ params[:grafana_url]
+ end
+ end
+
+ class InputFormatValidator
+ include ::Gitlab::Metrics::Dashboard::Errors
+
+ attr_reader :grafana_dashboard, :datasource, :panel, :query_params
+
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
+ $__interval_ms
+ $__timeFilter
+ $__name
+ $timeFilter
+ $interval
+ ).freeze
+
+ def initialize(grafana_dashboard, datasource, panel, query_params)
+ @grafana_dashboard = grafana_dashboard
+ @datasource = datasource
+ @panel = panel
+ @query_params = query_params
+ end
+
+ def validate!
+ validate_query_params!
+ validate_datasource!
+ validate_panel_type!
+ validate_variable_definitions!
+ validate_global_variables!
+ end
+
+ private
+
+ def validate_datasource!
+ return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
+
+ raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
+ end
+
+ def validate_query_params!
+ return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
+
+ raise_error 'Grafana query parameters must include panelId, from, and to.'
+ end
+
+ def validate_panel_type!
+ return if panel[:type] == 'graph' && panel[:lines]
+
+ raise_error 'Panel type must be a line graph.'
+ end
+
+ def validate_variable_definitions!
+ return unless grafana_dashboard[:dashboard][:templating]
+
+ return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
+ query_params[:"var-#{variable[:name]}"].present?
+ end
+
+ raise_error 'All Grafana variables must be defined in the query parameters.'
+ end
+
+ def validate_global_variables!
+ return unless panel_contains_unsupported_vars?
+
+ raise_error 'Prometheus must not include'
+ end
+
+ def panel_contains_unsupported_vars?
+ panel[:targets].any? do |target|
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
+ target[:expr].include?(variable)
+ end
+ end
+ end
+
+ def raise_error(message)
+ raise DashboardProcessingError.new(message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
index 643be309992..c0f67d445f8 100644
--- a/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
+++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
@@ -9,7 +9,7 @@ module Gitlab
# config. If there are no project-specific metrics,
# this will have no effect.
def transform!
- project.prometheus_metrics.each do |project_metric|
+ PrometheusMetricsFinder.new(project: project).execute.each do |project_metric|
group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
panel = find_or_create_panel(group[:panels], project_metric)
find_or_create_metric(panel[:metrics], project_metric)
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 94f8b2e02b1..712f769bbeb 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -14,17 +14,31 @@ module Gitlab
def regex
%r{
(?<url>
- #{Regexp.escape(Gitlab.config.gitlab.url)}
- \/#{Project.reference_pattern}
+ #{gitlab_pattern}
+ #{project_pattern}
(?:\/\-)?
\/environments
\/(?<environment>\d+)
\/metrics
- (?<query>
- \?[a-zA-Z0-9%.()+_=-]+
- (&[a-zA-Z0-9%.()+_=-]+)*
- )?
- (?<anchor>\#[a-z0-9_-]+)?
+ #{query_pattern}
+ #{anchor_pattern}
+ )
+ }x
+ end
+
+ # Matches dashboard urls for a Grafana embed.
+ #
+ # EX - https://<host>/<namespace>/<project>/grafana/metrics_dashboard
+ def grafana_regex
+ %r{
+ (?<url>
+ #{gitlab_pattern}
+ #{project_pattern}
+ (?:\/\-)?
+ \/grafana
+ \/metrics_dashboard
+ #{query_pattern}
+ #{anchor_pattern}
)
}x
end
@@ -45,6 +59,24 @@ module Gitlab
def build_dashboard_url(*args)
Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
end
+
+ private
+
+ def gitlab_pattern
+ Regexp.escape(Gitlab.config.gitlab.url)
+ end
+
+ def project_pattern
+ "\/#{Project.reference_pattern}"
+ end
+
+ def query_pattern
+ '(?<query>\?[a-zA-Z0-9%.()+_=-]+(&[a-zA-Z0-9%.()+_=-]+)*)?'
+ end
+
+ def anchor_pattern
+ '(?<anchor>\#[a-z0-9_-]+)?'
+ end
end
end
end
diff --git a/lib/gitlab/metrics/exporter/web_exporter.rb b/lib/gitlab/metrics/exporter/web_exporter.rb
index 3940f6fa155..b6a27d8556a 100644
--- a/lib/gitlab/metrics/exporter/web_exporter.rb
+++ b/lib/gitlab/metrics/exporter/web_exporter.rb
@@ -20,6 +20,10 @@ module Gitlab
def initialize
super
+ # DEPRECATED:
+ # these `readiness_checks` are deprecated
+ # as presenting no value in a way how we run
+ # application: https://gitlab.com/gitlab-org/gitlab/issues/35343
self.readiness_checks = [
WebExporter::ExporterCheck.new(self),
Gitlab::HealthChecks::PumaCheck,
@@ -35,6 +39,10 @@ module Gitlab
File.join(Rails.root, 'log', 'web_exporter.log')
end
+ def mark_as_not_running!
+ @running = false
+ end
+
private
def start_working
@@ -43,24 +51,9 @@ module Gitlab
end
def stop_working
- @running = false
- wait_in_blackout_period if server && thread.alive?
+ mark_as_not_running!
super
end
-
- def wait_in_blackout_period
- return unless blackout_seconds > 0
-
- @server.logger.info(
- message: 'starting blackout...',
- duration_s: blackout_seconds)
-
- sleep(blackout_seconds)
- end
-
- def blackout_seconds
- settings['blackout_seconds'].to_i
- end
end
end
end
diff --git a/lib/gitlab/metrics/requests_rack_middleware.rb b/lib/gitlab/metrics/requests_rack_middleware.rb
index 085e28123a7..b57f9a19f8e 100644
--- a/lib/gitlab/metrics/requests_rack_middleware.rb
+++ b/lib/gitlab/metrics/requests_rack_middleware.rb
@@ -35,7 +35,7 @@ module Gitlab
def self.initialize_http_request_duration_seconds
HTTP_METHODS.each do |method, statuses|
statuses.each do |status|
- http_request_duration_seconds.get({ method: method, status: status.to_i })
+ http_request_duration_seconds.get({ method: method, status: status.to_s })
end
end
end
@@ -49,7 +49,7 @@ module Gitlab
status, headers, body = @app.call(env)
elapsed = Time.now.to_f - started
- RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status }, elapsed)
+ RequestsRackMiddleware.http_request_duration_seconds.observe({ method: method, status: status.to_s }, elapsed)
[status, headers, body]
rescue
diff --git a/lib/gitlab/pagination/base.rb b/lib/gitlab/pagination/base.rb
new file mode 100644
index 00000000000..90fa1f8d1ec
--- /dev/null
+++ b/lib/gitlab/pagination/base.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ class Base
+ private
+
+ def per_page
+ @per_page ||= params[:per_page]
+ end
+
+ def base_request_uri
+ @base_request_uri ||= URI.parse(request.url).tap do |uri|
+ uri.host = Gitlab.config.gitlab.host
+ uri.port = Gitlab.config.gitlab.port
+ end
+ end
+
+ def build_page_url(query_params:)
+ base_request_uri.tap do |uri|
+ uri.query = query_params
+ end.to_s
+ end
+
+ def page_href(next_page_params = {})
+ query_params = params.merge(**next_page_params, per_page: per_page).to_query
+
+ build_page_url(query_params: query_params)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/offset_pagination.rb b/lib/gitlab/pagination/offset_pagination.rb
new file mode 100644
index 00000000000..bf31f252a6b
--- /dev/null
+++ b/lib/gitlab/pagination/offset_pagination.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ class OffsetPagination < Base
+ attr_reader :request_context
+ delegate :params, :header, :request, to: :request_context
+
+ def initialize(request_context)
+ @request_context = request_context
+ end
+
+ def paginate(relation)
+ paginate_with_limit_optimization(add_default_order(relation)).tap do |data|
+ add_pagination_headers(data)
+ end
+ end
+
+ private
+
+ def paginate_with_limit_optimization(relation)
+ pagination_data = relation.page(params[:page]).per(params[:per_page])
+ return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation)
+ return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit)
+
+ limited_total_count = pagination_data.total_count_with_limit
+ if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT
+ # The call to `total_count_with_limit` memoizes `@arel` because of a call to `references_eager_loaded_tables?`
+ # We need to call `reset` because `without_count` relies on `@arel` being unmemoized
+ pagination_data.reset.without_count
+ else
+ pagination_data
+ end
+ end
+
+ def add_default_order(relation)
+ if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty?
+ relation = relation.order(:id) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ relation
+ end
+
+ def add_pagination_headers(paginated_data)
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+
+ return if data_without_counts?(paginated_data)
+
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', total_pages(paginated_data).to_s
+ end
+
+ def pagination_links(paginated_data)
+ [].tap do |links|
+ links << %(<#{page_href(page: paginated_data.prev_page)}>; rel="prev") if paginated_data.prev_page
+ links << %(<#{page_href(page: paginated_data.next_page)}>; rel="next") if paginated_data.next_page
+ links << %(<#{page_href(page: 1)}>; rel="first")
+
+ links << %(<#{page_href(page: total_pages(paginated_data))}>; rel="last") unless data_without_counts?(paginated_data)
+ end.join(', ')
+ end
+
+ def total_pages(paginated_data)
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
+ end
+
+ def data_without_counts?(paginated_data)
+ paginated_data.is_a?(Kaminari::PaginatableWithoutCount)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_authorizations.rb b/lib/gitlab/project_authorizations.rb
index a9270cd536e..4e5e2d4a6a9 100644
--- a/lib/gitlab/project_authorizations.rb
+++ b/lib/gitlab/project_authorizations.rb
@@ -57,7 +57,7 @@ module Gitlab
private
# Builds a recursive CTE that gets all the groups the current user has
- # access to, including any nested groups.
+ # access to, including any nested groups and any shared groups.
def recursive_cte
cte = Gitlab::SQL::RecursiveCTE.new(:namespaces_cte)
members = Member.arel_table
@@ -68,20 +68,27 @@ module Gitlab
.select([namespaces[:id], members[:access_level]])
.except(:order)
+ if Feature.enabled?(:share_group_with_group)
+ # Namespaces shared with any of the group
+ cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level'])
+ .joins(join_group_group_links)
+ .joins(join_members_on_group_group_links)
+ end
+
# Sub groups of any groups the user is a member of.
cte << Group.select([
namespaces[:id],
greatest(members[:access_level], cte.table[:access_level], 'access_level')
])
.joins(join_cte(cte))
- .joins(join_members)
+ .joins(join_members_on_namespaces)
.except(:order)
cte
end
# Builds a LEFT JOIN to join optional memberships onto the CTE.
- def join_members
+ def join_members_on_namespaces
members = Member.arel_table
namespaces = Namespace.arel_table
@@ -94,6 +101,23 @@ module Gitlab
Arel::Nodes::OuterJoin.new(members, Arel::Nodes::On.new(cond))
end
+ def join_group_group_links
+ group_group_links = GroupGroupLink.arel_table
+ namespaces = Namespace.arel_table
+
+ cond = group_group_links[:shared_group_id].eq(namespaces[:id])
+ Arel::Nodes::InnerJoin.new(group_group_links, Arel::Nodes::On.new(cond))
+ end
+
+ def join_members_on_group_group_links
+ group_group_links = GroupGroupLink.arel_table
+ members = Member.arel_table
+
+ cond = group_group_links[:shared_with_group_id].eq(members[:source_id])
+ .and(members[:user_id].eq(user.id))
+ Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
+ end
+
# Builds an INNER JOIN to join namespaces onto the CTE.
def join_cte(cte)
namespaces = Namespace.arel_table
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index fa1d1203842..279fc4aa375 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -53,7 +53,8 @@ module Gitlab
ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'),
ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'),
- ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg')
+ ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'),
+ ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
].freeze
class << self
diff --git a/lib/gitlab/prometheus/internal.rb b/lib/gitlab/prometheus/internal.rb
new file mode 100644
index 00000000000..d59352119ba
--- /dev/null
+++ b/lib/gitlab/prometheus/internal.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Prometheus
+ class Internal
+ def self.uri
+ return if listen_address.blank?
+
+ if listen_address.starts_with?('0.0.0.0:')
+ # 0.0.0.0:9090
+ port = ':' + listen_address.split(':').second
+ 'http://localhost' + port
+
+ elsif listen_address.starts_with?(':')
+ # :9090
+ 'http://localhost' + listen_address
+
+ elsif listen_address.starts_with?('http')
+ # https://localhost:9090
+ listen_address
+
+ else
+ # localhost:9090
+ 'http://' + listen_address
+ end
+ end
+
+ def self.listen_address
+ Gitlab.config.prometheus.listen_address.to_s if Gitlab.config.prometheus
+ rescue Settingslogic::MissingSetting
+ Gitlab::AppLogger.error('Prometheus listen_address is not present in config/gitlab.yml')
+
+ nil
+ end
+
+ def self.prometheus_enabled?
+ Gitlab.config.prometheus.enable if Gitlab.config.prometheus
+ rescue Settingslogic::MissingSetting
+ Gitlab::AppLogger.error('prometheus.enable is not present in config/gitlab.yml')
+
+ false
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb
index caf0d453b6f..1b6f7282eb3 100644
--- a/lib/gitlab/prometheus/metric_group.rb
+++ b/lib/gitlab/prometheus/metric_group.rb
@@ -11,13 +11,15 @@ module Gitlab
validates :name, :priority, :metrics, presence: true
def self.common_metrics
- all_groups = ::PrometheusMetric.common.group_by(&:group_title).map do |name, metrics|
- MetricGroup.new(
- name: name,
- priority: metrics.map(&:priority).max,
- metrics: metrics.map(&:to_query_metric)
- )
- end
+ all_groups = ::PrometheusMetricsFinder.new(common: true).execute
+ .group_by(&:group_title)
+ .map do |name, metrics|
+ MetricGroup.new(
+ name: name,
+ priority: metrics.map(&:priority).max,
+ metrics: metrics.map(&:to_query_metric)
+ )
+ end
all_groups.sort_by(&:priority).reverse
end
diff --git a/lib/gitlab/prometheus/queries/knative_invocation_query.rb b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
index 2691abe46d6..8873608c411 100644
--- a/lib/gitlab/prometheus/queries/knative_invocation_query.rb
+++ b/lib/gitlab/prometheus/queries/knative_invocation_query.rb
@@ -7,11 +7,14 @@ module Gitlab
include QueryAdditionalMetrics
def query(serverless_function_id)
- PrometheusMetric
- .find_by_identifier(:system_metrics_knative_function_invocation_count)
- .to_query_metric.tap do |q|
- q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
- end
+ PrometheusMetricsFinder
+ .new(identifier: :system_metrics_knative_function_invocation_count, common: true)
+ .execute
+ .first
+ .to_query_metric
+ .tap do |q|
+ q.queries[0][:result] = run_query(q.queries[0][:query_range], context(serverless_function_id))
+ end
end
protected
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 340ec75c5f1..942f90e8040 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -234,7 +234,7 @@ module Gitlab
"#{comment} #{SHRUG}"
end
- desc _("Append the comment with %{TABLEFLIP}") % { tableflip: TABLEFLIP }
+ desc _("Append the comment with %{tableflip}") % { tableflip: TABLEFLIP }
params '<Comment>'
types Issuable
substitution :tableflip do |comment|
diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb
index 404e0c31871..838aefb59f0 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -174,18 +174,14 @@ module Gitlab
params '<Zoom URL>'
types Issue
condition do
- zoom_link_service.can_add_link?
+ @zoom_service = zoom_link_service
+ @zoom_service.can_add_link?
end
parse_params do |link|
- zoom_link_service.parse_link(link)
+ @zoom_service.parse_link(link)
end
command :zoom do |link|
- result = zoom_link_service.add_link(link)
-
- if result.success?
- @updates[:description] = result.payload[:description]
- end
-
+ result = @zoom_service.add_link(link)
@execution_message[:zoom] = result.message
end
@@ -194,15 +190,11 @@ module Gitlab
execution_message _('Zoom meeting removed')
types Issue
condition do
- zoom_link_service.can_remove_link?
+ @zoom_service = zoom_link_service
+ @zoom_service.can_remove_link?
end
command :remove_zoom do
- result = zoom_link_service.remove_link
-
- if result.success?
- @updates[:description] = result.payload[:description]
- end
-
+ result = @zoom_service.remove_link
@execution_message[:remove_zoom] = result.message
end
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index fa1615a5953..412d00c6939 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -25,6 +25,8 @@ module Gitlab
if Sidekiq.server?
# the pool will be used in a multi-threaded context
size += Sidekiq.options[:concurrency]
+ elsif defined?(::Puma)
+ size += Puma.cli_config.options[:max_threads]
end
size
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 3d1f15c72ae..e3a434dfe35 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -120,13 +120,26 @@ module Gitlab
@breakline_regex ||= /\r\n|\r|\n/
end
+ # https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
+ def aws_account_id_regex
+ /\A\d{12}\z/
+ end
+
+ def aws_account_id_message
+ 'must be a 12-digit number'
+ end
+
# https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
def aws_arn_regex
/\Aarn:\S+\z/
end
def aws_arn_regex_message
- "must be a valid Amazon Resource Name"
+ 'must be a valid Amazon Resource Name'
+ end
+
+ def utc_date_regex
+ @utc_date_regex ||= /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/.freeze
end
end
end
diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb
index fa09ecbdf30..360239a84e4 100644
--- a/lib/gitlab/search/found_blob.rb
+++ b/lib/gitlab/search/found_blob.rb
@@ -8,20 +8,20 @@ module Gitlab
include BlobLanguageFromGitAttributes
include Gitlab::Utils::StrongMemoize
- attr_reader :project, :content_match, :blob_filename
+ attr_reader :project, :content_match, :blob_path
- FILENAME_REGEXP = /\A(?<ref>[^:]*):(?<filename>[^\x00]*)\x00/.freeze
- CONTENT_REGEXP = /^(?<ref>[^:]*):(?<filename>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
+ PATH_REGEXP = /\A(?<ref>[^:]*):(?<path>[^\x00]*)\x00/.freeze
+ CONTENT_REGEXP = /^(?<ref>[^:]*):(?<path>[^\x00]*)\x00(?<startline>\d+)\x00/.freeze
def self.preload_blobs(blobs)
- to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_filename }
+ to_fetch = blobs.select { |blob| blob.is_a?(self) && blob.blob_path }
to_fetch.each { |blob| blob.fetch_blob }
end
def initialize(opts = {})
@id = opts.fetch(:id, nil)
- @binary_filename = opts.fetch(:filename, nil)
+ @binary_path = opts.fetch(:path, nil)
@binary_basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
@@ -34,7 +34,7 @@ module Gitlab
# Allow those to just pass project_id instead.
@project_id = opts.fetch(:project_id, nil)
@content_match = opts.fetch(:content_match, nil)
- @blob_filename = opts.fetch(:blob_filename, nil)
+ @blob_path = opts.fetch(:blob_path, nil)
@repository = opts.fetch(:repository, nil)
end
@@ -50,16 +50,16 @@ module Gitlab
@startline ||= parsed_content[:startline]
end
- # binary_filename is used for running filters on all matches,
- # for grepped results (which use content_match), we get
- # filename from the beginning of the grepped result which is faster
- # then parsing whole snippet
- def binary_filename
- @binary_filename ||= content_match ? search_result_filename : parsed_content[:binary_filename]
+ # binary_path is used for running filters on all matches.
+ # For grepped results (which use content_match), we get
+ # the path from the beginning of the grepped result which is faster
+ # than parsing the whole snippet
+ def binary_path
+ @binary_path ||= content_match ? search_result_path : parsed_content[:binary_path]
end
- def filename
- @filename ||= encode_utf8(@binary_filename || parsed_content[:binary_filename])
+ def path
+ @path ||= encode_utf8(@binary_path || parsed_content[:binary_path])
end
def basename
@@ -70,10 +70,6 @@ module Gitlab
@data ||= encode_utf8(@binary_data || parsed_content[:binary_data])
end
- def path
- filename
- end
-
def project_id
@project_id || @project&.id
end
@@ -83,16 +79,16 @@ module Gitlab
end
def fetch_blob
- path = [ref, blob_filename]
- missing_blob = { binary_filename: blob_filename }
+ path = [ref, blob_path]
+ missing_blob = { binary_path: blob_path }
BatchLoader.for(path).batch(default_value: missing_blob) do |refs, loader|
Gitlab::Git::Blob.batch(repository, refs, blob_size_limit: 1024).each do |blob|
# if the blob couldn't be fetched for some reason,
- # show at least the blob filename
+ # show at least the blob path
data = {
id: blob.id,
- binary_filename: blob.path,
+ binary_path: blob.path,
binary_basename: path_without_extension(blob.path),
ref: ref,
startline: 1,
@@ -107,8 +103,8 @@ module Gitlab
private
- def search_result_filename
- content_match.match(FILENAME_REGEXP) { |matches| matches[:filename] }
+ def search_result_path
+ content_match.match(PATH_REGEXP) { |matches| matches[:path] }
end
def path_without_extension(path)
@@ -119,7 +115,7 @@ module Gitlab
strong_memoize(:parsed_content) do
if content_match
parse_search_result
- elsif blob_filename
+ elsif blob_path
fetch_blob
else
{}
@@ -129,7 +125,7 @@ module Gitlab
def parse_search_result
ref = nil
- filename = nil
+ path = nil
basename = nil
data = []
@@ -138,17 +134,17 @@ module Gitlab
content_match.each_line.each_with_index do |line, index|
prefix ||= line.match(CONTENT_REGEXP)&.tap do |matches|
ref = matches[:ref]
- filename = matches[:filename]
+ path = matches[:path]
startline = matches[:startline]
startline = startline.to_i - index
- basename = path_without_extension(filename)
+ basename = path_without_extension(path)
end
data << line.sub(prefix.to_s, '')
end
{
- binary_filename: filename,
+ binary_path: path,
binary_basename: basename,
ref: ref,
startline: startline,
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 8e2f16271eb..f96346322db 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -14,7 +14,71 @@ end
module Gitlab
class Seeder
+ extend ActionView::Helpers::NumberHelper
+
+ ESTIMATED_INSERT_PER_MINUTE = 2_000_000
+ MASS_INSERT_ENV = 'MASS_INSERT'
+
+ module ProjectSeed
+ extend ActiveSupport::Concern
+
+ included do
+ scope :not_mass_generated, -> do
+ where.not("path LIKE '#{Gitlab::Seeder::Projects::MASS_INSERT_NAME_START}%'")
+ end
+ end
+ end
+
+ module UserSeed
+ extend ActiveSupport::Concern
+
+ included do
+ scope :not_mass_generated, -> do
+ where.not("username LIKE '#{Gitlab::Seeder::Users::MASS_INSERT_USERNAME_START}%'")
+ end
+ end
+ end
+
+ def self.with_mass_insert(size, model)
+ humanized_model_name = model.is_a?(String) ? model : model.model_name.human.pluralize(size)
+
+ if !ENV[MASS_INSERT_ENV] && !ENV['CI']
+ puts "\nSkipping mass insertion for #{humanized_model_name}."
+ puts "Consider running the seed with #{MASS_INSERT_ENV}=1"
+ return
+ end
+
+ humanized_size = number_with_delimiter(size)
+ estimative = estimated_time_message(size)
+
+ puts "\nCreating #{humanized_size} #{humanized_model_name}."
+ puts estimative
+
+ yield
+
+ puts "\n#{number_with_delimiter(size)} #{humanized_model_name} created!"
+ end
+
+ def self.estimated_time_message(size)
+ estimated_minutes = (size.to_f / ESTIMATED_INSERT_PER_MINUTE).round
+ humanized_minutes = 'minute'.pluralize(estimated_minutes)
+
+ if estimated_minutes.zero?
+ "Rough estimated time: less than a minute â°"
+ else
+ "Rough estimated time: #{estimated_minutes} #{humanized_minutes} â°"
+ end
+ end
+
def self.quiet
+ # Disable database insertion logs so speed isn't limited by ability to print to console
+ old_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+
+ # Additional seed logic for models.
+ Project.include(ProjectSeed)
+ User.include(UserSeed)
+
mute_notifications
mute_mailer
@@ -23,6 +87,7 @@ module Gitlab
yield
SeedFu.quiet = false
+ ActiveRecord::Base.logger = old_logger
puts "\nOK".color(:green)
end
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
index eb242cc7c20..bb7571dd66a 100644
--- a/lib/gitlab/serializer/pagination.rb
+++ b/lib/gitlab/serializer/pagination.rb
@@ -4,7 +4,6 @@ module Gitlab
module Serializer
class Pagination
InvalidResourceError = Class.new(StandardError)
- include ::API::Helpers::Pagination
def initialize(request, response)
@request = request
@@ -13,13 +12,13 @@ module Gitlab
def paginate(resource)
if resource.respond_to?(:page)
- super(resource)
+ ::Gitlab::Pagination::OffsetPagination.new(self).paginate(resource)
else
raise InvalidResourceError
end
end
- # Methods needed by `API::Helpers::Pagination`
+ # Methods needed by `Gitlab::Pagination::OffsetPagination`
#
attr_reader :request
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 0d3e78c0a66..c449c6879bc 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -40,6 +40,11 @@ module Gitlab
config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
+
+ internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
+ FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
+ config[:internal_socket_dir] = internal_socket_dir
+
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 125d0d1cfbb..28e5d0ba8f5 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -285,18 +285,6 @@ module Gitlab
end
end
- # Check if such directory exists in repositories.
- #
- # Usage:
- # exists?(storage, 'gitlab')
- # exists?(storage, 'gitlab/cookies.git')
- #
- # rubocop: disable CodeReuse/ActiveRecord
- def exists?(storage, dir_name)
- Gitlab::GitalyClient::NamespaceService.new(storage).exists?(dir_name)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def repository_exists?(storage, dir_name)
Gitlab::Git::Repository.new(storage, dir_name, nil, nil).exists?
rescue GRPC::Internal
diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb
index a3d61c69ae1..0723b514c90 100644
--- a/lib/gitlab/sidekiq_daemon/monitor.rb
+++ b/lib/gitlab/sidekiq_daemon/monitor.rb
@@ -4,6 +4,7 @@ module Gitlab
module SidekiqDaemon
class Monitor < Daemon
include ::Gitlab::Utils::StrongMemoize
+ extend ::Gitlab::Utils::Override
NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'
CANCEL_DEADLINE = 24.hours.seconds
@@ -24,6 +25,11 @@ module Gitlab
@jobs_mutex = Mutex.new
end
+ override :thread_name
+ def thread_name
+ "job_monitor"
+ end
+
def within_job(worker_class, jid, queue)
jobs_mutex.synchronize do
jobs[jid] = { worker_class: worker_class, thread: Thread.current, started_at: Gitlab::Metrics::System.monotonic_time }
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index 853fb2777c3..ca9e3b8428c 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -36,11 +36,8 @@ module Gitlab
payload['message'] = "#{base_message(payload)}: start"
payload['job_status'] = 'start'
- # Old gitlab-shell messages don't provide enqueued_at/created_at attributes
- enqueued_at = payload['enqueued_at'] || payload['created_at']
- if enqueued_at
- payload['scheduling_latency_s'] = elapsed_by_absolute_time(Time.iso8601(enqueued_at))
- end
+ scheduling_latency_s = ::Gitlab::InstrumentationHelper.queue_duration_for_job(payload)
+ payload['scheduling_latency_s'] = scheduling_latency_s if scheduling_latency_s
payload
end
@@ -98,10 +95,6 @@ module Gitlab
end
end
- def elapsed_by_absolute_time(start)
- (Time.now.utc - start).to_f.round(6)
- end
-
def elapsed(t0)
t1 = get_time
{
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index 8af353d8674..bd819843bd4 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -9,43 +9,56 @@ module Gitlab
def initialize
@metrics = init_metrics
+
+ @metrics[:sidekiq_concurrency].set({}, Sidekiq.options[:concurrency].to_i)
end
def call(_worker, job, queue)
labels = create_labels(queue)
+ queue_duration = ::Gitlab::InstrumentationHelper.queue_duration_for_job(job)
+
+ @metrics[:sidekiq_jobs_queue_duration_seconds].observe(labels, queue_duration) if queue_duration
@metrics[:sidekiq_running_jobs].increment(labels, 1)
if job['retry_count'].present?
@metrics[:sidekiq_jobs_retried_total].increment(labels, 1)
end
+ job_succeeded = false
+ monotonic_time_start = Gitlab::Metrics::System.monotonic_time
job_thread_cputime_start = get_thread_cputime
-
- realtime = Benchmark.realtime do
+ begin
yield
- end
+ job_succeeded = true
+ ensure
+ monotonic_time_end = Gitlab::Metrics::System.monotonic_time
+ job_thread_cputime_end = get_thread_cputime
+
+ monotonic_time = monotonic_time_end - monotonic_time_start
+ job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- job_thread_cputime_end = get_thread_cputime
- job_thread_cputime = job_thread_cputime_end - job_thread_cputime_start
- @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
+ # sidekiq_running_jobs, sidekiq_jobs_failed_total should not include the job_status label
+ @metrics[:sidekiq_running_jobs].increment(labels, -1)
+ @metrics[:sidekiq_jobs_failed_total].increment(labels, 1) unless job_succeeded
- @metrics[:sidekiq_jobs_completion_seconds].observe(labels, realtime)
- rescue Exception # rubocop: disable Lint/RescueException
- @metrics[:sidekiq_jobs_failed_total].increment(labels, 1)
- raise
- ensure
- @metrics[:sidekiq_running_jobs].increment(labels, -1)
+ # job_status: done, fail match the job_status attribute in structured logging
+ labels[:job_status] = job_succeeded ? :done : :fail
+ @metrics[:sidekiq_jobs_cpu_seconds].observe(labels, job_thread_cputime)
+ @metrics[:sidekiq_jobs_completion_seconds].observe(labels, monotonic_time)
+ end
end
private
def init_metrics
{
- sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
- sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
- sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
- sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum)
+ sidekiq_jobs_cpu_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_cpu_seconds, 'Seconds of cpu time to run Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete Sidekiq job', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_queue_duration_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_queue_duration_seconds, 'Duration in seconds that a Sidekiq job was queued before being executed', {}, SIDEKIQ_LATENCY_BUCKETS),
+ sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
+ sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
+ sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :all),
+ sidekiq_concurrency: ::Gitlab::Metrics.gauge(:sidekiq_concurrency, 'Maximum number of Sidekiq jobs', {}, :all)
}
end
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index 079b5916566..239479f99d2 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -10,6 +10,7 @@ module Gitlab
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::IssueClose,
+ Gitlab::SlashCommands::IssueComment,
Gitlab::SlashCommands::Deploy,
Gitlab::SlashCommands::Run
]
diff --git a/lib/gitlab/slash_commands/issue_comment.rb b/lib/gitlab/slash_commands/issue_comment.rb
new file mode 100644
index 00000000000..cbb9c41aab0
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_comment.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ class IssueComment < IssueCommand
+ def self.match(text)
+ /\Aissue\s+comment\s+#{Issue.reference_prefix}?(?<iid>\d+)\n*(?<note_body>(.|\n)*)/.match(text)
+ end
+
+ def self.help_message
+ 'issue comment <id> *`⇧ Shift`*+*`↵ Enter`* <comment>'
+ end
+
+ def execute(match)
+ note_body = match[:note_body].to_s.strip
+ issue = find_by_iid(match[:iid])
+
+ return not_found unless issue
+ return access_denied unless can_create_note?(issue)
+
+ note = create_note(issue: issue, note: note_body)
+
+ if note.persisted?
+ presenter(note).present
+ else
+ presenter(note).display_errors
+ end
+ end
+
+ private
+
+ def can_create_note?(issue)
+ Ability.allowed?(current_user, :create_note, issue)
+ end
+
+ def not_found
+ Gitlab::SlashCommands::Presenters::Access.new.not_found
+ end
+
+ def access_denied
+ Gitlab::SlashCommands::Presenters::Access.new.generic_access_denied
+ end
+
+ def create_note(issue:, note:)
+ note_params = { noteable: issue, note: note }
+
+ Notes::CreateService.new(project, current_user, note_params).execute
+ end
+
+ def presenter(note)
+ Gitlab::SlashCommands::Presenters::IssueComment.new(note)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
index 9ce1bcfb37c..fbc3cf2e049 100644
--- a/lib/gitlab/slash_commands/presenters/access.rb
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -15,6 +15,10 @@ module Gitlab
MESSAGE
end
+ def generic_access_denied
+ ephemeral_response(text: 'You are not allowed to perform the given chatops command.')
+ end
+
def deactivated
ephemeral_response(text: <<~MESSAGE)
You are not allowed to perform the given chatops command since
diff --git a/lib/gitlab/slash_commands/presenters/issue_comment.rb b/lib/gitlab/slash_commands/presenters/issue_comment.rb
new file mode 100644
index 00000000000..cce71e23b21
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_comment.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class IssueComment < Presenters::Base
+ include Presenters::NoteBase
+
+ def present
+ ephemeral_response(new_note)
+ end
+
+ private
+
+ def new_note
+ {
+ attachments: [
+ {
+ title: "#{issue.title} · #{issue.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New comment on #{issue.to_reference}: #{issue.title}",
+ pretext: pretext,
+ color: color,
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I commented on an issue on #{author_profile_link}'s behalf: *#{issue.to_reference}* in #{project_link}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/note_base.rb b/lib/gitlab/slash_commands/presenters/note_base.rb
new file mode 100644
index 00000000000..7758fc740de
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/note_base.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SlashCommands
+ module Presenters
+ module NoteBase
+ GREEN = '#38ae67'
+
+ def color
+ GREEN
+ end
+
+ def issue
+ resource.noteable
+ end
+
+ def project
+ issue.project
+ end
+
+ def project_link
+ "[#{project.full_name}](#{project.web_url})"
+ end
+
+ def author
+ resource.author
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+
+ def fields
+ [
+ {
+ title: 'Comment',
+ value: resource.note
+ }
+ ]
+ end
+
+ private
+
+ attr_reader :resource
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sourcegraph.rb b/lib/gitlab/sourcegraph.rb
new file mode 100644
index 00000000000..d0f12c8364a
--- /dev/null
+++ b/lib/gitlab/sourcegraph.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class Sourcegraph
+ class << self
+ def feature_conditional?
+ feature.conditional?
+ end
+
+ def feature_available?
+ # The sourcegraph_bundle feature could be conditionally applied, so check if `!off?`
+ !feature.off?
+ end
+
+ def feature_enabled?(thing = nil)
+ feature.enabled?(thing)
+ end
+
+ private
+
+ def feature
+ Feature.get(:sourcegraph)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index f05592fc3a3..b15f2ca385a 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -29,7 +29,7 @@ module Gitlab
end
if fragments.any?
- fragments.join("\n#{union_keyword}\n")
+ "(" + fragments.join(")\n#{union_keyword}\n(") + ")"
else
'NULL'
end
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index 8532845f3cb..ac02ec635e4 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -158,15 +158,17 @@ module Gitlab
end
def checkout_or_clone_version(version:, repo:, target_dir:)
- version =
- if version.starts_with?("=")
- version.sub(/\A=/, '') # tag or branch
- else
- "v#{version}" # tag
- end
-
clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
- checkout_version(version, target_dir)
+ checkout_version(get_version(version), target_dir)
+ end
+
+ # this function implements the same logic we have in omnibus for dealing with components version
+ def get_version(component_version)
+ # If not a valid version string following SemVer it is probably a branch name or a SHA
+ # commit of one of our own component so it doesn't need `v` prepended
+ return component_version unless /^\d+\.\d+\.\d+(-rc\d+)?$/.match?(component_version)
+
+ "v#{component_version}"
end
def clone_repo(repo, target_dir)
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 2470685bc00..91e2ff0b10d 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -45,9 +45,10 @@ module Gitlab
namespace: SNOWPLOW_NAMESPACE,
hostname: Gitlab::CurrentSettings.snowplow_collector_hostname,
cookie_domain: Gitlab::CurrentSettings.snowplow_cookie_domain,
- app_id: Gitlab::CurrentSettings.snowplow_site_id,
+ app_id: Gitlab::CurrentSettings.snowplow_app_id,
form_tracking: additional_features,
- link_click_tracking: additional_features
+ link_click_tracking: additional_features,
+ iglu_registry_url: Gitlab::CurrentSettings.snowplow_iglu_registry_url
}.transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
end
@@ -58,7 +59,7 @@ module Gitlab
SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'),
SnowplowTracker::Subject.new,
SNOWPLOW_NAMESPACE,
- Gitlab::CurrentSettings.snowplow_site_id
+ Gitlab::CurrentSettings.snowplow_app_id
)
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cb492b69fec..b6effac25c6 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -13,7 +13,8 @@ module Gitlab
end
def uncached_data
- license_usage_data.merge(system_usage_data)
+ license_usage_data
+ .merge(system_usage_data)
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
@@ -66,17 +67,23 @@ module Gitlab
clusters_disabled: count(::Clusters::Cluster.disabled),
project_clusters_disabled: count(::Clusters::Cluster.disabled.project_type),
group_clusters_disabled: count(::Clusters::Cluster.disabled.group_type),
+ clusters_platforms_eks: count(::Clusters::Cluster.aws_installed.enabled),
clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled),
clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled),
clusters_applications_helm: count(::Clusters::Applications::Helm.available),
clusters_applications_ingress: count(::Clusters::Applications::Ingress.available),
clusters_applications_cert_managers: count(::Clusters::Applications::CertManager.available),
+ clusters_applications_crossplane: count(::Clusters::Applications::Crossplane.available),
clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.available),
clusters_applications_runner: count(::Clusters::Applications::Runner.available),
clusters_applications_knative: count(::Clusters::Applications::Knative.available),
+ clusters_applications_elastic_stack: count(::Clusters::Applications::ElasticStack.available),
in_review_folder: count(::Environment.in_review_folder),
+ grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group),
issues: count(Issue),
+ issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
+ issues_using_zoom_quick_actions: count(ZoomMeeting.select(:issue_id).distinct),
keys: count(Key),
label_lists: count(List.label),
lfs_objects: count(LfsObject),
@@ -127,7 +134,9 @@ module Gitlab
omniauth_enabled: Gitlab::Auth.omniauth_enabled?,
prometheus_metrics_enabled: Gitlab::Metrics.prometheus_metrics_enabled?,
reply_by_email_enabled: Gitlab::IncomingEmail.enabled?,
- signup_enabled: Gitlab::CurrentSettings.allow_signup?
+ signup_enabled: Gitlab::CurrentSettings.allow_signup?,
+ web_ide_clientside_preview_enabled: Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?,
+ ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
}
end
@@ -165,10 +174,13 @@ module Gitlab
types = {
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
- PrometheusService: :projects_prometheus_active
+ PrometheusService: :projects_prometheus_active,
+ CustomIssueTrackerService: :projects_custom_issue_tracker_active,
+ JenkinsService: :projects_jenkins_active,
+ MattermostService: :projects_mattermost_active
}
- results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1))
+ results = count(Service.active.by_type(types.keys).group(:type), fallback: Hash.new(-1))
types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 }
.merge(jira_usage)
end
@@ -183,8 +195,8 @@ module Gitlab
projects_jira_active: -1
}
- Service.unscoped
- .where(type: :JiraService, active: true)
+ Service.active
+ .by_type(:JiraService)
.includes(:jira_tracker_data)
.find_in_batches(batch_size: BATCH_SIZE) do |services|
diff --git a/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb
index 0718c1dd761..c012a6c96df 100644
--- a/lib/gitlab/usage_data_counters/web_ide_counter.rb
+++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb
@@ -8,6 +8,7 @@ module Gitlab
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'
class << self
def increment_commits_count
@@ -34,11 +35,22 @@ module Gitlab
total_count(VIEWS_COUNT_KEY)
end
+ def increment_previews_count
+ return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
+
+ increment(PREVIEW_COUNT_KEY)
+ end
+
+ def total_previews_count
+ total_count(PREVIEW_COUNT_KEY)
+ 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_merge_requests: total_merge_requests_count,
+ web_ide_previews: total_previews_count
}
end
end
diff --git a/lib/gitlab/utils/deep_size.rb b/lib/gitlab/utils/deep_size.rb
index 562cf09e249..ed2ceb8af7c 100644
--- a/lib/gitlab/utils/deep_size.rb
+++ b/lib/gitlab/utils/deep_size.rb
@@ -25,6 +25,10 @@ module Gitlab
!too_big? && !too_deep?
end
+ def self.human_default_max_size
+ ActiveSupport::NumberHelper.number_to_human_size(DEFAULT_MAX_SIZE)
+ end
+
private
def evaluate
diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb
index e9be6db50da..a963cc7954f 100644
--- a/lib/gitlab/wiki_file_finder.rb
+++ b/lib/gitlab/wiki_file_finder.rb
@@ -12,12 +12,12 @@ module Gitlab
private
- def search_filenames(query)
+ def search_paths(query)
safe_query = Regexp.escape(query.tr(' ', '-'))
safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
- filenames = repository.ls_files(ref)
+ paths = repository.ls_files(ref)
- filenames.grep(safe_query)
+ paths.grep(safe_query)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index db67e4fd479..713ca31bbc5 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -14,6 +14,7 @@ module Gitlab
NOTIFICATION_CHANNEL = 'workhorse:notifications'
ALLOWED_GIT_HTTP_ACTIONS = %w[git_receive_pack git_upload_pack info_refs].freeze
DETECT_HEADER = 'Gitlab-Workhorse-Detect-Content-Type'
+ ARCHIVE_FORMATS = %w(zip tar.gz tar.bz2 tar).freeze
include JwtAuthenticatable
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index 9085835dee6..99029b54a69 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -12,6 +12,7 @@ module GoogleApi
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
LEAST_TOKEN_LIFE_TIME = 10.minutes
CLUSTER_MASTER_AUTH_USERNAME = 'admin'
+ CLUSTER_IPV4_CIDR_BLOCK = '/16'
class << self
def session_key_for_token
@@ -97,7 +98,8 @@ module GoogleApi
enabled: legacy_abac
},
ip_allocation_policy: {
- use_ip_aliases: true
+ use_ip_aliases: true,
+ cluster_ipv4_cidr_block: CLUSTER_IPV4_CIDR_BLOCK
},
addons_config: enable_addons.each_with_object({}) do |addon, hash|
hash[addon] = { disabled: false }
diff --git a/lib/grafana/client.rb b/lib/grafana/client.rb
index 0765630f9bb..b419f79bace 100644
--- a/lib/grafana/client.rb
+++ b/lib/grafana/client.rb
@@ -11,6 +11,18 @@ module Grafana
@token = token
end
+ # @param uid [String] Unique identifier for a Grafana dashboard
+ def get_dashboard(uid:)
+ http_get("#{@api_url}/api/dashboards/uid/#{uid}")
+ end
+
+ # @param name [String] Unique identifier for a Grafana datasource
+ def get_datasource(name:)
+ # CGI#escape formats strings such that the Grafana endpoint
+ # will not recognize the dashboard name. Preferring URI#escape.
+ http_get("#{@api_url}/api/datasources/name/#{URI.escape(name)}") # rubocop:disable Lint/UriEscapeUnescape
+ end
+
# @param datasource_id [String] Grafana ID for the datasource
# @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range'
def proxy_datasource(datasource_id:, proxy_path:, query: {})
@@ -57,7 +69,7 @@ module Grafana
def handle_response(response)
return response if response.code == 200
- raise_error "Grafana response status code: #{response.code}"
+ raise_error "Grafana response status code: #{response.code}, Message: #{response.body}"
end
def raise_error(message)
diff --git a/lib/prometheus/pid_provider.rb b/lib/prometheus/pid_provider.rb
index e0f7e7e0a9e..228639357ac 100644
--- a/lib/prometheus/pid_provider.rb
+++ b/lib/prometheus/pid_provider.rb
@@ -6,7 +6,7 @@ module Prometheus
def worker_id
if Sidekiq.server?
- 'sidekiq'
+ sidekiq_worker_id
elsif defined?(Unicorn::Worker)
unicorn_worker_id
elsif defined?(::Puma)
@@ -18,6 +18,14 @@ module Prometheus
private
+ def sidekiq_worker_id
+ if worker = ENV['SIDEKIQ_WORKER_ID']
+ "sidekiq_#{worker}"
+ else
+ 'sidekiq'
+ end
+ end
+
def unicorn_worker_id
if matches = process_name.match(/unicorn.*worker\[([0-9]+)\]/)
"unicorn_#{matches[1]}"
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index 190b48ba7cb..cc899bf9374 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -12,7 +12,16 @@ module Quality
@namespace = namespace
end
- def cleanup(release_name:)
+ def cleanup(release_name:, wait: true)
+ selector = case release_name
+ when String
+ %(-l release="#{release_name}")
+ when Array
+ %(-l 'release in (#{release_name.join(', ')})')
+ else
+ raise ArgumentError, 'release_name must be a string or an array'
+ end
+
command = [
%(--namespace "#{namespace}"),
'delete',
@@ -20,7 +29,8 @@ module Quality
'--now',
'--ignore-not-found',
'--include-uninitialized',
- %(-l release="#{release_name}")
+ %(--wait=#{wait}),
+ selector
]
run_command(command)
diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb
index 07cca1c8d1e..6191d69c870 100644
--- a/lib/sentry/client.rb
+++ b/lib/sentry/client.rb
@@ -4,6 +4,7 @@ module Sentry
class Client
Error = Class.new(StandardError)
MissingKeysError = Class.new(StandardError)
+ ResponseInvalidSizeError = Class.new(StandardError)
attr_accessor :url, :token
@@ -12,9 +13,23 @@ module Sentry
@token = token
end
+ def issue_details(issue_id:)
+ issue = get_issue(issue_id: issue_id)
+
+ map_to_detailed_error(issue)
+ end
+
+ def issue_latest_event(issue_id:)
+ latest_event = get_issue_latest_event(issue_id: issue_id)
+
+ map_to_event(latest_event)
+ end
+
def list_issues(issue_status:, limit:)
issues = get_issues(issue_status: issue_status, limit: limit)
+ validate_size(issues)
+
handle_mapping_exceptions do
map_to_errors(issues)
end
@@ -30,6 +45,12 @@ module Sentry
private
+ def validate_size(issues)
+ return if Gitlab::Utils::DeepSize.new(issues).valid?
+
+ raise Client::ResponseInvalidSizeError, "Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."
+ end
+
def handle_mapping_exceptions(&block)
yield
rescue KeyError => e
@@ -61,6 +82,14 @@ module Sentry
})
end
+ def get_issue(issue_id:)
+ http_get(issue_api_url(issue_id))
+ end
+
+ def get_issue_latest_event(issue_id:)
+ http_get(issue_latest_event_api_url(issue_id))
+ end
+
def get_projects
http_get(projects_api_url)
end
@@ -88,7 +117,7 @@ module Sentry
raise_error "Sentry response status code: #{response.code}"
end
- response
+ response.parsed_response
end
def raise_error(message)
@@ -102,6 +131,20 @@ module Sentry
projects_url
end
+ def issue_api_url(issue_id)
+ issue_url = URI(@url)
+ issue_url.path = "/api/0/issues/#{issue_id}/"
+
+ issue_url
+ end
+
+ def issue_latest_event_api_url(issue_id)
+ latest_event_url = URI(@url)
+ latest_event_url.path = "/api/0/issues/#{issue_id}/events/latest/"
+
+ latest_event_url
+ end
+
def issues_api_url
issues_url = URI(@url + '/issues/')
issues_url.path.squeeze!('/')
@@ -119,38 +162,87 @@ module Sentry
def issue_url(id)
issues_url = @url + "/issues/#{id}"
- issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url)
- uri = URI(issues_url)
+ parse_sentry_url(issues_url)
+ end
+
+ def project_url
+ parse_sentry_url(@url)
+ end
+
+ def parse_sentry_url(api_url)
+ url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(api_url)
+
+ uri = URI(url)
uri.path.squeeze!('/')
+ # Remove trailing spaces
+ uri = uri.to_s.gsub(/\/\z/, '')
- uri.to_s
+ uri
end
- def map_to_error(issue)
- id = issue.fetch('id')
+ def map_to_event(event)
+ stack_trace = parse_stack_trace(event)
- count = issue.fetch('count', nil)
+ Gitlab::ErrorTracking::ErrorEvent.new(
+ issue_id: event.dig('groupID'),
+ date_received: event.dig('dateReceived'),
+ stack_trace_entries: stack_trace
+ )
+ end
- frequency = issue.dig('stats', '24h')
- message = issue.dig('metadata', 'value')
+ def parse_stack_trace(event)
+ exception_entry = event.dig('entries')&.detect { |h| h['type'] == 'exception' }
+ return unless exception_entry
- external_url = issue_url(id)
+ exception_values = exception_entry.dig('data', 'values')
+ stack_trace_entry = exception_values&.detect { |h| h['stacktrace'].present? }
+ return unless stack_trace_entry
+
+ stack_trace_entry.dig('stacktrace', 'frames')
+ end
+
+ def map_to_detailed_error(issue)
+ Gitlab::ErrorTracking::DetailedError.new(
+ id: issue.fetch('id'),
+ first_seen: issue.fetch('firstSeen', nil),
+ last_seen: issue.fetch('lastSeen', nil),
+ title: issue.fetch('title', nil),
+ type: issue.fetch('type', nil),
+ user_count: issue.fetch('userCount', nil),
+ count: issue.fetch('count', nil),
+ message: issue.dig('metadata', 'value'),
+ culprit: issue.fetch('culprit', nil),
+ external_url: issue_url(issue.fetch('id')),
+ external_base_url: project_url,
+ short_id: issue.fetch('shortId', nil),
+ status: issue.fetch('status', nil),
+ frequency: issue.dig('stats', '24h'),
+ project_id: issue.dig('project', 'id'),
+ project_name: issue.dig('project', 'name'),
+ project_slug: issue.dig('project', 'slug'),
+ first_release_last_commit: issue.dig('firstRelease', 'lastCommit'),
+ last_release_last_commit: issue.dig('lastRelease', 'lastCommit'),
+ first_release_short_version: issue.dig('firstRelease', 'shortVersion'),
+ last_release_short_version: issue.dig('lastRelease', 'shortVersion')
+ )
+ end
+ def map_to_error(issue)
Gitlab::ErrorTracking::Error.new(
- id: id,
+ id: issue.fetch('id'),
first_seen: issue.fetch('firstSeen', nil),
last_seen: issue.fetch('lastSeen', nil),
title: issue.fetch('title', nil),
type: issue.fetch('type', nil),
user_count: issue.fetch('userCount', nil),
- count: count,
- message: message,
+ count: issue.fetch('count', nil),
+ message: issue.dig('metadata', 'value'),
culprit: issue.fetch('culprit', nil),
- external_url: external_url,
+ external_url: issue_url(issue.fetch('id')),
short_id: issue.fetch('shortId', nil),
status: issue.fetch('status', nil),
- frequency: frequency,
+ frequency: issue.dig('stats', '24h'),
project_id: issue.dig('project', 'id'),
project_name: issue.dig('project', 'name'),
project_slug: issue.dig('project', 'slug')
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index b1db4dc94a6..0488f26318a 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -5,6 +5,10 @@ namespace :dev do
task setup: :environment do
ENV['force'] = 'yes'
Rake::Task["gitlab:setup"].invoke
+
+ # Make sure DB statistics are up to date.
+ ActiveRecord::Base.connection.execute('ANALYZE')
+
Rake::Task["gitlab:shell:setup"].invoke
end
diff --git a/lib/tasks/gitlab/graphql.rake b/lib/tasks/gitlab/graphql.rake
index 902f22684ee..f8ce3cd46a8 100644
--- a/lib/tasks/gitlab/graphql.rake
+++ b/lib/tasks/gitlab/graphql.rake
@@ -2,10 +2,24 @@
return if Rails.env.production?
+require 'graphql/rake_task'
+
namespace :gitlab do
OUTPUT_DIR = Rails.root.join("doc/api/graphql/reference")
TEMPLATES_DIR = 'lib/gitlab/graphql/docs/templates/'
+ # Defines tasks for dumping the GraphQL schema:
+ # - gitlab:graphql:schema:dump
+ # - gitlab:graphql:schema:idl
+ # - gitlab:graphql:schema:json
+ GraphQL::RakeTask.new(
+ schema_name: 'GitlabSchema',
+ dependencies: [:environment],
+ directory: OUTPUT_DIR,
+ idl_outfile: "gitlab_schema.graphql",
+ json_outfile: "gitlab_schema.json"
+ )
+
namespace :graphql do
desc 'GitLab | Generate GraphQL docs'
task compile_docs: :environment do
@@ -25,11 +39,20 @@ namespace :gitlab do
if doc == renderer.contents
puts "GraphQL documentation is up to date"
else
- puts '#' * 10
- puts '#'
- puts '# GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.'
- puts '#'
- puts '#' * 10
+ format_output('GraphQL documentation is outdated! Please update it by running `bundle exec rake gitlab:graphql:compile_docs`.')
+ abort
+ end
+ end
+
+ desc 'GitLab | Check if GraphQL schemas are up to date'
+ task check_schema: :environment do
+ idl_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.graphql'))
+ json_doc = File.read(Rails.root.join(OUTPUT_DIR, 'gitlab_schema.json'))
+
+ if idl_doc == GitlabSchema.to_definition && json_doc == GitlabSchema.to_json
+ puts "GraphQL schema is up to date"
+ else
+ format_output('GraphQL schema is outdated! Please update it by running `bundle exec rake gitlab:graphql:schema:dump`.')
abort
end
end
@@ -42,3 +65,12 @@ def render_options
template: Rails.root.join(TEMPLATES_DIR, 'default.md.haml')
}
end
+
+def format_output(str)
+ heading = '#' * 10
+ puts heading
+ puts '#'
+ puts "# #{str}"
+ puts '#'
+ puts heading
+end
diff --git a/lib/tasks/gitlab/seed.rake b/lib/tasks/gitlab/seed.rake
index d76e38b73b5..d758280ba69 100644
--- a/lib/tasks/gitlab/seed.rake
+++ b/lib/tasks/gitlab/seed.rake
@@ -22,7 +22,7 @@ namespace :gitlab do
[project]
else
- Project.find_each
+ Project.not_mass_generated.find_each
end
projects.each do |project|
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index abd47f018f1..a592015963d 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -43,7 +43,7 @@ namespace :gitlab do
[
%w(bin/install) + repository_storage_paths_args,
- %w(bin/compile)
+ %w(make build)
].each do |cmd|
unless Kernel.system(*cmd)
raise "command failed: #{cmd.join(' ')}"
diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake
index 2eeb694d341..74db0060b8d 100644
--- a/lib/tasks/gitlab/uploads/legacy.rake
+++ b/lib/tasks/gitlab/uploads/legacy.rake
@@ -15,7 +15,7 @@ namespace :gitlab do
batch_size = 5000
delay_interval = 5.minutes.to_i
- Upload.where(uploader: 'AttachmentUploader').each_batch(of: batch_size) do |relation, index|
+ Upload.where(uploader: 'AttachmentUploader', model_type: 'Note').each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
delay = index * delay_interval
diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po
index e543747177e..322db0c0322 100644
--- a/locale/ar_SA/gitlab.po
+++ b/locale/ar_SA/gitlab.po
@@ -7791,7 +7791,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16734,7 +16734,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 8190c27e65c..0b6e55cab8e 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/bn_BD/gitlab.po b/locale/bn_BD/gitlab.po
index 0d25da46afd..291dca380fc 100644
--- a/locale/bn_BD/gitlab.po
+++ b/locale/bn_BD/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/bn_IN/gitlab.po b/locale/bn_IN/gitlab.po
index 945f7f66d52..f95d71eec5d 100644
--- a/locale/bn_IN/gitlab.po
+++ b/locale/bn_IN/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ca_ES/gitlab.po b/locale/ca_ES/gitlab.po
index 318efa98afa..a30f508f4b6 100644
--- a/locale/ca_ES/gitlab.po
+++ b/locale/ca_ES/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr "Enrere"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po
index 2123f8235de..b9fd29985ae 100644
--- a/locale/cs_CZ/gitlab.po
+++ b/locale/cs_CZ/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/cy_GB/gitlab.po b/locale/cy_GB/gitlab.po
index f831c880f7d..c09b3a6695c 100644
--- a/locale/cy_GB/gitlab.po
+++ b/locale/cy_GB/gitlab.po
@@ -7791,7 +7791,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16734,7 +16734,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/da_DK/gitlab.po b/locale/da_DK/gitlab.po
index 352219b4745..8747bc5c3c2 100644
--- a/locale/da_DK/gitlab.po
+++ b/locale/da_DK/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 57f17ae854d..3103adacbf2 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr "Zurück"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/el_GR/gitlab.po b/locale/el_GR/gitlab.po
index b1af45f753a..e1f4ab78994 100644
--- a/locale/el_GR/gitlab.po
+++ b/locale/el_GR/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 76d9514c20c..e7ac98e864a 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index b5968b6981d..851123d9c35 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr "Go Micro es un framework para el desarrollo de microservicios."
msgid "Go back"
msgstr "Volver"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr "Volver (mientras busca archivos"
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr "Para abrir Jaeger y ver fácilmente la trazabilidad desde GitLab, enlace
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr "Para mantener el rendimiento, solo se muestran <strong>%{display_size} de %{real_size}</strong> archivos."
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/et_EE/gitlab.po b/locale/et_EE/gitlab.po
index 49d881a72a5..7cf77be629a 100644
--- a/locale/et_EE/gitlab.po
+++ b/locale/et_EE/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/fa_IR/gitlab.po b/locale/fa_IR/gitlab.po
index 413cf75f212..22519e0bfc0 100644
--- a/locale/fa_IR/gitlab.po
+++ b/locale/fa_IR/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po
index 314a38468db..150e5733bdc 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 18d5cc1cd33..e1894f867b1 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr "Retour"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5a200bf7cdd..f1b14d78292 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -62,6 +62,9 @@ msgstr ""
msgid " or references (e.g. path/to/project!merge_request_id)"
msgstr ""
+msgid "\"%{path}\" did not exist on \"%{ref}\""
+msgstr ""
+
msgid "%d comment"
msgid_plural "%d comments"
msgstr[0] ""
@@ -207,6 +210,11 @@ msgstr ""
msgid "%{count} more assignees"
msgstr ""
+msgid "%{count} more release"
+msgid_plural "%{count} more releases"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{count} of %{required} approvals from %{name}"
msgstr ""
@@ -253,9 +261,6 @@ msgstr ""
msgid "%{from} to %{to}"
msgstr ""
-msgid "%{gitlab_ci_yml} not found in this commit"
-msgstr ""
-
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 ""
@@ -316,6 +321,14 @@ msgstr ""
msgid "%{percent}%% complete"
msgstr ""
+msgid "%{primary} (%{secondary})"
+msgstr ""
+
+msgid "%{releases} release"
+msgid_plural "%{releases} releases"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{service_title} activated."
msgstr ""
@@ -644,6 +657,9 @@ msgstr ""
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 ""
+msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -686,7 +702,7 @@ msgstr ""
msgid "A ready-to-go template for use with iOS Swift apps."
msgstr ""
-msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
+msgid "A regular expression that will be used to find the test coverage output in the job log. Leave blank to disable"
msgstr ""
msgid "A secure token that identifies an external storage request."
@@ -701,6 +717,18 @@ msgstr ""
msgid "API Token"
msgstr ""
+msgid "AWS Access Key"
+msgstr ""
+
+msgid "AWS Access Key. Only required if not using role instance credentials"
+msgstr ""
+
+msgid "AWS Secret Access Key"
+msgstr ""
+
+msgid "AWS Secret Access Key. Only required if not using role instance credentials"
+msgstr ""
+
msgid "Abort"
msgstr ""
@@ -821,6 +849,9 @@ msgstr ""
msgid "Account"
msgstr ""
+msgid "Account ID"
+msgstr ""
+
msgid "Account and limit"
msgstr ""
@@ -874,6 +905,9 @@ msgstr ""
msgid "Add Kubernetes cluster"
msgstr ""
+msgid "Add LICENSE"
+msgstr ""
+
msgid "Add README"
msgstr ""
@@ -922,6 +956,9 @@ msgstr ""
msgid "Add an SSH key"
msgstr ""
+msgid "Add an existing issue to the epic."
+msgstr ""
+
msgid "Add an issue"
msgstr ""
@@ -979,6 +1016,9 @@ msgstr ""
msgid "Add reaction"
msgstr ""
+msgid "Add request manually"
+msgstr ""
+
msgid "Add to Slack"
msgstr ""
@@ -1362,6 +1402,9 @@ msgstr ""
msgid "All groups and projects"
msgstr ""
+msgid "All issues for this milestone are closed."
+msgstr ""
+
msgid "All issues for this milestone are closed. You may close this milestone now."
msgstr ""
@@ -1377,6 +1420,9 @@ msgstr ""
msgid "All projects"
msgstr ""
+msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1392,9 +1438,6 @@ msgstr ""
msgid "Allow group owners to manage LDAP-related settings"
msgstr ""
-msgid "Allow mirrors to be set up for projects"
-msgstr ""
-
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
@@ -1407,6 +1450,9 @@ msgstr ""
msgid "Allow rendering of PlantUML diagrams in Asciidoc documents."
msgstr ""
+msgid "Allow repository mirroring to be configured by project maintainers"
+msgstr ""
+
msgid "Allow requests to the local network from hooks and services."
msgstr ""
@@ -1446,6 +1492,18 @@ msgstr ""
msgid "Alternate support URL for help page and help dropdown"
msgstr ""
+msgid "Amazon EKS"
+msgstr ""
+
+msgid "Amazon EKS integration allows you to provision EKS clusters from GitLab."
+msgstr ""
+
+msgid "Amazon Web Services"
+msgstr ""
+
+msgid "Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service."
+msgstr ""
+
msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication"
msgstr ""
@@ -1491,6 +1549,9 @@ msgstr ""
msgid "An error occurred when updating the issue weight"
msgstr ""
+msgid "An error occurred while checking group path"
+msgstr ""
+
msgid "An error occurred while deleting the approvers group"
msgstr ""
@@ -1515,6 +1576,9 @@ msgstr ""
msgid "An error occurred while fetching environments."
msgstr ""
+msgid "An error occurred while fetching exposed artifacts."
+msgstr ""
+
msgid "An error occurred while fetching folder content."
msgstr ""
@@ -1590,6 +1654,9 @@ msgstr ""
msgid "An error occurred while loading filenames"
msgstr ""
+msgid "An error occurred while loading issues"
+msgstr ""
+
msgid "An error occurred while loading the file"
msgstr ""
@@ -1650,6 +1717,9 @@ msgstr ""
msgid "An error occurred while updating the comment"
msgstr ""
+msgid "An error occurred while validating group path"
+msgstr ""
+
msgid "An error occurred while validating username"
msgstr ""
@@ -1737,6 +1807,9 @@ msgstr ""
msgid "Any user"
msgstr ""
+msgid "App ID"
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -1746,10 +1819,10 @@ msgstr ""
msgid "Appearance was successfully updated."
msgstr ""
-msgid "Append the comment with %{TABLEFLIP}"
+msgid "Append the comment with %{shrug}"
msgstr ""
-msgid "Append the comment with %{shrug}"
+msgid "Append the comment with %{tableflip}"
msgstr ""
msgid "Application"
@@ -1886,6 +1959,9 @@ 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>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>"
msgstr ""
+msgid "Are you setting up GitLab for a company?"
+msgstr ""
+
msgid "Are you sure that you want to archive this project?"
msgstr ""
@@ -2115,6 +2191,9 @@ msgstr ""
msgid "Authenticate with GitHub"
msgstr ""
+msgid "Authenticating"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -2424,18 +2503,15 @@ msgstr ""
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
msgstr ""
-msgid "BillingPlans|Your GitLab.com trial expired on %{expiration_date}. %{learn_more_text}"
+msgid "BillingPlans|Your GitLab.com Gold trial expired on %{expiration_date}. You can restore access to the Gold features at any time by upgrading below."
msgstr ""
-msgid "BillingPlans|Your GitLab.com trial will <strong>expire after %{expiration_date}</strong>. You can learn more about GitLab.com Gold by reading about our %{features_link}."
+msgid "BillingPlans|Your GitLab.com Gold trial will <strong>expire after %{expiration_date}</strong>. You can retain access to the Gold features by upgrading below."
msgstr ""
msgid "BillingPlans|billed annually at %{price_per_year}"
msgstr ""
-msgid "BillingPlans|features"
-msgstr ""
-
msgid "BillingPlans|frequently asked questions"
msgstr ""
@@ -2874,9 +2950,6 @@ msgstr ""
msgid "Certificate (PEM)"
msgstr ""
-msgid "Change Weight"
-msgstr ""
-
msgid "Change assignee"
msgstr ""
@@ -2886,6 +2959,9 @@ msgstr ""
msgid "Change assignee(s)."
msgstr ""
+msgid "Change branches"
+msgstr ""
+
msgid "Change label"
msgstr ""
@@ -2949,6 +3025,9 @@ msgstr ""
msgid "Changes won't take place until the index is %{link_start}recreated%{link_end}."
msgstr ""
+msgid "Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}"
+msgstr ""
+
msgid "Changing group path can have unintended side effects."
msgstr ""
@@ -2958,7 +3037,7 @@ msgstr ""
msgid "Chat"
msgstr ""
-msgid "ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status} in %{duration}"
+msgid "ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}"
msgstr ""
msgid "ChatMessage|Branch"
@@ -2979,7 +3058,7 @@ msgstr ""
msgid "ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}"
msgstr ""
-msgid "ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{branch_link} by %{user_combined_name} %{humanized_status}"
+msgid "ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}"
msgstr ""
msgid "ChatMessage|Tag"
@@ -3030,6 +3109,9 @@ msgstr ""
msgid "Checking branch availability..."
msgstr ""
+msgid "Checking group path availability..."
+msgstr ""
+
msgid "Checking username availability..."
msgstr ""
@@ -3072,9 +3154,6 @@ msgstr ""
msgid "Choose a type..."
msgstr ""
-msgid "Choose an existing tag, or create a new one"
-msgstr ""
-
msgid "Choose any color."
msgstr ""
@@ -3237,6 +3316,9 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
+msgid "Class"
+msgstr ""
+
msgid "Classification Label (optional)"
msgstr ""
@@ -3375,6 +3457,9 @@ msgstr ""
msgid "ClusterIntegration|%{title} updated successfully."
msgstr ""
+msgid "ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges."
+msgstr ""
+
msgid "ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges."
msgstr ""
@@ -3447,6 +3532,12 @@ msgstr ""
msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster."
msgstr ""
+msgid "ClusterIntegration|Authenticate with AWS"
+msgstr ""
+
+msgid "ClusterIntegration|Authenticate with Amazon Web Services"
+msgstr ""
+
msgid "ClusterIntegration|Base domain"
msgstr ""
@@ -3465,10 +3556,13 @@ msgstr ""
msgid "ClusterIntegration|Choose a prefix to be used for your namespaces. Defaults to your project path."
msgstr ""
-msgid "ClusterIntegration|Choose the %{startLink}security groups%{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets."
+msgid "ClusterIntegration|Choose the %{startLink}security group %{externalLinkIcon} %{endLink} to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets."
+msgstr ""
+
+msgid "ClusterIntegration|Choose the %{startLink}subnets %{externalLinkIcon} %{endLink} in your VPC where your worker nodes will run."
msgstr ""
-msgid "ClusterIntegration|Choose the %{startLink}subnets%{endLink} in your VPC where your worker nodes will run."
+msgid "ClusterIntegration|Choose the worker node %{startLink}instance type %{externalLinkIcon} %{endLink}."
msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications."
@@ -3483,6 +3577,9 @@ msgstr ""
msgid "ClusterIntegration|Cluster health"
msgstr ""
+msgid "ClusterIntegration|Cluster management project (alpha)"
+msgstr ""
+
msgid "ClusterIntegration|Cluster name is required."
msgstr ""
@@ -3501,6 +3598,9 @@ msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname"
msgstr ""
+msgid "ClusterIntegration|Copy Kibana Hostname"
+msgstr ""
+
msgid "ClusterIntegration|Copy Knative Endpoint"
msgstr ""
@@ -3519,6 +3619,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load VPCs for the selected region"
msgstr ""
+msgid "ClusterIntegration|Could not load instance types"
+msgstr ""
+
msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr ""
@@ -3531,6 +3634,9 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster on"
msgstr ""
@@ -3543,9 +3649,21 @@ msgstr ""
msgid "ClusterIntegration|Create new Cluster on GKE"
msgstr ""
+msgid "ClusterIntegration|Creating Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Crossplane"
+msgstr ""
+
+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 ""
+
msgid "ClusterIntegration|Did you know?"
msgstr ""
+msgid "ClusterIntegration|Elastic Stack"
+msgstr ""
+
msgid "ClusterIntegration|Enable Cloud Run on GKE (beta)"
msgstr ""
@@ -3555,6 +3673,9 @@ msgstr ""
msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)."
msgstr ""
+msgid "ClusterIntegration|Enabled stack"
+msgstr ""
+
msgid "ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster"
msgstr ""
@@ -3567,9 +3688,15 @@ 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 ""
+msgid "ClusterIntegration|Failed to configure EKS provider: %{message}"
+msgstr ""
+
msgid "ClusterIntegration|Failed to configure Google Kubernetes Engine Cluster: %{message}"
msgstr ""
+msgid "ClusterIntegration|Failed to fetch CloudFormation stack: %{message}"
+msgstr ""
+
msgid "ClusterIntegration|Failed to request to Google Cloud Platform: %{message}"
msgstr ""
@@ -3597,6 +3724,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab-managed cluster"
msgstr ""
+msgid "ClusterIntegration|Gitlab Integration"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
@@ -3642,6 +3772,9 @@ msgstr ""
msgid "ClusterIntegration|Instance cluster"
msgstr ""
+msgid "ClusterIntegration|Instance type"
+msgstr ""
+
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr ""
@@ -3666,6 +3799,9 @@ msgstr ""
msgid "ClusterIntegration|Key pair name"
msgstr ""
+msgid "ClusterIntegration|Kibana Hostname"
+msgstr ""
+
msgid "ClusterIntegration|Knative"
msgstr ""
@@ -3687,13 +3823,13 @@ msgstr ""
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
+msgid "ClusterIntegration|Kubernetes cluster is being created..."
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster name"
msgstr ""
-msgid "ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine."
+msgid "ClusterIntegration|Kubernetes cluster was successfully created."
msgstr ""
msgid "ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way."
@@ -3714,7 +3850,7 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
-msgid "ClusterIntegration|Learn more about %{startLink}Regions%{endLink}."
+msgid "ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}."
msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes"
@@ -3741,6 +3877,9 @@ msgstr ""
msgid "ClusterIntegration|Loading VPCs"
msgstr ""
+msgid "ClusterIntegration|Loading instance types"
+msgstr ""
+
msgid "ClusterIntegration|Loading security groups"
msgstr ""
@@ -3765,6 +3904,9 @@ msgstr ""
msgid "ClusterIntegration|No VPCs found"
msgstr ""
+msgid "ClusterIntegration|No instance type found"
+msgstr ""
+
msgid "ClusterIntegration|No machine types matched your search"
msgstr ""
@@ -3816,6 +3958,9 @@ msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
msgstr ""
+msgid "ClusterIntegration|Provision Role ARN"
+msgstr ""
+
msgid "ClusterIntegration|RBAC-enabled cluster"
msgstr ""
@@ -3858,6 +4003,9 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs"
msgstr ""
+msgid "ClusterIntegration|Search instance types"
+msgstr ""
+
msgid "ClusterIntegration|Search machine types"
msgstr ""
@@ -3876,7 +4024,7 @@ msgstr ""
msgid "ClusterIntegration|Search zones"
msgstr ""
-msgid "ClusterIntegration|Security groups"
+msgid "ClusterIntegration|Security group"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
@@ -3888,7 +4036,10 @@ msgstr ""
msgid "ClusterIntegration|Select a VPC to choose a subnet"
msgstr ""
-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%{endLink}."
+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 different AWS role"
msgstr ""
msgid "ClusterIntegration|Select a region to choose a Key Pair"
@@ -3897,6 +4048,9 @@ msgstr ""
msgid "ClusterIntegration|Select a region to choose a VPC"
msgstr ""
+msgid "ClusterIntegration|Select a stack to install Crossplane."
+msgstr ""
+
msgid "ClusterIntegration|Select machine type"
msgstr ""
@@ -3909,10 +4063,10 @@ msgstr ""
msgid "ClusterIntegration|Select project to choose zone"
msgstr ""
-msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services%{endLink}."
+msgid "ClusterIntegration|Select the IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr ""
-msgid "ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services%{endLink}."
+msgid "ClusterIntegration|Select the key pair name that will be used to create EC2 nodes. To use a new key pair name, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr ""
msgid "ClusterIntegration|Select zone"
@@ -3933,7 +4087,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
+msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
@@ -3948,7 +4102,10 @@ msgstr ""
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 ""
-msgid "ClusterIntegration|Subnet"
+msgid "ClusterIntegration|Subnets"
+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 ""
msgid "ClusterIntegration|The Kubernetes certificate used to authenticate to the cluster."
@@ -3969,6 +4126,9 @@ msgstr ""
msgid "ClusterIntegration|The associated private key will be deleted and cannot be restored."
msgstr ""
+msgid "ClusterIntegration|The elastic stack collects logs from all pods in your cluster"
+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 ""
@@ -4017,6 +4177,9 @@ msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
+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 ""
+
msgid "ClusterIntegration|You must have an RBAC-enabled cluster to install Knative."
msgstr ""
@@ -4059,6 +4222,9 @@ msgstr ""
msgid "ClusterIntergation|Select a subnet"
msgstr ""
+msgid "ClusterIntergation|Select an instance type"
+msgstr ""
+
msgid "ClusterIntergation|Select key pair"
msgstr ""
@@ -4196,6 +4362,9 @@ msgstr ""
msgid "Commits per weekday"
msgstr ""
+msgid "Commits to"
+msgstr ""
+
msgid "Commits|An error occurred while fetching merge requests data."
msgstr ""
@@ -4238,6 +4407,9 @@ msgstr ""
msgid "Compare changes with the merge request target branch"
msgstr ""
+msgid "Compare with previous version"
+msgstr ""
+
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr ""
@@ -4277,6 +4449,9 @@ msgstr ""
msgid "Configure Prometheus"
msgstr ""
+msgid "Configure Security %{wordBreakOpportunity}and Compliance"
+msgstr ""
+
msgid "Configure Tracing"
msgstr ""
@@ -4298,7 +4473,7 @@ msgstr ""
msgid "Configure paths to be protected by Rack Attack. A web server restart is required after changing these settings."
msgstr ""
-msgid "Configure push mirrors."
+msgid "Configure repository mirroring."
msgstr ""
msgid "Configure storage path settings."
@@ -4334,6 +4509,9 @@ msgstr ""
msgid "Connect your external repositories, and CI/CD pipelines will run for new commits. A GitLab project will be created with only CI/CD features enabled."
msgstr ""
+msgid "Connecting"
+msgstr ""
+
msgid "Connecting to terminal sync service"
msgstr ""
@@ -4480,18 +4658,6 @@ msgstr ""
msgid "Contributors"
msgstr ""
-msgid "ContributorsPage|%{startDate} – %{endDate}"
-msgstr ""
-
-msgid "ContributorsPage|Building repository graph."
-msgstr ""
-
-msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
-msgstr ""
-
-msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
-msgstr ""
-
msgid "Control emails linked to your account"
msgstr ""
@@ -4516,6 +4682,9 @@ msgstr ""
msgid "Copy"
msgstr ""
+msgid "Copy %{field}"
+msgstr ""
+
msgid "Copy %{http_label} clone URL"
msgstr ""
@@ -4525,6 +4694,12 @@ msgstr ""
msgid "Copy %{proxy_url}"
msgstr ""
+msgid "Copy Account ID to clipboard"
+msgstr ""
+
+msgid "Copy External ID to clipboard"
+msgstr ""
+
msgid "Copy ID"
msgstr ""
@@ -4588,9 +4763,6 @@ msgstr ""
msgid "Could not add admins as members"
msgstr ""
-msgid "Could not add prometheus URL to whitelist"
-msgstr ""
-
msgid "Could not authorize chat nickname. Try again!"
msgstr ""
@@ -4669,12 +4841,18 @@ msgstr ""
msgid "Create a new issue"
msgstr ""
+msgid "Create a new issue and add it to the epic."
+msgstr ""
+
msgid "Create a new repository"
msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr ""
+msgid "Create an issue"
+msgstr ""
+
msgid "Create an issue. Issues are created for each alert triggered."
msgstr ""
@@ -4777,6 +4955,12 @@ msgstr ""
msgid "Created a branch and a merge request to resolve this issue."
msgstr ""
+msgid "Created after"
+msgstr ""
+
+msgid "Created before"
+msgstr ""
+
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
msgstr ""
@@ -4822,6 +5006,9 @@ msgstr ""
msgid "Cron syntax"
msgstr ""
+msgid "Crossplane"
+msgstr ""
+
msgid "Current Branch"
msgstr ""
@@ -4837,12 +5024,18 @@ msgstr ""
msgid "Current password"
msgstr ""
+msgid "Current vulnerabilities count"
+msgstr ""
+
msgid "CurrentUser|Profile"
msgstr ""
msgid "CurrentUser|Settings"
msgstr ""
+msgid "CurrentUser|Start a Gold trial"
+msgstr ""
+
msgid "Custom CI configuration path"
msgstr ""
@@ -4939,15 +5132,42 @@ msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
+msgid "CycleAnalyticsEvent|Issue closed"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Issue created"
msgstr ""
+msgid "CycleAnalyticsEvent|Issue first added to a board"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Issue first associated with a milestone"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board"
msgstr ""
msgid "CycleAnalyticsEvent|Issue first mentioned in a commit"
msgstr ""
+msgid "CycleAnalyticsEvent|Issue label was added"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Issue label was removed"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Issue last edited"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Merge Request label was added"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Merge Request label was removed"
+msgstr ""
+
+msgid "CycleAnalyticsEvent|Merge request closed"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Merge request created"
msgstr ""
@@ -4960,6 +5180,9 @@ msgstr ""
msgid "CycleAnalyticsEvent|Merge request last build start time"
msgstr ""
+msgid "CycleAnalyticsEvent|Merge request last edited"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Merge request merged"
msgstr ""
@@ -4984,6 +5207,12 @@ msgstr ""
msgid "CycleAnalyticsStage|Test"
msgstr ""
+msgid "CycleAnalyticsStage|is not available for the selected group"
+msgstr ""
+
+msgid "CycleAnalyticsStage|should be under a group"
+msgstr ""
+
msgid "CycleAnalytics|%{projectName}"
msgid_plural "CycleAnalytics|%d projects selected"
msgstr[0] ""
@@ -5003,6 +5232,9 @@ msgstr ""
msgid "CycleAnalytics|group dropdown filter"
msgstr ""
+msgid "CycleAnalytics|not allowed for the given start event"
+msgstr ""
+
msgid "CycleAnalytics|project dropdown filter"
msgstr ""
@@ -5078,6 +5310,9 @@ msgstr ""
msgid "Default Branch"
msgstr ""
+msgid "Default CI configuration path"
+msgstr ""
+
msgid "Default artifacts expiration"
msgstr ""
@@ -5210,6 +5445,9 @@ msgstr ""
msgid "Dependencies"
msgstr ""
+msgid "Dependencies help page link"
+msgstr ""
+
msgid "Dependencies|%d additional vulnerability not shown"
msgid_plural "Dependencies|%d additional vulnerabilities not shown"
msgstr[0] ""
@@ -5475,6 +5713,9 @@ msgstr ""
msgid "Descending"
msgstr ""
+msgid "Describe the goal of the changes and what reviewers should be aware of."
+msgstr ""
+
msgid "Description"
msgstr ""
@@ -5562,6 +5803,9 @@ msgstr ""
msgid "DesignManagement|Upload and view the latest designs for this issue. Consistent and easy to find, so everyone is up to date."
msgstr ""
+msgid "DesignManagement|We could not delete %{design}. Please try again."
+msgstr ""
+
msgid "DesignManagement|We could not delete design(s). Please try again."
msgstr ""
@@ -5586,6 +5830,9 @@ msgstr ""
msgid "Diff limits"
msgstr ""
+msgid "Difference between start date and now"
+msgstr ""
+
msgid "DiffsCompareBaseBranch|(base)"
msgstr ""
@@ -5607,9 +5854,6 @@ msgstr ""
msgid "Disable"
msgstr ""
-msgid "Disable email notifications"
-msgstr ""
-
msgid "Disable for this project"
msgstr ""
@@ -5709,12 +5953,21 @@ msgstr ""
msgid "Display name"
msgstr ""
+msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan"
+msgstr ""
+
+msgid "Do not display offers from third parties within GitLab"
+msgstr ""
+
msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?"
msgstr ""
msgid "Dockerfile"
msgstr ""
+msgid "Documentation"
+msgstr ""
+
msgid "Documentation for popular identity providers"
msgstr ""
@@ -5799,6 +6052,9 @@ msgstr ""
msgid "Due date"
msgstr ""
+msgid "Duration"
+msgstr ""
+
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
msgstr ""
@@ -5847,6 +6103,9 @@ msgstr ""
msgid "Edit comment"
msgstr ""
+msgid "Edit dashboard"
+msgstr ""
+
msgid "Edit description"
msgstr ""
@@ -5892,6 +6151,9 @@ msgstr ""
msgid "Elasticsearch"
msgstr ""
+msgid "Elasticsearch AWS IAM credentials"
+msgstr ""
+
msgid "Elasticsearch indexing restrictions"
msgstr ""
@@ -6000,6 +6262,9 @@ msgstr ""
msgid "Enable Incident Management inbound alert limit"
msgstr ""
+msgid "Enable PlantUML"
+msgstr ""
+
msgid "Enable Pseudonymizer data collection"
msgstr ""
@@ -6102,13 +6367,13 @@ msgstr ""
msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster."
msgstr ""
-msgid "Enter IP address range"
+msgid "Enter Admin Mode"
msgstr ""
-msgid "Enter a number"
+msgid "Enter IP address range"
msgstr ""
-msgid "Enter admin mode"
+msgid "Enter a number"
msgstr ""
msgid "Enter at least three characters to search"
@@ -6162,6 +6427,12 @@ msgstr ""
msgid "Environment:"
msgstr ""
+msgid "EnvironmentDashboard|API"
+msgstr ""
+
+msgid "EnvironmentDashboard|Created through the Deployment API"
+msgstr ""
+
msgid "Environments"
msgstr ""
@@ -6240,15 +6511,24 @@ msgstr ""
msgid "Environments|Job"
msgstr ""
+msgid "Environments|Learn about environments"
+msgstr ""
+
msgid "Environments|Learn more about stopping environments"
msgstr ""
msgid "Environments|New environment"
msgstr ""
+msgid "Environments|No deployed environments"
+msgstr ""
+
msgid "Environments|No deployments yet"
msgstr ""
+msgid "Environments|No pods to display"
+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 ""
@@ -6306,10 +6586,10 @@ msgstr ""
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 ""
-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?"
+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 staging 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?"
+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 ""
msgid "Environments|Updated"
@@ -6414,6 +6694,9 @@ msgstr ""
msgid "Error"
msgstr ""
+msgid "Error Details"
+msgstr ""
+
msgid "Error Tracking"
msgstr ""
@@ -6426,7 +6709,7 @@ msgstr ""
msgid "Error deleting %{issuableType}"
msgstr ""
-msgid "Error fetching contributors data."
+msgid "Error details"
msgstr ""
msgid "Error fetching diverging counts for branches. Please try again."
@@ -6666,6 +6949,9 @@ msgstr ""
msgid "Except policy:"
msgstr ""
+msgid "Excluding merge commits. Limited to 6,000 commits."
+msgstr ""
+
msgid "Existing"
msgstr ""
@@ -6747,6 +7033,9 @@ msgstr ""
msgid "External Classification Policy Authorization"
msgstr ""
+msgid "External ID"
+msgstr ""
+
msgid "External URL"
msgstr ""
@@ -6843,6 +7132,9 @@ msgstr ""
msgid "Failed to deploy to"
msgstr ""
+msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
+msgstr ""
+
msgid "Failed to get ref."
msgstr ""
@@ -6852,6 +7144,9 @@ msgstr ""
msgid "Failed to load emoji list."
msgstr ""
+msgid "Failed to load error details from Sentry."
+msgstr ""
+
msgid "Failed to load errors from Sentry. Error message: %{errorMessage}"
msgstr ""
@@ -6861,6 +7156,9 @@ msgstr ""
msgid "Failed to load related branches"
msgstr ""
+msgid "Failed to load stacktrace."
+msgstr ""
+
msgid "Failed to mark this issue as a duplicate because referenced issue was not found."
msgstr ""
@@ -7041,6 +7339,9 @@ msgstr ""
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
+msgid "FeatureFlags|ID"
+msgstr ""
+
msgid "FeatureFlags|Inactive"
msgstr ""
@@ -7190,6 +7491,9 @@ msgstr ""
msgid "Filter by two-factor authentication"
msgstr ""
+msgid "Filter by user"
+msgstr ""
+
msgid "Filter projects"
msgstr ""
@@ -7247,6 +7551,9 @@ msgstr ""
msgid "First name"
msgstr ""
+msgid "First seen"
+msgstr ""
+
msgid "Fixed date"
msgstr ""
@@ -7370,6 +7677,9 @@ msgstr ""
msgid "From %{providerTitle}"
msgstr ""
+msgid "From %{source_title} into"
+msgstr ""
+
msgid "From Bitbucket"
msgstr ""
@@ -7391,9 +7701,6 @@ msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
-msgid "From milestones:"
-msgstr ""
-
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
@@ -7451,15 +7758,24 @@ msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr ""
+msgid "GeoNodes|Attachments"
+msgstr ""
+
msgid "GeoNodes|Checksummed"
msgstr ""
+msgid "GeoNodes|Container repositories"
+msgstr ""
+
msgid "GeoNodes|Data is out of date from %{timeago}"
msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr ""
+msgid "GeoNodes|Design repositories"
+msgstr ""
+
msgid "GeoNodes|Does not match the primary storage configuration"
msgstr ""
@@ -7481,6 +7797,12 @@ msgstr ""
msgid "GeoNodes|Internal URL"
msgstr ""
+msgid "GeoNodes|Job artifacts"
+msgstr ""
+
+msgid "GeoNodes|LFS objects"
+msgstr ""
+
msgid "GeoNodes|Last event ID processed by cursor"
msgstr ""
@@ -7502,18 +7824,6 @@ msgstr ""
msgid "GeoNodes|Loading nodes"
msgstr ""
-msgid "GeoNodes|Local LFS objects"
-msgstr ""
-
-msgid "GeoNodes|Local attachments"
-msgstr ""
-
-msgid "GeoNodes|Local container repositories"
-msgstr ""
-
-msgid "GeoNodes|Local job artifacts"
-msgstr ""
-
msgid "GeoNodes|New node"
msgstr ""
@@ -7937,6 +8247,9 @@ msgstr ""
msgid "GitLabPages|%{domain} is not verified. To learn how to verify ownership, visit your %{link_start}domain details%{link_end}."
msgstr ""
+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 ""
+
msgid "GitLabPages|Access pages"
msgstr ""
@@ -7949,10 +8262,10 @@ msgstr ""
msgid "GitLabPages|Configure pages"
msgstr ""
-msgid "GitLabPages|Details"
+msgid "GitLabPages|Domains"
msgstr ""
-msgid "GitLabPages|Domains"
+msgid "GitLabPages|Edit"
msgstr ""
msgid "GitLabPages|Expired"
@@ -7961,6 +8274,9 @@ msgstr ""
msgid "GitLabPages|Force HTTPS (requires valid certificates)"
msgstr ""
+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 ""
+
msgid "GitLabPages|It may take up to 30 minutes before the site is available after the first deployment."
msgstr ""
@@ -8045,7 +8361,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -8156,6 +8472,9 @@ msgstr ""
msgid "Golden Tanuki"
msgstr ""
+msgid "Google Cloud Platform"
+msgstr ""
+
msgid "Google Code import"
msgstr ""
@@ -8174,6 +8493,27 @@ msgstr ""
msgid "Grafana URL"
msgstr ""
+msgid "GrafanaIntegration|API Token"
+msgstr ""
+
+msgid "GrafanaIntegration|Active"
+msgstr ""
+
+msgid "GrafanaIntegration|Embed Grafana charts in GitLab issues."
+msgstr ""
+
+msgid "GrafanaIntegration|Enter the Grafana API Token."
+msgstr ""
+
+msgid "GrafanaIntegration|Enter the base URL of the Grafana instance."
+msgstr ""
+
+msgid "GrafanaIntegration|Grafana Authentication"
+msgstr ""
+
+msgid "GrafanaIntegration|Grafana URL"
+msgstr ""
+
msgid "Grant access"
msgstr ""
@@ -8237,12 +8577,24 @@ msgstr ""
msgid "Group name"
msgstr ""
+msgid "Group overview"
+msgstr ""
+
msgid "Group overview content"
msgstr ""
+msgid "Group path is already taken. Suggestions: "
+msgstr ""
+
+msgid "Group path is available."
+msgstr ""
+
msgid "Group pipeline minutes were successfully reset."
msgstr ""
+msgid "Group variables (inherited)"
+msgstr ""
+
msgid "Group was successfully updated."
msgstr ""
@@ -8285,6 +8637,9 @@ msgstr ""
msgid "GroupSAML|Configuration"
msgstr ""
+msgid "GroupSAML|Copy SAML Response XML"
+msgstr ""
+
msgid "GroupSAML|Enable SAML authentication for this group."
msgstr ""
@@ -8318,6 +8673,18 @@ 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 ""
+msgid "GroupSAML|NameID"
+msgstr ""
+
+msgid "GroupSAML|NameID Format"
+msgstr ""
+
+msgid "GroupSAML|SAML Response Output"
+msgstr ""
+
+msgid "GroupSAML|SAML Response XML"
+msgstr ""
+
msgid "GroupSAML|SAML Single Sign On"
msgstr ""
@@ -8345,12 +8712,24 @@ msgstr ""
msgid "GroupSAML|Toggle SAML authentication"
msgstr ""
+msgid "GroupSAML|Valid SAML Response"
+msgstr ""
+
msgid "GroupSAML|With group managed accounts enabled, all the users without a group managed account will be excluded from the group."
msgstr ""
msgid "GroupSAML|Your SCIM token"
msgstr ""
+msgid "GroupSAML|must match stored NameID of \"%{extern_uid}\" as we use this to identify users. If the NameID changes users will be unable to sign in."
+msgstr ""
+
+msgid "GroupSAML|should be \"persistent\""
+msgstr ""
+
+msgid "GroupSAML|should be a random persistent ID, emails are discouraged"
+msgstr ""
+
msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
msgstr ""
@@ -8444,6 +8823,9 @@ msgstr ""
msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group"
msgstr ""
+msgid "GroupSettings|cannot change when group contains projects with NPM packages"
+msgstr ""
+
msgid "GroupSettings|remove the share with group lock from %{ancestor_group_name}"
msgstr ""
@@ -8614,6 +8996,9 @@ msgstr[1] ""
msgid "Hide values"
msgstr ""
+msgid "Hiding all labels"
+msgstr ""
+
msgid "Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0."
msgstr ""
@@ -8755,7 +9140,7 @@ msgstr ""
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 ""
-msgid "If disabled, only admins will be able to set up mirrors in projects."
+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."
@@ -8782,6 +9167,9 @@ msgstr ""
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
+msgid "Iglu registry URL (optional)"
+msgstr ""
+
msgid "ImageDiffViewer|2-up"
msgstr ""
@@ -8926,7 +9314,7 @@ msgstr ""
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
msgstr ""
-msgid "In order to tailor your experience with GitLab<br>we would like to know a bit more about you."
+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."
@@ -8983,6 +9371,9 @@ msgstr ""
msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}."
msgstr ""
+msgid "Inherited:"
+msgstr ""
+
msgid "Inline"
msgstr ""
@@ -9090,6 +9481,9 @@ msgstr ""
msgid "Invalid date"
msgstr ""
+msgid "Invalid date format. Please use UTC format as YYYY-MM-DD"
+msgstr ""
+
msgid "Invalid feature"
msgstr ""
@@ -9342,7 +9736,7 @@ msgstr ""
msgid "Job is stuck. Check runners."
msgstr ""
-msgid "Job traces and artifacts"
+msgid "Job logs and artifacts"
msgstr ""
msgid "Job was retried"
@@ -9429,6 +9823,9 @@ msgstr ""
msgid "June"
msgstr ""
+msgid "Key"
+msgstr ""
+
msgid "Key (PEM)"
msgstr ""
@@ -9453,7 +9850,7 @@ msgstr ""
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr ""
-msgid "Kubernetes cluster integration was not removed."
+msgid "Kubernetes cluster integration and resources are being removed."
msgstr ""
msgid "Kubernetes cluster integration was successfully removed."
@@ -9471,6 +9868,9 @@ msgstr ""
msgid "Kubernetes error: %{error_code}"
msgstr ""
+msgid "Kubernetes popover"
+msgstr ""
+
msgid "LDAP"
msgstr ""
@@ -9683,7 +10083,7 @@ msgstr ""
msgid "Leave"
msgstr ""
-msgid "Leave admin mode"
+msgid "Leave Admin Mode"
msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
@@ -9760,11 +10160,21 @@ msgid_plural "LicenseCompliance|License Compliance detected %d licenses for the
msgstr[0] ""
msgstr[1] ""
+msgid "LicenseCompliance|License Compliance detected %d license for the source branch only; approval required"
+msgid_plural "LicenseCompliance|License Compliance detected %d licenses for the source branch only; approval required"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "LicenseCompliance|License Compliance detected %d new license"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses"
msgstr[0] ""
msgstr[1] ""
+msgid "LicenseCompliance|License Compliance detected %d new license; approval required"
+msgid_plural "LicenseCompliance|License Compliance detected %d new licenses; approval required"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "LicenseCompliance|License Compliance detected no licenses for the source branch only"
msgstr ""
@@ -9813,6 +10223,27 @@ msgstr ""
msgid "Licenses"
msgstr ""
+msgid "License|Buy license"
+msgstr ""
+
+msgid "License|License"
+msgstr ""
+
+msgid "License|You can restore access to the Gold features at any time by upgrading."
+msgstr ""
+
+msgid "License|You can start a free trial of GitLab Ultimate without any obligation or payment details."
+msgstr ""
+
+msgid "License|You do not have a license."
+msgstr ""
+
+msgid "License|Your License"
+msgstr ""
+
+msgid "License|Your free trial of GitLab Ultimate expired on %{trial_ends_on}."
+msgstr ""
+
msgid "Limit display of time tracking units to hours."
msgstr ""
@@ -9863,6 +10294,9 @@ msgstr ""
msgid "Loading contribution stats for group members"
msgstr ""
+msgid "Loading files, directories, and submodules in the path %{path} for commit reference %{ref}"
+msgstr ""
+
msgid "Loading functions timed out. Please reload the page to try again."
msgstr ""
@@ -9932,6 +10366,9 @@ msgstr ""
msgid "Logs"
msgstr ""
+msgid "Logs|To see the pod logs, deploy your code to an environment."
+msgstr ""
+
msgid "MD5"
msgstr ""
@@ -10223,6 +10660,9 @@ msgstr ""
msgid "Merge in progress"
msgstr ""
+msgid "Merge options"
+msgstr ""
+
msgid "Merge request"
msgstr ""
@@ -10331,13 +10771,10 @@ msgstr ""
msgid "MergeRequests|started a thread on %{linkStart}the diff%{linkEnd}"
msgstr ""
-msgid "MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}"
+msgid "MergeRequests|started a thread on an outdated change in commit %{linkStart}%{commitDisplay}%{linkEnd}"
msgstr ""
-msgid "MergeRequests|started a thread on commit %{linkStart}%{commitId}%{linkEnd}"
-msgstr ""
-
-msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
+msgid "MergeRequests|started a thread on commit %{linkStart}%{commitDisplay}%{linkEnd}"
msgstr ""
msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
@@ -10430,9 +10867,6 @@ msgstr ""
msgid "Metrics|Label of the y-axis (usually the unit). The x-axis always represents time."
msgstr ""
-msgid "Metrics|Learn about environments"
-msgstr ""
-
msgid "Metrics|Legend label (optional)"
msgstr ""
@@ -10448,9 +10882,6 @@ msgstr ""
msgid "Metrics|New metric"
msgstr ""
-msgid "Metrics|No deployed environments"
-msgstr ""
-
msgid "Metrics|PromQL query is valid"
msgstr ""
@@ -10463,6 +10894,9 @@ msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
+msgid "Metrics|There was an error fetching the logs, please try again"
+msgstr ""
+
msgid "Metrics|There was an error getting deployment information."
msgstr ""
@@ -10478,9 +10912,6 @@ msgstr ""
msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
msgstr ""
-msgid "Metrics|Unexpected metrics data response from prometheus endpoint"
-msgstr ""
-
msgid "Metrics|Unit label"
msgstr ""
@@ -10490,6 +10921,9 @@ 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 ""
+msgid "Metrics|Validating query"
+msgstr ""
+
msgid "Metrics|Y-axis label"
msgstr ""
@@ -10511,6 +10945,9 @@ msgstr ""
msgid "Metrics|e.g. req/sec"
msgstr ""
+msgid "Microsoft Azure"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -10741,6 +11178,9 @@ msgstr ""
msgid "Naming, visibility"
msgstr ""
+msgid "Navigate to the project to close the milestone."
+msgstr ""
+
msgid "Nav|Help"
msgstr ""
@@ -10995,7 +11435,7 @@ msgstr ""
msgid "No issues for the selected time period."
msgstr ""
-msgid "No job trace"
+msgid "No job log"
msgstr ""
msgid "No jobs to show"
@@ -11037,7 +11477,7 @@ msgstr ""
msgid "No preview for this file type"
msgstr ""
-msgid "No prioritised labels with such name or description"
+msgid "No prioritized labels with such name or description"
msgstr ""
msgid "No public groups"
@@ -11142,9 +11582,6 @@ msgstr ""
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
-msgid "Note: the container registry is always visible when a project is public"
-msgstr ""
-
msgid "NoteForm|Note"
msgstr ""
@@ -11274,6 +11711,9 @@ 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 ""
+msgid "Number of commits"
+msgstr ""
+
msgid "Number of commits per MR"
msgstr ""
@@ -11474,6 +11914,9 @@ msgstr ""
msgid "Or you can choose one of the suggested colors below"
msgstr ""
+msgid "Origin"
+msgstr ""
+
msgid "Other Labels"
msgstr ""
@@ -11486,9 +11929,6 @@ msgstr ""
msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
-msgid "Our Privacy Policy has changed, please visit %{privacy_policy_link} to review these changes."
-msgstr ""
-
msgid "Outbound requests"
msgstr ""
@@ -11507,24 +11947,72 @@ msgstr ""
msgid "Owner"
msgstr ""
+msgid "Package deleted successfully"
+msgstr ""
+
msgid "Package information"
msgstr ""
msgid "Package was removed"
msgstr ""
+msgid "PackageRegistry|Copy npm command"
+msgstr ""
+
+msgid "PackageRegistry|Copy npm setup command"
+msgstr ""
+
+msgid "PackageRegistry|Copy yarn command"
+msgstr ""
+
+msgid "PackageRegistry|Copy yarn setup command"
+msgstr ""
+
msgid "PackageRegistry|Delete Package Version"
msgstr ""
+msgid "PackageRegistry|Delete package"
+msgstr ""
+
+msgid "PackageRegistry|Installation"
+msgstr ""
+
+msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
+msgstr ""
+
+msgid "PackageRegistry|Package installation"
+msgstr ""
+
+msgid "PackageRegistry|Registry Setup"
+msgstr ""
+
+msgid "PackageRegistry|Remove package"
+msgstr ""
+
+msgid "PackageRegistry|There are no packages yet"
+msgstr ""
+
msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr ""
msgid "PackageRegistry|Unable to load package"
msgstr ""
+msgid "PackageRegistry|You are about to delete <b>%{packageName}</b>, this operation is irreversible, are you sure?"
+msgstr ""
+
msgid "PackageRegistry|You are about to delete version %{boldStart}%{version}%{boldEnd} of %{boldStart}%{name}%{boldEnd}. Are you sure?"
msgstr ""
+msgid "PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
+msgstr ""
+
+msgid "PackageRegistry|npm"
+msgstr ""
+
+msgid "PackageRegistry|yarn"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -11582,6 +12070,12 @@ msgstr ""
msgid "Part of merge request changes"
msgstr ""
+msgid "Participants"
+msgstr ""
+
+msgid "Passed"
+msgstr ""
+
msgid "Password"
msgstr ""
@@ -11987,7 +12481,7 @@ msgstr ""
msgid "Please check the configuration file to ensure that it is available and the YAML is valid"
msgstr ""
-msgid "Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}."
+msgid "Please check your email (%{email}) to verify that you own this address and unlock the power of CI/CD. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}."
msgstr ""
msgid "Please choose a group URL with no special characters."
@@ -12089,6 +12583,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Pod logs"
+msgstr ""
+
msgid "Pod not found"
msgstr ""
@@ -12110,6 +12607,9 @@ msgstr ""
msgid "Preferences|Choose what content you want to see on a project’s overview page."
msgstr ""
+msgid "Preferences|Customize integrations with third party services."
+msgstr ""
+
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
msgstr ""
@@ -12119,9 +12619,15 @@ msgstr ""
msgid "Preferences|Display time in 24-hour format"
msgstr ""
+msgid "Preferences|Enable integrated code intelligence on code views"
+msgstr ""
+
msgid "Preferences|For example: 30 mins ago."
msgstr ""
+msgid "Preferences|Integrations"
+msgstr ""
+
msgid "Preferences|Layout width"
msgstr ""
@@ -12134,6 +12640,9 @@ msgstr ""
msgid "Preferences|Show whitespace in diffs"
msgstr ""
+msgid "Preferences|Sourcegraph"
+msgstr ""
+
msgid "Preferences|Syntax highlighting theme"
msgstr ""
@@ -12659,6 +13168,9 @@ msgstr ""
msgid "Project details"
msgstr ""
+msgid "Project does not exist or you don't have permission to perform this action"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr ""
@@ -12683,6 +13195,12 @@ msgstr ""
msgid "Project name"
msgstr ""
+msgid "Project order will not be saved as local storage is not available."
+msgstr ""
+
+msgid "Project overview"
+msgstr ""
+
msgid "Project slug"
msgstr ""
@@ -12797,12 +13315,18 @@ msgstr ""
msgid "ProjectSettings|All discussions must be resolved"
msgstr ""
+msgid "ProjectSettings|Allow users to request access"
+msgstr ""
+
msgid "ProjectSettings|Automatically resolve merge request diff discussions when they become outdated"
msgstr ""
msgid "ProjectSettings|Badges"
msgstr ""
+msgid "ProjectSettings|Build, test, and deploy your changes"
+msgstr ""
+
msgid "ProjectSettings|Choose your merge method, merge options, and merge checks."
msgstr ""
@@ -12812,12 +13336,33 @@ msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
+msgid "ProjectSettings|Container registry"
+msgstr ""
+
msgid "ProjectSettings|Customize your project badges."
msgstr ""
+msgid "ProjectSettings|Disable email notifications"
+msgstr ""
+
+msgid "ProjectSettings|Enable 'Delete source branch' option by default"
+msgstr ""
+
msgid "ProjectSettings|Every merge creates a merge commit"
msgstr ""
+msgid "ProjectSettings|Every project can have its own space to store its Docker images"
+msgstr ""
+
+msgid "ProjectSettings|Every project can have its own space to store its packages"
+msgstr ""
+
+msgid "ProjectSettings|Everyone"
+msgstr ""
+
+msgid "ProjectSettings|Existing merge requests and protected branches are not affected"
+msgstr ""
+
msgid "ProjectSettings|Failed to protect the tag"
msgstr ""
@@ -12830,9 +13375,24 @@ msgstr ""
msgid "ProjectSettings|Fast-forward merges only"
msgstr ""
+msgid "ProjectSettings|Git Large File Storage"
+msgstr ""
+
+msgid "ProjectSettings|Internal"
+msgstr ""
+
+msgid "ProjectSettings|Issues"
+msgstr ""
+
msgid "ProjectSettings|Learn more about badges."
msgstr ""
+msgid "ProjectSettings|Lightweight issue tracking system for this project"
+msgstr ""
+
+msgid "ProjectSettings|Manages large files such as audio, video, and graphics files"
+msgstr ""
+
msgid "ProjectSettings|Merge checks"
msgstr ""
@@ -12851,21 +13411,60 @@ msgstr ""
msgid "ProjectSettings|Merge pipelines will try to validate the post-merge result prior to merging"
msgstr ""
+msgid "ProjectSettings|Merge requests"
+msgstr ""
+
msgid "ProjectSettings|No merge commits are created"
msgstr ""
+msgid "ProjectSettings|Note: the container registry is always visible when a project is public"
+msgstr ""
+
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
+msgid "ProjectSettings|Packages"
+msgstr ""
+
+msgid "ProjectSettings|Pages"
+msgstr ""
+
+msgid "ProjectSettings|Pages for project documentation"
+msgstr ""
+
+msgid "ProjectSettings|Pipelines"
+msgstr ""
+
msgid "ProjectSettings|Pipelines must succeed"
msgstr ""
msgid "ProjectSettings|Pipelines need to be configured to enable this feature."
msgstr ""
+msgid "ProjectSettings|Private"
+msgstr ""
+
+msgid "ProjectSettings|Project visibility"
+msgstr ""
+
+msgid "ProjectSettings|Public"
+msgstr ""
+
+msgid "ProjectSettings|Repository"
+msgstr ""
+
+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 ""
+msgid "ProjectSettings|Snippets"
+msgstr ""
+
+msgid "ProjectSettings|Submit changes to be merged upstream"
+msgstr ""
+
msgid "ProjectSettings|These checks must pass before merge requests can be merged"
msgstr ""
@@ -12878,15 +13477,27 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
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 ""
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
msgstr ""
+msgid "ProjectSettings|View and edit files in this project"
+msgstr ""
+
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
msgstr ""
+msgid "ProjectSettings|Wiki"
+msgstr ""
+
+msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -12932,6 +13543,9 @@ msgstr ""
msgid "ProjectTemplates|Ruby on Rails"
msgstr ""
+msgid "ProjectTemplates|Serverless Framework/JS"
+msgstr ""
+
msgid "ProjectTemplates|Spring"
msgstr ""
@@ -13028,9 +13642,6 @@ msgstr ""
msgid "ProjectsNew|Want to house several dependent projects under the same namespace? %{link_start}Create a group.%{link_end}"
msgstr ""
-msgid "Prometheus listen_address in config/gitlab.yml is not a valid URI"
-msgstr ""
-
msgid "PrometheusAlerts|%{count} alerts applied"
msgstr ""
@@ -13178,12 +13789,24 @@ 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|Learn more"
+msgstr ""
+
+msgid "Promotions|See the other features in the %{subscription_link_start}bronze plan%{subscription_link_end}"
+msgstr ""
+
msgid "Promotions|This feature is locked."
msgstr ""
msgid "Promotions|Upgrade plan"
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 "Prompt users to upload SSH keys"
msgstr ""
@@ -13507,6 +14130,9 @@ msgstr ""
msgid "Regex pattern"
msgstr ""
+msgid "Region that Elasticsearch is configured"
+msgstr ""
+
msgid "Register"
msgstr ""
@@ -13522,12 +14148,6 @@ msgstr ""
msgid "Register Universal Two-Factor (U2F) Device"
msgstr ""
-msgid "Register and see your runners for this group."
-msgstr ""
-
-msgid "Register and see your runners for this project."
-msgstr ""
-
msgid "Register for GitLab"
msgstr ""
@@ -13562,7 +14182,9 @@ msgid "Related merge requests"
msgstr ""
msgid "Release"
-msgstr ""
+msgid_plural "Releases"
+msgstr[0] ""
+msgstr[1] ""
msgid "Release notes"
msgstr ""
@@ -13810,6 +14432,9 @@ msgstr ""
msgid "Report abuse to admin"
msgstr ""
+msgid "Reported %{timeAgo} by %{reportedBy}"
+msgstr ""
+
msgid "Reporting"
msgstr ""
@@ -13894,7 +14519,7 @@ msgstr ""
msgid "Repository maintenance"
msgstr ""
-msgid "Repository mirror"
+msgid "Repository mirroring"
msgstr ""
msgid "Repository static objects"
@@ -14113,6 +14738,9 @@ msgstr ""
msgid "Rollback"
msgstr ""
+msgid "Rook"
+msgstr ""
+
msgid "Run CI/CD pipelines for external repositories"
msgstr ""
@@ -14158,6 +14786,9 @@ msgstr ""
msgid "Runners activated for this project"
msgstr ""
+msgid "Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project."
+msgstr ""
+
msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr ""
@@ -14576,9 +15207,30 @@ msgstr ""
msgid "Security Reports|While it's rare to have no vulnerabilities for your group, 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 "Security configuration help link"
+msgstr ""
+
msgid "Security dashboard"
msgstr ""
+msgid "SecurityConfiguration|Configured"
+msgstr ""
+
+msgid "SecurityConfiguration|Feature"
+msgstr ""
+
+msgid "SecurityConfiguration|Feature documentation"
+msgstr ""
+
+msgid "SecurityConfiguration|Not yet configured"
+msgstr ""
+
+msgid "SecurityConfiguration|Secure features"
+msgstr ""
+
+msgid "SecurityConfiguration|Status"
+msgstr ""
+
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
@@ -14663,6 +15315,9 @@ msgstr ""
msgid "Select Page"
msgstr ""
+msgid "Select Stack"
+msgstr ""
+
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -14702,6 +15357,9 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
+msgid "Select branch"
+msgstr ""
+
msgid "Select branch/tag"
msgstr ""
@@ -14756,6 +15414,9 @@ msgstr ""
msgid "Select user"
msgstr ""
+msgid "Select your role"
+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 ""
@@ -14780,6 +15441,9 @@ msgstr ""
msgid "Sentry API URL"
msgstr ""
+msgid "Sentry event"
+msgstr ""
+
msgid "Sep"
msgstr ""
@@ -15121,6 +15785,9 @@ msgstr ""
msgid "Showing all issues"
msgstr ""
+msgid "Showing all labels"
+msgstr ""
+
msgid "Showing last %{size} of log -"
msgstr ""
@@ -15211,9 +15878,6 @@ msgstr ""
msgid "Single or combined queries"
msgstr ""
-msgid "Site ID"
-msgstr ""
-
msgid "Size"
msgstr ""
@@ -15229,6 +15893,9 @@ msgstr ""
msgid "Skip this for now"
msgstr ""
+msgid "Skipped"
+msgstr ""
+
msgid "Slack application"
msgstr ""
@@ -15331,6 +15998,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
+msgid "Something went wrong while deleting the package."
+msgstr ""
+
msgid "Something went wrong while deleting the source branch. Please try again."
msgstr ""
@@ -15346,18 +16016,30 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching description changes. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching group member contributions"
msgstr ""
msgid "Something went wrong while fetching latest comments."
msgstr ""
+msgid "Something went wrong while fetching projects"
+msgstr ""
+
msgid "Something went wrong while fetching related merge requests."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
+msgid "Something went wrong while fetching the package."
+msgstr ""
+
+msgid "Something went wrong while fetching the packages list."
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -15574,6 +16256,51 @@ msgstr ""
msgid "Source project cannot be found."
msgstr ""
+msgid "Sourcegraph"
+msgstr ""
+
+msgid "SourcegraphAdmin|Block on private and internal projects"
+msgstr ""
+
+msgid "SourcegraphAdmin|Configure the URL to a Sourcegraph instance which can read your GitLab projects."
+msgstr ""
+
+msgid "SourcegraphAdmin|Enable Sourcegraph"
+msgstr ""
+
+msgid "SourcegraphAdmin|Enable code intelligence powered by %{link_start}Sourcegraph%{link_end} on your GitLab instance's code views and merge requests."
+msgstr ""
+
+msgid "SourcegraphAdmin|If checked, only public projects will have code intelligence and communicate with Sourcegraph."
+msgstr ""
+
+msgid "SourcegraphAdmin|More information"
+msgstr ""
+
+msgid "SourcegraphAdmin|Save changes"
+msgstr ""
+
+msgid "SourcegraphAdmin|Sourcegraph URL"
+msgstr ""
+
+msgid "SourcegraphAdmin|e.g. https://sourcegraph.example.com"
+msgstr ""
+
+msgid "SourcegraphPreferences|This feature is experimental and currently limited to certain projects."
+msgstr ""
+
+msgid "SourcegraphPreferences|This feature is experimental and limited to public projects."
+msgstr ""
+
+msgid "SourcegraphPreferences|This feature is experimental."
+msgstr ""
+
+msgid "SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}."
+msgstr ""
+
+msgid "SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}."
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -15598,6 +16325,9 @@ msgstr ""
msgid "Squash commits"
msgstr ""
+msgid "Stack trace"
+msgstr ""
+
msgid "Stage"
msgstr ""
@@ -15658,7 +16388,7 @@ msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
-msgid "Start a Free Trial"
+msgid "Start a Free Gold Trial"
msgstr ""
msgid "Start a new discussion..."
@@ -15778,6 +16508,9 @@ msgstr ""
msgid "StorageSize|Unknown"
msgstr ""
+msgid "Subgroup overview"
+msgstr ""
+
msgid "SubgroupCreationLevel|Allowed to create subgroups"
msgstr ""
@@ -15841,6 +16574,15 @@ msgstr ""
msgid "Subscription"
msgstr ""
+msgid "Subscription deletion failed."
+msgstr ""
+
+msgid "Subscription successfully created."
+msgstr ""
+
+msgid "Subscription successfully deleted."
+msgstr ""
+
msgid "SubscriptionTable|Billing"
msgstr ""
@@ -16015,6 +16757,12 @@ msgstr ""
msgid "Suggestions:"
msgstr ""
+msgid "Suite"
+msgstr ""
+
+msgid "Summary"
+msgstr ""
+
msgid "Sunday"
msgstr ""
@@ -16207,9 +16955,6 @@ msgstr ""
msgid "Terms of Service and Privacy Policy"
msgstr ""
-msgid "Test SAML SSO"
-msgstr ""
-
msgid "Test coverage parsing"
msgstr ""
@@ -16243,6 +16988,39 @@ msgstr ""
msgid "TestHooks|Ensure the wiki is enabled and has pages."
msgstr ""
+msgid "TestReports|%{count} errors"
+msgstr ""
+
+msgid "TestReports|%{count} failures"
+msgstr ""
+
+msgid "TestReports|%{count} jobs"
+msgstr ""
+
+msgid "TestReports|%{rate}%{sign} success rate"
+msgstr ""
+
+msgid "TestReports|Test suites"
+msgstr ""
+
+msgid "TestReports|Tests"
+msgstr ""
+
+msgid "TestReports|There are no test cases to display."
+msgstr ""
+
+msgid "TestReports|There are no test suites to show."
+msgstr ""
+
+msgid "TestReports|There are no tests to show."
+msgstr ""
+
+msgid "TestReports|There was an error fetching the test reports."
+msgstr ""
+
+msgid "Tests"
+msgstr ""
+
msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr ""
@@ -16278,6 +17056,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
+msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
+msgstr ""
+
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
@@ -16296,12 +17077,18 @@ msgstr ""
msgid "The collection of events added to the data gathered for that stage."
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 ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr ""
+msgid "The default CI configuration path for new projects."
+msgstr ""
+
msgid "The dependency list details information about the components used within your project."
msgstr ""
@@ -16347,6 +17134,9 @@ msgstr ""
msgid "The group and its projects can only be viewed by members."
msgstr ""
+msgid "The group has already been shared with this group"
+msgstr ""
+
msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}."
msgstr ""
@@ -16605,6 +17395,9 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
+msgid "There was a problem saving your custom stage, please try again"
+msgstr ""
+
msgid "There was a problem sending the confirmation email"
msgstr ""
@@ -16623,7 +17416,13 @@ msgstr ""
msgid "There was an error fetching configuration for charts"
msgstr ""
-msgid "There was an error fetching data for the form"
+msgid "There was an error fetching cycle analytics stages."
+msgstr ""
+
+msgid "There was an error fetching data for the selected stage"
+msgstr ""
+
+msgid "There was an error fetching label data for the selected group"
msgstr ""
msgid "There was an error gathering the chart data"
@@ -16665,12 +17464,18 @@ msgstr ""
msgid "There was an error while fetching cycle analytics data."
msgstr ""
+msgid "There was an error while fetching cycle analytics summary data."
+msgstr ""
+
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
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 ""
+
msgid "They can be managed using the %{link}."
msgstr ""
@@ -16905,6 +17710,9 @@ msgstr ""
msgid "This option is disabled as you don't have write permissions for the current branch"
msgstr ""
+msgid "This option is only available on GitLab.com"
+msgstr ""
+
msgid "This page is unavailable because you are not allowed to read information across multiple projects."
msgstr ""
@@ -16929,6 +17737,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 path either does not exist or is private."
+msgstr ""
+
msgid "This repository"
msgstr ""
@@ -16941,9 +17752,6 @@ msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
-msgid "This setting will override user notification preferences for all project members."
-msgstr ""
-
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
msgstr ""
@@ -17275,7 +18083,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
@@ -17419,6 +18227,9 @@ msgstr ""
msgid "Total: %{total}"
msgstr ""
+msgid "Trace"
+msgstr ""
+
msgid "Tracing"
msgstr ""
@@ -17446,6 +18257,9 @@ msgstr ""
msgid "TransferGroup|Database is not supported."
msgstr ""
+msgid "TransferGroup|Group contains projects with NPM packages."
+msgstr ""
+
msgid "TransferGroup|Group is already a root group."
msgstr ""
@@ -17473,6 +18287,9 @@ 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 ""
+
msgid "TransferProject|Transfer failed, please contact an admin."
msgstr ""
@@ -17578,6 +18395,9 @@ msgstr ""
msgid "URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...)."
msgstr ""
+msgid "URL or request ID"
+msgstr ""
+
msgid "Unable to apply suggestions to a deleted line."
msgstr ""
@@ -17797,6 +18617,9 @@ msgstr ""
msgid "Updated %{updated_at} by %{updated_by}"
msgstr ""
+msgid "Updated at"
+msgstr ""
+
msgid "Updated to"
msgstr ""
@@ -17821,9 +18644,6 @@ msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
msgstr ""
-msgid "Upgrade your plan to activate Issue weight."
-msgstr ""
-
msgid "Upgrade your plan to improve Issue boards."
msgstr ""
@@ -18139,6 +18959,9 @@ 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 ""
+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 ""
+
msgid "UserOnboardingTour|Thanks for the feedback! %{thumbsUp}"
msgstr ""
@@ -18343,6 +19166,9 @@ msgstr ""
msgid "Verified"
msgstr ""
+msgid "Verify SAML Configuration"
+msgstr ""
+
msgid "Version"
msgstr ""
@@ -18393,7 +19219,7 @@ msgstr ""
msgid "View job"
msgstr ""
-msgid "View job trace"
+msgid "View job log"
msgstr ""
msgid "View jobs"
@@ -18501,6 +19327,9 @@ msgstr ""
msgid "VulnerabilityChart|%{formattedStartDate} to today"
msgstr ""
+msgid "VulnerabilityChart|Severity"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -18618,6 +19447,9 @@ msgstr ""
msgid "Welcome to GitLab"
msgstr ""
+msgid "Welcome to GitLab @%{username}!"
+msgstr ""
+
msgid "Welcome to the Guided GitLab Tour"
msgstr ""
@@ -18962,6 +19794,9 @@ msgstr ""
msgid "You can see your chat accounts."
msgstr ""
+msgid "You can set up as many Runners as you need to run your jobs."
+msgstr ""
+
msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas."
msgstr ""
@@ -18971,6 +19806,9 @@ msgstr ""
msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
msgstr ""
+msgid "You can try again using %{begin_link}basic search%{end_link}"
+msgstr ""
+
msgid "You cannot access the raw file. Please wait a minute."
msgstr ""
@@ -19073,6 +19911,9 @@ 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 ""
+msgid "You may close the milestone now."
+msgstr ""
+
msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr ""
@@ -19088,6 +19929,9 @@ msgstr ""
msgid "You must provide your current password in order to change it."
msgstr ""
+msgid "You must select a stack for configuring your cloud provider. Learn more about"
+msgstr ""
+
msgid "You need a different license to enable FileLocks feature"
msgstr ""
@@ -19319,6 +20163,9 @@ msgstr ""
msgid "a deleted user"
msgstr ""
+msgid "a design"
+msgstr ""
+
msgid "added %{created_at_timeago}"
msgstr ""
@@ -19713,6 +20560,9 @@ msgstr ""
msgid "design"
msgstr ""
+msgid "designs"
+msgstr ""
+
msgid "detached"
msgstr ""
@@ -19772,6 +20622,9 @@ msgstr ""
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr ""
+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 ""
@@ -19885,6 +20738,9 @@ msgstr ""
msgid "leave %{group_name}"
msgstr ""
+msgid "limit of %{project_limit} reached"
+msgstr ""
+
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
@@ -19950,9 +20806,6 @@ msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
-msgid "mrWidget|Added to the merge train at position %{mergeTrainPosition}"
-msgstr ""
-
msgid "mrWidget|Added to the merge train by"
msgstr ""
@@ -20034,6 +20887,9 @@ msgstr ""
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 ""
+
msgid "mrWidget|Loading deployment statistics"
msgstr ""
@@ -20241,6 +21097,9 @@ msgstr ""
msgid "nounSeries|%{item}, and %{lastItem}"
msgstr ""
+msgid "opened %{timeAgoString} by %{user}"
+msgstr ""
+
msgid "or %{link_start}create a new Google account%{link_end}"
msgstr ""
@@ -20377,6 +21236,9 @@ msgstr ""
msgid "started"
msgstr ""
+msgid "started a discussion on %{design_link}"
+msgstr ""
+
msgid "started on %{milestone_start_date}"
msgstr ""
@@ -20395,6 +21257,9 @@ msgstr ""
msgid "syntax is incorrect"
msgstr ""
+msgid "tag name"
+msgstr ""
+
msgid "this document"
msgstr ""
@@ -20478,10 +21343,5 @@ msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
-msgid "within %d minute "
-msgid_plural "within %d minutes "
-msgstr[0] ""
-msgstr[1] ""
-
msgid "yaml invalid"
msgstr ""
diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po
index 40738f1c576..9c5817a6bbc 100644
--- a/locale/gl_ES/gitlab.po
+++ b/locale/gl_ES/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/he_IL/gitlab.po b/locale/he_IL/gitlab.po
index bc16e8b528b..40a8dc37d2d 100644
--- a/locale/he_IL/gitlab.po
+++ b/locale/he_IL/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/hi_IN/gitlab.po b/locale/hi_IN/gitlab.po
index 0f51e91d180..9d986e64534 100644
--- a/locale/hi_IN/gitlab.po
+++ b/locale/hi_IN/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/hr_HR/gitlab.po b/locale/hr_HR/gitlab.po
index 42ec800d560..b24fd8c1468 100644
--- a/locale/hr_HR/gitlab.po
+++ b/locale/hr_HR/gitlab.po
@@ -7608,7 +7608,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16470,7 +16470,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/hu_HU/gitlab.po b/locale/hu_HU/gitlab.po
index c58c10829c3..3b25068e22a 100644
--- a/locale/hu_HU/gitlab.po
+++ b/locale/hu_HU/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po
index 2df79fe4a42..860c508e2bc 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index dcf71315806..c1e48014704 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 9d4f49abbd4..0f974ba2df7 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr "å‰ã«æˆ»ã‚‹"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ka_GE/gitlab.po b/locale/ka_GE/gitlab.po
index 4807ae7d883..8f01df42567 100644
--- a/locale/ka_GE/gitlab.po
+++ b/locale/ka_GE/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index f51f1290306..67cb7a1a8c0 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr "뒤로 가기"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/mn_MN/gitlab.po b/locale/mn_MN/gitlab.po
index a29ea775eab..fa697d18245 100644
--- a/locale/mn_MN/gitlab.po
+++ b/locale/mn_MN/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/nb_NO/gitlab.po b/locale/nb_NO/gitlab.po
index d7ea32e6542..44ff0f9513d 100644
--- a/locale/nb_NO/gitlab.po
+++ b/locale/nb_NO/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 03a0b331986..97128936449 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/pa_IN/gitlab.po b/locale/pa_IN/gitlab.po
index 6fb77c5e652..9674b40f3cd 100644
--- a/locale/pa_IN/gitlab.po
+++ b/locale/pa_IN/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index bda1ec45c8b..3f3656e9c2b 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 93912e40b6d..120127de15f 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr "Go Micro é um framework para desenvolvimento de microsserviços."
msgid "Go back"
msgstr "Voltar"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/pt_PT/gitlab.po b/locale/pt_PT/gitlab.po
index 6e13301ff56..ef265dc1b8c 100644
--- a/locale/pt_PT/gitlab.po
+++ b/locale/pt_PT/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ro_RO/gitlab.po b/locale/ro_RO/gitlab.po
index 6b40862685d..c433d8f5ba2 100644
--- a/locale/ro_RO/gitlab.po
+++ b/locale/ro_RO/gitlab.po
@@ -7608,7 +7608,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16470,7 +16470,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index f0d5c6afc5f..0571d4745a2 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr "Go Micro — Ñто фреймворк Ð´Ð»Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ миÐ
msgid "Go back"
msgstr "ВернутьÑÑ"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sk_SK/gitlab.po b/locale/sk_SK/gitlab.po
index ac90c490f1a..a2a7bc42f0c 100644
--- a/locale/sk_SK/gitlab.po
+++ b/locale/sk_SK/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sq_AL/gitlab.po b/locale/sq_AL/gitlab.po
index 30ac40fcfd5..072729c9ea2 100644
--- a/locale/sq_AL/gitlab.po
+++ b/locale/sq_AL/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sr_CS/gitlab.po b/locale/sr_CS/gitlab.po
index 14a837f6957..d8c8708cdc7 100644
--- a/locale/sr_CS/gitlab.po
+++ b/locale/sr_CS/gitlab.po
@@ -7608,7 +7608,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16470,7 +16470,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sr_SP/gitlab.po b/locale/sr_SP/gitlab.po
index ba836cc6b0b..8dc6be6c1ea 100644
--- a/locale/sr_SP/gitlab.po
+++ b/locale/sr_SP/gitlab.po
@@ -7608,7 +7608,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16470,7 +16470,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sv_SE/gitlab.po b/locale/sv_SE/gitlab.po
index 1f7ad2d35fb..17699450a60 100644
--- a/locale/sv_SE/gitlab.po
+++ b/locale/sv_SE/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/sw_KE/gitlab.po b/locale/sw_KE/gitlab.po
index 9ba58b06425..230cb6fc2e6 100644
--- a/locale/sw_KE/gitlab.po
+++ b/locale/sw_KE/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po
index e2434b056a6..6958899c85d 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -7547,7 +7547,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16382,7 +16382,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 8511ce52d39..be2c217ad8e 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -7669,7 +7669,7 @@ msgstr "Go Micro — це фреймворк Ð´Ð»Ñ Ñ€Ð¾Ð·Ñ€Ð¾Ð±ÐºÐ¸ мікроÑ
msgid "Go back"
msgstr "ПовернутиÑÑ"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr "Перейти назад (при пошуку файлів"
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16558,7 +16558,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr "Ð”Ð»Ñ Ð·Ð±ÐµÑ€ÐµÐ¶ÐµÐ½Ð½Ñ ÑˆÐ²Ð¸Ð´ÐºÐ¾Ð´Ñ–Ñ— відображаютьÑÑ Ð»Ð¸ÑˆÐµ <strong>%{display_size} із %{real_size}</strong> файлів."
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/vi_VN/gitlab.po b/locale/vi_VN/gitlab.po
index a7249349838..6369e477289 100644
--- a/locale/vi_VN/gitlab.po
+++ b/locale/vi_VN/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 4b19f016a59..2c6a67d6ecc 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr "Go Micro是一个微æœåŠ¡å¼€å‘的框架。"
msgid "Go back"
msgstr "返回"
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr "请将%{link} 页é¢è¿žæŽ¥åˆ°æ‚¨çš„ Jaeger æœåŠ¡å™¨ï¼Œä»¥ä¾¿åœ¨ GitLab
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr "为了ä¿æŒæ€§èƒ½ï¼Œä»…显示文件中的 <strong>%{display_size}/%{real_size}</strong>。"
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index e1050515e4d..13f814fe62e 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr ""
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 89f48dcb07f..f533e52c6f1 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -7486,7 +7486,7 @@ msgstr ""
msgid "Go back"
msgstr "上一é "
-msgid "Go back (while searching for files"
+msgid "Go back (while searching for files)"
msgstr ""
msgid "Go back to %{startTag}Open issues%{endTag} and select some issues to add to your board."
@@ -16294,7 +16294,7 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
-msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visiblity to private."
+msgid "To protect this issue's confidentiality, %{link_start}fork the project%{link_end} and set the forks visibility to private."
msgstr ""
msgid "To protect this issue's confidentiality, a private fork of this project was selected."
diff --git a/package.json b/package.json
index 0d951a58406..016f4f96e21 100644
--- a/package.json
+++ b/package.json
@@ -37,11 +37,13 @@
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.6.2",
- "@gitlab/svgs": "^1.78.0",
- "@gitlab/ui": "5.36.0",
- "@gitlab/visual-review-tools": "1.0.3",
- "apollo-cache-inmemory": "^1.5.1",
- "apollo-client": "^2.5.1",
+ "@gitlab/svgs": "^1.82.0",
+ "@gitlab/ui": "7.11.0",
+ "@gitlab/visual-review-tools": "1.2.0",
+ "@sentry/browser": "^5.7.1",
+ "@sourcegraph/code-host-integration": "^0.0.13",
+ "apollo-cache-inmemory": "^1.6.3",
+ "apollo-client": "^2.6.4",
"apollo-link": "^1.2.11",
"apollo-link-batch-http": "^1.2.11",
"apollo-upload-client": "^10.0.0",
@@ -74,7 +76,7 @@
"d3-time-format": "^2.1.1",
"d3-transition": "^1.1.1",
"dateformat": "^3.0.3",
- "deckar01-task_list": "^2.2.0",
+ "deckar01-task_list": "^2.2.1",
"diff": "^3.4.0",
"document-register-element": "1.13.1",
"dropzone": "^4.2.0",
@@ -98,7 +100,7 @@
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
"marked": "^0.3.12",
- "mermaid": "^8.2.6",
+ "mermaid": "^8.4.2",
"monaco-editor": "^0.15.6",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
@@ -109,9 +111,8 @@
"prosemirror-markdown": "^1.3.0",
"prosemirror-model": "^1.6.4",
"raphael": "^2.2.7",
- "raven-js": "^3.22.1",
"raw-loader": "^3.1.0",
- "sanitize-html": "^1.16.1",
+ "sanitize-html": "^1.20.0",
"select2": "3.5.2-browserify",
"sha1": "^1.1.1",
"smooshpack": "^0.0.54",
@@ -174,6 +175,7 @@
"jasmine-diff": "^0.1.3",
"jasmine-jquery": "^2.1.1",
"jest": "^24.1.0",
+ "jest-canvas-mock": "^2.1.2",
"jest-environment-jsdom": "^24.0.0",
"jest-junit": "^6.3.0",
"jest-util": "^24.0.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index f04ecb13879..5266fc57b0a 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -2,16 +2,21 @@ source 'https://rubygems.org'
gem 'gitlab-qa'
gem 'activesupport', '5.2.3' # This should stay in sync with the root's Gemfile
-gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem 'capybara', '~> 2.16.1'
gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12'
gem 'airborne', '~> 0.2.13'
-gem 'nokogiri', '~> 1.10.4'
+gem 'nokogiri', '~> 1.10.5'
gem 'rspec-retry', '~> 0.6.1'
gem 'rspec_junit_formatter', '~> 0.4.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
gem 'knapsack', '~> 1.17'
gem 'parallel_tests', '~> 2.29'
+
+group :test do
+ gem 'pry-byebug', '~> 3.5.1', platform: :mri
+ gem "ruby-debug-ide", "~> 0.7.0"
+ gem "debase", "~> 0.2.4.1"
+end
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index d582d77c5cd..84eab990c95 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -29,6 +29,9 @@ GEM
ffi (~> 1.0, >= 1.0.11)
coderay (1.1.2)
concurrent-ruby (1.1.5)
+ debase (0.2.4.1)
+ debase-ruby_core_source (>= 0.10.2)
+ debase-ruby_core_source (0.10.6)
diff-lcs (1.3)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
@@ -52,7 +55,7 @@ GEM
mini_portile2 (2.4.0)
minitest (5.11.3)
netrc (0.11.0)
- nokogiri (1.10.4)
+ nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
parallel (1.17.0)
parallel_tests (2.29.0)
@@ -67,7 +70,7 @@ GEM
rack (2.0.6)
rack-test (0.8.2)
rack (>= 1.0, < 3)
- rake (12.3.0)
+ rake (12.3.3)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
@@ -89,6 +92,8 @@ GEM
rspec-support (3.7.0)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
+ ruby-debug-ide (0.7.0)
+ rake (>= 0.8.1)
rubyzip (1.2.2)
selenium-webdriver (3.141.0)
childprocess (~> 0.5)
@@ -110,16 +115,18 @@ DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
+ debase (~> 0.2.4.1)
faker (~> 1.6, >= 1.6.6)
gitlab-qa
knapsack (~> 1.17)
- nokogiri (~> 1.10.4)
+ nokogiri (~> 1.10.5)
parallel_tests (~> 2.29)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
+ ruby-debug-ide (~> 0.7.0)
selenium-webdriver (~> 3.12)
BUNDLED WITH
diff --git a/qa/qa.rb b/qa/qa.rb
index a628c0e0e3f..6397e4216d9 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -10,6 +10,14 @@ require_relative '../config/initializers/0_inject_enterprise_edition_module'
module QA
##
+ # Helper classes to represent frequently used sequences of actions
+ # (e.g., login)
+ #
+ module Flow
+ autoload :Login, 'qa/flow/login'
+ end
+
+ ##
# GitLab QA runtime classes, mostly singletons.
#
module Runtime
@@ -157,6 +165,7 @@ module QA
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
+ autoload :Welcome, 'qa/page/dashboard/welcome'
module Snippet
autoload :New, 'qa/page/dashboard/snippet/new'
@@ -331,6 +340,7 @@ module QA
module Component
autoload :IpLimits, 'qa/page/admin/settings/component/ip_limits'
+ autoload :OutboundRequests, 'qa/page/admin/settings/component/outbound_requests'
autoload :RepositoryStorage, 'qa/page/admin/settings/component/repository_storage'
autoload :AccountAndLimit, 'qa/page/admin/settings/component/account_and_limit'
autoload :PerformanceBar, 'qa/page/admin/settings/component/performance_bar'
@@ -406,7 +416,9 @@ module QA
module DockerRun
autoload :Base, 'qa/service/docker_run/base'
+ autoload :Jenkins, 'qa/service/docker_run/jenkins'
autoload :LDAP, 'qa/service/docker_run/ldap'
+ autoload :Maven, 'qa/service/docker_run/maven'
autoload :NodeJs, 'qa/service/docker_run/node_js'
autoload :GitlabRunner, 'qa/service/docker_run/gitlab_runner'
end
@@ -419,6 +431,7 @@ module QA
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
autoload :ParallelRunner, 'qa/specs/parallel_runner'
+ autoload :LoopRunner, 'qa/specs/loop_runner'
module Helpers
autoload :Quarantine, 'qa/specs/helpers/quarantine'
@@ -436,6 +449,17 @@ module QA
end
end
+ module Jenkins
+ module Page
+ autoload :Base, 'qa/vendor/jenkins/page/base'
+ autoload :Login, 'qa/vendor/jenkins/page/login'
+ autoload :Configure, 'qa/vendor/jenkins/page/configure'
+ autoload :NewCredentials, 'qa/vendor/jenkins/page/new_credentials'
+ autoload :NewJob, 'qa/vendor/jenkins/page/new_job'
+ autoload :ConfigureJob, 'qa/vendor/jenkins/page/configure_job'
+ end
+ end
+
module Github
module Page
autoload :Base, 'qa/vendor/github/page/base'
diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb
new file mode 100644
index 00000000000..d84dfaa9377
--- /dev/null
+++ b/qa/qa/flow/login.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module QA
+ module Flow
+ module Login
+ module_function
+
+ def while_signed_in(as: nil)
+ Page::Main::Menu.perform(&:sign_out_if_signed_in)
+
+ sign_in(as: as)
+
+ yield
+
+ Page::Main::Menu.perform(&:sign_out)
+ end
+
+ def while_signed_in_as_admin
+ while_signed_in(as: Runtime::User.admin) do
+ yield
+ end
+ end
+
+ def sign_in(as: nil)
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as) }
+ end
+
+ def sign_in_as_admin
+ sign_in(as: Runtime::User.admin)
+ end
+
+ def sign_in_unless_signed_in(as: nil)
+ sign_in(as: as) unless Page::Main::Menu.perform(&:signed_in?)
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/overview/users/show.rb b/qa/qa/page/admin/overview/users/show.rb
index 11ea7bcabc8..f15ef0492fc 100644
--- a/qa/qa/page/admin/overview/users/show.rb
+++ b/qa/qa/page/admin/overview/users/show.rb
@@ -10,9 +10,19 @@ module QA
element :impersonate_user_link
end
+ view 'app/views/admin/users/show.html.haml' do
+ element :confirm_user_button
+ end
+
def click_impersonate_user
click_element(:impersonate_user_link)
end
+
+ def confirm_user
+ accept_confirm do
+ click_element :confirm_user_button
+ end
+ end
end
end
end
diff --git a/qa/qa/page/admin/settings/component/outbound_requests.rb b/qa/qa/page/admin/settings/component/outbound_requests.rb
new file mode 100644
index 00000000000..248ea5b6715
--- /dev/null
+++ b/qa/qa/page/admin/settings/component/outbound_requests.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ module Settings
+ module Component
+ class OutboundRequests < Page::Base
+ view 'app/views/admin/application_settings/_outbound.html.haml' do
+ element :allow_requests_from_services_checkbox
+ element :save_changes_button
+ end
+
+ def allow_requests_to_local_network_from_services
+ check_allow_requests_to_local_network_from_services_checkbox
+ click_save_changes_button
+ end
+
+ private
+
+ def check_allow_requests_to_local_network_from_services_checkbox
+ check_element :allow_requests_from_services_checkbox
+ end
+
+ def click_save_changes_button
+ click_element :save_changes_button
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/settings/network.rb b/qa/qa/page/admin/settings/network.rb
index fdb8fcda281..83566d3d1ca 100644
--- a/qa/qa/page/admin/settings/network.rb
+++ b/qa/qa/page/admin/settings/network.rb
@@ -9,6 +9,7 @@ module QA
view 'app/views/admin/application_settings/network.html.haml' do
element :ip_limits_section
+ element :outbound_requests_section
end
def expand_ip_limits(&block)
@@ -16,6 +17,12 @@ module QA
Component::IpLimits.perform(&block)
end
end
+
+ def expand_outbound_requests(&block)
+ expand_section(:outbound_requests_section) do
+ Component::OutboundRequests.perform(&block)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 71df90f2f42..ed4d33dc7a3 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -111,12 +111,18 @@ module QA
element.select value
end
- def has_element?(name, text: nil, wait: Capybara.default_max_wait_time)
- has_css?(element_selector_css(name), wait: wait, text: text)
+ def has_element?(name, **kwargs)
+ wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time
+ text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil
+
+ has_css?(element_selector_css(name, kwargs), text: text, wait: wait)
end
- def has_no_element?(name, text: nil, wait: Capybara.default_max_wait_time)
- has_no_css?(element_selector_css(name), wait: wait, text: text)
+ def has_no_element?(name, **kwargs)
+ wait = kwargs[:wait] ? kwargs[:wait] && kwargs.delete(:wait) : Capybara.default_max_wait_time
+ text = kwargs[:text] ? kwargs[:text] && kwargs.delete(:text) : nil
+
+ has_no_css?(element_selector_css(name, kwargs), wait: wait, text: text)
end
def has_text?(text)
@@ -135,6 +141,40 @@ module QA
has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time)
end
+ def has_loaded_all_images?
+ # I don't know of a foolproof way to wait for all images to load
+ # This loop gives time for the img tags to be rendered and for
+ # images to start loading.
+ previous_total_images = 0
+ wait(interval: 1) do
+ current_total_images = all("img").size
+ result = previous_total_images == current_total_images
+ previous_total_images = current_total_images
+ result
+ end
+
+ # Retry until all images found can be fetched via HTTP, and
+ # check that the image has a non-zero natural width (a broken
+ # img tag could have a width, but wouldn't have a natural width)
+
+ # Unfortunately, this doesn't account for SVGs. They're rendered
+ # as HTML, so there doesn't seem to be a way to check that they
+ # display properly via Selenium. However, if the SVG couldn't be
+ # rendered (e.g., because the file doesn't exist), the whole page
+ # won't display properly, so we should catch that with the test
+ # this method is called from.
+
+ # The user's avatar is an img, which could be a gravatar image,
+ # so we skip that by only checking for images hosted internally
+ retry_until(sleep_interval: 1) do
+ all("img").all? do |image|
+ next true unless URI(image['src']).host == URI(page.current_url).host
+
+ asset_exists?(image['src']) && image['naturalWidth'].to_i > 0
+ end
+ end
+ end
+
def wait_for_animated_element(name)
# It would be ideal if we could detect when the animation is complete
# but in some cases there's nothing we can easily access via capybara
@@ -165,8 +205,8 @@ module QA
scroll_to(element_selector_css(name), *args)
end
- def element_selector_css(name)
- Page::Element.new(name).selector_css
+ def element_selector_css(name, *attributes)
+ Page::Element.new(name, *attributes).selector_css
end
def click_link_with_text(text)
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
index d05c44d22b2..8fe6a4a75b3 100644
--- a/qa/qa/page/component/select2.rb
+++ b/qa/qa/page/component/select2.rb
@@ -20,12 +20,20 @@ module QA
def search_and_select(item_text)
find('.select2-input').set(item_text)
+
+ wait_for_search_to_complete
+
select_item(item_text)
end
def expand_select_list
find('span.select2-arrow').click
end
+
+ def wait_for_search_to_complete
+ has_css?('.select2-active')
+ has_no_css?('.select2-active', wait: 30)
+ end
end
end
end
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 378ac793f7b..c103bc26a36 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -18,6 +18,10 @@ module QA
'/'
end
+ def clear_project_filter
+ fill_element(:project_filter_form, "")
+ end
+
private
def filter_by_name(name)
diff --git a/qa/qa/page/dashboard/welcome.rb b/qa/qa/page/dashboard/welcome.rb
new file mode 100644
index 00000000000..b54205780d9
--- /dev/null
+++ b/qa/qa/page/dashboard/welcome.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Dashboard
+ class Welcome < Page::Base
+ view 'app/views/dashboard/projects/_zero_authorized_projects.html.haml' do
+ element :welcome_title_content
+ end
+
+ def has_welcome_title?(text)
+ has_element?(:welcome_title_content, text: text)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb
index 9e6fd2fdd4f..6bfdf98587b 100644
--- a/qa/qa/page/element.rb
+++ b/qa/qa/page/element.rb
@@ -28,7 +28,7 @@ module QA
end
def selector_css
- %Q([data-qa-selector="#{@name}"],.#{selector})
+ %Q([data-qa-selector="#{@name}"]#{additional_selectors},.#{selector})
end
def expression
@@ -42,6 +42,14 @@ module QA
def matches?(line)
!!(line =~ /["']#{name}['"]|#{expression}/)
end
+
+ private
+
+ def additional_selectors
+ @attributes.dup.delete_if { |attr| attr == :pattern || attr == :required }.map do |key, value|
+ %Q([data-qa-#{key.to_s.tr('_', '-')}="#{value}"])
+ end.join
+ 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 d8e751dd7b6..559b4c6ceea 100644
--- a/qa/qa/page/file/shared/commit_button.rb
+++ b/qa/qa/page/file/shared/commit_button.rb
@@ -13,6 +13,10 @@ module QA
def commit_changes
click_element(:commit_button)
+
+ wait(reload: false, max: 60) do
+ finished_loading?
+ end
end
end
end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index d4c4be0d6ca..e1f319da134 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -18,6 +18,10 @@ module QA
element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern
end
+ view 'app/views/shared/members/_access_request_links.html.haml' do
+ element :leave_group_link
+ end
+
def click_subgroup(name)
click_link name
end
@@ -42,6 +46,12 @@ module QA
click_element :new_in_group_button
end
+ def leave_group
+ accept_alert do
+ click_element :leave_group_link
+ end
+ end
+
private
def select_kind(kind)
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 024f56db8e2..49c48568e68 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -20,7 +20,7 @@ module QA
element :admin_area_link
element :projects_dropdown, required: true
element :groups_dropdown, required: true
- element :more_dropdown, required: true
+ element :more_dropdown
element :snippets_link
end
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
index befee25b37a..a6ccee4353b 100644
--- a/qa/qa/page/project/issue/index.rb
+++ b/qa/qa/page/project/issue/index.rb
@@ -36,6 +36,10 @@ module QA
def click_closed_issues_link
click_element :closed_issues_link
end
+
+ def has_issue?(issue)
+ has_element? :issue, issue_title: issue.to_s
+ end
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index d2732eb7dd2..6ec80b7c9cc 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -108,6 +108,10 @@ module QA
find_element(:more_assignees_link)
end
+ def noteable_note_item
+ find_element(:noteable_note_item)
+ end
+
def select_all_activities_filter
select_filter_with_text('Show all activity')
end
@@ -161,7 +165,15 @@ module QA
def select_user(username)
find("#{element_selector_css(:assignee_block)} input").set(username)
- find('.dropdown-menu-user-link', text: "@#{username}").click
+
+ dropdown_menu_user_link_selector = '.dropdown-menu-user-link'
+ at_username = "@#{username}"
+ ten_seconds = 10
+
+ wait(reload: false, max: ten_seconds, interval: 1) do
+ has_css?(dropdown_menu_user_link_selector, wait: ten_seconds, text: at_username)
+ end
+ find(dropdown_menu_user_link_selector, text: at_username).click
end
def wait_assignees_block_finish_loading
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index fae7818f871..b52f3e99a36 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -7,6 +7,10 @@ module QA::Page
element :pipeline_link, 'class="js-pipeline-url-link' # rubocop:disable QA/ElementWithPattern
end
+ view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do
+ element :pipeline_commit_status
+ end
+
def click_on_latest_pipeline
css = '.js-pipeline-url-link'
@@ -16,6 +20,14 @@ module QA::Page
link.click
end
+
+ def wait_for_latest_pipeline_success
+ wait(reload: false, max: 300) do
+ within_element_by_index(:pipeline_commit_status, 0) do
+ has_text?('passed')
+ end
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 45040cf4660..46f93fad61e 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -35,3 +35,5 @@ module QA
end
end
end
+
+QA::Page::Project::Settings::CICD.prepend_if_ee('QA::EE::Page::Project::Settings::CICD')
diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb
index 1cd39fcff58..8be442ba35d 100644
--- a/qa/qa/page/project/sub_menus/settings.rb
+++ b/qa/qa/page/project/sub_menus/settings.rb
@@ -13,6 +13,7 @@ module QA
element :settings_item
element :link_members_settings
element :general_settings_link
+ element :integrations_settings_link
end
end
end
@@ -55,6 +56,14 @@ module QA
end
end
+ def go_to_integrations_settings
+ hover_settings do
+ within_submenu do
+ click_element :integrations_settings_link
+ end
+ end
+ end
+
private
def hover_settings
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 88069df6ade..ae20ca1a98e 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -64,7 +64,12 @@ module QA
end
def visit!
- visit(web_url)
+ Runtime::Logger.debug(%Q[Visiting #{self.class.name} at "#{web_url}"]) if Runtime::Env.debug?
+
+ Support::Retrier.retry_until do
+ visit(web_url)
+ wait { current_url.include?(URI.parse(web_url).path.split('/').last || web_url) }
+ end
end
def populate(*attributes)
@@ -72,7 +77,9 @@ module QA
end
def wait(max: 60, interval: 0.1)
- QA::Support::Waiter.wait(max: max, interval: interval)
+ QA::Support::Waiter.wait(max: max, interval: interval) do
+ yield
+ end
end
private
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index e11bd5728fb..7511396251d 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -8,7 +8,10 @@ module QA
attr_accessor :path, :description
attribute :sandbox do
- Sandbox.fabricate!
+ Sandbox.fabricate_via_api! do |sandbox|
+ sandbox.user = user
+ sandbox.api_client = api_client
+ end
end
attribute :id
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index 0817a9de06f..3bcff6a10ac 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -38,6 +38,10 @@ module QA
end
end
+ def to_s
+ @title
+ end
+
def api_get_path
"/projects/#{project.id}/issues/#{id}"
end
diff --git a/qa/qa/resource/members.rb b/qa/qa/resource/members.rb
index d70a2907523..c738a91a77f 100644
--- a/qa/qa/resource/members.rb
+++ b/qa/qa/resource/members.rb
@@ -11,6 +11,10 @@ module QA
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
+ def list_members
+ JSON.parse(get(Runtime::API::Request.new(api_client, api_members_path).url).body)
+ end
+
def api_members_path
"#{api_get_path}/members"
end
diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb
index fe7eeeed37a..1a6de8de456 100644
--- a/qa/qa/resource/merge_request.rb
+++ b/qa/qa/resource/merge_request.rb
@@ -26,8 +26,6 @@ module QA
end
attribute :target do
- project.visit!
-
Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.branch_name = 'master'
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index caaa766e982..3bebe2aaeda 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -9,6 +9,7 @@ module QA
include Members
attr_writer :initialize_with_readme
+ attr_writer :auto_devops_enabled
attr_writer :visibility
attribute :id
@@ -47,6 +48,7 @@ module QA
@standalone = false
@description = 'My awesome project'
@initialize_with_readme = false
+ @auto_devops_enabled = true
@visibility = 'public'
end
@@ -101,7 +103,8 @@ module QA
name: name,
description: description,
visibility: @visibility,
- initialize_with_readme: @initialize_with_readme
+ initialize_with_readme: @initialize_with_readme,
+ auto_devops_enabled: @auto_devops_enabled
}
unless @standalone
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index 1be2429bc04..102c1ec83f5 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -36,7 +36,6 @@ module QA
runner.tags = tags
runner.image = image
runner.config = config if config
- runner.run_untagged = true
runner.register!
end
end
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
index 6ee3dcf350f..6c87fcb377a 100644
--- a/qa/qa/resource/sandbox.rb
+++ b/qa/qa/resource/sandbox.rb
@@ -7,6 +7,8 @@ module QA
# creating it if it doesn't yet exist.
#
class Sandbox < Base
+ include Members
+
attr_accessor :path
attribute :id
diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb
index dcf145c9882..bdbe5f3ef51 100644
--- a/qa/qa/resource/user.rb
+++ b/qa/qa/resource/user.rb
@@ -7,16 +7,21 @@ module QA
class User < Base
attr_reader :unique_id
attr_writer :username, :password
- attr_accessor :provider, :extern_uid
+ attr_accessor :admin, :provider, :extern_uid
attribute :id
attribute :name
attribute :email
def initialize
+ @admin = false
@unique_id = SecureRandom.hex(8)
end
+ def admin?
+ api_resource&.dig(:is_admin) || false
+ end
+
def username
@username || "qa-user-#{unique_id}"
end
@@ -71,6 +76,16 @@ module QA
super
end
+ def api_delete
+ super
+
+ QA::Runtime::Logger.debug("Deleted user '#{username}'") if Runtime::Env.debug?
+ end
+
+ def api_delete_path
+ "/users/#{id}"
+ end
+
def api_get_path
"/users/#{fetch_id(username)}"
end
@@ -81,6 +96,7 @@ module QA
def api_post_body
{
+ admin: admin,
email: email,
password: password,
username: username,
@@ -93,6 +109,7 @@ module QA
if Runtime::Env.signup_disabled?
self.fabricate_via_api! do |user|
user.username = username
+ user.password = password
end
else
self.fabricate!
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 1b0adbc9053..83fbb8f15d2 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -46,19 +46,24 @@ module QA
end
def create_personal_access_token
- Page::Main::Menu.perform(&:sign_out) if @is_new_session && Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
+ signed_in_initially = Page::Main::Menu.perform(&:signed_in?)
- unless Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
- Runtime::Browser.visit(@address, Page::Main::Login)
- Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: @user) }
- end
+ Page::Main::Menu.perform(&:sign_out) if @is_new_session && signed_in_initially
+
+ Flow::Login.sign_in_unless_signed_in(as: @user)
token = Resource::PersonalAccessToken.fabricate!.access_token
# If this is a new session, that tests that follow could fail if they
- # try to sign in without starting a new session
+ # try to sign in without starting a new session.
+ # Also, if the browser wasn't already signed in, leaving it
+ # signed in could cause tests to fail when they try to sign
+ # in again. For example, that would happen if a test has a
+ # before(:context) block that fabricates via the API, and
+ # it's the first test to run so it creates an access token
+ #
# Sign out so the tests can successfully sign in
- Page::Main::Menu.perform(&:sign_out) if @is_new_session
+ Page::Main::Menu.perform(&:sign_out) if @is_new_session || !signed_in_initially
token
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 4789b380377..7e45e5e86ea 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -19,6 +19,12 @@ module QA
self.class.configure!
end
+ def self.blank_page?
+ ['', 'about:blank', 'data:,'].include?(Capybara.current_session.driver.browser.current_url)
+ rescue
+ true
+ end
+
##
# Visit a page that belongs to a GitLab instance under given address.
#
@@ -51,13 +57,13 @@ module QA
Capybara.register_driver QA::Runtime::Env.browser do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.send(QA::Runtime::Env.browser,
- # This enables access to logs with `page.driver.manage.get_log(:browser)`
- loggingPrefs: {
- browser: "ALL",
- client: "ALL",
- driver: "ALL",
- server: "ALL"
- })
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
+ })
if QA::Runtime::Env.accept_insecure_certs?
capabilities['acceptInsecureCerts'] = true
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index b4047ef5088..bcd2a225469 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -261,6 +261,10 @@ module QA
ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES']
end
+ def gitlab_qa_loop_runner_minutes
+ ENV.fetch('GITLAB_QA_LOOP_RUNNER_MINUTES', 1).to_i
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb
index b74f343ba7b..8c19436ee12 100644
--- a/qa/qa/runtime/feature.rb
+++ b/qa/qa/runtime/feature.rb
@@ -7,6 +7,7 @@ module QA
extend Support::Api
SetFeatureError = Class.new(RuntimeError)
+ AuthorizationError = Class.new(RuntimeError)
def enable(key)
QA::Runtime::Logger.info("Enabling feature: #{key}")
@@ -18,6 +19,28 @@ module QA
set_feature(key, false)
end
+ def remove(key)
+ request = Runtime::API::Request.new(api_client, "/features/#{key}")
+ response = delete(request.url)
+ unless response.code == QA::Support::Api::HTTP_STATUS_NO_CONTENT
+ raise SetFeatureError, "Deleting feature flag #{key} failed with `#{response}`."
+ end
+ end
+
+ def enable_and_verify(key)
+ Support::Retrier.retry_on_exception(sleep_interval: 2) do
+ enable(key)
+
+ is_enabled = false
+
+ QA::Support::Waiter.wait(interval: 1) do
+ is_enabled = enabled?(key)
+ end
+
+ raise SetFeatureError, "#{key} was not enabled!" unless is_enabled
+ end
+ end
+
def enabled?(key)
feature = JSON.parse(get_features).find { |flag| flag["name"] == key }
feature && feature["state"] == "on"
@@ -26,7 +49,22 @@ module QA
private
def api_client
- @api_client ||= Runtime::API::Client.new(:gitlab)
+ @api_client ||= begin
+ if Runtime::Env.admin_personal_access_token
+ Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token)
+ else
+ user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.admin_username
+ user.password = Runtime::User.admin_password
+ end
+
+ unless user.admin?
+ raise AuthorizationError, "Administrator access is required to enable/disable feature flags. User '#{user.username}' is not an administrator."
+ end
+
+ Runtime::API::Client.new(:gitlab, user: user)
+ end
+ end
end
def set_feature(key, value)
diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb
index f91218ea0b5..ed051b18a9a 100644
--- a/qa/qa/runtime/fixtures.rb
+++ b/qa/qa/runtime/fixtures.rb
@@ -30,7 +30,7 @@ module QA
yield dir
ensure
- FileUtils.remove_entry(dir)
+ FileUtils.remove_entry(dir, true)
end
private
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index 3c26a3ad691..c50fcc25304 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -5,6 +5,10 @@ module QA
module User
extend self
+ def admin
+ Struct.new(:username, :password).new(admin_username, admin_password)
+ end
+
def default_username
'root'
end
diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb
index 52f50ec8c27..bb45c4ce4cb 100644
--- a/qa/qa/scenario/shared_attributes.rb
+++ b/qa/qa/scenario/shared_attributes.rb
@@ -8,6 +8,7 @@ module QA
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
attribute :parallel, '--parallel', 'Execute tests in parallel'
+ attribute :loop, '--loop', 'Execute test repeatedly'
end
end
end
diff --git a/qa/qa/service/docker_run/jenkins.rb b/qa/qa/service/docker_run/jenkins.rb
new file mode 100644
index 00000000000..00b63282484
--- /dev/null
+++ b/qa/qa/service/docker_run/jenkins.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module DockerRun
+ class Jenkins < Base
+ def initialize
+ @image = 'registry.gitlab.com/gitlab-org/gitlab-qa/jenkins-gitlab:version1'
+ @name = 'jenkins-server'
+ @port = '8080'
+ super()
+ end
+
+ def host_address
+ "http://#{host_name}:#{@port}"
+ end
+
+ def host_name
+ return 'localhost' unless QA::Runtime::Env.running_in_ci?
+
+ super
+ end
+
+ def register!
+ command = <<~CMD.tr("\n", ' ')
+ docker run -d --rm
+ --network #{network}
+ --hostname #{host_name}
+ --name #{@name}
+ --env JENKINS_HOME=jenkins_home
+ --publish #{@port}:8080
+ --publish 50000:50000
+ #{@image}
+ CMD
+
+ command.gsub!("--network #{network} ", '') unless QA::Runtime::Env.running_in_ci?
+
+ shell command
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/docker_run/maven.rb b/qa/qa/service/docker_run/maven.rb
new file mode 100644
index 00000000000..8bdea20963d
--- /dev/null
+++ b/qa/qa/service/docker_run/maven.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ module DockerRun
+ class Maven < Base
+ def initialize(volume_host_path)
+ @image = 'maven:3.6.2-ibmjava-8-alpine'
+ @name = "qa-maven-#{SecureRandom.hex(8)}"
+ @volume_host_path = volume_host_path
+
+ super()
+ end
+
+ def publish!
+ # When we run the tests via gitlab-qa, we use docker-in-docker
+ # which means that host of a volume mount would be the host that
+ # started the gitlab-qa QA container (e.g., the CI runner),
+ # not the gitlab-qa container itself. That means we can't
+ # mount a volume from the file system inside the gitlab-qa
+ # container.
+ #
+ # Instead, we copy the files into the container.
+ shell <<~CMD.tr("\n", ' ')
+ docker run -d --rm
+ --network #{network}
+ --hostname #{host_name}
+ --name #{@name}
+ --volume #{@volume_host_path}:/home/maven
+ #{@image} sh -c "sleep 300"
+ CMD
+ shell "docker cp #{@volume_host_path}/. #{@name}:/home/maven"
+ shell "docker exec -t #{@name} sh -c 'cd /home/maven && mvn deploy -s settings.xml'"
+
+ # Stop the container when `mvn deploy` is finished otherwise
+ # the sleeping container will hold onto the files in @volume_host_path,
+ # which causes problems when they're created in a tmp dir
+ # that we want to delete
+ shell "docker stop #{@name}"
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
index 101143399f6..ad67f02eaca 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb
@@ -8,7 +8,9 @@ module QA
Page::Main::Login.perform(&:sign_in_with_saml)
- Vendor::SAMLIdp::Page::Login.perform(&:login)
+ Vendor::SAMLIdp::Page::Login.perform do |login_page|
+ login_page.login('user1', 'user1pass')
+ end
expect(page).to have_content('Welcome to GitLab')
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb
new file mode 100644
index 00000000000..6a5bc6173e0
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'nokogiri'
+
+module QA
+ context 'Manage' do
+ describe 'Check for broken images', :requires_admin do
+ before(:context) do
+ admin = QA::Resource::User.new.tap do |user|
+ user.username = QA::Runtime::User.admin_username
+ user.password = QA::Runtime::User.admin_password
+ end
+ @api_client = Runtime::API::Client.new(:gitlab, user: admin)
+ @new_user = Resource::User.fabricate_via_api! do |user|
+ user.api_client = @api_client
+ end
+ @new_admin = Resource::User.fabricate_via_api! do |user|
+ user.admin = true
+ user.api_client = @api_client
+ end
+
+ Page::Main::Menu.perform(&:sign_out_if_signed_in)
+ end
+
+ after(:context) do
+ @new_user.remove_via_api!
+ @new_admin.remove_via_api!
+ end
+
+ shared_examples 'loads all images' do
+ it 'loads all images' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: new_user) }
+
+ Page::Dashboard::Welcome.perform do |welcome|
+ expect(welcome).to have_welcome_title("Welcome to GitLab")
+
+ # This would be better if it were a visual validation test
+ expect(welcome).to have_loaded_all_images
+ end
+ end
+ end
+
+ context 'when logged in as a new user' do
+ it_behaves_like 'loads all images' do
+ let(:new_user) { @new_user }
+ end
+ end
+
+ context 'when logged in as a new admin' do
+ it_behaves_like 'loads all images' do
+ let(:new_user) { @new_admin }
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
index 55e15b19200..69389672a6d 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
@@ -2,13 +2,12 @@
module QA
context 'Plan' do
- describe 'check xss occurence in @mentions in issues' do
+ describe 'check xss occurence in @mentions in issues', :requires_admin do
it 'user mentions a user in comment' do
QA::Runtime::Env.personal_access_token = QA::Runtime::Env.admin_personal_access_token
unless QA::Runtime::Env.personal_access_token
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_admin_credentials)
+ Flow::Login.sign_in_as_admin
end
user = Resource::User.fabricate_via_api! do |user|
@@ -20,9 +19,7 @@ module QA
Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
-
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'xss-test-for-mentions-project'
@@ -42,7 +39,7 @@ module QA
Page::Project::Issue::Show.perform do |show|
show.select_all_activities_filter
- show.comment('cc-ing you here @eve')
+ show.comment("cc-ing you here @#{user.username}")
expect do
expect(show).to have_content("cc-ing you here")
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
index 2bcc89cb338..dc7fa9f3859 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/close_issue_spec.rb
@@ -7,8 +7,7 @@ module QA
let(:commit_message) { 'Closes' }
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = issue_title
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
index ad70f6813fb..77fcc4e9b6a 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/collapse_comments_in_discussions_spec.rb
@@ -6,8 +6,7 @@ module QA
let(:my_first_reply) { 'My first reply' }
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = 'issue title'
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
index 0b1bd00ac8d..77489c0ecf5 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/comment_issue_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Plan' do
describe 'Issue comments' do
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = 'issue title'
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 04ae4963d3a..254efb741b3 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
@@ -6,23 +6,25 @@ module QA
let(:issue_title) { 'issue title' }
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
end
it 'user creates an issue' do
- Resource::Issue.fabricate_via_browser_ui! do |issue|
+ issue = Resource::Issue.fabricate_via_browser_ui! do |issue|
issue.title = issue_title
end
Page::Project::Menu.perform(&:click_issues)
- expect(page).to have_content(issue_title)
+ Page::Project::Issue::Index.perform do |index|
+ expect(index).to have_issue(issue)
+ end
end
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', 'banana_sample.gif'))
+ File.absolute_path(File.join('spec', 'fixtures', gif_file_name))
end
before do
@@ -37,15 +39,7 @@ module QA
Page::Project::Issue::Show.perform do |show|
show.comment('See attached banana for scale', attachment: file_to_attach)
- show.refresh
-
- image_url = find('a[href$="banana_sample.gif"]')[:href]
-
- found = show.wait(reload: false) do
- show.asset_exists?(image_url)
- end
-
- expect(found).to be_truthy
+ expect(show.noteable_note_item.find("img[src$='#{gif_file_name}']")).to be_visible
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 317e31feea8..a4f6b0bb1bf 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -6,8 +6,7 @@ module QA
let(:issue_title) { 'issue title' }
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
issue = Resource::Issue.fabricate_via_api! do |issue|
issue.title = issue_title
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
index c42c2cedde0..e15afd1f576 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/issue_suggestions_spec.rb
@@ -6,8 +6,7 @@ module QA
let(:issue_title) { 'Issue Lists are awesome' }
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
project = Resource::Project.fabricate_via_api! do |resource|
resource.name = 'project-for-issue-suggestions'
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index 45c14d0537c..b1a80ad75cd 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -4,8 +4,7 @@ module QA
context 'Plan', :smoke do
describe 'mention' do
before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
+ Flow::Login.sign_in
@user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
index 891cef6c420..0eaec61b2fa 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
@@ -4,9 +4,6 @@ module QA
context 'Create' do
describe 'Download merge request patch and diff' do
before(:context) do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
-
project = Resource::Project.fabricate_via_api! do |project|
project.name = 'project'
end
@@ -19,6 +16,8 @@ module QA
end
it 'views the merge request email patches' do
+ Flow::Login.sign_in
+
@merge_request.visit!
Page::MergeRequest::Show.perform(&:view_email_patches)
@@ -28,6 +27,8 @@ module QA
end
it 'views the merge request plain diff' do
+ Flow::Login.sign_in
+
@merge_request.visit!
Page::MergeRequest::Show.perform(&:view_plain_diff)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
index e42d538fdf8..d2fd1d743fb 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb
@@ -1,27 +1,17 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/34551
+ context 'Create', :quarantine do
describe 'File templates' do
include Runtime::Fixtures
- def login
- unless Page::Main::Menu.perform(&:signed_in?)
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- end
- end
-
before(:all) do
- login
-
- @project = Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate_via_api! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Files view'
project.initialize_with_readme = true
end
-
- Page::Main::Menu.perform(&:sign_out)
end
templates = [
@@ -55,7 +45,8 @@ module QA
it "user adds #{template[:file_name]} via file template #{template[:name]}" do
content = fetch_template_from_api(template[:api_path], template[:api_key])
- login
+ Flow::Login.sign_in
+
@project.visit!
Page::Project::Show.perform(&:create_new_file!)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index bb1e3ced333..3306c5f5c50 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -66,24 +66,22 @@ module QA
expect(page).to have_content(commit_message_of_second_branch)
expect(page).to have_content(commit_message_of_third_branch)
- Page::Project::Branches::Show.perform do |branches|
- expect(branches).to have_branch_with_badge(second_branch, 'merged')
- end
+ Page::Project::Branches::Show.perform do |branches_page|
+ expect(branches_page).to have_branch_with_badge(second_branch, 'merged')
- Page::Project::Branches::Show.perform do |branches_view|
- branches_view.delete_branch(third_branch)
- expect(branches_view).to have_no_branch(third_branch)
- end
+ branches_page.delete_branch(third_branch)
+
+ expect(branches_page).to have_no_branch(third_branch)
+
+ branches_page.delete_merged_branches
- Page::Project::Branches::Show.perform(&:delete_merged_branches)
+ expect(branches_page).to have_content(
+ 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
+ )
- expect(page).to have_content(
- 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
- )
+ branches_page.refresh
- page.refresh
- Page::Project::Branches::Show.perform do |branches_view|
- expect(branches_view).to have_no_branch(second_branch, reload: true)
+ expect(branches_page).to have_no_branch(second_branch, reload: true)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index f2584f55a60..0650c8395c7 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -4,14 +4,10 @@ module QA
context 'Create' do
describe 'Git clone over HTTP', :ldap_no_tls do
before(:all) do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
-
- @project = Resource::Project.fabricate! do |scenario|
+ @project = Resource::Project.fabricate_via_api! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
- @project.visit!
Git::Repository.perform do |repository|
repository.uri = @project.repository_http_location.uri
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
index b2eef38f896..aee62bacfa8 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
@@ -4,9 +4,6 @@ module QA
context 'Create' do
describe 'Commit data' do
before(:context) do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
-
# Get the user's details to confirm they're included in the email patch
@user = Resource::User.fabricate_via_api! do |user|
user.username = Runtime::User.username
@@ -34,9 +31,11 @@ module QA
end
def view_commit
+ Flow::Login.sign_in
+
@project.visit!
- Page::Project::Show.perform do |page| # rubocop:disable QA/AmbiguousPageObjectName
- page.click_commit(@commit_message)
+ Page::Project::Show.perform do |show|
+ show.click_commit(@commit_message)
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index 0a89f0c9d41..318adc3c272 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -1,25 +1,17 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/gitlab/issues/34551
+ context 'Create', :quarantine do
describe 'Web IDE file templates' do
include Runtime::Fixtures
- def login
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- end
-
before(:all) do
- login
-
- @project = Resource::Project.fabricate! do |project|
+ @project = Resource::Project.fabricate_via_api! do |project|
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
project.initialize_with_readme = true
end
-
- Page::Main::Menu.perform(&:sign_out)
end
templates = [
@@ -53,7 +45,8 @@ module QA
it "user adds #{template[:file_name]} via file template #{template[:name]}" do
content = fetch_template_from_api(template[:api_path], template[:api_key])
- login
+ Flow::Login.sign_in
+
@project.visit!
Page::Project::Show.perform(&:open_web_ide!)
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index e45ce438fc2..9dc4bcc8a03 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -6,6 +6,10 @@ module QA
context 'Release', :docker do
describe 'Git clone using a deploy key' do
before do
+ # Handle WIP Job Logs flag - https://gitlab.com/gitlab-org/gitlab/issues/31162
+ @job_log_json_flag_enabled = Runtime::Feature.enabled?('job_log_json')
+ Runtime::Feature.disable('job_log_json') if @job_log_json_flag_enabled
+
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
@@ -26,6 +30,7 @@ module QA
end
after do
+ Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
Service::DockerRun::GitlabRunner.new(@runner_name).remove!
end
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 3f99ae644c7..e9a3b0f75e6 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
@@ -55,7 +55,8 @@ module QA
end
end
- describe 'Auto DevOps support', :orchestrated, :kubernetes do
+ # https://gitlab.com/gitlab-org/gitlab/issues/35156
+ describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do
context 'when rbac is enabled' do
before(:all) do
@cluster = Service::KubernetesCluster.new.create!
diff --git a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
index 4fca2db3d0f..187c4a2a248 100644
--- a/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
+++ b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
@@ -2,7 +2,7 @@
module QA
context 'Performance bar' do
- context 'when logged in as an admin user' do
+ context 'when logged in as an admin user', :requires_admin do
before do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_admin_credentials)
diff --git a/qa/qa/specs/loop_runner.rb b/qa/qa/specs/loop_runner.rb
new file mode 100644
index 00000000000..f97f5cbbd81
--- /dev/null
+++ b/qa/qa/specs/loop_runner.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module QA
+ module Specs
+ module LoopRunner
+ module_function
+
+ def run(args)
+ start = Time.now
+ loop_duration = 60 * QA::Runtime::Env.gitlab_qa_loop_runner_minutes
+
+ while Time.now - start < loop_duration
+ RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
+ abort if status.nonzero?
+ end
+ RSpec.clear_examples
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 6aa08cf77b4..ac73cc00dbf 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -63,6 +63,8 @@ module QA
if Runtime::Scenario.attributes[:parallel]
ParallelRunner.run(args.flatten)
+ elsif Runtime::Scenario.attributes[:loop]
+ LoopRunner.run(args.flatten)
else
RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status|
abort if status.nonzero?
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index d0ff1f8bc2c..cd496efb4db 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -14,7 +14,7 @@ module QA
payload: payload,
verify_ssl: false)
rescue RestClient::ExceptionWithResponse => e
- e.response
+ return_response_or_raise(e)
end
def get(url, raw_response: false)
@@ -24,7 +24,7 @@ module QA
verify_ssl: false,
raw_response: raw_response)
rescue RestClient::ExceptionWithResponse => e
- e.response
+ return_response_or_raise(e)
end
def put(url, payload)
@@ -34,7 +34,7 @@ module QA
payload: payload,
verify_ssl: false)
rescue RestClient::ExceptionWithResponse => e
- e.response
+ return_response_or_raise(e)
end
def delete(url)
@@ -43,7 +43,7 @@ module QA
url: url,
verify_ssl: false)
rescue RestClient::ExceptionWithResponse => e
- e.response
+ return_response_or_raise(e)
end
def head(url)
@@ -52,12 +52,18 @@ module QA
url: url,
verify_ssl: false)
rescue RestClient::ExceptionWithResponse => e
- e.response
+ return_response_or_raise(e)
end
def parse_body(response)
JSON.parse(response.body, symbolize_names: true)
end
+
+ def return_response_or_raise(error)
+ raise error unless error.respond_to?(:response) && error.response
+
+ error.response
+ end
end
end
end
diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb
index f6e72bb01f9..e581edcb7c7 100644
--- a/qa/qa/vendor/github/page/login.rb
+++ b/qa/qa/vendor/github/page/login.rb
@@ -12,11 +12,15 @@ module QA
fill_in 'password', with: QA::Runtime::Env.github_password
click_on 'Sign in'
- otp = OnePassword::CLI.new.otp
+ Support::Retrier.retry_until(exit_on_failure: true, sleep_interval: 35) do
+ otp = OnePassword::CLI.new.otp
- fill_in 'otp', with: otp
+ fill_in 'otp', with: otp
- click_on 'Verify'
+ click_on 'Verify'
+
+ !has_text?('Two-factor authentication failed', wait: 1.0)
+ end
click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
end
diff --git a/qa/qa/vendor/jenkins/page/base.rb b/qa/qa/vendor/jenkins/page/base.rb
new file mode 100644
index 00000000000..8dfbe7570f8
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/base.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class Base
+ include Capybara::DSL
+ include Scenario::Actable
+
+ attr_reader :path
+
+ class << self
+ attr_accessor :host
+ end
+
+ def visit!
+ page.visit URI.join(Base.host, path).to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/configure.rb b/qa/qa/vendor/jenkins/page/configure.rb
new file mode 100644
index 00000000000..8851a2564fd
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/configure.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class Configure < Page::Base
+ def initialize
+ @path = 'configure'
+ end
+
+ def visit_and_setup_gitlab_connection(gitlab_host, token_description)
+ visit!
+ fill_in '_.name', with: 'GitLab'
+ find('.setting-name', text: "Gitlab host URL").find(:xpath, "..").find('input').set gitlab_host
+
+ dropdown_element = find('.setting-name', text: "Credentials").find(:xpath, "..").find('select')
+
+ QA::Support::Retrier.retry_until(exit_on_failure: true) do
+ dropdown_element.select "GitLab API token (#{token_description})"
+ dropdown_element.value != ''
+ end
+
+ yield if block_given?
+
+ click_save
+ end
+
+ def click_test_connection
+ click_on 'Test Connection'
+ end
+
+ def has_success?
+ has_css?('div.ok', text: "Success")
+ end
+
+ private
+
+ def click_save
+ click_on 'Save'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/configure_job.rb b/qa/qa/vendor/jenkins/page/configure_job.rb
new file mode 100644
index 00000000000..ab16e895fa9
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/configure_job.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class ConfigureJob < Page::Base
+ attr_accessor :job_name
+
+ def initialize
+ @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
+ end
+
+ private
+
+ def set_git_source_code_management_url(repository_url)
+ select_git_source_code_management
+ set_repository_url(repository_url)
+ end
+
+ def click_build_when_change_is_pushed_to_gitlab
+ find('label', text: 'Build when a change is pushed to GitLab').find(:xpath, "..").find('input').click
+ end
+
+ def set_publish_status_to_gitlab
+ click_add_post_build_action
+ select_publish_build_status_to_gitlab
+ end
+
+ def click_save
+ click_on 'Save'
+ end
+
+ def select_git_source_code_management
+ find('#radio-block-1').click
+ end
+
+ def set_repository_url(repository_url)
+ find('.setting-name', text: "Repository URL").find(:xpath, "..").find('input').set repository_url
+ end
+
+ def click_add_post_build_action
+ click_on "Add post-build action"
+ end
+
+ def select_publish_build_status_to_gitlab
+ click_link "Publish build status to GitLab"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/login.rb b/qa/qa/vendor/jenkins/page/login.rb
new file mode 100644
index 00000000000..7b3558b25e2
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/login.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class Login < Page::Base
+ def initialize
+ @path = 'login'
+ end
+
+ def visit!
+ super
+
+ QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, exit_on_failure: true) do
+ page.has_text? 'Welcome to Jenkins!'
+ end
+ end
+
+ def login
+ fill_in 'j_username', with: 'admin'
+ fill_in 'j_password', with: 'password'
+ click_on 'Sign in'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/new_credentials.rb b/qa/qa/vendor/jenkins/page/new_credentials.rb
new file mode 100644
index 00000000000..bdef1a13fd4
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/new_credentials.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class NewCredentials < Page::Base
+ def initialize
+ @path = 'credentials/store/system/domain/_/newCredentials'
+ end
+
+ def visit_and_set_gitlab_api_token(api_token, description)
+ visit!
+ wait_for_page_to_load
+ select_gitlab_api_token
+ set_api_token(api_token)
+ set_description(description)
+ click_ok
+ end
+
+ private
+
+ def select_gitlab_api_token
+ find('.setting-name', text: "Kind").find(:xpath, "..").find('select').select "GitLab API token"
+ end
+
+ def set_api_token(api_token)
+ fill_in '_.apiToken', with: api_token
+ end
+
+ def set_description(description)
+ fill_in '_.description', with: description
+ end
+
+ def click_ok
+ click_on 'OK'
+ end
+
+ def wait_for_page_to_load
+ QA::Support::Waiter.wait(interval: 1.0) do
+ page.has_css?('.setting-name', text: "Description")
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/new_job.rb b/qa/qa/vendor/jenkins/page/new_job.rb
new file mode 100644
index 00000000000..11fa4ca8a53
--- /dev/null
+++ b/qa/qa/vendor/jenkins/page/new_job.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+
+module QA
+ module Vendor
+ module Jenkins
+ module Page
+ class NewJob < Page::Base
+ def initialize
+ @path = 'newJob'
+ end
+
+ def visit_and_create_new_job_with_name(new_job_name)
+ visit!
+ set_new_job_name(new_job_name)
+ click_free_style_project
+ click_ok
+ end
+
+ private
+
+ def set_new_job_name(new_job_name)
+ fill_in 'name', with: new_job_name
+ end
+
+ def click_free_style_project
+ find('.hudson_model_FreeStyleProject').click
+ end
+
+ def click_ok
+ click_on 'OK'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb
index 1b8c926532a..9ebcabe15fc 100644
--- a/qa/qa/vendor/saml_idp/page/login.rb
+++ b/qa/qa/vendor/saml_idp/page/login.rb
@@ -7,18 +7,22 @@ module QA
module SAMLIdp
module Page
class Login < Page::Base
- def login
- fill_in 'username', with: 'user1'
- fill_in 'password', with: 'user1pass'
+ def login(username, password)
+ QA::Runtime::Logger.debug("Logging into SAMLIdp with username: #{username} and password:#{password}") if QA::Runtime::Env.debug?
+
+ fill_in 'username', with: username
+ fill_in 'password', with: password
click_on 'Login'
end
- def login_if_required
- login if login_required?
+ def login_if_required(username, password)
+ login(username, password) if login_required?
end
def login_required?
- page.has_text?('Enter your username and password')
+ login_required = page.has_text?('Enter your username and password')
+ QA::Runtime::Logger.debug("login_required: #{login_required}") if QA::Runtime::Env.debug?
+ login_required
end
end
end
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index ff5e118cefa..3f64743ffac 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -117,5 +117,23 @@ describe QA::Page::Element do
it 'properly translates to a data-qa-selector' do
expect(subject.selector_css).to include(%q([data-qa-selector="my_element"]))
end
+
+ context 'additional selectors' do
+ let(:element) { described_class.new(:my_element, index: 3, another_match: 'something') }
+ let(:required_element) { described_class.new(:my_element, required: true, index: 3) }
+
+ it 'matches on additional data-qa properties' do
+ expect(element.selector_css).to include(%q([data-qa-selector="my_element"][data-qa-index="3"]))
+ end
+
+ it 'doesnt conflict with element requirement' do
+ expect(required_element).to be_required
+ expect(required_element.selector_css).not_to include(%q(data-qa-required))
+ end
+
+ it 'translates snake_case to kebab-case' do
+ expect(element.selector_css).to include(%q(data-qa-another-match))
+ end
+ end
end
end
diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb
index 4a6b76c869f..fe84b3d024a 100644
--- a/qa/spec/resource/base_spec.rb
+++ b/qa/spec/resource/base_spec.rb
@@ -269,6 +269,8 @@ describe QA::Resource::Base do
end
it 'calls #visit with the underlying #web_url' do
+ allow(resource).to receive(:current_url).and_return(subject.current_url)
+
resource.web_url = subject.current_url
resource.visit!
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 363980acc33..42f1e6f292a 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -20,7 +20,25 @@ RSpec.configure do |config|
QA::Specs::Helpers::Quarantine.configure_rspec
config.before do |example|
- QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug?
+ QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") if QA::Runtime::Env.debug?
+ end
+
+ config.after(:context) do
+ if !QA::Runtime::Browser.blank_page? && QA::Page::Main::Menu.perform(&:signed_in?)
+ QA::Page::Main::Menu.perform(&:sign_out)
+ raise(
+ <<~ERROR
+ The test left the browser signed in.
+
+ Usually, Capybara prevents this from happening but some things can
+ interfere. For example, if it has an `after(:context)` block that logs
+ in, the browser will stay logged in and this will cause the next test
+ to fail.
+
+ Please make sure the test does not leave the browser signed in.
+ ERROR
+ )
+ end
end
config.expect_with :rspec do |expectations|
diff --git a/rubocop/cop/rspec/any_instance_of.rb b/rubocop/cop/rspec/any_instance_of.rb
new file mode 100644
index 00000000000..a939af36c13
--- /dev/null
+++ b/rubocop/cop/rspec/any_instance_of.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module RSpec
+ # This cop checks for `allow_any_instance_of` or `expect_any_instance_of`
+ # usage in specs.
+ #
+ # @example
+ #
+ # # bad
+ # allow_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+ #
+ # # bad
+ # expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+ #
+ # # good
+ # allow_next_instance_of(User) do |instance|
+ # allow(instance).to receive(:invalidate_issue_cache_counts)
+ # end
+ #
+ # # good
+ # expect_next_instance_of(User) do |instance|
+ # expect(instance).to receive(:invalidate_issue_cache_counts)
+ # end
+ #
+ class AnyInstanceOf < RuboCop::Cop::Cop
+ MESSAGE_EXPECT = 'Do not use `expect_any_instance_of` method, use `expect_next_instance_of` instead.'
+ MESSAGE_ALLOW = 'Do not use `allow_any_instance_of` method, use `allow_next_instance_of` instead.'
+
+ def_node_search :expect_any_instance_of?, <<~PATTERN
+ (send (send nil? :expect_any_instance_of ...) ...)
+ PATTERN
+ def_node_search :allow_any_instance_of?, <<~PATTERN
+ (send (send nil? :allow_any_instance_of ...) ...)
+ PATTERN
+
+ def on_send(node)
+ if expect_any_instance_of?(node)
+ add_offense(node, location: :expression, message: MESSAGE_EXPECT)
+ elsif allow_any_instance_of?(node)
+ add_offense(node, location: :expression, message: MESSAGE_ALLOW)
+ end
+ end
+
+ def autocorrect(node)
+ replacement =
+ if expect_any_instance_of?(node)
+ replacement_any_instance_of(node, 'expect')
+ elsif allow_any_instance_of?(node)
+ replacement_any_instance_of(node, 'allow')
+ end
+
+ lambda do |corrector|
+ corrector.replace(node.loc.expression, replacement)
+ end
+ end
+
+ private
+
+ def replacement_any_instance_of(node, rspec_prefix)
+ method_call =
+ node.receiver.source.sub(
+ "#{rspec_prefix}_any_instance_of",
+ "#{rspec_prefix}_next_instance_of")
+
+ block = <<~RUBY.chomp
+ do |instance|
+ #{rspec_prefix}(instance).#{node.method_name} #{node.children.last.source}
+ end
+ RUBY
+
+ "#{method_call} #{block}"
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 70679aa1e78..159892ae0c1 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -32,6 +32,7 @@ require_relative 'cop/migration/timestamps'
require_relative 'cop/migration/update_column_in_batches'
require_relative 'cop/migration/update_large_table'
require_relative 'cop/project_path_helper'
+require_relative 'cop/rspec/any_instance_of'
require_relative 'cop/rspec/be_success_matcher'
require_relative 'cop/rspec/env_assignment'
require_relative 'cop/rspec/factories_in_migration_specs'
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index d097c2aee91..7f5d6130fe4 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
cd "$(dirname "$0")/.."
+echo "=> Linting documents at path $(pwd) as $(whoami)..."
# Use long options (e.g. --header instead of -H) for curl examples in documentation.
echo '=> Checking for cURL short options...'
@@ -25,7 +26,7 @@ fi
# Make sure no files in doc/ are executable
EXEC_PERM_COUNT=$(find doc/ -type f -perm 755 | wc -l)
-echo '=> Checking for executable permissions...'
+echo "=> Checking $(pwd)/doc for executable permissions..."
if [ "${EXEC_PERM_COUNT}" -ne 0 ]
then
echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2
diff --git a/scripts/notify-slack b/scripts/notify-slack
deleted file mode 100755
index 5907fd8b986..00000000000
--- a/scripts/notify-slack
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-# Sends Slack notification MSG to CI_SLACK_WEBHOOK_URL (which needs to be set).
-# ICON_EMOJI needs to be set to an icon emoji name (without the `:` around it).
-
-CHANNEL=$1
-MSG=$2
-ICON_EMOJI=$3
-
-if [ -z "$CHANNEL" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ] || [ -z "$MSG" ] || [ -z "$ICON_EMOJI" ]; then
- echo "Missing argument(s) - Use: $0 channel message icon_emoji"
- echo "and set CI_SLACK_WEBHOOK_URL environment variable."
-else
- curl -X POST --data-urlencode 'payload={"channel": "#'"$CHANNEL"'", "username": "GitLab QA Bot", "text": "'"$MSG"'", "icon_emoji": "'":$ICON_EMOJI:"'"}' "$CI_SLACK_WEBHOOK_URL"
-fi
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index 9edc1a2b857..c7ab8829088 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -58,10 +58,16 @@ class AutomatedCleanup
checked_environments = []
delete_threshold = threshold_time(days: days_for_delete)
stop_threshold = threshold_time(days: days_for_stop)
+ deployments_look_back_threshold = threshold_time(days: days_for_delete * 5)
+
+ releases_to_delete = []
+
+ gitlab.deployments(project_path, per_page: DEPLOYMENTS_PER_PAGE, sort: 'desc').auto_paginate do |deployment|
+ break if Time.parse(deployment.created_at) < deployments_look_back_threshold
- gitlab.deployments(project_path, per_page: DEPLOYMENTS_PER_PAGE).auto_paginate do |deployment|
environment = deployment.environment
+ next unless environment
next unless environment.name.start_with?('review/')
next if checked_environments.include?(environment.slug)
@@ -71,7 +77,7 @@ class AutomatedCleanup
if deployed_at < delete_threshold
delete_environment(environment, deployment)
release = Quality::HelmClient::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace)
- delete_helm_release(release)
+ releases_to_delete << release
elsif deployed_at < stop_threshold
stop_environment(environment, deployment)
else
@@ -80,6 +86,8 @@ class AutomatedCleanup
checked_environments << environment.slug
end
+
+ delete_helm_releases(releases_to_delete)
end
def perform_helm_releases_cleanup!(days:)
@@ -87,13 +95,20 @@ class AutomatedCleanup
threshold_day = threshold_time(days: days)
+ releases_to_delete = []
+
helm_releases.each do |release|
+ # Prevents deleting `dns-gitlab-review-app` releases or other unrelated releases
+ next unless release.name.start_with?('review-')
+
if release.status == 'FAILED' || release.last_update < threshold_day
- delete_helm_release(release)
+ releases_to_delete << release
else
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'leaving')
end
end
+
+ delete_helm_releases(releases_to_delete)
end
private
@@ -114,10 +129,17 @@ class AutomatedCleanup
helm.releases(args: args)
end
- def delete_helm_release(release)
- print_release_state(subject: 'Release', release_name: release.name, release_status: release.status, release_date: release.last_update, action: 'cleaning')
- helm.delete(release_name: release.name)
- kubernetes.cleanup(release_name: release.name)
+ def delete_helm_releases(releases)
+ return if releases.empty?
+
+ releases.each do |release|
+ print_release_state(subject: 'Release', release_name: release.name, release_status: release.status, release_date: release.last_update, action: 'cleaning')
+ end
+
+ releases_names = releases.map(&:name)
+ helm.delete(release_name: releases_names)
+ kubernetes.cleanup(release_name: releases_names, wait: false)
+
rescue Quality::HelmClient::CommandFailedError => ex
raise ex unless ignore_exception?(ex.message, IGNORED_HELM_ERRORS)
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 573a5ccde11..7aaa7544c19 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -14,11 +14,11 @@ gitlab:
gitaly:
resources:
requests:
- cpu: 300m
- memory: 200M
+ cpu: 1200m
+ memory: 240M
limits:
- cpu: 600m
- memory: 420M
+ cpu: 1800m
+ memory: 360M
persistence:
size: 10G
gitlab-exporter:
@@ -35,22 +35,25 @@ gitlab:
gitlab-shell:
resources:
requests:
- cpu: 125m
- memory: 20M
+ cpu: 500m
+ memory: 25M
limits:
- cpu: 250m
- memory: 40M
+ cpu: 750m
+ memory: 37.5M
maxReplicas: 3
hpa:
targetAverageValue: 130m
+ deployment:
+ livenessProbe:
+ timeoutSeconds: 5
sidekiq:
resources:
requests:
- cpu: 300m
- memory: 800M
+ cpu: 650m
+ memory: 880M
limits:
- cpu: 400m
- memory: 1.6G
+ cpu: 975m
+ memory: 1320M
task-runner:
resources:
requests:
@@ -62,11 +65,11 @@ gitlab:
unicorn:
resources:
requests:
- cpu: 400m
- memory: 1.4G
+ cpu: 500m
+ memory: 1540M
limits:
- cpu: 800m
- memory: 2.8G
+ cpu: 750m
+ memory: 2310M
deployment:
readinessProbe:
initialDelaySeconds: 5 # Default is 0
@@ -75,11 +78,11 @@ gitlab:
workhorse:
resources:
requests:
- cpu: 175m
- memory: 100M
+ cpu: 250m
+ memory: 50M
limits:
- cpu: 350m
- memory: 200M
+ cpu: 375m
+ memory: 75M
readinessProbe:
initialDelaySeconds: 5 # Default is 0
periodSeconds: 15 # Default is 10
@@ -87,11 +90,11 @@ gitlab:
gitlab-runner:
resources:
requests:
- cpu: 355m
- memory: 300M
+ cpu: 450m
+ memory: 100M
limits:
- cpu: 710m
- memory: 600M
+ cpu: 675m
+ memory: 150M
minio:
resources:
requests:
@@ -108,10 +111,10 @@ nginx-ingress:
resources:
requests:
cpu: 100m
- memory: 250M
+ memory: 450M
limits:
cpu: 200m
- memory: 500M
+ memory: 675M
minAvailable: 1
service:
enableHttp: false
@@ -133,10 +136,11 @@ postgresql:
enabled: false
resources:
requests:
- cpu: 250m
- memory: 256M
+ cpu: 300m
+ memory: 250M
limits:
- cpu: 500m
+ cpu: 450m
+ memory: 375M
prometheus:
install: false
redis:
@@ -157,8 +161,8 @@ registry:
minReplicas: 1
resources:
requests:
- cpu: 50m
- memory: 32M
- limits:
cpu: 100m
- memory: 64M
+ memory: 30M
+ limits:
+ cpu: 200m
+ memory: 45M
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 51768d07860..ed872783856 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -1,5 +1,4 @@
[[ "$TRACE" ]] && set -x
-export TILLER_NAMESPACE="$KUBE_NAMESPACE"
function deploy_exists() {
local namespace="${1}"
@@ -14,16 +13,18 @@ function deploy_exists() {
}
function previous_deploy_failed() {
- local deploy="${1}"
+ local namespace="${1}"
+ local deploy="${2}"
+
echoinfo "Checking for previous deployment of ${deploy}" true
- helm status "${deploy}" >/dev/null 2>&1
+ helm status --tiller-namespace "${namespace}" "${deploy}" >/dev/null 2>&1
local status=$?
# if `status` is `0`, deployment exists, has a status
if [ $status -eq 0 ]; then
echoinfo "Previous deployment found, checking status..."
- deployment_status=$(helm status "${deploy}" | grep ^STATUS | cut -d' ' -f2)
+ deployment_status=$(helm status --tiller-namespace "${namespace}" "${deploy}" | grep ^STATUS | cut -d' ' -f2)
echoinfo "Previous deployment state: ${deployment_status}"
if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then
status=0;
@@ -37,16 +38,17 @@ function previous_deploy_failed() {
}
function delete_release() {
- if [ -z "$CI_ENVIRONMENT_SLUG" ]; then
+ local namespace="${KUBE_NAMESPACE}"
+ local deploy="${CI_ENVIRONMENT_SLUG}"
+
+ if [ -z "$deploy" ]; then
echoerr "No release given, aborting the delete!"
return
fi
- local name="$CI_ENVIRONMENT_SLUG"
-
- echoinfo "Deleting release '$name'..." true
+ echoinfo "Deleting release '$deploy'..." true
- helm delete --purge "$name"
+ helm delete --purge --tiller-namespace "${namespace}" "${deploy}"
}
function delete_failed_release() {
@@ -59,7 +61,7 @@ function delete_failed_release() {
echoinfo "No Review App with ${CI_ENVIRONMENT_SLUG} is currently deployed."
else
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
- if previous_deploy_failed "$CI_ENVIRONMENT_SLUG" ; then
+ if previous_deploy_failed "${KUBE_NAMESPACE}" "$CI_ENVIRONMENT_SLUG" ; then
echoinfo "Review App deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
delete_release
else
@@ -117,6 +119,7 @@ function ensure_namespace() {
}
function install_tiller() {
+ local TILLER_NAMESPACE="$KUBE_NAMESPACE"
echoinfo "Checking deployment/tiller-deploy status in the ${TILLER_NAMESPACE} namespace..." true
echoinfo "Initiating the Helm client..."
@@ -131,11 +134,12 @@ function install_tiller() {
--override "spec.template.spec.tolerations[0].key"="dedicated" \
--override "spec.template.spec.tolerations[0].operator"="Equal" \
--override "spec.template.spec.tolerations[0].value"="helm" \
- --override "spec.template.spec.tolerations[0].effect"="NoSchedule"
+ --override "spec.template.spec.tolerations[0].effect"="NoSchedule" \
+ --tiller-namespace "${TILLER_NAMESPACE}"
kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy"
- if ! helm version --debug; then
+ if ! helm version --debug --tiller-namespace "${TILLER_NAMESPACE}"; then
echo "Failed to init Tiller."
return 1
fi
@@ -147,7 +151,7 @@ function install_external_dns() {
domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
echoinfo "Installing external DNS for domain ${domain}..." true
- if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${release_name}" ; then
+ if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${KUBE_NAMESPACE}" "${release_name}" ; then
echoinfo "Installing external-dns Helm chart"
helm repo update
# Default requested: CPU => 0, memory => 0
@@ -179,6 +183,17 @@ function create_application_secret() {
"${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password" \
--from-literal="password=${REVIEW_APPS_ROOT_PASSWORD}" \
--dry-run -o json | kubectl apply -f -
+
+ if [ -z "${REVIEW_APPS_EE_LICENSE}" ]; then echo "License not found" && return; fi
+
+ echoinfo "Creating the ${CI_ENVIRONMENT_SLUG}-gitlab-license secret in the ${KUBE_NAMESPACE} namespace..." true
+
+ echo "${REVIEW_APPS_EE_LICENSE}" > /tmp/license.gitlab
+
+ kubectl create secret generic -n "$KUBE_NAMESPACE" \
+ "${CI_ENVIRONMENT_SLUG}-gitlab-license" \
+ --from-file=license=/tmp/license.gitlab \
+ --dry-run -o json | kubectl apply -f -
}
function download_chart() {
@@ -195,9 +210,19 @@ function download_chart() {
helm dependency build .
}
+function base_config_changed() {
+ if [ -z "${CI_MERGE_REQUEST_IID}" ]; then return; fi
+
+ curl "${CI_API_V4_URL}/projects/${CI_MERGE_REQUEST_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/changes" | jq '.changes | any(.old_path == "scripts/review_apps/base-config.yaml")'
+}
+
function deploy() {
local name="$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"
+
echoinfo "Deploying ${name}..." true
IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
@@ -239,12 +264,20 @@ HELM_CMD=$(cat << EOF
EOF
)
+if [ -n "${REVIEW_APPS_EE_LICENSE}" ]; then
+HELM_CMD=$(cat << EOF
+ ${HELM_CMD} \
+ --set global.gitlab.license.secret="${CI_ENVIRONMENT_SLUG}-gitlab-license"
+EOF
+)
+fi
+
HELM_CMD=$(cat << EOF
- $HELM_CMD \
+ ${HELM_CMD} \
--namespace="$KUBE_NAMESPACE" \
- --version="$CI_PIPELINE_ID-$CI_JOB_ID" \
- -f "../scripts/review_apps/base-config.yaml" \
- "$name" .
+ --version="${CI_PIPELINE_ID}-${CI_JOB_ID}" \
+ -f "${base_config_file}" \
+ "${name}" .
EOF
)
@@ -263,34 +296,3 @@ function display_deployment_debug() {
echoinfo "Unsuccessful Jobs for release ${CI_ENVIRONMENT_SLUG}"
kubectl get jobs -n "$KUBE_NAMESPACE" -lrelease=${CI_ENVIRONMENT_SLUG} --field-selector=status.successful!=1
}
-
-function add_license() {
- if [ -z "${REVIEW_APPS_EE_LICENSE}" ]; then echo "License not found" && return; fi
-
- task_runner_pod=$(get_pod "task-runner");
- if [ -z "${task_runner_pod}" ]; then echo "Task runner pod not found" && return; fi
-
- echoinfo "Installing license..." true
-
- echo "${REVIEW_APPS_EE_LICENSE}" > /tmp/license.gitlab
- kubectl -n "$KUBE_NAMESPACE" cp /tmp/license.gitlab "${task_runner_pod}":/tmp/license.gitlab
- rm /tmp/license.gitlab
-
- kubectl -n "$KUBE_NAMESPACE" exec -it "${task_runner_pod}" -- /srv/gitlab/bin/rails runner -e production \
- '
- content = File.read("/tmp/license.gitlab").strip;
- FileUtils.rm_f("/tmp/license.gitlab");
-
- unless License.where(data:content).empty?
- puts "License already exists";
- Kernel.exit 0;
- end
-
- unless License.new(data: content).save
- puts "Could not add license";
- Kernel.exit 0;
- end
-
- puts "License added";
- '
-}
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 602cd847a71..b7f7100c365 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -26,17 +26,35 @@ def emit_errors(static_analysis)
end
end
-tasks = [
- %w[bin/rake lint:all],
- %w[bundle exec license_finder],
- %w[yarn run eslint],
- %w[yarn run stylelint],
- %w[yarn run prettier-all],
- %w[bundle exec rubocop --parallel],
- %w[scripts/lint-conflicts.sh],
- %w[scripts/lint-rugged]
-]
+def jobs_to_run(node_index, node_total)
+ all_tasks = [
+ %w[bin/rake lint:all],
+ %w[bundle exec license_finder],
+ %w[yarn run eslint],
+ %w[yarn run stylelint],
+ %w[yarn run prettier-all],
+ %w[bundle exec rubocop --parallel],
+ %w[scripts/lint-conflicts.sh],
+ %w[scripts/lint-rugged]
+ ]
+ case node_total
+ when 1
+ all_tasks
+ when 2
+ rake_lint_all, *rest_jobs = all_tasks
+ case node_index
+ when 1
+ [rake_lint_all]
+ else
+ rest_jobs
+ end
+ else
+ raise "Parallelization > 2 (currently set to #{node_total}) isn't supported yet!"
+ end
+end
+
+tasks = jobs_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i)
static_analysis = Gitlab::Popen::Runner.new
static_analysis.run(tasks) do |cmd, &run|
diff --git a/scripts/sync-stable-branch.sh b/scripts/sync-stable-branch.sh
new file mode 100644
index 00000000000..fc62453d743
--- /dev/null
+++ b/scripts/sync-stable-branch.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+
+# This script triggers a merge train job to sync an EE stable branch to its
+# corresponding CE stable branch.
+
+set -e
+
+if [[ "$MERGE_TRAIN_TRIGGER_TOKEN" == '' ]]
+then
+ echo 'The variable MERGE_TRAIN_TRIGGER_TOKEN must be set to a non-empy value'
+ exit 1
+fi
+
+if [[ "$MERGE_TRAIN_TRIGGER_URL" == '' ]]
+then
+ echo 'The variable MERGE_TRAIN_TRIGGER_URL must be set to a non-empy value'
+ exit 1
+fi
+
+if [[ "$CI_COMMIT_REF_NAME" == '' ]]
+then
+ echo 'The variable CI_COMMIT_REF_NAME must be set to a non-empy value'
+ exit 1
+fi
+
+curl -X POST \
+ -F token="$MERGE_TRAIN_TRIGGER_TOKEN" \
+ -F ref=master \
+ -F "variables[MERGE_FOSS]=1" \
+ -F "variables[SOURCE_BRANCH]=$CI_COMMIT_REF_NAME" \
+ -F "variables[TARGET_BRANCH]=${CI_COMMIT_REF_NAME/-ee/}" \
+ "$MERGE_TRAIN_TRIGGER_URL"
diff --git a/scripts/trigger-build b/scripts/trigger-build
index badbb562021..74c1df258c0 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -17,7 +17,7 @@ module Trigger
end
class Base
- def invoke!(post_comment: false)
+ def invoke!(post_comment: false, downstream_job_name: nil)
pipeline = Gitlab.run_trigger(
downstream_project_path,
trigger_token,
@@ -28,7 +28,18 @@ module Trigger
puts "Waiting for downstream pipeline status"
Trigger::CommitComment.post!(pipeline, access_token) if post_comment
- Trigger::Pipeline.new(downstream_project_path, pipeline.id, access_token)
+ downstream_job =
+ if downstream_job_name
+ Gitlab.pipeline_jobs(downstream_project_path, pipeline.id).auto_paginate.find do |potential_job|
+ potential_job.name == downstream_job_name
+ end
+ end
+
+ if downstream_job
+ Trigger::Job.new(downstream_project_path, downstream_job.id, access_token)
+ else
+ Trigger::Pipeline.new(downstream_project_path, pipeline.id, access_token)
+ end
end
private
@@ -187,6 +198,14 @@ module Trigger
attr_reader :project, :id, :api_token
+ def self.unscoped_class_name
+ name.split('::').last
+ end
+
+ def self.gitlab_api_method_name
+ unscoped_class_name.downcase
+ end
+
def initialize(project, id, api_token)
@project = project
@id = id
@@ -199,17 +218,17 @@ module Trigger
def wait!
loop do
- raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout?
+ raise "#{self.class.unscoped_class_name} timed out after waiting for #{duration} minutes!" if timeout?
case status
when :created, :pending, :running
print "."
sleep INTERVAL
when :success
- puts "Pipeline succeeded in #{duration} minutes!"
+ puts "#{self.class.unscoped_class_name} succeeded in #{duration} minutes!"
break
else
- raise "Pipeline did not succeed!"
+ raise "#{self.class.unscoped_class_name} did not succeed!"
end
STDOUT.flush
@@ -225,7 +244,7 @@ module Trigger
end
def status
- Gitlab.pipeline(project, id).status.to_sym
+ Gitlab.public_send(self.class.gitlab_api_method_name, project, id).status.to_sym # rubocop:disable GitlabSecurity/PublicSend
rescue Gitlab::Error::Error => error
puts "Ignoring the following error: #{error}"
# Ignore GitLab API hiccups. If GitLab is really down, we'll hit the job
@@ -233,11 +252,13 @@ module Trigger
:running
end
end
+
+ Job = Class.new(Pipeline)
end
case ARGV[0]
when 'omnibus'
- Trigger::Omnibus.new.invoke!(post_comment: true).wait!
+ Trigger::Omnibus.new.invoke!(post_comment: true, downstream_job_name: 'Trigger:qa-test').wait!
when 'cng'
Trigger::CNG.new.invoke!.wait!
else
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index e360ab68cf2..e573ef4be49 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -49,7 +49,9 @@ describe AbuseReportsController do
end
it 'calls notify' do
- expect_any_instance_of(AbuseReport).to receive(:notify)
+ expect_next_instance_of(AbuseReport) do |instance|
+ expect(instance).to receive(:notify)
+ end
post :create, params: { abuse_report: attrs }
end
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index 233710b9fc3..ebae931764d 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -73,7 +73,7 @@ describe Admin::ClustersController do
end
describe 'GET #new' do
- def get_new(provider: 'gke')
+ def get_new(provider: 'gcp')
get :new, params: { provider: provider }
end
@@ -227,16 +227,17 @@ describe Admin::ClustersController do
describe 'security' do
before do
- allow_any_instance_of(described_class)
- .to receive(:token_in_session).and_return('token')
- allow_any_instance_of(described_class)
- .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
- self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
- status: 'RUNNING'
- )
+ allow_next_instance_of(described_class) do |instance|
+ allow(instance).to receive(:token_in_session).and_return('token')
+ allow(instance).to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ end
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ allow(instance).to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
end
allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
@@ -248,6 +249,69 @@ describe Admin::ClustersController do
end
end
+ describe 'POST #create_aws' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_aws_attributes: {
+ key_name: 'key',
+ role_arn: 'arn:role',
+ region: 'region',
+ vpc_id: 'vpc',
+ instance_type: 'instance type',
+ num_nodes: 3,
+ security_group_id: 'security group',
+ subnet_ids: %w(subnet1 subnet2)
+ }
+ }
+ }
+ end
+
+ def post_create_aws
+ post :create_aws, params: params
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_aws }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Aws.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response.status).to eq(201)
+ expect(response.location).to eq(admin_cluster_path(cluster))
+ expect(cluster).to be_aws
+ expect(cluster).to be_kubernetes
+ end
+
+ context 'params are invalid' do
+ let(:params) do
+ {
+ cluster: { name: '' }
+ }
+ end
+
+ it 'does not create a cluster' do
+ expect { post_create_aws }.not_to change { Clusters::Cluster.count }
+
+ expect(response.status).to eq(422)
+ expect(response.content_type).to eq('application/json')
+ expect(response.body).to include('is invalid')
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(WaitForClusterCreationWorker).to receive(:perform_in)
+ end
+
+ it { expect { post_create_aws }.to be_allowed_for(:admin) }
+ it { expect { post_create_aws }.to be_denied_for(:user) }
+ it { expect { post_create_aws }.to be_denied_for(:external) }
+ end
+ end
+
describe 'POST #create_user' do
let(:params) do
{
@@ -318,6 +382,72 @@ describe Admin::ClustersController do
end
end
+ describe 'POST authorize AWS role for EKS cluster' do
+ let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
+ let(:role_external_id) { '12345' }
+
+ let(:params) do
+ {
+ cluster: {
+ role_arn: role_arn,
+ role_external_id: role_external_id
+ }
+ }
+ end
+
+ def go
+ post :authorize_aws_role, params: params
+ end
+
+ it 'creates an Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 201
+
+ role = Aws::Role.last
+ expect(role.user).to eq admin
+ expect(role.role_arn).to eq role_arn
+ expect(role.role_external_id).to eq role_external_id
+ end
+
+ context 'role cannot be created' do
+ let(:role_arn) { 'invalid-role' }
+
+ it 'does not create a record' do
+ expect { go }.not_to change { Aws::Role.count }
+
+ expect(response.status).to eq 422
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE revoke AWS role for EKS cluster' do
+ let!(:role) { create(:aws_role, user: admin) }
+
+ def go
+ delete :revoke_aws_role
+ end
+
+ it 'deletes the Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 204
+ expect(admin.reload_aws_role).to be_nil
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
describe 'GET #cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, :instance) }
@@ -338,7 +468,9 @@ describe Admin::ClustersController do
end
it 'invokes schedule_status_update on each application' do
- expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+ expect_next_instance_of(Clusters::Applications::Ingress) do |instance|
+ expect(instance).to receive(:schedule_status_update)
+ end
get_cluster_status
end
diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb
index 68695afdb61..256aafe09f8 100644
--- a/spec/controllers/admin/identities_controller_spec.rb
+++ b/spec/controllers/admin/identities_controller_spec.rb
@@ -13,7 +13,9 @@ describe Admin::IdentitiesController do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
- expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+ expect_next_instance_of(RepairLdapBlockedUserService) do |instance|
+ expect(instance).to receive(:execute)
+ end
put :update, params: { user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' } }
end
@@ -23,7 +25,9 @@ describe Admin::IdentitiesController do
let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
it 'repairs ldap blocks' do
- expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+ expect_next_instance_of(RepairLdapBlockedUserService) do |instance|
+ expect(instance).to receive(:execute)
+ end
delete :destroy, params: { user_id: user.username, id: user.ldap_identity.id }
end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 3bc49023357..baf4216dcde 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -27,7 +27,7 @@ describe Admin::SpamLogsController do
expect(response).to have_gitlab_http_status(200)
end
- it 'removes user and his spam logs when removing the user' do
+ it 'removes user and his spam logs when removing the user', :sidekiq_might_not_need_inline do
delete :destroy, params: { id: first_spam.id, remove_user: true }
expect(flash[:notice]).to eq "User #{user.username} was successfully removed."
@@ -39,7 +39,9 @@ describe Admin::SpamLogsController do
describe '#mark_as_ham' do
before do
- allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive(:submit_ham).and_return(true)
+ end
end
it 'submits the log as ham' do
post :mark_as_ham, params: { id: first_spam.id }
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index afe21c8b34a..50ba7418d2c 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -35,7 +35,7 @@ describe Admin::UsersController do
end
end
- describe 'DELETE #user with projects' do
+ describe 'DELETE #user with projects', :sidekiq_might_not_need_inline do
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 993e4020a75..4a10e7b5325 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -96,30 +96,14 @@ describe ApplicationController do
request.path = '/-/peek'
end
- # TODO:
- # remove line below once `privacy_policy_update_callout`
- # feature flag is removed and `gon` reverts back to
- # to not setting any variables.
- if Gitlab.ee?
- it_behaves_like 'setting gon variables'
- else
- it_behaves_like 'not setting gon variables'
- end
+ it_behaves_like 'not setting gon variables'
end
end
context 'with json format' do
let(:format) { :json }
- # TODO:
- # remove line below once `privacy_policy_update_callout`
- # feature flag is removed and `gon` reverts back to
- # to not setting any variables.
- if Gitlab.ee?
- it_behaves_like 'setting gon variables'
- else
- it_behaves_like 'not setting gon variables'
- end
+ it_behaves_like 'not setting gon variables'
end
end
@@ -655,7 +639,7 @@ describe ApplicationController do
context 'given a 422 error page' do
controller do
def index
- render 'errors/omniauth_error', layout: 'errors', status: 422
+ render 'errors/omniauth_error', layout: 'errors', status: :unprocessable_entity
end
end
@@ -669,7 +653,7 @@ describe ApplicationController do
context 'given a 500 error page' do
controller do
def index
- render 'errors/omniauth_error', layout: 'errors', status: 500
+ render 'errors/omniauth_error', layout: 'errors', status: :internal_server_error
end
end
@@ -683,7 +667,7 @@ describe ApplicationController do
context 'given a 200 success page' do
controller do
def index
- render 'errors/omniauth_error', layout: 'errors', status: 200
+ render 'errors/omniauth_error', layout: 'errors', status: :ok
end
end
@@ -843,7 +827,7 @@ describe ApplicationController do
end
end
- describe '#require_role' do
+ describe '#required_signup_info' do
controller(described_class) do
def index; end
end
@@ -852,7 +836,7 @@ describe ApplicationController do
let(:experiment_enabled) { true }
before do
- stub_experiment(signup_flow: experiment_enabled)
+ stub_experiment_for_user(signup_flow: experiment_enabled)
end
context 'experiment enabled and user with required role' do
@@ -865,7 +849,7 @@ describe ApplicationController do
it { is_expected.to redirect_to users_sign_up_welcome_path }
end
- context 'experiment enabled and user without a role' do
+ context 'experiment enabled and user without a required role' do
before do
sign_in(user)
get :index
@@ -874,7 +858,7 @@ describe ApplicationController do
it { is_expected.not_to redirect_to users_sign_up_welcome_path }
end
- context 'experiment disabled and user with required role' do
+ context 'experiment disabled' do
let(:experiment_enabled) { false }
before do
diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb
index 0c598a360af..25429cdd149 100644
--- a/spec/controllers/concerns/confirm_email_warning_spec.rb
+++ b/spec/controllers/concerns/confirm_email_warning_spec.rb
@@ -19,7 +19,7 @@ describe ConfirmEmailWarning do
RSpec::Matchers.define :set_confirm_warning_for do |email|
match do |response|
- expect(response).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address.")
+ expect(response).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
index a71e34fd1ca..ff2b6fbb8ec 100644
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -3,9 +3,11 @@
require 'spec_helper'
describe MetricsDashboard do
+ include MetricsDashboardHelpers
+
describe 'GET #metrics_dashboard' do
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:project) { project_with_dashboard('.gitlab/dashboards/test.yml') }
let_it_be(:environment) { create(:environment, project: project) }
before do
@@ -31,11 +33,13 @@ describe MetricsDashboard do
end
context 'when params are provided' do
+ let(:params) { { environment: environment } }
+
before do
allow(controller).to receive(:project).and_return(project)
allow(controller)
.to receive(:metrics_dashboard_params)
- .and_return(environment: environment)
+ .and_return(params)
end
it 'returns the specified dashboard' do
@@ -43,6 +47,15 @@ describe MetricsDashboard do
expect(json_response).not_to have_key('all_dashboards')
end
+ context 'when the params are in an alternate format' do
+ let(:params) { ActionController::Parameters.new({ environment: environment }).permit! }
+
+ it 'returns the specified dashboard' do
+ expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
+ expect(json_response).not_to have_key('all_dashboards')
+ end
+ end
+
context 'when parameters are provided and the list of all dashboards is required' do
before do
allow(controller).to receive(:include_all_dashboards?).and_return(true)
@@ -52,6 +65,36 @@ describe MetricsDashboard do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).to have_key('all_dashboards')
end
+
+ context 'in all_dashboard list' do
+ let(:system_dashboard) { json_response['all_dashboards'].find { |dashboard| dashboard["system_dashboard"] == true } }
+ let(:project_dashboard) { json_response['all_dashboards'].find { |dashboard| dashboard["system_dashboard"] == false } }
+
+ it 'includes project_blob_path only for project dashboards' do
+ expect(system_dashboard['project_blob_path']).to be_nil
+ expect(project_dashboard['project_blob_path']).to eq("/#{project.namespace.path}/#{project.name}/blob/master/.gitlab/dashboards/test.yml")
+ end
+
+ describe 'project permissions' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:can_collaborate, :system_can_edit, :project_can_edit) do
+ false | false | false
+ true | false | true
+ end
+
+ with_them do
+ before do
+ allow(controller).to receive(:can_collaborate_with_project?).and_return(can_collaborate)
+ end
+
+ it "sets can_edit appropriately" do
+ expect(system_dashboard["can_edit"]).to eq(system_can_edit)
+ expect(project_dashboard["can_edit"]).to eq(project_can_edit)
+ end
+ end
+ end
+ end
end
end
end
diff --git a/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb b/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb
new file mode 100644
index 00000000000..903100ba93f
--- /dev/null
+++ b/spec/controllers/concerns/redirects_for_missing_path_on_tree_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RedirectsForMissingPathOnTree, type: :controller do
+ controller(ActionController::Base) do
+ include Gitlab::Routing.url_helpers
+ include RedirectsForMissingPathOnTree
+
+ def fake
+ redirect_to_tree_root_for_missing_path(Project.find(params[:project_id]), params[:ref], params[:file_path])
+ end
+ end
+
+ let(:project) { create(:project) }
+
+ before do
+ routes.draw { get 'fake' => 'anonymous#fake' }
+ end
+
+ describe '#redirect_to_root_path' do
+ it 'redirects to the tree path with a notice' do
+ long_file_path = ('a/b/' * 30) + 'foo.txt'
+ truncated_file_path = '...b/' + ('a/b/' * 12) + 'foo.txt'
+ expected_message = "\"#{truncated_file_path}\" did not exist on \"theref\""
+
+ get :fake, params: { project_id: project.id, ref: 'theref', file_path: long_file_path }
+
+ expect(response).to redirect_to project_tree_path(project, 'theref')
+ expect(response.flash[:notice]).to eq(expected_message)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/renders_commits_spec.rb b/spec/controllers/concerns/renders_commits_spec.rb
new file mode 100644
index 00000000000..79350847383
--- /dev/null
+++ b/spec/controllers/concerns/renders_commits_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RendersCommits do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:user) { create(:user) }
+
+ controller(ApplicationController) do
+ # `described_class` is not available in this context
+ include RendersCommits # rubocop:disable RSpec/DescribedClass
+
+ def index
+ @merge_request = MergeRequest.find(params[:id])
+ @commits = set_commits_for_rendering(
+ @merge_request.recent_commits.with_latest_pipeline(@merge_request.source_branch),
+ commits_count: @merge_request.commits_count
+ )
+
+ render json: { html: view_to_html_string('projects/merge_requests/_commits') }
+ end
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ def go
+ get :index, params: { id: merge_request.id }
+ end
+
+ it 'sets instance variables for counts' do
+ stub_const("MergeRequestDiff::COMMITS_SAFE_SIZE", 10)
+
+ go
+
+ expect(assigns[:total_commit_count]).to eq(29)
+ expect(assigns[:hidden_commit_count]).to eq(19)
+ expect(assigns[:commits].size).to eq(10)
+ end
+
+ context 'rendering commits' do
+ render_views
+
+ it 'avoids N + 1' do
+ stub_const("MergeRequestDiff::COMMITS_SAFE_SIZE", 5)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ go
+ end.count
+
+ stub_const("MergeRequestDiff::COMMITS_SAFE_SIZE", 15)
+
+ expect do
+ go
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/sourcegraph_gon_spec.rb b/spec/controllers/concerns/sourcegraph_gon_spec.rb
new file mode 100644
index 00000000000..4fb7e37d148
--- /dev/null
+++ b/spec/controllers/concerns/sourcegraph_gon_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SourcegraphGon do
+ let_it_be(:enabled_user) { create(:user, sourcegraph_enabled: true) }
+ let_it_be(:disabled_user) { create(:user, sourcegraph_enabled: false) }
+ let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:internal_project) { create(:project, :internal) }
+
+ let(:sourcegraph_url) { 'http://sourcegraph.gitlab.com' }
+ let(:feature_enabled) { true }
+ let(:sourcegraph_enabled) { true }
+ let(:sourcegraph_public_only) { false }
+ let(:format) { :html }
+ let(:user) { enabled_user }
+ let(:project) { internal_project }
+
+ controller(ApplicationController) do
+ include SourcegraphGon # rubocop:disable RSpec/DescribedClass
+
+ def index
+ head :ok
+ end
+ end
+
+ before do
+ Feature.get(:sourcegraph).enable(feature_enabled)
+
+ stub_application_setting(sourcegraph_url: sourcegraph_url, sourcegraph_enabled: sourcegraph_enabled, sourcegraph_public_only: sourcegraph_public_only)
+
+ allow(controller).to receive(:project).and_return(project)
+
+ Gon.clear
+
+ sign_in user if user
+ end
+
+ after do
+ Feature.get(:sourcegraph).disable
+ end
+
+ subject do
+ get :index, format: format
+
+ Gon.sourcegraph
+ end
+
+ shared_examples 'enabled' do
+ it { is_expected.to eq({ url: sourcegraph_url }) }
+ end
+
+ shared_examples 'disabled' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'with feature enabled, application enabled, and user enabled' do
+ it_behaves_like 'enabled'
+ end
+
+ context 'with feature enabled for specific project' do
+ let(:feature_enabled) { project }
+
+ it_behaves_like 'enabled'
+ end
+
+ context 'with feature enabled for different project' do
+ let(:feature_enabled) { create(:project) }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with feature disabled' do
+ let(:feature_enabled) { false }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with admin settings disabled' do
+ let(:sourcegraph_enabled) { false }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with public only' do
+ let(:sourcegraph_public_only) { true }
+
+ context 'with internal project' do
+ let(:project) { internal_project }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with public project' do
+ let(:project) { public_project }
+
+ it_behaves_like 'enabled'
+ end
+ end
+
+ context 'with user disabled' do
+ let(:user) { disabled_user }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with no user' do
+ let(:user) { nil }
+
+ it_behaves_like 'disabled'
+ end
+
+ context 'with non-html format' do
+ let(:format) { :json }
+
+ it_behaves_like 'disabled'
+ end
+end
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
index 940bf9c6828..4d200140f16 100644
--- a/spec/controllers/google_api/authorizations_controller_spec.rb
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -13,8 +13,9 @@ describe GoogleApi::AuthorizationsController do
before do
sign_in(user)
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:get_token).and_return([token, expires_at])
+ 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
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 51a6dcca640..d027405703b 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -85,7 +85,7 @@ describe Groups::ClustersController do
end
describe 'GET new' do
- def go(provider: 'gke')
+ def go(provider: 'gcp')
get :new, params: { group_id: group, provider: provider }
end
@@ -372,6 +372,150 @@ describe Groups::ClustersController do
end
end
+ describe 'POST #create_aws' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_aws_attributes: {
+ key_name: 'key',
+ role_arn: 'arn:role',
+ region: 'region',
+ vpc_id: 'vpc',
+ instance_type: 'instance type',
+ num_nodes: 3,
+ security_group_id: 'security group',
+ subnet_ids: %w(subnet1 subnet2)
+ }
+ }
+ }
+ end
+
+ def post_create_aws
+ post :create_aws, params: params.merge(group_id: group)
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_aws }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Aws.count }
+
+ cluster = group.clusters.first
+
+ expect(response.status).to eq(201)
+ expect(response.location).to eq(group_cluster_path(group, cluster))
+ expect(cluster).to be_aws
+ expect(cluster).to be_kubernetes
+ end
+
+ context 'params are invalid' do
+ let(:params) do
+ {
+ cluster: { name: '' }
+ }
+ end
+
+ it 'does not create a cluster' do
+ expect { post_create_aws }.not_to change { Clusters::Cluster.count }
+
+ expect(response.status).to eq(422)
+ expect(response.content_type).to eq('application/json')
+ expect(response.body).to include('is invalid')
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(WaitForClusterCreationWorker).to receive(:perform_in)
+ end
+
+ it { expect { post_create_aws }.to be_allowed_for(:admin) }
+ it { expect { post_create_aws }.to be_allowed_for(:owner).of(group) }
+ it { expect { post_create_aws }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { post_create_aws }.to be_denied_for(:developer).of(group) }
+ it { expect { post_create_aws }.to be_denied_for(:reporter).of(group) }
+ it { expect { post_create_aws }.to be_denied_for(:guest).of(group) }
+ it { expect { post_create_aws }.to be_denied_for(:user) }
+ it { expect { post_create_aws }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST authorize AWS role for EKS cluster' do
+ let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
+ let(:role_external_id) { '12345' }
+
+ let(:params) do
+ {
+ cluster: {
+ role_arn: role_arn,
+ role_external_id: role_external_id
+ }
+ }
+ end
+
+ def go
+ post :authorize_aws_role, params: params.merge(group_id: group)
+ end
+
+ it 'creates an Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 201
+
+ role = Aws::Role.last
+ expect(role.user).to eq user
+ expect(role.role_arn).to eq role_arn
+ expect(role.role_external_id).to eq role_external_id
+ end
+
+ context 'role cannot be created' do
+ let(:role_arn) { 'invalid-role' }
+
+ it 'does not create a record' do
+ expect { go }.not_to change { Aws::Role.count }
+
+ expect(response.status).to eq 422
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE revoke AWS role for EKS cluster' do
+ let!(:role) { create(:aws_role, user: user) }
+
+ def go
+ delete :revoke_aws_role, params: { group_id: group }
+ end
+
+ it 'deletes the Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 204
+ expect(user.reload_aws_role).to be_nil
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(group) }
+ it { expect { go }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { go }.to be_denied_for(:developer).of(group) }
+ it { expect { go }.to be_denied_for(:reporter).of(group) }
+ it { expect { go }.to be_denied_for(:guest).of(group) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, cluster_type: :group_type, groups: [group]) }
diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb
new file mode 100644
index 00000000000..8f04822fee6
--- /dev/null
+++ b/spec/controllers/groups/group_links_controller_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::GroupLinksController do
+ let(:shared_with_group) { create(:group, :private) }
+ let(:shared_group) { create(:group, :private) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#create' do
+ let(:shared_with_group_id) { shared_with_group.id }
+
+ subject do
+ post(:create,
+ params: { group_id: shared_group,
+ shared_with_group_id: shared_with_group_id,
+ shared_group_access: GroupGroupLink.default_access })
+ end
+
+ context 'when user has correct access to both groups' do
+ let(:group_member) { create(:user) }
+
+ before do
+ shared_with_group.add_developer(user)
+ shared_group.add_owner(user)
+
+ shared_with_group.add_developer(group_member)
+ end
+
+ it 'links group with selected group' do
+ expect { subject }.to change { shared_with_group.shared_groups.include?(shared_group) }.from(false).to(true)
+ end
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ end
+
+ it 'allows access for group member' do
+ expect { subject }.to change { group_member.can?(:read_group, shared_group) }.from(false).to(true)
+ end
+
+ context 'when shared with group id is not present' do
+ let(:shared_with_group_id) { nil }
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ expect(flash[:alert]).to eq('Please select a group.')
+ end
+ end
+
+ context 'when link is not persisted in the database' do
+ before do
+ allow(::Groups::GroupLinks::CreateService).to(
+ receive_message_chain(:new, :execute)
+ .and_return({ status: :error,
+ http_status: 409,
+ message: 'error' }))
+ end
+
+ it 'redirects to group links page' do
+ subject
+
+ expect(response).to(redirect_to(group_group_members_path(shared_group)))
+ 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(404)
+ end
+ end
+ end
+
+ context 'when user does not have access to the group' do
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user does not have admin access to the shared group' do
+ before do
+ shared_with_group.add_developer(user)
+ shared_group.add_developer(user)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index e0a3605d50a..4f4f9e5143b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -314,6 +314,24 @@ describe Groups::MilestonesController do
expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group))
end
+ context 'with an AJAX request' do
+ it 'redirects to the canonical path but does not set flash message' do
+ get :merge_requests, params: { group_id: redirect_route.path, id: title }, xhr: true
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
+ context 'with JSON format' do
+ it 'redirects to the canonical path but does not set flash message' do
+ get :merge_requests, params: { group_id: redirect_route.path, id: title }, format: :json
+
+ expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title, format: :json))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
context 'when the old group path is a substring of the scheme or host' do
let(:redirect_route) { group.redirect_routes.create(path: 'http') }
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 3c39a6468e5..2ed2b319298 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -45,7 +45,7 @@ describe GroupsController do
it { is_expected.to render_template('groups/show') }
- it 'assigns events for all the projects in the group' do
+ it 'assigns events for all the projects in the group', :sidekiq_might_not_need_inline do
subject
expect(assigns(:events)).to contain_exactly(event)
end
@@ -125,7 +125,7 @@ describe GroupsController do
end
context 'as json' do
- it 'includes events from all projects in group and subgroups' do
+ it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do
2.times do
project = create(:project, group: group)
create(:event, project: project)
@@ -255,7 +255,7 @@ describe GroupsController do
end
end
- describe 'GET #issues' do
+ describe 'GET #issues', :sidekiq_might_not_need_inline do
let(:issue_1) { create(:issue, project: project, title: 'foo') }
let(:issue_2) { create(:issue, project: project, title: 'bar') }
@@ -304,7 +304,7 @@ describe GroupsController do
end
end
- describe 'GET #merge_requests' do
+ describe 'GET #merge_requests', :sidekiq_might_not_need_inline do
let(:merge_request_1) { create(:merge_request, source_project: project) }
let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
deleted file mode 100644
index 8a2291bccd7..00000000000
--- a/spec/controllers/health_controller_spec.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe HealthController do
- include StubENV
-
- let(:token) { Gitlab::CurrentSettings.health_check_access_token }
- let(:whitelisted_ip) { '127.0.0.1' }
- let(:not_whitelisted_ip) { '127.0.0.2' }
-
- before do
- allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip])
- stub_storage_settings({}) # Hide the broken storage
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- end
-
- describe '#readiness' do
- shared_context 'endpoint responding with readiness data' do
- let(:request_params) { {} }
-
- subject { get :readiness, params: request_params }
-
- it 'responds with readiness checks data' do
- subject
-
- expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['gitaly_check']).to contain_exactly(
- { 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
- end
-
- it 'responds with readiness checks data when a failure happens' do
- allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
- Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
-
- subject
-
- expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
- expect(json_response['redis_check']).to contain_exactly(
- { 'status' => 'failed', 'message' => 'check error' })
-
- expect(response.status).to eq(503)
- expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
- end
- end
-
- context 'accessed from whitelisted ip' do
- before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
- end
-
- it_behaves_like 'endpoint responding with readiness data'
- end
-
- context 'accessed from not whitelisted ip' do
- before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
- end
-
- it 'responds with resource not found' do
- get :readiness
-
- expect(response.status).to eq(404)
- end
-
- context 'accessed with valid token' do
- context 'token passed in request header' do
- before do
- request.headers['TOKEN'] = token
- end
-
- it_behaves_like 'endpoint responding with readiness data'
- end
- end
-
- context 'token passed as URL param' do
- it_behaves_like 'endpoint responding with readiness data' do
- let(:request_params) { { token: token } }
- end
- end
- end
- end
-
- describe '#liveness' do
- shared_context 'endpoint responding with liveness data' do
- subject { get :liveness }
-
- it 'responds with liveness checks data' do
- subject
-
- expect(json_response).to eq('status' => 'ok')
- end
- end
-
- context 'accessed from whitelisted ip' do
- before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
- end
-
- it_behaves_like 'endpoint responding with liveness data'
- end
-
- context 'accessed from not whitelisted ip' do
- before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
- end
-
- it 'responds with resource not found' do
- get :liveness
-
- expect(response.status).to eq(404)
- end
-
- context 'accessed with valid token' do
- context 'token passed in request header' do
- before do
- request.headers['TOKEN'] = token
- end
-
- it_behaves_like 'endpoint responding with liveness data'
- end
-
- context 'token passed as URL param' do
- it_behaves_like 'endpoint responding with liveness data' do
- subject { get :liveness, params: { token: token } }
- end
- end
- end
- end
- end
-end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e465eca6c71..6a3713a1212 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -20,8 +20,9 @@ describe Import::GitlabController do
describe "GET callback" do
it "updates access token" do
- allow_any_instance_of(Gitlab::GitlabImport::Client)
- .to receive(:get_token).and_return(token)
+ allow_next_instance_of(Gitlab::GitlabImport::Client) do |instance|
+ allow(instance).to receive(:get_token).and_return(token)
+ end
stub_omniauth_provider('gitlab')
get :callback
diff --git a/spec/controllers/import/phabricator_controller_spec.rb b/spec/controllers/import/phabricator_controller_spec.rb
index 85085a8e996..a127e3cda3a 100644
--- a/spec/controllers/import/phabricator_controller_spec.rb
+++ b/spec/controllers/import/phabricator_controller_spec.rb
@@ -52,7 +52,7 @@ describe Import::PhabricatorController do
namespace_id: current_user.namespace_id }
end
- it 'creates a project to import' do
+ it 'creates a project to import', :sidekiq_might_not_need_inline do
expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
expect(importer).to receive(:execute)
end
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
index 6d588c8f915..ceab9754617 100644
--- a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -11,6 +11,14 @@ describe Ldap::OmniauthCallbacksController do
expect(request.env['warden']).to be_authenticated
end
+ context 'with sign in prevented' do
+ let(:ldap_settings) { ldap_setting_defaults.merge(prevent_ldap_sign_in: true) }
+
+ it 'does not allow sign in' do
+ expect { post provider }.to raise_error(ActionController::UrlGenerationError)
+ end
+ end
+
it 'respects remember me checkbox' do
expect do
post provider, params: { remember_me: '1' }
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 7fb3578cd0a..1d378b9b9dc 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -23,7 +23,9 @@ describe MetricsController do
allow(Prometheus::Client.configuration).to receive(:multiprocess_files_dir).and_return(metrics_multiproc_dir)
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true)
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip, whitelisted_ip_range])
- allow_any_instance_of(MetricsService).to receive(:metrics_text).and_return("prometheus_counter 1")
+ allow_next_instance_of(MetricsService) do |instance|
+ allow(instance).to receive(:metrics_text).and_return("prometheus_counter 1")
+ end
end
describe '#index' do
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index f901fd45604..dd7c0f45dc2 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -25,14 +25,25 @@ describe Projects::BlameController do
})
end
- context "valid file" do
+ context "valid branch, valid file" do
let(:id) { 'master/files/ruby/popen.rb' }
+
it { is_expected.to respond_with(:success) }
end
- context "invalid file" do
- let(:id) { 'master/files/ruby/missing_file.rb'}
- it { expect(response).to have_gitlab_http_status(404) }
+ context "valid branch, invalid file" do
+ let(:id) { 'master/files/ruby/invalid-path.rb' }
+
+ it 'redirects' do
+ expect(subject)
+ .to redirect_to("/#{project.full_path}/tree/master")
+ end
+ end
+
+ context "invalid branch, valid file" do
+ let(:id) { 'invalid-branch/files/ruby/missing_file.rb'}
+
+ it { is_expected.to respond_with(:not_found) }
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 17964c78e8d..78599935910 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -24,26 +24,34 @@ describe Projects::BlobController do
context "valid branch, valid file" do
let(:id) { 'master/README.md' }
+
it { is_expected.to respond_with(:success) }
end
context "valid branch, invalid file" do
let(:id) { 'master/invalid-path.rb' }
- it { is_expected.to respond_with(:not_found) }
+
+ it 'redirects' do
+ expect(subject)
+ .to redirect_to("/#{project.full_path}/tree/master")
+ end
end
context "invalid branch, valid file" do
let(:id) { 'invalid-branch/README.md' }
+
it { is_expected.to respond_with(:not_found) }
end
context "binary file" do
let(:id) { 'binary-encoding/encoding/binary-1.bin' }
+
it { is_expected.to respond_with(:success) }
end
context "Markdown file" do
let(:id) { 'master/README.md' }
+
it { is_expected.to respond_with(:success) }
end
end
@@ -104,6 +112,7 @@ describe Projects::BlobController do
context 'redirect to tree' do
let(:id) { 'markdown/doc' }
+
it 'redirects' do
expect(subject)
.to redirect_to("/#{project.full_path}/tree/markdown/doc")
@@ -311,7 +320,7 @@ describe Projects::BlobController do
default_params[:project_id] = forked_project
end
- it 'redirects to blob' do
+ it 'redirects to blob', :sidekiq_might_not_need_inline do
put :update, params: default_params
expect(response).to redirect_to(project_blob_path(forked_project, 'master/CHANGELOG'))
@@ -319,7 +328,7 @@ describe Projects::BlobController do
end
context 'when editing on the original repository' do
- it "redirects to forked project new merge request" do
+ it "redirects to forked project new merge request", :sidekiq_might_not_need_inline do
default_params[:branch_name] = "fork-test-1"
default_params[:create_merge_request] = 1
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index e1f6d571d27..5a0512a042e 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -79,7 +79,7 @@ describe Projects::ClustersController do
end
describe 'GET new' do
- def go(provider: 'gke')
+ def go(provider: 'gcp')
get :new, params: {
namespace_id: project.namespace,
project_id: project,
@@ -373,6 +373,150 @@ describe Projects::ClustersController do
end
end
+ describe 'POST #create_aws' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_aws_attributes: {
+ key_name: 'key',
+ role_arn: 'arn:role',
+ region: 'region',
+ vpc_id: 'vpc',
+ instance_type: 'instance type',
+ num_nodes: 3,
+ security_group_id: 'security group',
+ subnet_ids: %w(subnet1 subnet2)
+ }
+ }
+ }
+ end
+
+ def post_create_aws
+ post :create_aws, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_aws }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Aws.count }
+
+ cluster = project.clusters.first
+
+ expect(response.status).to eq(201)
+ expect(response.location).to eq(project_cluster_path(project, cluster))
+ expect(cluster).to be_aws
+ expect(cluster).to be_kubernetes
+ end
+
+ context 'params are invalid' do
+ let(:params) do
+ {
+ cluster: { name: '' }
+ }
+ end
+
+ it 'does not create a cluster' do
+ expect { post_create_aws }.not_to change { Clusters::Cluster.count }
+
+ expect(response.status).to eq(422)
+ expect(response.content_type).to eq('application/json')
+ expect(response.body).to include('is invalid')
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(WaitForClusterCreationWorker).to receive(:perform_in)
+ end
+
+ it { expect { post_create_aws }.to be_allowed_for(:admin) }
+ it { expect { post_create_aws }.to be_allowed_for(:owner).of(project) }
+ it { expect { post_create_aws }.to be_allowed_for(:maintainer).of(project) }
+ it { expect { post_create_aws }.to be_denied_for(:developer).of(project) }
+ it { expect { post_create_aws }.to be_denied_for(:reporter).of(project) }
+ it { expect { post_create_aws }.to be_denied_for(:guest).of(project) }
+ it { expect { post_create_aws }.to be_denied_for(:user) }
+ it { expect { post_create_aws }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST authorize AWS role for EKS cluster' do
+ let(:role_arn) { 'arn:aws:iam::123456789012:role/role-name' }
+ let(:role_external_id) { '12345' }
+
+ let(:params) do
+ {
+ cluster: {
+ role_arn: role_arn,
+ role_external_id: role_external_id
+ }
+ }
+ end
+
+ def go
+ post :authorize_aws_role, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
+ it 'creates an Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 201
+
+ role = Aws::Role.last
+ expect(role.user).to eq user
+ expect(role.role_arn).to eq role_arn
+ expect(role.role_external_id).to eq role_external_id
+ end
+
+ context 'role cannot be created' do
+ let(:role_arn) { 'invalid-role' }
+
+ it 'does not create a record' do
+ expect { go }.not_to change { Aws::Role.count }
+
+ expect(response.status).to eq 422
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ 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) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE revoke AWS role for EKS cluster' do
+ let!(:role) { create(:aws_role, user: user) }
+
+ def go
+ delete :revoke_aws_role, params: { namespace_id: project.namespace, project_id: project }
+ end
+
+ it 'deletes the Aws::Role record' do
+ expect { go }.to change { Aws::Role.count }
+
+ expect(response.status).to eq 204
+ expect(user.reload_aws_role).to be_nil
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ 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) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+ end
+
describe 'GET cluster_status' do
let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 6ed822bbb10..d59f76c1b32 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -104,7 +104,9 @@ describe Projects::DiscussionsController do
end
it "sends notifications if all discussions are resolved" do
- expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+ expect_next_instance_of(MergeRequests::ResolvedDiscussionNotificationService) do |instance|
+ expect(instance).to receive(:execute).with(merge_request)
+ end
post :resolve, params: request_params
end
@@ -122,8 +124,10 @@ describe Projects::DiscussionsController do
end
it "renders discussion with serializer" do
- expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
+ expect_next_instance_of(DiscussionSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
+ end
post :resolve, params: request_params
end
@@ -193,8 +197,10 @@ describe Projects::DiscussionsController do
end
it "renders discussion with serializer" do
- expect_any_instance_of(DiscussionSerializer).to receive(:represent)
- .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
+ expect_next_instance_of(DiscussionSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class), render_truncated_diff_lines: true })
+ end
delete :unresolve, params: request_params
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 3fe5ff5feee..7bb956201fd 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -330,11 +330,11 @@ describe Projects::EnvironmentsController do
expect(response).to redirect_to(environment_metrics_path(environment))
end
- it 'redirects to empty page if no environment exists' do
+ it 'redirects to empty metrics page if no environment exists' do
get :metrics_redirect, params: { namespace_id: project.namespace, project_id: project }
expect(response).to be_ok
- expect(response).to render_template 'empty'
+ expect(response).to render_template 'empty_metrics'
end
end
diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb
index 31868f5f717..8155d6ddafe 100644
--- a/spec/controllers/projects/error_tracking_controller_spec.rb
+++ b/spec/controllers/projects/error_tracking_controller_spec.rb
@@ -46,17 +46,6 @@ describe Projects::ErrorTrackingController do
end
describe 'format json' do
- shared_examples 'no data' do
- it 'returns no data' do
- get :index, params: project_params(format: :json)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('error_tracking/index')
- expect(json_response['external_url']).to be_nil
- expect(json_response['errors']).to eq([])
- end
- end
-
let(:list_issues_service) { spy(:list_issues_service) }
let(:external_url) { 'http://example.com' }
@@ -66,6 +55,19 @@ describe Projects::ErrorTrackingController do
.and_return(list_issues_service)
end
+ context 'no data' do
+ before do
+ expect(list_issues_service).to receive(:execute)
+ .and_return(status: :error, http_status: :no_content)
+ end
+
+ it 'returns no data' do
+ get :index, params: project_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
context 'service result is successful' do
before do
expect(list_issues_service).to receive(:execute)
@@ -232,8 +234,186 @@ describe Projects::ErrorTrackingController do
end
end
+ describe 'GET #issue_details' do
+ let_it_be(:issue_id) { 1234 }
+
+ let(:issue_details_service) { spy(:issue_details_service) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(
+ { issue_id: issue_id.to_s }
+ ).permit!
+ end
+
+ before do
+ expect(ErrorTracking::IssueDetailsService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(issue_details_service)
+ end
+
+ describe 'format json' do
+ context 'no data' do
+ before do
+ expect(issue_details_service).to receive(:execute)
+ .and_return(status: :error, http_status: :no_content)
+ end
+
+ it 'returns no data' do
+ get :details, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'service result is successful' do
+ before do
+ expect(issue_details_service).to receive(:execute)
+ .and_return(status: :success, issue: error)
+ end
+
+ let(:error) { build(:detailed_error_tracking_error) }
+
+ it 'returns an error' do
+ get :details, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('error_tracking/issue_detailed')
+ expect(json_response['error']).to eq(error.as_json)
+ end
+ end
+
+ context 'service result is erroneous' do
+ let(:error_message) { 'error message' }
+
+ context 'without http_status' do
+ before do
+ expect(issue_details_service).to receive(:execute)
+ .and_return(status: :error, message: error_message)
+ end
+
+ it 'returns 400 with message' do
+ get :details, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+
+ context 'with explicit http_status' do
+ let(:http_status) { :no_content }
+
+ before do
+ expect(issue_details_service).to receive(:execute).and_return(
+ status: :error,
+ message: error_message,
+ http_status: http_status
+ )
+ end
+
+ it 'returns http_status with message' do
+ get :details, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(http_status)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET #stack_trace' do
+ let_it_be(:issue_id) { 1234 }
+
+ let(:issue_stack_trace_service) { spy(:issue_stack_trace_service) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(
+ { issue_id: issue_id.to_s }
+ ).permit!
+ end
+
+ before do
+ expect(ErrorTracking::IssueLatestEventService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(issue_stack_trace_service)
+ end
+
+ describe 'format json' do
+ context 'awaiting data' do
+ before do
+ expect(issue_stack_trace_service).to receive(:execute)
+ .and_return(status: :error, http_status: :no_content)
+ end
+
+ it 'returns no data' do
+ get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'service result is successful' do
+ before do
+ expect(issue_stack_trace_service).to receive(:execute)
+ .and_return(status: :success, latest_event: error_event)
+ end
+
+ let(:error_event) { build(:error_tracking_error_event) }
+
+ it 'returns an error' do
+ get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('error_tracking/issue_stack_trace')
+ expect(json_response['error']).to eq(error_event.as_json)
+ end
+ end
+
+ context 'service result is erroneous' do
+ let(:error_message) { 'error message' }
+
+ context 'without http_status' do
+ before do
+ expect(issue_stack_trace_service).to receive(:execute)
+ .and_return(status: :error, message: error_message)
+ end
+
+ it 'returns 400 with message' do
+ get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+
+ context 'with explicit http_status' do
+ let(:http_status) { :no_content }
+
+ before do
+ expect(issue_stack_trace_service).to receive(:execute).and_return(
+ status: :error,
+ message: error_message,
+ http_status: http_status
+ )
+ end
+
+ it 'returns http_status with message' do
+ get :stack_trace, params: issue_params(issue_id: issue_id, format: :json)
+
+ expect(response).to have_gitlab_http_status(http_status)
+ expect(json_response['message']).to eq(error_message)
+ end
+ end
+ end
+ end
+ end
+
private
+ def issue_params(opts = {})
+ project_params.reverse_merge(opts)
+ end
+
def project_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, project_id: project)
end
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
index 352a364295b..0ef96514961 100644
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -94,4 +94,75 @@ describe Projects::GrafanaApiController do
end
end
end
+
+ describe 'GET #metrics_dashboard' do
+ let(:service_result) { { status: :success, dashboard: '{}' } }
+ let(:params) do
+ {
+ format: :json,
+ embedded: true,
+ grafana_url: 'https://grafana.example.com',
+ namespace_id: project.namespace.full_path,
+ project_id: project.name
+ }
+ end
+
+ before do
+ allow(Gitlab::Metrics::Dashboard::Finder)
+ .to receive(:find)
+ .and_return(service_result)
+ end
+
+ context 'when the result is still processing' do
+ let(:service_result) { nil }
+
+ it 'returns 204 no content' do
+ get :metrics_dashboard, params: params
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when the result was successful' do
+ it 'returns the dashboard response' do
+ get :metrics_dashboard, params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({
+ 'dashboard' => '{}',
+ 'status' => 'success'
+ })
+ end
+ end
+
+ context 'when an error has occurred' do
+ shared_examples_for 'error response' do |http_status|
+ it "returns #{http_status}" do
+ get :metrics_dashboard, params: params
+
+ expect(response).to have_gitlab_http_status(http_status)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+
+ context 'with an error accessing grafana' do
+ let(:service_result) do
+ {
+ http_status: :service_unavailable,
+ status: :error,
+ message: 'error message'
+ }
+ end
+
+ it_behaves_like 'error response', :service_unavailable
+ end
+
+ context 'with a processing error' do
+ let(:service_result) { { status: :error, message: 'error message' } }
+
+ it_behaves_like 'error response', :bad_request
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index d36336a9f67..8770a5ee303 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1252,7 +1252,7 @@ describe Projects::IssuesController do
stub_feature_flags(create_confidential_merge_request: true)
end
- it 'creates a new merge request' do
+ 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
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 90ccb884927..349d73f13ca 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -154,7 +154,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
.and_return(merge_request)
end
- it 'does not serialize builds in exposed stages' do
+ it 'does not serialize builds in exposed stages', :sidekiq_might_not_need_inline do
get_show_json
json_response.dig('pipeline', 'details', 'stages').tap do |stages|
@@ -183,7 +183,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'job is cancelable' do
let(:job) { create(:ci_build, :running, pipeline: pipeline) }
- it 'cancel_path is present with correct redirect' do
+ it 'cancel_path is present with correct redirect', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['cancel_path']).to include(CGI.escape(json_response['build_path']))
@@ -193,7 +193,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'with web terminal' do
let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }
- it 'exposes the terminal path' do
+ it 'exposes the terminal path', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['terminal_path']).to match(%r{/terminal})
@@ -268,7 +268,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
project.add_maintainer(user) # Need to be a maintianer to view cluster.path
end
- it 'exposes the deployment information' do
+ it 'exposes the deployment information', :sidekiq_might_not_need_inline do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -292,7 +292,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can edit runner' do
+ it 'user can edit runner', :sidekiq_might_not_need_inline do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -312,7 +312,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can not edit runner' do
+ it 'user can not edit runner', :sidekiq_might_not_need_inline do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -331,7 +331,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
sign_in(user)
end
- it 'user can not edit runner' do
+ it 'user can not edit runner', :sidekiq_might_not_need_inline do
get_show_json
expect(response).to have_gitlab_http_status(:ok)
@@ -412,7 +412,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when job has trace' do
let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
- it "has_trace is true" do
+ it "has_trace is true", :sidekiq_might_not_need_inline do
get_show_json
expect(response).to match_response_schema('job/job_details')
@@ -458,7 +458,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
end
- context 'user is a maintainer' do
+ context 'user is a maintainer', :sidekiq_might_not_need_inline do
before do
project.add_maintainer(user)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index ff089df37f7..aee017b211a 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -204,6 +204,24 @@ describe Projects::LabelsController do
expect(response).to redirect_to(project_labels_path(project))
expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, project))
end
+
+ context 'with an AJAX request' do
+ it 'redirects to the canonical path but does not set flash message' do
+ get :index, params: { namespace_id: project.namespace, project_id: project.to_param + 'old' }, xhr: true
+
+ expect(response).to redirect_to(project_labels_path(project))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
+
+ context 'with JSON format' do
+ it 'redirects to the canonical path but does not set flash message' do
+ get :index, params: { namespace_id: project.namespace, project_id: project.to_param + 'old' }, format: :json
+
+ expect(response).to redirect_to(project_labels_path(project, format: :json))
+ expect(controller).not_to set_flash[:notice]
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 45125385d9e..64440ed585d 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -13,8 +13,9 @@ describe Projects::MattermostsController do
describe 'GET #new' do
before do
- allow_any_instance_of(MattermostSlashCommandsService)
- .to receive(:list_teams).and_return([])
+ allow_next_instance_of(MattermostSlashCommandsService) do |instance|
+ allow(instance).to receive(:list_teams).and_return([])
+ end
end
it 'accepts the request' do
@@ -42,7 +43,9 @@ describe Projects::MattermostsController do
context 'no request can be made to mattermost' do
it 'shows the error' do
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"])
+ allow_next_instance_of(MattermostSlashCommandsService) do |instance|
+ allow(instance).to receive(:configure).and_return([false, "error message"])
+ end
expect(subject).to redirect_to(new_project_mattermost_url(project))
end
@@ -50,7 +53,9 @@ describe Projects::MattermostsController do
context 'the request is succesull' do
before do
- allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token')
+ allow_next_instance_of(Mattermost::Command) do |instance|
+ allow(instance).to receive(:create).and_return('token')
+ end
end
it 'redirects to the new page' do
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index ce977f26ec6..1bbb80f9904 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -85,7 +85,9 @@ describe Projects::MergeRequests::CreationsController do
describe 'GET diffs' do
context 'when merge request cannot be created' do
it 'does not assign diffs var' do
- allow_any_instance_of(MergeRequest).to receive(:can_be_created).and_return(false)
+ allow_next_instance_of(MergeRequest) do |instance|
+ allow(instance).to receive(:can_be_created).and_return(false)
+ end
get :diffs, params: get_diff_params.merge(format: 'json')
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 5c02e8d6461..06d9af33189 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -34,6 +34,16 @@ describe Projects::MergeRequests::DiffsController do
it 'saves the preferred diff view in a cookie' do
expect(response.cookies['diff_view']).to eq('parallel')
end
+
+ it 'only renders the required view', :aggregate_failures do
+ diff_files_without_deletions = json_response['diff_files'].reject { |f| f['deleted_file'] }
+ have_no_inline_diff_lines = satisfy('have no inline diff lines') do |diff_file|
+ !diff_file.has_key?('highlighted_diff_lines')
+ end
+
+ expect(diff_files_without_deletions).to all(have_key('parallel_diff_lines'))
+ expect(diff_files_without_deletions).to all(have_no_inline_diff_lines)
+ end
end
context 'when the user cannot view the merge request' do
@@ -76,7 +86,9 @@ describe Projects::MergeRequests::DiffsController do
end
it 'serializes merge request diff collection' do
- expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+ expect_next_instance_of(DiffsSerializer) do |instance|
+ expect(instance).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+ end
go
end
@@ -88,7 +100,9 @@ describe Projects::MergeRequests::DiffsController do
end
it 'serializes merge request diff collection' do
- expect_any_instance_of(DiffsSerializer).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+ expect_next_instance_of(DiffsSerializer) do |instance|
+ expect(instance).to receive(:represent).with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff), an_instance_of(Hash))
+ end
go
end
@@ -259,7 +273,7 @@ describe Projects::MergeRequests::DiffsController do
it 'only renders the diffs for the path given' do
diff_for_path(old_path: existing_path, new_path: existing_path)
- paths = json_response["diff_files"].map { |file| file['new_path'] }
+ paths = json_response['diff_files'].map { |file| file['new_path'] }
expect(paths).to include(existing_path)
end
@@ -344,6 +358,7 @@ describe Projects::MergeRequests::DiffsController do
let(:expected_options) do
{
merge_request: merge_request,
+ diff_view: :inline,
pagination_data: {
current_page: 1,
next_page: nil,
@@ -367,6 +382,7 @@ describe Projects::MergeRequests::DiffsController do
let(:expected_options) do
{
merge_request: merge_request,
+ diff_view: :inline,
pagination_data: {
current_page: 2,
next_page: 3,
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index ea702792557..9f7fde2f0da 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Projects::MergeRequestsController do
include ProjectForksHelper
+ include Gitlab::Routing
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
@@ -206,7 +207,7 @@ describe Projects::MergeRequestsController do
it 'redirects to last_page if page number is larger than number of pages' do
get_merge_requests(last_page + 1)
- expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'redirects to specified page' do
@@ -227,7 +228,7 @@ describe Projects::MergeRequestsController do
host: external_host
}
- expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ expect(response).to redirect_to(project_merge_requests_path(project, page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
end
@@ -404,7 +405,7 @@ describe Projects::MergeRequestsController do
end
it 'starts the merge immediately with permitted params' do
- expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'squash' => false })
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, { 'sha' => merge_request.diff_head_sha })
merge_with_sha
end
@@ -430,10 +431,15 @@ describe Projects::MergeRequestsController do
context 'when a squash commit message is passed' do
let(:message) { 'My custom squash commit message' }
- it 'passes the same message to SquashService' do
- params = { squash: '1', squash_commit_message: message }
+ it 'passes the same message to SquashService', :sidekiq_might_not_need_inline do
+ params = { squash: '1',
+ squash_commit_message: message,
+ sha: merge_request.diff_head_sha }
+ expected_squash_params = { squash_commit_message: message,
+ sha: merge_request.diff_head_sha,
+ merge_request: merge_request }
- expect_next_instance_of(MergeRequests::SquashService, project, user, params.merge(merge_request: merge_request)) do |squash_service|
+ expect_next_instance_of(MergeRequests::SquashService, project, user, expected_squash_params) do |squash_service|
expect(squash_service).to receive(:execute).and_return({
status: :success,
squash_sha: SecureRandom.hex(20)
@@ -723,7 +729,7 @@ describe Projects::MergeRequestsController do
context 'with private builds' do
context 'for the target project member' do
- it 'does not respond with serialized pipelines' do
+ it 'does not respond with serialized pipelines', :sidekiq_might_not_need_inline do
expect(json_response['pipelines']).to be_empty
expect(json_response['count']['all']).to eq(0)
expect(response).to include_pagination_headers
@@ -733,7 +739,7 @@ describe Projects::MergeRequestsController do
context 'for the source project member' do
let(:user) { fork_user }
- it 'responds with serialized pipelines' do
+ it 'responds with serialized pipelines', :sidekiq_might_not_need_inline do
expect(json_response['pipelines']).to be_present
expect(json_response['count']['all']).to eq(1)
expect(response).to include_pagination_headers
@@ -749,7 +755,7 @@ describe Projects::MergeRequestsController do
end
context 'for the target project member' do
- it 'does not respond with serialized pipelines' do
+ it 'does not respond with serialized pipelines', :sidekiq_might_not_need_inline do
expect(json_response['pipelines']).to be_present
expect(json_response['count']['all']).to eq(1)
expect(response).to include_pagination_headers
@@ -759,7 +765,7 @@ describe Projects::MergeRequestsController do
context 'for the source project member' do
let(:user) { fork_user }
- it 'responds with serialized pipelines' do
+ it 'responds with serialized pipelines', :sidekiq_might_not_need_inline do
expect(json_response['pipelines']).to be_present
expect(json_response['count']['all']).to eq(1)
expect(response).to include_pagination_headers
@@ -770,6 +776,172 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET exposed_artifacts' 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,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ let!(:job) { create(:ci_build, pipeline: pipeline, options: job_options) }
+ let!(:job_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:find_exposed_artifacts)
+ .and_return(report)
+
+ allow_any_instance_of(MergeRequest)
+ .to receive(:actual_head_pipeline)
+ .and_return(pipeline)
+ end
+
+ subject do
+ get :exposed_artifacts, 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: [] } }
+ let(:job_options) { {} }
+
+ 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(404)
+ 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(404)
+ expect(response.body).to be_blank
+ end
+ end
+ end
+
+ context 'when pipeline has jobs with exposed artifacts' do
+ let(:job_options) do
+ {
+ artifacts: {
+ paths: ['ci_artifacts.txt'],
+ expose_as: 'Exposed artifact'
+ }
+ }
+ end
+
+ context 'when fetching exposed artifacts 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 fetching exposed artifacts is completed' do
+ let(:data) do
+ Ci::GenerateExposedArtifactsReportService.new(project, user)
+ .execute(nil, pipeline)
+ end
+
+ let(:report) { { status: :parsed, data: data } }
+
+ it 'returns exposed artifacts' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['status']).to eq('parsed')
+ expect(json_response['data']).to eq([{
+ 'job_name' => 'test',
+ 'job_path' => project_job_path(project, job),
+ 'url' => file_project_job_artifacts_path(project, job, 'ci_artifacts.txt'),
+ 'text' => 'Exposed artifact'
+ }])
+ 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
+ let(:report) { double }
+ let(:job_options) do
+ {
+ artifacts: {
+ paths: ['ci_artifacts.txt']
+ }
+ }
+ end
+
+ it 'returns no content' do
+ subject
+
+ expect(response).to have_gitlab_http_status(204)
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
describe 'GET test_reports' do
let(:merge_request) do
create(:merge_request,
@@ -879,23 +1051,6 @@ describe Projects::MergeRequestsController do
expect(json_response).to eq({ 'status_reason' => 'Failed to parse test reports' })
end
end
-
- context 'when something went wrong on our system' do
- let(:comparison_status) { {} }
-
- it 'does not send polling interval' do
- expect(Gitlab::PollingInterval).not_to receive(:set_header)
-
- subject
- end
-
- it 'returns 500 HTTP status' do
- subject
-
- expect(response).to have_gitlab_http_status(:internal_server_error)
- expect(json_response).to eq({ 'status_reason' => 'Unknown error' })
- end
- end
end
describe 'POST remove_wip' do
@@ -1019,13 +1174,13 @@ describe Projects::MergeRequestsController do
create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline)
end
- it 'links to the environment on that project' do
+ it 'links to the environment on that project', :sidekiq_might_not_need_inline do
get_ci_environments_status
expect(json_response.first['url']).to match /#{forked.full_path}/
end
- context "when environment_target is 'merge_commit'" do
+ context "when environment_target is 'merge_commit'", :sidekiq_might_not_need_inline do
it 'returns nothing' do
get_ci_environments_status(environment_target: 'merge_commit')
@@ -1056,13 +1211,13 @@ describe Projects::MergeRequestsController do
# we're trying to reduce the overall number of queries for this method.
# set a hard limit for now. https://gitlab.com/gitlab-org/gitlab-foss/issues/52287
- it 'keeps queries in check' do
+ it 'keeps queries in check', :sidekiq_might_not_need_inline do
control_count = ActiveRecord::QueryRecorder.new { get_ci_environments_status }.count
expect(control_count).to be <= 137
end
- it 'has no N+1 SQL issues for environments', :request_store, retry: 0 do
+ it 'has no N+1 SQL issues for environments', :request_store, :sidekiq_might_not_need_inline, retry: 0 do
# First run to insert test data from lets, which does take up some 30 queries
get_ci_environments_status
@@ -1225,6 +1380,33 @@ describe Projects::MergeRequestsController do
end
end
+ context 'with SELECT FOR UPDATE lock' do
+ before do
+ stub_feature_flags(merge_request_rebase_nowait_lock: false)
+ end
+
+ it 'executes rebase' do
+ allow_any_instance_of(MergeRequest).to receive(:with_lock).with(true).and_call_original
+ expect(RebaseWorker).to receive(:perform_async)
+
+ post_rebase
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'with NOWAIT lock' do
+ it 'returns a 409' do
+ allow_any_instance_of(MergeRequest).to receive(:with_lock).with('FOR UPDATE NOWAIT').and_raise(ActiveRecord::LockWaitTimeout)
+ expect(RebaseWorker).not_to receive(:perform_async)
+
+ post_rebase
+
+ expect(response.status).to eq(409)
+ expect(json_response['merge_error']).to eq(MergeRequest::REBASE_LOCK_MESSAGE)
+ end
+ end
+
context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) }
@@ -1253,7 +1435,7 @@ describe Projects::MergeRequestsController do
sign_in(fork_owner)
end
- it 'returns 200' do
+ it 'returns 200', :sidekiq_might_not_need_inline do
expect_rebase_worker_for(fork_owner)
post_rebase
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index fb3dd75460a..e14686970a1 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -51,10 +51,6 @@ describe Projects::MirrorsController do
sign_in(project.owner)
end
- around do |example|
- Sidekiq::Testing.fake! { example.run }
- end
-
context 'With valid URL for a push' do
let(:remote_mirror_attributes) do
{ "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 3ab191c0032..e576a3d2d40 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -518,7 +518,7 @@ describe Projects::NotesController do
project.id && Project.maximum(:id).succ
end
- it 'returns a 404' do
+ it 'returns a 404', :sidekiq_might_not_need_inline do
create!
expect(response).to have_gitlab_http_status(404)
end
@@ -527,13 +527,13 @@ describe Projects::NotesController do
context 'when the user has no access to the fork' do
let(:fork_visibility) { Gitlab::VisibilityLevel::PRIVATE }
- it 'returns a 404' do
+ it 'returns a 404', :sidekiq_might_not_need_inline do
create!
expect(response).to have_gitlab_http_status(404)
end
end
- context 'when the user has access to the fork' do
+ context 'when the user has access to the fork', :sidekiq_might_not_need_inline do
let!(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) }
let(:fork_visibility) { Gitlab::VisibilityLevel::PUBLIC }
@@ -785,7 +785,9 @@ describe Projects::NotesController do
end
it "sends notifications if all discussions are resolved" do
- expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+ expect_next_instance_of(MergeRequests::ResolvedDiscussionNotificationService) do |instance|
+ expect(instance).to receive(:execute).with(merge_request)
+ end
post :resolve, params: request_params
end
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 032f4f1418f..3987bebb124 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -32,10 +32,10 @@ describe Projects::PagesDomainsController do
get(:show, params: request_params.merge(id: pages_domain.domain))
end
- it "displays the 'show' page" do
+ it "redirects to the 'edit' page" do
make_request
- expect(response).to have_gitlab_http_status(200)
- expect(response).to render_template('show')
+
+ expect(response).to redirect_to(edit_project_pages_domain_path(project, pages_domain.domain))
end
context 'when user is developer' do
@@ -69,7 +69,7 @@ describe Projects::PagesDomainsController do
created_domain = PagesDomain.reorder(:id).last
expect(created_domain).to be_present
- expect(response).to redirect_to(project_pages_domain_path(project, created_domain))
+ expect(response).to redirect_to(edit_project_pages_domain_path(project, created_domain))
end
end
@@ -160,7 +160,7 @@ describe Projects::PagesDomainsController do
post :verify, params: params
- expect(response).to redirect_to project_pages_domain_path(project, pages_domain)
+ expect(response).to redirect_to edit_project_pages_domain_path(project, pages_domain)
expect(flash[:notice]).to eq('Successfully verified domain ownership')
end
@@ -169,7 +169,7 @@ describe Projects::PagesDomainsController do
post :verify, params: params
- expect(response).to redirect_to project_pages_domain_path(project, pages_domain)
+ expect(response).to redirect_to edit_project_pages_domain_path(project, pages_domain)
expect(flash[:alert]).to eq('Failed to verify domain ownership')
end
@@ -190,6 +190,56 @@ describe Projects::PagesDomainsController do
end
end
+ describe 'DELETE #clean_certificate' do
+ subject do
+ delete(:clean_certificate, params: request_params.merge(id: pages_domain.domain))
+ end
+
+ it 'redirects to edit page' do
+ subject
+
+ expect(response).to redirect_to(edit_project_pages_domain_path(project, pages_domain))
+ end
+
+ it 'removes certificate' do
+ expect do
+ subject
+ end.to change { pages_domain.reload.certificate }.to(nil)
+ .and change { pages_domain.reload.key }.to(nil)
+ end
+
+ it 'sets certificate source to user_provided' do
+ pages_domain.update!(certificate_source: :gitlab_provided)
+
+ expect do
+ subject
+ end.to change { pages_domain.reload.certificate_source }.from("gitlab_provided").to("user_provided")
+ end
+
+ context 'when pages_https_only is set' do
+ before do
+ project.update!(pages_https_only: true)
+ stub_pages_setting(external_https: '127.0.0.1')
+ end
+
+ it 'does not remove certificate' do
+ subject
+
+ pages_domain.reload
+ expect(pages_domain.certificate).to be_present
+ expect(pages_domain.key).to be_present
+ end
+
+ it 'redirects to edit page with a flash message' do
+ subject
+
+ expect(flash[:alert]).to include('Certificate')
+ expect(flash[:alert]).to include('Key')
+ expect(response).to redirect_to(edit_project_pages_domain_path(project, pages_domain))
+ end
+ end
+ end
+
context 'pages disabled' do
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index e3ad36f8d24..3c7f69f0e6e 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -93,7 +93,7 @@ describe Projects::PipelinesController do
end
context 'when performing gitaly calls', :request_store do
- it 'limits the Gitaly requests' do
+ it 'limits the Gitaly requests', :sidekiq_might_not_need_inline do
# Isolate from test preparation (Repository#exists? is also cached in RequestStore)
RequestStore.end!
RequestStore.clear!
@@ -149,7 +149,7 @@ describe Projects::PipelinesController do
end
describe 'GET show.json' do
- let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
it 'returns the pipeline' do
get_pipeline_json
@@ -571,7 +571,7 @@ describe Projects::PipelinesController do
format: :json
end
- it 'cancels a pipeline without returning any content' do
+ it 'cancels a pipeline without returning any content', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(:no_content)
expect(pipeline.reload).to be_canceled
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 2f473d395ad..072df1f5060 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -45,7 +45,9 @@ describe Projects::ProjectMembersController do
end
it 'adds user to members' do
- expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :success)
+ expect_next_instance_of(Members::CreateService) do |instance|
+ expect(instance).to receive(:execute).and_return(status: :success)
+ end
post :create, params: {
namespace_id: project.namespace,
@@ -59,7 +61,9 @@ describe Projects::ProjectMembersController do
end
it 'adds no user to members' do
- expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :failure, message: 'Message')
+ expect_next_instance_of(Members::CreateService) do |instance|
+ expect(instance).to receive(:execute).and_return(status: :failure, message: 'Message')
+ end
post :create, params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index 17f9483be98..afdb8bbc983 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -85,7 +85,9 @@ describe Projects::Prometheus::MetricsController do
end
it 'calls prometheus adapter service' do
- expect_any_instance_of(::Prometheus::AdapterService).to receive(:prometheus_adapter)
+ expect_next_instance_of(::Prometheus::AdapterService) do |instance|
+ expect(instance).to receive(:prometheus_adapter)
+ end
subject.__send__(:prometheus_adapter)
end
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index 5b9d21d3d5b..562119d967f 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -3,10 +3,36 @@
require 'spec_helper'
describe Projects::ReleasesController do
- let!(:project) { create(:project, :repository, :public) }
- let!(:user) { create(:user) }
+ let!(:project) { create(:project, :repository, :public) }
+ let!(:private_project) { create(:project, :repository, :private) }
+ let(:user) { developer }
+ let(:developer) { create(:user) }
+ let(:reporter) { create(:user) }
+ let!(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) }
+ let!(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) }
- describe 'GET #index' do
+ before do
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
+
+ shared_examples_for 'successful request' do
+ it 'renders a 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+
+ shared_examples_for 'not found' do
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ shared_examples 'common access controls' do
it 'renders a 200' do
get_index
@@ -14,36 +40,135 @@ describe Projects::ReleasesController do
end
context 'when the project is private' do
- let!(:project) { create(:project, :repository, :private) }
+ let(:project) { private_project }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user is a developer' do
+ let(:user) { developer }
- it 'renders a 302' do
- get_index
+ it 'renders a 200 for a logged in developer' do
+ sign_in(user)
- expect(response.status).to eq(302)
+ get_index
+
+ expect(response.status).to eq(200)
+ end
end
- it 'renders a 200 for a logged in developer' do
- project.add_developer(user)
- sign_in(user)
+ context 'when user is an external user' do
+ let(:user) { create(:user) }
- get_index
+ it 'renders a 404 when logged in but not in the project' do
+ sign_in(user)
- expect(response.status).to eq(200)
+ get_index
+
+ expect(response.status).to eq(404)
+ end
end
+ end
+ end
- it 'renders a 404 when logged in but not in the project' do
- sign_in(user)
+ describe 'GET #index' do
+ before do
+ get_index
+ end
- get_index
+ context 'as html' do
+ let(:format) { :html }
- expect(response.status).to eq(404)
+ it 'returns a text/html content_type' do
+ expect(response.content_type).to eq 'text/html'
end
+
+ it_behaves_like 'common access controls'
+
+ context 'when the project is private and the user is not logged in' do
+ let(:project) { private_project }
+
+ it 'returns a redirect' do
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+
+ context 'as json' do
+ let(:format) { :json }
+
+ it 'returns an application/json content_type' do
+ expect(response.content_type).to eq 'application/json'
+ end
+
+ it "returns the project's releases as JSON, ordered by released_at" do
+ expect(response.body).to eq([release_2, release_1].to_json)
+ end
+
+ it_behaves_like 'common access controls'
+
+ context 'when the project is private and the user is not logged in' do
+ let(:project) { private_project }
+
+ it 'returns a redirect' do
+ expect(response).to have_gitlab_http_status(:redirect)
+ end
+ end
+ end
+ end
+
+ describe 'GET #edit' do
+ subject do
+ get :edit, params: { namespace_id: project.namespace, project_id: project, tag: tag }
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ let!(:release) { create(:release, project: project) }
+ let(:tag) { CGI.escape(release.tag) }
+
+ it_behaves_like 'successful request'
+
+ context 'when tag name contains slash' do
+ let!(:release) { create(:release, project: project, tag: 'awesome/v1.0') }
+ let(:tag) { CGI.escape(release.tag) }
+
+ it_behaves_like 'successful request'
+
+ it 'is accesible at a URL encoded path' do
+ expect(edit_project_release_path(project, release))
+ .to eq("/#{project.namespace.path}/#{project.name}/-/releases/awesome%252Fv1.0/edit")
+ end
+ end
+
+ context 'when feature flag `release_edit_page` is disabled' do
+ before do
+ stub_feature_flags(release_edit_page: false)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'when release does not exist' do
+ let!(:release) { }
+ let(:tag) { 'non-existent-tag' }
+
+ it_behaves_like 'not found'
+ end
+
+ context 'when user is a reporter' do
+ let(:user) { reporter }
+
+ it_behaves_like 'not found'
end
end
private
def get_index
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ get :index, params: { namespace_id: project.namespace, project_id: project, format: format }
end
end
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index eccc8e1d5de..73fb0fad646 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -13,6 +13,10 @@ describe Projects::Serverless::FunctionsController do
let(:environment) { create(:environment, project: project) }
let!(:deployment) { create(:deployment, :success, environment: environment, cluster: cluster) }
let(:knative_services_finder) { environment.knative_services_finder }
+ let(:function_description) { 'A serverless function' }
+ let(:knative_stub_options) do
+ { namespace: namespace.namespace, name: cluster.project.name, description: function_description }
+ end
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -114,40 +118,33 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200)
expect(json_response).to include(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com",
- "podcount" => 1
+ 'name' => project.name,
+ 'url' => "http://#{project.name}.#{namespace.namespace}.example.com",
+ 'description' => function_description,
+ 'podcount' => 1
)
end
end
- context 'on Knative 0.5' do
+ context 'on Knative 0.5.0' do
+ before do
+ prepare_knative_stubs(knative_05_service(knative_stub_options))
+ end
+
+ include_examples 'GET #show with valid data'
+ end
+
+ context 'on Knative 0.6.0' do
before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(
- legacy_knative: true,
- namespace: namespace.namespace,
- name: cluster.project.name
- )["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ prepare_knative_stubs(knative_06_service(knative_stub_options))
end
include_examples 'GET #show with valid data'
end
- context 'on Knative 0.6 or 0.7' do
+ context 'on Knative 0.7.0' do
before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ prepare_knative_stubs(knative_07_service(knative_stub_options))
end
include_examples 'GET #show with valid data'
@@ -172,11 +169,12 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200)
expect(json_response).to match({
- "knative_installed" => "checking",
- "functions" => [
+ 'knative_installed' => 'checking',
+ 'functions' => [
a_hash_including(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ 'name' => project.name,
+ 'url' => "http://#{project.name}.#{namespace.namespace}.example.com",
+ 'description' => function_description
)
]
})
@@ -189,36 +187,38 @@ describe Projects::Serverless::FunctionsController do
end
end
- context 'on Knative 0.5' do
+ context 'on Knative 0.5.0' do
before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(
- legacy_knative: true,
- namespace: namespace.namespace,
- name: cluster.project.name
- )["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ prepare_knative_stubs(knative_05_service(knative_stub_options))
end
include_examples 'GET #index with data'
end
- context 'on Knative 0.6 or 0.7' do
+ context 'on Knative 0.6.0' do
before do
- stub_kubeclient_service_pods
- stub_reactive_cache(knative_services_finder,
- {
- services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
- pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- },
- *knative_services_finder.cache_args)
+ prepare_knative_stubs(knative_06_service(knative_stub_options))
end
include_examples 'GET #index with data'
end
+
+ context 'on Knative 0.7.0' do
+ before do
+ prepare_knative_stubs(knative_07_service(knative_stub_options))
+ end
+
+ include_examples 'GET #index with data'
+ end
+ end
+
+ def prepare_knative_stubs(knative_service)
+ stub_kubeclient_service_pods
+ stub_reactive_cache(knative_services_finder,
+ {
+ services: [knative_service],
+ pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
+ },
+ *knative_services_finder.cache_args)
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index c67e7f7dadd..98f8826397f 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -125,7 +125,9 @@ describe Projects::Settings::CiCdController do
context 'when run_auto_devops_pipeline is true' do
before do
- expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true)
+ expect_next_instance_of(Projects::UpdateService) do |instance|
+ expect(instance).to receive(:run_auto_devops_pipeline?).and_return(true)
+ end
end
context 'when the project repository is empty' do
@@ -159,7 +161,9 @@ describe Projects::Settings::CiCdController do
context 'when run_auto_devops_pipeline is not true' do
before do
- expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false)
+ expect_next_instance_of(Projects::UpdateService) do |instance|
+ expect(instance).to receive(:run_auto_devops_pipeline?).and_return(false)
+ end
end
it 'does not queue a CreatePipelineWorker' do
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 0b34656e9e2..667a6336952 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -186,7 +186,8 @@ describe Projects::Settings::OperationsController do
{
grafana_integration_attributes: {
grafana_url: 'https://grafana.gitlab.com',
- token: 'eyJrIjoicDRlRTREdjhhOEZ5WjZPWXUzazJOSW0zZHJUejVOd3IiLCJuIjoiVGVzdCBLZXkiLCJpZCI6MX0='
+ token: 'eyJrIjoicDRlRTREdjhhOEZ5WjZPWXUzazJOSW0zZHJUejVOd3IiLCJuIjoiVGVzdCBLZXkiLCJpZCI6MX0=',
+ enabled: 'true'
}
}
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 042a5542786..d372a94db56 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -92,7 +92,9 @@ describe Projects::SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive(:spam?).and_return(true)
+ end
end
context 'when the snippet is private' do
@@ -170,7 +172,9 @@ describe Projects::SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive(:spam?).and_return(true)
+ end
end
context 'when the snippet is private' do
@@ -278,7 +282,9 @@ describe Projects::SnippetsController do
let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
before do
- allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive_messages(submit_spam: true)
+ end
stub_application_setting(akismet_enabled: true)
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index 7f7cabe3b0c..c0c11db5dd6 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -30,46 +30,61 @@ describe Projects::TreeController do
context "valid branch, no path" do
let(:id) { 'master' }
+
it { is_expected.to respond_with(:success) }
end
context "valid branch, valid path" do
let(:id) { 'master/encoding/' }
+
it { is_expected.to respond_with(:success) }
end
context "valid branch, invalid path" do
let(:id) { 'master/invalid-path/' }
- it { is_expected.to respond_with(:not_found) }
+
+ it 'redirects' do
+ expect(subject)
+ .to redirect_to("/#{project.full_path}/tree/master")
+ end
end
context "invalid branch, valid path" do
let(:id) { 'invalid-branch/encoding/' }
+
it { is_expected.to respond_with(:not_found) }
end
context "valid empty branch, invalid path" do
let(:id) { 'empty-branch/invalid-path/' }
- it { is_expected.to respond_with(:not_found) }
+
+ it 'redirects' do
+ expect(subject)
+ .to redirect_to("/#{project.full_path}/tree/empty-branch")
+ end
end
context "valid empty branch" do
let(:id) { 'empty-branch' }
+
it { is_expected.to respond_with(:success) }
end
context "invalid SHA commit ID" do
let(:id) { 'ff39438/.gitignore' }
+
it { is_expected.to respond_with(:not_found) }
end
context "valid SHA commit ID" do
let(:id) { '6d39438' }
+
it { is_expected.to respond_with(:success) }
end
context "valid SHA commit ID with path" do
let(:id) { '6d39438/.gitignore' }
+
it { expect(response).to have_gitlab_http_status(302) }
end
end
@@ -108,6 +123,7 @@ describe Projects::TreeController do
context 'redirect to blob' do
let(:id) { 'master/README.md' }
+
it 'redirects' do
redirect_url = "/#{project.full_path}/blob/master/README.md"
expect(subject)
diff --git a/spec/controllers/projects/usage_ping_controller_spec.rb b/spec/controllers/projects/usage_ping_controller_spec.rb
new file mode 100644
index 00000000000..a9abbff160d
--- /dev/null
+++ b/spec/controllers/projects/usage_ping_controller_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+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
+
+ context 'when web ide clientside preview is enabled' do
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: true)
+ end
+
+ context 'when the user is not authenticated' do
+ let(:user) { nil }
+
+ it 'returns 302' do
+ subject
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when the user has access to the project' do
+ let(:user) { project.owner }
+
+ it 'increments the counter' do
+ expect do
+ subject
+ end.to change { Gitlab::UsageDataCounters::WebIdeCounter.total_previews_count }.by(1)
+ end
+ end
+ end
+
+ context 'when web ide clientside preview is not enabled' do
+ let(:user) { project.owner }
+
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: false)
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e0df9556eb8..ff0259cd40d 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -653,7 +653,7 @@ describe ProjectsController do
describe "#destroy" do
let(:admin) { create(:admin) }
- it "redirects to the dashboard" do
+ it "redirects to the dashboard", :sidekiq_might_not_need_inline do
controller.instance_variable_set(:@project, project)
sign_in(admin)
@@ -674,7 +674,7 @@ describe ProjectsController do
target_project: project)
end
- it "closes all related merge requests" do
+ it "closes all related merge requests", :sidekiq_might_not_need_inline do
project.merge_requests << merge_request
sign_in(admin)
@@ -927,6 +927,30 @@ describe ProjectsController do
expect(json_response['body']).to match(/\!#{merge_request.iid} \(closed\)/)
end
end
+
+ context 'when path parameter is 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",
+ path: 'files/images/README.md'
+ }
+ end
+
+ before do
+ project_with_repo.add_maintainer(user)
+ end
+
+ it 'renders JSON body with image links expanded' do
+ expanded_path = "/#{project_with_repo.full_path}/raw/master/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 ebeed94c274..c5cfdd32619 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -9,6 +9,51 @@ describe RegistrationsController do
stub_feature_flags(invisible_captcha: false)
end
+ describe '#new' do
+ subject { get :new }
+
+ context 'with the experimental signup flow enabled and the user is part of the experimental group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: true)
+ end
+
+ it 'tracks the event with the right parameters' do
+ expect(Gitlab::Tracking).to receive(:event).with(
+ 'Growth::Acquisition::Experiment::SignUpFlow',
+ 'start',
+ label: anything,
+ property: 'experimental_group'
+ )
+ subject
+ end
+
+ it 'renders new template and sets the resource variable' do
+ expect(subject).to render_template(:new)
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:resource)).to be_a(User)
+ end
+ end
+
+ context 'with the experimental signup flow enabled and the user is part of the control group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: false)
+ end
+
+ it 'does not track the event' do
+ expect(Gitlab::Tracking).not_to receive(:event)
+ subject
+ end
+
+ it 'renders new template and sets the resource variable' do
+ subject
+ expect(response).to have_gitlab_http_status(302)
+ expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
+ end
+ end
+ end
+
describe '#create' do
let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } }
let(:user_params) { { user: base_user_params } }
@@ -217,6 +262,37 @@ describe RegistrationsController do
end
end
+ describe 'tracking data' do
+ context 'with the experimental signup flow enabled and the user is part of the control group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: false)
+ end
+
+ it 'tracks the event with the right parameters' do
+ expect(Gitlab::Tracking).to receive(:event).with(
+ 'Growth::Acquisition::Experiment::SignUpFlow',
+ 'end',
+ label: anything,
+ property: 'control_group'
+ )
+ post :create, params: user_params
+ end
+ end
+
+ context 'with the experimental signup flow enabled and the user is part of the experimental group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: true)
+ end
+
+ it 'does not track the event' do
+ expect(Gitlab::Tracking).not_to receive(:event)
+ post :create, params: user_params
+ end
+ end
+ end
+
it "logs a 'User Created' message" do
stub_feature_flags(registrations_recaptcha: false)
@@ -304,4 +380,22 @@ describe RegistrationsController do
end
end
end
+
+ describe '#update_registration' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: true)
+ sign_in(create(:user))
+ end
+
+ it 'tracks the event with the right parameters' do
+ expect(Gitlab::Tracking).to receive(:event).with(
+ 'Growth::Acquisition::Experiment::SignUpFlow',
+ 'end',
+ label: anything,
+ property: 'experimental_group'
+ )
+ patch :update_registration, params: { user: { name: 'New name', role: 'software_developer', setup_for_company: 'false' } }
+ end
+ end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 2108cf1c8ae..1e47df150b4 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe SessionsController do
include DeviseHelpers
+ include LdapHelpers
describe '#new' do
before do
@@ -34,6 +35,63 @@ describe SessionsController do
end
end
end
+
+ context 'with LDAP enabled' do
+ before do
+ stub_ldap_setting(enabled: true)
+ end
+
+ it 'assigns ldap_servers' do
+ get(:new)
+
+ expect(assigns[:ldap_servers].first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
+ end
+
+ context 'with sign_in disabled' do
+ before do
+ stub_ldap_setting(prevent_ldap_sign_in: true)
+ end
+
+ it 'assigns no ldap_servers' do
+ get(:new)
+
+ expect(assigns[:ldap_servers]).to eq []
+ end
+ end
+ end
+
+ describe 'tracking data' do
+ context 'when the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(signup_flow: true)
+ end
+
+ it 'doesn\'t pass tracking parameters to the frontend' do
+ get(:new)
+ expect(Gon.tracking_data).to be_nil
+ end
+ end
+
+ context 'with the experimental signup flow enabled and the user is part of the control group' do
+ before do
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: false)
+ allow_any_instance_of(described_class).to receive(:experimentation_subject_id).and_return('uuid')
+ end
+
+ it 'passes the right tracking parameters to the frontend' do
+ get(:new)
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Growth::Acquisition::Experiment::SignUpFlow',
+ action: 'start',
+ label: 'uuid',
+ property: 'control_group'
+ }
+ )
+ end
+ end
+ end
end
describe '#create' do
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index e892c736c69..054d448c28d 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -251,7 +251,9 @@ describe SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive(:spam?).and_return(true)
+ end
end
context 'when the snippet is private' do
@@ -323,7 +325,9 @@ describe SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive(:spam?).and_return(true)
+ end
end
context 'when the snippet is private' do
@@ -431,7 +435,9 @@ describe SnippetsController do
let(:snippet) { create(:personal_snippet, :public, author: user) }
before do
- allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_next_instance_of(AkismetService) do |instance|
+ allow(instance).to receive_messages(submit_spam: true)
+ end
stub_application_setting(akismet_enabled: true)
end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 5566df0c216..bbbb9691f53 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -174,7 +174,9 @@ describe UsersController do
let(:user) { create(:user) }
before do
- allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
+ allow_next_instance_of(User) do |instance|
+ allow(instance).to receive(:contributed_projects_ids).and_return([project.id])
+ end
sign_in(user)
project.add_developer(user)
@@ -348,6 +350,48 @@ describe UsersController do
end
end
+ describe 'GET #suggests' do
+ context 'when user exists' do
+ it 'returns JSON indicating the user exists and a suggestion' do
+ get :suggests, params: { username: user.username }
+
+ expected_json = { exists: true, suggests: ["#{user.username}1"] }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+
+ context 'when the casing is different' do
+ let(:user) { create(:user, username: 'CamelCaseUser') }
+
+ it 'returns JSON indicating the user exists and a suggestion' do
+ get :suggests, params: { username: user.username.downcase }
+
+ expected_json = { exists: true, suggests: ["#{user.username.downcase}1"] }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
+ end
+
+ context 'when the user does not exist' do
+ it 'returns JSON indicating the user does not exist' do
+ get :suggests, params: { username: 'foo' }
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+
+ context 'when a user changed their username' do
+ let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') }
+
+ it 'returns JSON indicating a user by that username does not exist' do
+ get :suggests, params: { username: 'old-username' }
+
+ expected_json = { exists: false, suggests: [] }.to_json
+ expect(response.body).to eq(expected_json)
+ end
+ end
+ end
+ end
+
describe '#ensure_canonical_path' do
before do
sign_in(user)
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 53f4a261092..e8b30868801 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -13,7 +13,7 @@ describe 'Database schema' do
# EE: edit the ee/spec/db/schema_support.rb
IGNORED_FK_COLUMNS = {
abuse_reports: %w[reporter_id user_id],
- application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_site_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],
@@ -120,9 +120,55 @@ describe 'Database schema' do
end
end
+ # These pre-existing enums have limits > 2 bytes
+ IGNORED_LIMIT_ENUMS = {
+ 'Analytics::CycleAnalytics::GroupStage' => %w[start_event_identifier end_event_identifier],
+ 'Analytics::CycleAnalytics::ProjectStage' => %w[start_event_identifier end_event_identifier],
+ 'Ci::Bridge' => %w[failure_reason],
+ 'Ci::Build' => %w[failure_reason],
+ 'Ci::BuildMetadata' => %w[timeout_source],
+ 'Ci::BuildTraceChunk' => %w[data_store],
+ 'Ci::JobArtifact' => %w[file_type],
+ 'Ci::Pipeline' => %w[source config_source failure_reason],
+ 'Ci::Runner' => %w[access_level],
+ 'Ci::Stage' => %w[status],
+ 'Clusters::Applications::Ingress' => %w[ingress_type],
+ 'Clusters::Cluster' => %w[platform_type provider_type],
+ 'CommitStatus' => %w[failure_reason],
+ 'GenericCommitStatus' => %w[failure_reason],
+ 'Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetric' => %w[group],
+ 'InternalId' => %w[usage],
+ 'List' => %w[list_type],
+ 'NotificationSetting' => %w[level],
+ 'Project' => %w[auto_cancel_pending_pipelines],
+ 'ProjectAutoDevops' => %w[deploy_strategy],
+ 'PrometheusMetric' => %w[group],
+ 'ResourceLabelEvent' => %w[action],
+ 'User' => %w[layout dashboard project_view],
+ 'UserCallout' => %w[feature_name],
+ 'PrometheusAlert' => %w[operator]
+ }.freeze
+
+ context 'for enums' do
+ ApplicationRecord.descendants.each do |model|
+ describe model do
+ let(:ignored_enums) { ignored_limit_enums(model.name) }
+ let(:enums) { model.defined_enums.keys - ignored_enums }
+
+ it 'uses smallint for enums' do
+ expect(model).to use_smallint_for_enums(enums)
+ end
+ end
+ end
+ end
+
private
def ignored_fk_columns(column)
IGNORED_FK_COLUMNS.fetch(column, [])
end
+
+ def ignored_limit_enums(model)
+ IGNORED_LIMIT_ENUMS.fetch(model, [])
+ end
end
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
index 8a685648c71..e0ea9c38e69 100644
--- a/spec/dependencies/omniauth_saml_spec.rb
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -14,7 +14,9 @@ describe 'processing of SAMLResponse in dependencies' do
before do
allow(saml_strategy).to receive(:session).and_return(session_mock)
- allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:is_valid?).and_return(true)
+ allow_next_instance_of(OneLogin::RubySaml::Response) do |instance|
+ allow(instance).to receive(:is_valid?).and_return(true)
+ end
saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index fefd89728e6..e2ec9d496bc 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
+ # TODO: we can remove this factory in favour of :ci_pipeline
factory :ci_empty_pipeline, class: Ci::Pipeline do
source { :push }
ref { 'master' }
@@ -10,20 +11,6 @@ FactoryBot.define do
project
- factory :ci_pipeline_without_jobs do
- after(:build) do |pipeline|
- pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump({}))
- end
- end
-
- factory :ci_pipeline_with_one_job do
- after(:build) do |pipeline|
- allow(pipeline).to receive(:ci_yaml_file) do
- pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump({ rspec: { script: "ls" } }))
- end
- end
- end
-
# Persist merge request head_pipeline_id
# on pipeline factories to avoid circular references
transient { head_pipeline_of { nil } }
@@ -34,24 +21,8 @@ FactoryBot.define do
end
factory :ci_pipeline do
- transient { config { nil } }
-
- after(:build) do |pipeline, evaluator|
- if evaluator.config
- pipeline.instance_variable_set(:@ci_yaml_file, YAML.dump(evaluator.config))
-
- # Populates pipeline with errors
- pipeline.config_processor if evaluator.config
- else
- pipeline.instance_variable_set(:@ci_yaml_file, File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
- end
- end
-
trait :invalid do
- config do
- { rspec: nil }
- end
-
+ yaml_errors { 'invalid YAML' }
failure_reason { :config_error }
end
@@ -95,6 +66,17 @@ FactoryBot.define do
end
end
+ trait :with_exposed_artifacts do
+ status { :success }
+
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, :artifacts,
+ pipeline: pipeline,
+ project: pipeline.project,
+ options: { artifacts: { expose_as: 'the artifact', paths: ['ci_artifacts.txt'] } })
+ end
+ end
+
trait :with_job do
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index c7ec7c11743..0e59f8cb9ec 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -79,6 +79,15 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
+ factory :clusters_applications_elastic_stack, class: Clusters::Applications::ElasticStack do
+ cluster factory: %i(cluster with_installed_helm provided_by_gcp)
+ end
+
+ factory :clusters_applications_crossplane, class: Clusters::Applications::Crossplane do
+ stack { 'gcp' }
+ cluster factory: %i(cluster with_installed_helm provided_by_gcp)
+ end
+
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 63f33633a3c..609e7e20187 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -93,5 +93,25 @@ FactoryBot.define do
trait :not_managed do
managed { false }
end
+
+ trait :cleanup_not_started do
+ cleanup_status { 1 }
+ end
+
+ trait :cleanup_uninstalling_applications do
+ cleanup_status { 2 }
+ end
+
+ trait :cleanup_removing_project_namespaces do
+ cleanup_status { 3 }
+ end
+
+ trait :cleanup_removing_service_account do
+ cleanup_status { 4 }
+ end
+
+ trait :cleanup_errored do
+ cleanup_status { 5 }
+ end
end
end
diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb
index 2757498e36b..dbcb838e9da 100644
--- a/spec/factories/clusters/platforms/kubernetes.rb
+++ b/spec/factories/clusters/platforms/kubernetes.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do
- cluster
+ association :cluster, platform_type: :kubernetes, provider_type: :user
namespace { nil }
api_url { 'https://kubernetes.example.com' }
token { 'a' * 40 }
diff --git a/spec/factories/clusters/providers/aws.rb b/spec/factories/clusters/providers/aws.rb
index f4bc61455c5..e4b10aa5f33 100644
--- a/spec/factories/clusters/providers/aws.rb
+++ b/spec/factories/clusters/providers/aws.rb
@@ -2,8 +2,7 @@
FactoryBot.define do
factory :cluster_provider_aws, class: Clusters::Providers::Aws do
- cluster
- created_by_user factory: :user
+ association :cluster, platform_type: :kubernetes, provider_type: :aws
role_arn { 'arn:aws:iam::123456789012:role/role-name' }
vpc_id { 'vpc-00000000000000000' }
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
index 83b65dc8087..216c4d4fa31 100644
--- a/spec/factories/clusters/providers/gcp.rb
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -2,7 +2,7 @@
FactoryBot.define do
factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do
- cluster
+ association :cluster, platform_type: :kubernetes, provider_type: :gcp
gcp_project_id { 'test-gcp-project' }
trait :scheduled do
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 3ce71a1b05d..5d635d93ff2 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -7,7 +7,7 @@ FactoryBot.define do
stage_idx { 0 }
status { 'success' }
description { 'commit status'}
- pipeline factory: :ci_pipeline_with_one_job
+ pipeline factory: :ci_pipeline
started_at { 'Tue, 26 Jan 2016 08:21:42 +0100'}
finished_at { 'Tue, 26 Jan 2016 08:23:42 +0100'}
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index f4da206990c..f8738d28d83 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -51,6 +51,10 @@ FactoryBot.define do
finished_at { Time.now }
end
+ trait :created do
+ status { :created }
+ end
+
# This trait hooks the state maechine's events
trait :succeed do
after(:create) do |deployment, evaluator|
diff --git a/spec/factories/error_tracking/detailed_error.rb b/spec/factories/error_tracking/detailed_error.rb
new file mode 100644
index 00000000000..cf7de2ece96
--- /dev/null
+++ b/spec/factories/error_tracking/detailed_error.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :detailed_error_tracking_error, class: Gitlab::ErrorTracking::DetailedError do
+ id { 'id' }
+ title { 'title' }
+ type { 'error' }
+ user_count { 1 }
+ count { 2 }
+ first_seen { Time.now }
+ last_seen { Time.now }
+ message { 'message' }
+ culprit { 'culprit' }
+ external_url { 'http://example.com/id' }
+ external_base_url { 'http://example.com' }
+ project_id { 'project1' }
+ project_name { 'project name' }
+ project_slug { 'project_name' }
+ short_id { 'ID' }
+ status { 'unresolved' }
+ frequency { [] }
+ first_release_last_commit { '68c914da9' }
+ last_release_last_commit { '9ad419c86' }
+ first_release_short_version { 'abc123' }
+ last_release_short_version { 'abc123' }
+
+ skip_create
+ end
+end
diff --git a/spec/factories/error_tracking/error_event.rb b/spec/factories/error_tracking/error_event.rb
new file mode 100644
index 00000000000..44c127e7bf5
--- /dev/null
+++ b/spec/factories/error_tracking/error_event.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :error_tracking_error_event, class: Gitlab::ErrorTracking::ErrorEvent do
+ issue_id { 'id' }
+ date_received { Time.now.iso8601 }
+ stack_trace_entries do
+ {
+ 'stacktrace' =>
+ {
+ 'frames' => [{ 'file' => 'test.rb' }]
+ }
+ }
+ end
+
+ skip_create
+ end
+end
diff --git a/spec/factories/grafana_integrations.rb b/spec/factories/grafana_integrations.rb
index c19417f5a90..ae819ca828c 100644
--- a/spec/factories/grafana_integrations.rb
+++ b/spec/factories/grafana_integrations.rb
@@ -3,7 +3,8 @@
FactoryBot.define do
factory :grafana_integration, class: GrafanaIntegration do
project
- grafana_url { 'https://grafana.com' }
+ grafana_url { 'https://grafana.example.com' }
token { SecureRandom.hex(10) }
+ enabled { true }
end
end
diff --git a/spec/factories/group_group_links.rb b/spec/factories/group_group_links.rb
new file mode 100644
index 00000000000..0711a15b8dd
--- /dev/null
+++ b/spec/factories/group_group_links.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :group_group_link do
+ shared_group { create(:group) }
+ shared_with_group { create(:group) }
+ group_access { GroupMember::DEVELOPER }
+ end
+end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 46910078ee5..24c12a66599 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
project
author { project.creator }
updated_by { author }
+ relative_position { RelativePositioning::START_POSITION }
trait :confidential do
confidential { true }
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index d16e0c10671..42248dc1165 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -100,6 +100,7 @@ FactoryBot.define do
auto_merge_enabled { true }
auto_merge_strategy { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
merge_user { author }
+ merge_params { { sha: diff_head_sha } }
end
trait :remove_source_branch do
@@ -120,6 +121,18 @@ FactoryBot.define do
end
end
+ trait :with_exposed_artifacts do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_exposed_artifacts,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
trait :with_legacy_detached_merge_request_pipeline do
after(:create) do |merge_request|
merge_request.pipelines_for_merge_request << create(:ci_pipeline,
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 9477eeb18d4..2608f717f1c 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -234,10 +234,7 @@ FactoryBot.define do
trait :broken_repo do
after(:create) do |project|
- raise "Failed to create repository!" unless project.create_repository
-
- project.gitlab_shell.rm_directory(project.repository_storage,
- File.join("#{project.disk_path}.git", 'refs'))
+ TestEnv.rm_storage_dir(project.repository_storage, "#{project.disk_path}.git/refs")
end
end
diff --git a/spec/factories/zoom_meetings.rb b/spec/factories/zoom_meetings.rb
new file mode 100644
index 00000000000..b280deca012
--- /dev/null
+++ b/spec/factories/zoom_meetings.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :zoom_meeting do
+ project { issue.project }
+ issue
+ url { 'https://zoom.us/j/123456789' }
+ issue_status { :added }
+
+ trait :added_to_issue do
+ issue_status { :added }
+ end
+
+ trait :removed_from_issue do
+ issue_status { :removed }
+ end
+ end
+end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 48fff9e57d3..93051a8a355 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -51,5 +51,29 @@ describe "Admin::AbuseReports", :js do
end
end
end
+
+ describe 'filtering by user' do
+ let!(:user2) { create(:user) }
+ let!(:abuse_report) { create(:abuse_report, user: user) }
+ let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+
+ it 'shows only single user report' do
+ visit admin_abuse_reports_path
+
+ page.within '.filter-form' do
+ click_button 'User'
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ wait_for_requests
+ end
+
+ expect(page).to have_content(user2.name)
+ expect(page).not_to have_content(user.name)
+ end
+ end
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 058e548208f..7c40ac5bde3 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -73,8 +73,9 @@ describe "Admin::Projects" do
before do
create(:group, name: 'Web')
- allow_any_instance_of(Projects::TransferService)
- .to receive(:move_uploads_to_new_namespace).and_return(true)
+ allow_next_instance_of(Projects::TransferService) do |instance|
+ allow(instance).to receive(:move_uploads_to_new_namespace).and_return(true)
+ end
end
it 'transfers project to group web', :js do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e1c9364067a..99a6165cfc9 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV
include TermsHelper
+ include MobileHelpers
let(:admin) { create(:admin) }
@@ -450,6 +451,32 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(page).to have_link(text: 'Support', href: new_support_url)
end
end
+
+ it 'Shows admin dashboard links on bigger screen' do
+ visit root_dashboard_path
+
+ page.within '.navbar' do
+ expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+ end
+
+ it 'Relocates admin dashboard links to dropdown list on smaller screen', :js do
+ resize_screen_xs
+ visit root_dashboard_path
+
+ page.within '.navbar' do
+ expect(page).not_to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+
+ find('.header-more').click
+
+ page.within '.navbar' do
+ expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+ end
end
context 'when in admin_mode' do
@@ -462,7 +489,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
it 'can leave admin mode' do
page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list
- click_on 'Leave admin mode', match: :first
+ click_on 'Leave Admin Mode', match: :first
expect(page).to have_link(href: new_admin_session_path)
end
@@ -481,7 +508,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
before do
page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list
- click_on 'Leave admin mode', match: :first
+ click_on 'Leave Admin Mode', match: :first
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 29f29e58917..0c8cd895c00 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -179,7 +179,9 @@ describe "Admin::Users" do
end
it "calls send mail" do
- expect_any_instance_of(NotificationService).to receive(:new_user)
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:new_user)
+ end
click_button "Create user"
end
diff --git a/spec/features/admin/clusters/eks_spec.rb b/spec/features/admin/clusters/eks_spec.rb
new file mode 100644
index 00000000000..b262db1ad7c
--- /dev/null
+++ b/spec/features/admin/clusters/eks_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Instance-level AWS EKS Cluster', :js do
+ let(:user) { create(:admin) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when user does not have a cluster and visits group clusters page' do
+ before do
+ visit admin_clusters_path
+
+ click_link 'Add Kubernetes cluster'
+ end
+
+ context 'when user creates a cluster on AWS EKS' do
+ before do
+ click_link 'Amazon EKS'
+ end
+
+ it 'user sees a form to create an EKS cluster' do
+ expect(page).to have_content('Create new Cluster on EKS')
+ end
+ end
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 235b6d0fd40..bac5c9f568e 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -134,11 +134,9 @@ describe 'Contributions Calendar', :js do
shared_examples 'a day with activity' do |contribution_count:|
include_context 'visit user page'
- it 'displays calendar activity square color for 1 contribution' do
+ it 'displays calendar activity square for 1 contribution', :sidekiq_might_not_need_inline do
expect(find('#js-overview')).to have_selector(get_cell_color_selector(contribution_count), count: 1)
- end
- it 'displays calendar activity square on the correct date' do
today = Date.today.strftime(date_format)
expect(find('#js-overview')).to have_selector(get_cell_date_selector(contribution_count, today), count: 1)
end
@@ -154,7 +152,7 @@ describe 'Contributions Calendar', :js do
describe 'issue title is shown on activity page' do
include_context 'visit user page'
- it 'displays calendar activity log' do
+ it 'displays calendar activity log', :sidekiq_might_not_need_inline do
expect(find('#js-overview .overview-content-list .event-target-title')).to have_content issue_title
end
end
@@ -186,11 +184,11 @@ describe 'Contributions Calendar', :js do
end
include_context 'visit user page'
- it 'displays calendar activity squares for both days' do
+ it 'displays calendar activity squares for both days', :sidekiq_might_not_need_inline do
expect(find('#js-overview')).to have_selector(get_cell_color_selector(1), count: 2)
end
- it 'displays calendar activity square for yesterday' do
+ it 'displays calendar activity square for yesterday', :sidekiq_might_not_need_inline do
yesterday = Date.yesterday.strftime(date_format)
expect(find('#js-overview')).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
end
diff --git a/spec/features/clusters/installing_applications_shared_examples.rb b/spec/features/clusters/installing_applications_shared_examples.rb
index cb8fd8c607c..988cd228c1c 100644
--- a/spec/features/clusters/installing_applications_shared_examples.rb
+++ b/spec/features/clusters/installing_applications_shared_examples.rb
@@ -178,6 +178,37 @@ shared_examples "installing applications on a cluster" do
end
end
+ context 'when user installs Elastic Stack' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1', cluster: cluster)
+
+ page.within('.js-cluster-application-row-elastic_stack') do
+ click_button 'Install'
+ end
+ end
+
+ it 'shows status transition' do
+ page.within('.js-cluster-application-row-elastic_stack') do
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_elastic_stack.make_installing!
+
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_elastic_stack.make_installed!
+
+ expect(page).to have_css('.js-cluster-application-uninstall-button', exact_text: 'Uninstall')
+ end
+
+ expect(page).to have_content('Elastic Stack was successfully installed on your Kubernetes cluster')
+ end
+ end
+
context 'when user installs Ingress' do
before do
allow(ClusterInstallAppWorker).to receive(:perform_async)
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 96d8da845cb..f538df89fd3 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -102,7 +102,7 @@ describe 'Commits' do
end
describe 'Cancel all builds' do
- it 'cancels commit', :js do
+ it 'cancels commit', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
@@ -110,7 +110,7 @@ describe 'Commits' do
end
describe 'Cancel build' do
- it 'cancels build', :js do
+ it 'cancels build', :js, :sidekiq_might_not_need_inline do
visit pipeline_path(pipeline)
find('.js-btn-cancel-pipeline').click
expect(page).to have_content 'canceled'
@@ -157,39 +157,6 @@ describe 'Commits' do
end
end
end
-
- describe '.gitlab-ci.yml not found warning' do
- before do
- project.add_reporter(user)
- end
-
- context 'ci builds enabled' do
- it 'does not show warning' do
- visit pipeline_path(pipeline)
-
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
-
- it 'shows warning' do
- stub_ci_pipeline_yaml_file(nil)
-
- visit pipeline_path(pipeline)
-
- expect(page).to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
-
- context 'ci builds disabled' do
- it 'does not show warning' do
- stub_ci_builds_disabled
- stub_ci_pipeline_yaml_file(nil)
-
- visit pipeline_path(pipeline)
-
- expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
- end
- end
- end
end
context 'viewing commits for a branch' do
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 03a2402a2d6..28b68e699e8 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -42,7 +42,7 @@ describe 'Container Registry', :js do
expect(page).to have_content('my/image')
end
- it 'user removes entire container repository' do
+ it 'user removes entire container repository', :sidekiq_might_not_need_inline do
visit_container_registry
expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true)
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 07f0864fb3b..0fc4841ee0e 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -40,7 +40,9 @@ describe 'Cycle Analytics', :js do
context "when there's cycle analytics data" do
before do
- allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+ allow_next_instance_of(Gitlab::ReferenceExtractor) do |instance|
+ allow(instance).to receive(:issues).and_return([issue])
+ end
project.add_maintainer(user)
@build = create_cycle(user, project, issue, mr, milestone, pipeline)
@@ -56,7 +58,7 @@ describe 'Cycle Analytics', :js do
expect(deploys_counter).to have_content('1')
end
- it 'shows data on each stage' do
+ it 'shows data on each stage', :sidekiq_might_not_need_inline do
expect_issue_to_be_present
click_stage('Plan')
@@ -99,7 +101,9 @@ describe 'Cycle Analytics', :js do
project.add_developer(user)
project.add_guest(guest)
- allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+ allow_next_instance_of(Gitlab::ReferenceExtractor) do |instance|
+ allow(instance).to receive(:issues).and_return([issue])
+ end
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 973d5a2dcfc..f10cdf6da1e 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -216,8 +216,7 @@ describe 'Dashboard Projects' do
expect(page).to have_selector('.merge-request-form')
expect(current_path).to eq project_new_merge_request_path(project)
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
- expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
- expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
+ expect(page).to have_content "From feature into master"
end
end
diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb
index 81c77a29ecd..eff63d6a788 100644
--- a/spec/features/explore/groups_spec.rb
+++ b/spec/features/explore/groups_spec.rb
@@ -26,6 +26,10 @@ describe 'Explore Groups', :js do
end
end
+ before do
+ stub_feature_flags({ vue_issuables_list: { enabled: false, thing: group } })
+ end
+
shared_examples 'renders public and internal projects' do
it do
visit_page
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index 00fa85930b1..c499fac6bc0 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -21,7 +21,9 @@ describe 'Global search' do
describe 'I search through the issues and I see pagination' do
before do
- allow_any_instance_of(Gitlab::SearchResults).to receive(:per_page).and_return(1)
+ allow_next_instance_of(Gitlab::SearchResults) do |instance|
+ allow(instance).to receive(:per_page).and_return(1)
+ end
create_list(:issue, 2, project: project, title: 'initial')
end
diff --git a/spec/features/groups/clusters/eks_spec.rb b/spec/features/groups/clusters/eks_spec.rb
new file mode 100644
index 00000000000..b6942304c22
--- /dev/null
+++ b/spec/features/groups/clusters/eks_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Group AWS EKS Cluster', :js do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ gitlab_sign_in(user)
+
+ allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ allow_any_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
+ end
+
+ context 'when user does not have a cluster and visits group clusters page' do
+ before do
+ visit group_clusters_path(group)
+
+ click_link 'Add Kubernetes cluster'
+ end
+
+ context 'when user creates a cluster on AWS EKS' do
+ before do
+ click_link 'Amazon EKS'
+ end
+
+ it 'user sees a form to create an EKS cluster' do
+ expect(page).to have_content('Create new Cluster on EKS')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 8891866c1f8..e06f2efe183 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -13,8 +13,12 @@ describe 'User Cluster', :js do
gitlab_sign_in(user)
allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
- allow_any_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
- allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
+ allow_next_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService) do |instance|
+ allow(instance).to receive(:execute)
+ end
+ allow_next_instance_of(Clusters::Cluster) do |instance|
+ allow(instance).to receive(:retrieve_connection_status).and_return(:connected)
+ end
end
context 'when user does not have a cluster and visits cluster index page' do
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
index c05c3f4f3d6..823c8cc8fad 100644
--- a/spec/features/groups/group_page_with_external_authorization_service_spec.rb
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -15,7 +15,7 @@ describe 'The group page' do
def expect_all_sidebar_links
within('.nav-sidebar') do
- expect(page).to have_link('Overview')
+ expect(page).to have_link('Group overview')
expect(page).to have_link('Details')
expect(page).to have_link('Activity')
expect(page).to have_link('Issues')
@@ -44,7 +44,7 @@ describe 'The group page' do
visit group_path(group)
within('.nav-sidebar') do
- expect(page).to have_link('Overview')
+ expect(page).to have_link('Group overview')
expect(page).to have_link('Details')
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution Analytics')
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 5d87c9d7be8..b9b233026fd 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -11,6 +11,10 @@ describe 'Group issues page' do
let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
let(:path) { issues_group_path(group) }
+ before do
+ stub_feature_flags({ vue_issuables_list: { enabled: false, thing: group } })
+ end
+
context 'with shared examples' do
let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 17738905e8d..65ef0af5be3 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe 'Group milestones' do
- let(:group) { create(:group) }
- let!(:project) { create(:project_empty_repo, group: group) }
- let(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project_empty_repo, group: group) }
+ let_it_be(:user) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
around do |example|
Timecop.freeze { example.run }
@@ -71,9 +71,9 @@ describe 'Group milestones' do
end
context 'when milestones exists' do
- let!(:other_project) { create(:project_empty_repo, group: group) }
+ let_it_be(:other_project) { create(:project_empty_repo, group: group) }
- let!(:active_project_milestone1) do
+ let_it_be(:active_project_milestone1) do
create(
:milestone,
project: project,
@@ -83,12 +83,12 @@ describe 'Group milestones' do
description: 'Lorem Ipsum is simply dummy text'
)
end
- let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.1') }
- let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
- let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
- let!(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
- let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
- let!(:issue) do
+ let_it_be(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.1') }
+ let_it_be(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
+ let_it_be(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
+ let_it_be(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
+ let_it_be(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
+ let_it_be(:issue) do
create :issue, project: project, assignees: [user], author: user, milestone: active_project_milestone1
end
@@ -143,38 +143,111 @@ describe 'Group milestones' do
expect(page).to have_content('Issues 1 Open: 1 Closed: 0')
expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue))
end
+ end
+ end
+
+ describe 'milestone tabs', :js do
+ context 'for a legacy group milestone' do
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:label) { create(:label, project: project) }
+ let_it_be(:issue) { create(:labeled_issue, project: project, milestone: milestone, labels: [label], assignees: [create(:user)]) }
+ let_it_be(:mr) { create(:merge_request, source_project: project, milestone: milestone) }
+
+ before do
+ visit group_milestone_path(group, milestone.title, title: milestone.title)
+ end
+
+ it 'renders the issues tab' do
+ within('#tab-issues') do
+ expect(page).to have_content issue.title
+ end
+ end
+
+ it 'renders the merge requests tab' do
+ within('.js-milestone-tabs') do
+ click_link('Merge Requests')
+ end
- describe 'labels' do
- before do
- create(:label, project: project, title: 'bug') do |label|
- issue.labels << label
- end
+ within('#tab-merge-requests') do
+ expect(page).to have_content mr.title
+ end
+ end
+
+ it 'renders the participants tab' do
+ within('.js-milestone-tabs') do
+ click_link('Participants')
+ end
- create(:label, project: project, title: 'feature') do |label|
- issue.labels << label
- end
+ within('#tab-participants') do
+ expect(page).to have_content issue.assignees.first.name
end
+ end
- it 'renders labels' do
- click_link 'v1.0'
+ it 'renders the labels tab' do
+ within('.js-milestone-tabs') do
+ click_link('Labels')
+ end
- page.within('#tab-issues') do
- expect(page).to have_content 'bug'
- expect(page).to have_content 'feature'
- end
+ within('#tab-labels') do
+ expect(page).to have_content label.title
end
+ end
+ end
+
+ context 'for a group milestone' do
+ let_it_be(:other_project) { create(:project_empty_repo, group: group) }
+ let_it_be(:milestone) { create(:milestone, group: group) }
- it 'renders labels list', :js do
- click_link 'v1.0'
+ let_it_be(:project_label) { create(:label, project: project) }
+ let_it_be(:other_project_label) { create(:label, project: other_project) }
- page.within('.content .nav-links') do
- page.find(:xpath, "//a[@href='#tab-labels']").click
- end
+ let_it_be(:project_issue) { create(:labeled_issue, project: project, milestone: milestone, labels: [project_label], assignees: [create(:user)]) }
+ let_it_be(:other_project_issue) { create(:labeled_issue, project: other_project, milestone: milestone, labels: [other_project_label], assignees: [create(:user)]) }
+
+ let_it_be(:project_mr) { create(:merge_request, source_project: project, milestone: milestone) }
+ let_it_be(:other_project_mr) { create(:merge_request, source_project: other_project, milestone: milestone) }
+
+ before do
+ visit group_milestone_path(group, milestone)
+ end
+
+ it 'renders the issues tab' do
+ within('#tab-issues') do
+ expect(page).to have_content project_issue.title
+ expect(page).to have_content other_project_issue.title
+ end
+ end
+
+ it 'renders the merge requests tab' do
+ within('.js-milestone-tabs') do
+ click_link('Merge Requests')
+ end
+
+ within('#tab-merge-requests') do
+ expect(page).to have_content project_mr.title
+ expect(page).to have_content other_project_mr.title
+ end
+ end
+
+ it 'renders the participants tab' do
+ within('.js-milestone-tabs') do
+ click_link('Participants')
+ end
+
+ within('#tab-participants') do
+ expect(page).to have_content project_issue.assignees.first.name
+ expect(page).to have_content other_project_issue.assignees.first.name
+ end
+ end
+
+ it 'renders the labels tab' do
+ within('.js-milestone-tabs') do
+ click_link('Labels')
+ end
- page.within('#tab-labels') do
- expect(page).to have_content 'bug'
- expect(page).to have_content 'feature'
- end
+ within('#tab-labels') do
+ expect(page).to have_content project_label.title
+ expect(page).to have_content other_project_label.title
end
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index ca994c95df8..e958ebb1275 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -189,7 +189,7 @@ describe 'Group' do
expect(page).to have_selector '#confirm_name_input:focus'
end
- it 'removes group' do
+ it 'removes group', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1)
expect(group.members.all.count).to be_zero
expect(page).to have_content "scheduled for deletion"
@@ -237,14 +237,28 @@ describe 'Group' do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
- let!(:path) { group_path(group) }
it 'renders projects and groups on the page' do
- visit path
+ visit group_path(group)
wait_for_requests
expect(page).to have_content(nested_group.name)
expect(page).to have_content(project.name)
+ expect(page).to have_link('Group overview')
+ end
+
+ it 'renders subgroup page with the text "Subgroup overview"' do
+ visit group_path(nested_group)
+ wait_for_requests
+
+ expect(page).to have_link('Subgroup overview')
+ end
+
+ it 'renders project page with the text "Project overview"' do
+ visit project_path(project)
+ wait_for_requests
+
+ expect(page).to have_link('Project overview')
end
end
diff --git a/spec/features/import/manifest_import_spec.rb b/spec/features/import/manifest_import_spec.rb
index e9471257544..89bf69dea7d 100644
--- a/spec/features/import/manifest_import_spec.rb
+++ b/spec/features/import/manifest_import_spec.rb
@@ -24,7 +24,7 @@ describe 'Import multiple repositories by uploading a manifest file', :js do
expect(page).to have_content('https://android-review.googlesource.com/platform/build/blueprint')
end
- it 'imports successfully imports a project' do
+ it 'imports successfully imports a project', :sidekiq_might_not_need_inline do
visit new_import_manifest_path
attach_file('manifest', Rails.root.join('spec/fixtures/aosp_manifest.xml'))
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index f3b534bca49..efd84cf67b0 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -64,7 +64,7 @@ describe "Internal references", :js do
visit(project_issue_path(public_project, public_project_issue))
end
- it "shows references" do
+ it "shows references", :sidekiq_might_not_need_inline do
page.within("#merge-requests .merge-requests-title") do
expect(page).to have_content("Related merge requests")
expect(page).to have_css(".mr-count-badge")
@@ -133,7 +133,7 @@ describe "Internal references", :js do
visit(project_merge_request_path(public_project, public_project_merge_request))
end
- it "shows references" do
+ it "shows references", :sidekiq_might_not_need_inline do
expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
.and have_content(private_project_user.name)
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 8085918f533..c5818691b3c 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -17,7 +17,9 @@ describe "Jira", :js do
stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
+ allow_next_instance_of(JIRA::Resource::Issue) do |instance|
+ allow(instance).to receive(:remotelink).and_return(remotelink)
+ end
sign_in(user)
@@ -46,7 +48,7 @@ describe "Jira", :js do
end
end
- it "creates a note on the referenced issues" do
+ it "creates a note on the referenced issues", :sidekiq_might_not_need_inline do
click_button("Comment")
wait_for_requests
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index b4531f5da4e..b7813c8ba30 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -57,7 +57,7 @@ describe 'Sort Issuable List' do
it 'is "last updated"' do
visit_merge_requests_with_state(project, 'merged')
- expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(find('.filter-dropdown-container')).to have_content('Last updated')
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
end
@@ -69,7 +69,7 @@ describe 'Sort Issuable List' do
it 'is "last updated"' do
visit_merge_requests_with_state(project, 'closed')
- expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(find('.filter-dropdown-container')).to have_content('Last updated')
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
end
@@ -81,7 +81,7 @@ describe 'Sort Issuable List' do
it 'is "created date"' do
visit_merge_requests_with_state(project, 'all')
- expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(find('.filter-dropdown-container')).to have_content('Created date')
expect(first_merge_request).to include(last_created_issuable.title)
expect(last_merge_request).to include(first_created_issuable.title)
end
@@ -94,7 +94,7 @@ describe 'Sort Issuable List' do
it 'supports sorting in asc and desc order' do
visit_merge_requests_with_state(project, 'open')
- page.within('.issues-other-filters') do
+ page.within('.filter-dropdown-container') do
click_button('Created date')
click_link('Last updated')
end
@@ -102,7 +102,7 @@ describe 'Sort Issuable List' do
expect(first_merge_request).to include(last_updated_issuable.title)
expect(last_merge_request).to include(first_updated_issuable.title)
- find('.issues-other-filters .filter-dropdown-container .rspec-reverse-sort').click
+ find('.filter-dropdown-container .rspec-reverse-sort').click
expect(first_merge_request).to include(first_updated_issuable.title)
expect(last_merge_request).to include(last_updated_issuable.title)
@@ -133,7 +133,7 @@ describe 'Sort Issuable List' do
it 'is "created date"' do
visit_issues project
- expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(find('.filter-dropdown-container')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
@@ -145,7 +145,7 @@ describe 'Sort Issuable List' do
it 'is "created date"' do
visit_issues_with_state(project, 'open')
- expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(find('.filter-dropdown-container')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
@@ -157,7 +157,7 @@ describe 'Sort Issuable List' do
it 'is "last updated"' do
visit_issues_with_state(project, 'closed')
- expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(find('.filter-dropdown-container')).to have_content('Last updated')
expect(first_issue).to include(last_updated_issuable.title)
expect(last_issue).to include(first_updated_issuable.title)
end
@@ -169,7 +169,7 @@ describe 'Sort Issuable List' do
it 'is "created date"' do
visit_issues_with_state(project, 'all')
- expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(find('.filter-dropdown-container')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
@@ -183,7 +183,7 @@ describe 'Sort Issuable List' do
end
it 'shows the sort order as created date' do
- expect(find('.issues-other-filters')).to have_content('Created date')
+ expect(find('.filter-dropdown-container')).to have_content('Created date')
expect(first_issue).to include(last_created_issuable.title)
expect(last_issue).to include(first_created_issuable.title)
end
@@ -196,7 +196,7 @@ describe 'Sort Issuable List' do
it 'supports sorting in asc and desc order' do
visit_issues_with_state(project, 'open')
- page.within('.issues-other-filters') do
+ page.within('.filter-dropdown-container') do
click_button('Created date')
click_link('Last updated')
end
@@ -204,7 +204,7 @@ describe 'Sort Issuable List' do
expect(first_issue).to include(last_updated_issuable.title)
expect(last_issue).to include(first_updated_issuable.title)
- find('.issues-other-filters .filter-dropdown-container .rspec-reverse-sort').click
+ find('.filter-dropdown-container .rspec-reverse-sort').click
expect(first_issue).to include(first_updated_issuable.title)
expect(last_issue).to include(last_updated_issuable.title)
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 1c56902a27d..bb57d69148b 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -68,7 +68,7 @@ describe 'Dropdown hint', :js do
it 'filters with text' do
filtered_search.set('a')
- expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
+ expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
end
end
@@ -104,6 +104,15 @@ describe 'Dropdown hint', :js do
expect_filtered_search_input_empty
end
+ it 'opens the release dropdown when you click on release' do
+ click_hint('release')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-release', visible: true)
+ expect_tokens([{ name: 'Release' }])
+ expect_filtered_search_input_empty
+ end
+
it 'opens the label dropdown when you click on label' do
click_hint('label')
diff --git a/spec/features/issues/filtered_search/dropdown_release_spec.rb b/spec/features/issues/filtered_search/dropdown_release_spec.rb
new file mode 100644
index 00000000000..eea7f2d7848
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_release_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Dropdown release', :js do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:project, :repository) }
+ let!(:user) { create(:user) }
+ let!(:release) { create(:release, tag: 'v1.0', project: project) }
+ let!(:crazy_release) { create(:release, tag: '☺!/"#%&\'{}+,-.<>;=@]_`{|}🚀', project: project) }
+
+ def filtered_search
+ find('.filtered-search')
+ end
+
+ def filter_dropdown
+ find('#js-dropdown-release .filter-dropdown')
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ create(:issue, project: project)
+
+ visit project_issues_path(project)
+ end
+
+ describe 'behavior' do
+ before do
+ filtered_search.set('release:')
+ end
+
+ def expect_results(count)
+ expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: count)
+ end
+
+ it 'loads all the releases when opened' do
+ expect_results(2)
+ end
+
+ it 'filters by tag name' do
+ filtered_search.send_keys("☺")
+ expect_results(1)
+ end
+
+ it 'fills in the release name when the autocomplete hint is clicked' do
+ find('#js-dropdown-release .filter-dropdown-item', text: crazy_release.tag).click
+
+ expect(page).to have_css('#js-dropdown-release', visible: false)
+ expect_tokens([release_token(crazy_release.tag)])
+ expect_filtered_search_input_empty
+ end
+ end
+end
diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb
index 5247baa58a1..74eb699c7ef 100644
--- a/spec/features/issues/notes_on_issues_spec.rb
+++ b/spec/features/issues/notes_on_issues_spec.rb
@@ -23,7 +23,7 @@ describe 'Create notes on issues', :js do
submit_comment(note_text)
end
- it 'creates a note with reference and cross references the issue' do
+ it 'creates a note with reference and cross references the issue', :sidekiq_might_not_need_inline do
page.within('div#notes li.note div.note-text') do
expect(page).to have_content(note_text)
expect(page.find('a')).to have_content(mention.to_reference)
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 be31c45b373..8322a6afa04 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
@@ -67,7 +67,7 @@ describe 'User creates branch and merge request on issue page', :js do
end
context 'when branch name is auto-generated' do
- it 'creates a merge request' do
+ it 'creates a merge request', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
select_dropdown_option('create-mr')
@@ -96,7 +96,7 @@ describe 'User creates branch and merge request on issue page', :js do
context 'when branch name is custom' do
let(:branch_name) { 'custom-branch-name' }
- it 'creates a merge request' do
+ it 'creates a merge request', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
select_dropdown_option('create-mr', branch_name)
diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
index 24089bdeb81..838c0a6349c 100644
--- a/spec/features/issues/user_creates_confidential_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb
@@ -42,7 +42,7 @@ describe 'User creates confidential merge request on issue page', :js do
visit_confidential_issue
end
- it 'create merge request in fork' do
+ it 'create merge request in fork', :sidekiq_might_not_need_inline do
click_button 'Create confidential merge request'
page.within '.create-confidential-merge-request-dropdown-menu' do
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index a71395c0e47..39ce3415727 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -92,19 +92,6 @@ describe "User creates issue" do
.and have_content(label_titles.first)
end
end
-
- context "with Zoom link" do
- it "adds Zoom button" do
- issue_title = "Issue containing Zoom meeting link"
- zoom_url = "https://gitlab.zoom.us/j/123456789"
-
- fill_in("Title", with: issue_title)
- fill_in("Description", with: zoom_url)
- click_button("Submit issue")
-
- expect(page).to have_link('Join Zoom meeting', href: zoom_url)
- end
- end
end
context "when signed in as user with special characters in their name" do
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 165d41950da..ba167362511 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -33,7 +33,6 @@ describe "User toggles subscription", :js do
it 'is disabled' do
expect(page).to have_content('Notifications have been disabled by the project or group owner')
- expect(page).to have_selector('.js-emails-disabled', visible: true)
expect(page).not_to have_selector('.js-issuable-subscribe-button')
end
end
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index 4de67cfcdbe..e7fec41fae3 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching do
+describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidekiq_might_not_need_inline do
include PrometheusHelpers
+ include GrafanaApiHelpers
let(:user) { create(:user) }
let(:project) { create(:prometheus_project) }
@@ -14,11 +15,7 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching do
before do
configure_host
- import_common_metrics
- stub_any_prometheus_request_with_response
-
project.add_developer(user)
-
sign_in(user)
end
@@ -26,31 +23,58 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching do
restore_host
end
- it 'shows embedded metrics' do
- visit project_issue_path(project, issue)
+ context 'internal metrics embeds' do
+ before do
+ import_common_metrics
+ stub_any_prometheus_request_with_response
+ end
+
+ it 'shows embedded metrics' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_css('div.prometheus-graph')
+ expect(page).to have_text('Memory Usage (Total)')
+ expect(page).to have_text('Core Usage (Total)')
+ end
+
+ context 'when dashboard params are in included the url' do
+ let(:metrics_url) { metrics_project_environment_url(project, environment, **chart_params) }
- expect(page).to have_css('div.prometheus-graph')
- expect(page).to have_text('Memory Usage (Total)')
- expect(page).to have_text('Core Usage (Total)')
+ let(:chart_params) do
+ {
+ group: 'System metrics (Kubernetes)',
+ title: 'Memory Usage (Pod average)',
+ y_label: 'Memory Used per Pod (MB)'
+ }
+ end
+
+ it 'shows embedded metrics for the specific chart' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_css('div.prometheus-graph')
+ expect(page).to have_text(chart_params[:title])
+ expect(page).to have_text(chart_params[:y_label])
+ end
+ end
end
- context 'when dashboard params are in included the url' do
- let(:metrics_url) { metrics_project_environment_url(project, environment, **chart_params) }
+ context 'grafana metrics embeds' do
+ let(:grafana_integration) { create(:grafana_integration, project: project) }
+ let(:grafana_base_url) { grafana_integration.grafana_url }
+ let(:metrics_url) { valid_grafana_dashboard_link(grafana_base_url) }
- let(:chart_params) do
- {
- group: 'System metrics (Kubernetes)',
- title: 'Memory Usage (Pod average)',
- y_label: 'Memory Used per Pod (MB)'
- }
+ before do
+ stub_dashboard_request(grafana_base_url)
+ stub_datasource_request(grafana_base_url)
+ stub_all_grafana_proxy_requests(grafana_base_url)
end
- it 'shows embedded metrics for the specifiec chart' do
+ it 'shows embedded metrics' do
visit project_issue_path(project, issue)
expect(page).to have_css('div.prometheus-graph')
- expect(page).to have_text(chart_params[:title])
- expect(page).to have_text(chart_params[:y_label])
+ expect(page).to have_text('Expired / Evicted')
+ expect(page).to have_text('expired - test-attribute-value')
end
end
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 030638cba71..4e161d530d3 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'a maintainer edits files on a source-branch of an MR from a fork', :js do
+describe 'a maintainer edits files on a source-branch of an MR from a fork', :js, :sidekiq_might_not_need_inline do
include ProjectForksHelper
let(:user) { create(:user, username: 'the-maintainer') }
let(:target_project) { create(:project, :public, :repository) }
@@ -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)
+ stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
target_project.add_maintainer(user)
sign_in(user)
@@ -32,6 +32,8 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
wait_for_requests
end
+ it_behaves_like 'rendering a single diff version'
+
it 'mentions commits will go to the source branch' do
expect(page).to have_content('Your changes can be committed to fix because a merge request is open.')
end
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index 4d305d43351..5e1ff232b80 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'User accepts a merge request', :js do
+describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inline do
let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index be403abcc4d..0ecd32565d0 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -23,7 +23,7 @@ describe 'create a merge request, allowing commits from members who can merge to
sign_in(user)
end
- it 'allows setting possible' do
+ it 'allows setting possible', :sidekiq_might_not_need_inline do
visit_new_merge_request
check 'Allow commits from members who can merge to the target branch'
@@ -35,7 +35,7 @@ describe 'create a merge request, allowing commits from members who can merge to
expect(page).to have_content('Allows commits from members who can merge to the target branch')
end
- it 'shows a message when one of the projects is private' do
+ it 'shows a message when one of the projects is private', :sidekiq_might_not_need_inline do
source_project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
visit_new_merge_request
@@ -43,7 +43,7 @@ describe 'create a merge request, allowing commits from members who can merge to
expect(page).to have_content('Not available for private projects')
end
- it 'shows a message when the source branch is protected' do
+ it 'shows a message when the source branch is protected', :sidekiq_might_not_need_inline do
create(:protected_branch, project: source_project, name: 'fix')
visit_new_merge_request
diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb
index 19b8a7f74b7..6a23b6cdf60 100644
--- a/spec/features/merge_request/user_comments_on_diff_spec.rb
+++ b/spec/features/merge_request/user_comments_on_diff_spec.rb
@@ -13,12 +13,15 @@ describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
end
+ it_behaves_like 'rendering a single diff version'
+
context 'when viewing comments' do
context 'when toggling inline comments' do
context 'in a single file' do
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index e0724a04ea3..e6634a8ff39 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -9,6 +9,7 @@ describe 'Merge request > User creates image diff notes', :js do
let(:user) { project.creator }
before do
+ stub_feature_flags(single_mr_diff_view: false)
sign_in(user)
# Stub helper to return any blob file as image from public app folder.
@@ -17,6 +18,8 @@ describe 'Merge request > User creates image diff notes', :js do
allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png')
end
+ it_behaves_like 'rendering a single diff version'
+
context 'create commit diff notes' do
commit_id = '2f63565e7aac07bcdadb654e253078b727143ec4'
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index f92791cc810..67f6d8ebe32 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -25,6 +25,11 @@ describe "User creates a merge request", :js do
click_button("Compare branches")
+ page.within('.merge-request-form') do
+ expect(page.find('#merge_request_title')['placeholder']).to eq 'Title'
+ expect(page.find('#merge_request_description')['placeholder']).to eq 'Describe the goal of the changes and what reviewers should be aware of.'
+ end
+
fill_in("Title", with: title)
click_button("Submit merge request")
@@ -36,7 +41,7 @@ describe "User creates a merge request", :js do
context "to a forked project" do
let(:forked_project) { fork_project(project, user, namespace: user.namespace, repository: true) }
- it "creates a merge request" do
+ it "creates a merge request", :sidekiq_might_not_need_inline do
visit(project_new_merge_request_path(forked_project))
expect(page).to have_content("Source branch").and have_content("Target branch")
diff --git a/spec/features/merge_request/user_edits_merge_request_spec.rb b/spec/features/merge_request/user_edits_merge_request_spec.rb
index 81c56855961..821db8a1d5b 100644
--- a/spec/features/merge_request/user_edits_merge_request_spec.rb
+++ b/spec/features/merge_request/user_edits_merge_request_spec.rb
@@ -17,7 +17,7 @@ describe 'User edits a merge request', :js do
end
it 'changes the target branch' do
- expect(page).to have_content('Target branch')
+ expect(page).to have_content('From master into feature')
select2('merge-test', from: '#merge_request_target_branch')
click_button('Save changes')
diff --git a/spec/features/merge_request/user_expands_diff_spec.rb b/spec/features/merge_request/user_expands_diff_spec.rb
index f7317ec5ca7..ba7abd3af2c 100644
--- a/spec/features/merge_request/user_expands_diff_spec.rb
+++ b/spec/features/merge_request/user_expands_diff_spec.rb
@@ -7,6 +7,8 @@ describe 'User expands diff', :js do
let(:merge_request) { create(:merge_request, source_branch: 'expand-collapse-files', source_project: project, target_project: project) }
before do
+ stub_feature_flags(single_mr_diff_view: false)
+
allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(100.kilobytes)
allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(10.kilobytes)
@@ -15,6 +17,8 @@ describe 'User expands diff', :js do
wait_for_requests
end
+ it_behaves_like 'rendering a single diff version'
+
it 'allows user to expand diff' do
page.within find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do
click_link 'Click to expand it.'
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index da15a4bda4b..32e40740a61 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -10,7 +10,7 @@ describe "User merges a merge request", :js do
end
shared_examples "fast forward merge a merge request" do
- it "merges a merge request" do
+ it "merges a merge request", :sidekiq_might_not_need_inline do
expect(page).to have_content("Fast-forward merge without a merge commit").and have_button("Merge")
page.within(".mr-state-widget") do
diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
index 4afbf30ece4..419f741d0ea 100644
--- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb
@@ -89,12 +89,12 @@ describe 'Merge request > User merges only if pipeline succeeds', :js do
context 'when CI skipped' do
let(:status) { :skipped }
- it 'allows MR to be merged' do
+ it 'does not allow MR to be merged' do
visit project_merge_request_path(project, merge_request)
wait_for_requests
- expect(page).to have_button 'Merge'
+ expect(page).not_to have_button 'Merge'
end
end
end
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index ffc12ffdbaf..e40276f74e4 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -142,7 +142,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
refresh
end
- it 'merges merge request' do
+ it 'merges merge request', :sidekiq_might_not_need_inline do
expect(page).to have_content 'The changes were merged'
expect(merge_request.reload).to be_merged
end
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 8b16760606c..6328c0a5133 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -14,12 +14,15 @@ describe 'Merge request > User posts diff notes', :js do
let(:test_note_comment) { 'this is a test note!' }
before do
+ stub_feature_flags(single_mr_diff_view: false)
set_cookie('sidebar_collapsed', 'true')
project.add_developer(user)
sign_in(user)
end
+ it_behaves_like 'rendering a single diff version'
+
context 'when hovering over a parallel view diff file' do
before do
visit diffs_project_merge_request_path(project, merge_request, view: 'parallel')
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index e3ee80a47d7..f0949fefa3b 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -9,6 +9,7 @@ describe 'Merge request > User resolves conflicts', :js do
before do
# In order to have the diffs collapsed, we need to disable the increase feature
stub_feature_flags(gitlab_git_diff_size_limit_increase: false)
+ stub_feature_flags(single_mr_diff_view: false)
end
def create_merge_request(source_branch)
@@ -17,7 +18,9 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- shared_examples "conflicts are resolved in Interactive mode" do
+ it_behaves_like 'rendering a single diff version'
+
+ shared_examples 'conflicts are resolved in Interactive mode' do
it 'conflicts are resolved in Interactive mode' do
within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do
click_button 'Use ours'
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 8b41ef86791..7cb46d90092 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
@@ -19,6 +19,12 @@ describe 'Merge request > User resolves diff notes and threads', :js do
)
end
+ before do
+ stub_feature_flags(single_mr_diff_view: false)
+ end
+
+ it_behaves_like 'rendering a single diff version'
+
context 'no threads' do
before do
project.add_maintainer(user)
diff --git a/spec/features/merge_request/user_reverts_merge_request_spec.rb b/spec/features/merge_request/user_reverts_merge_request_spec.rb
index 71270b13c14..906ff1d61b2 100644
--- a/spec/features/merge_request/user_reverts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_reverts_merge_request_spec.rb
@@ -20,7 +20,7 @@ describe 'User reverts a merge request', :js do
visit(merge_request_path(merge_request))
end
- it 'reverts a merge request' do
+ it 'reverts a merge request', :sidekiq_might_not_need_inline do
find("a[href='#modal-revert-commit']").click
page.within('#modal-revert-commit') do
@@ -33,7 +33,7 @@ describe 'User reverts a merge request', :js do
wait_for_requests
end
- it 'does not revert a merge request that was previously reverted' do
+ it 'does not revert a merge request that was previously reverted', :sidekiq_might_not_need_inline do
find("a[href='#modal-revert-commit']").click
page.within('#modal-revert-commit') do
@@ -51,7 +51,7 @@ describe 'User reverts a merge request', :js do
expect(page).to have_content('Sorry, we cannot revert this merge request automatically.')
end
- it 'reverts a merge request in a new merge request' do
+ it 'reverts a merge request in a new merge request', :sidekiq_might_not_need_inline do
find("a[href='#modal-revert-commit']").click
page.within('#modal-revert-commit') do
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index baef831c40e..e882b401122 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -7,8 +7,8 @@ describe 'Merge request > User sees avatars on diff notes', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
- let(:path) { "files/ruby/popen.rb" }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: 'Bug NS-04') }
+ let(:path) { 'files/ruby/popen.rb' }
let(:position) do
Gitlab::Diff::Position.new(
old_path: path,
@@ -21,12 +21,15 @@ describe 'Merge request > User sees avatars on diff notes', :js do
let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
before do
+ stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in user
set_cookie('sidebar_collapsed', 'true')
end
+ it_behaves_like 'rendering a single diff version'
+
context 'discussion tab' do
before do
visit project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
index 1d62f7f0702..d7675cd06a8 100644
--- a/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_cherry_pick_modal_spec.rb
@@ -15,7 +15,7 @@ describe 'Merge request > User cherry-picks', :js do
context 'Viewing a merged merge request' do
before do
- service = MergeRequests::MergeService.new(project, user)
+ service = MergeRequests::MergeService.new(project, user, sha: merge_request.diff_head_sha)
perform_enqueued_jobs do
service.execute(merge_request)
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 87fb3f5b3e7..cdffd2ae2f6 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -11,7 +11,7 @@ describe 'Merge request > User sees deployment widget', :js do
let(:role) { :developer }
let(:ref) { merge_request.target_branch }
let(:sha) { project.commit(ref).id }
- let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) }
+ let(:pipeline) { create(:ci_pipeline, sha: sha, project: project, ref: ref) }
let!(:manual) { }
before do
@@ -33,7 +33,7 @@ describe 'Merge request > User sees deployment widget', :js do
end
context 'when a user created a new merge request with the same SHA' do
- let(:pipeline2) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: 'new-patch-1') }
+ let(:pipeline2) { create(:ci_pipeline, sha: sha, project: project, ref: 'new-patch-1') }
let(:build2) { create(:ci_build, :success, pipeline: pipeline2) }
let(:environment2) { create(:environment, project: project) }
let!(:deployment2) { create(:deployment, environment: environment2, sha: sha, ref: 'new-patch-1', deployable: build2) }
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index 8eeed7b0843..82dd779577c 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -9,6 +9,12 @@ describe 'Merge request > User sees diff', :js do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ stub_feature_flags(single_mr_diff_view: false)
+ end
+
+ it_behaves_like 'rendering a single diff version'
+
context 'when linking to note' do
describe 'with unresolved note' do
let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request }
@@ -62,7 +68,7 @@ describe 'Merge request > User sees diff', :js do
end
context 'as author' do
- it 'shows direct edit link' do
+ it 'shows direct edit link', :sidekiq_might_not_need_inline do
sign_in(author_user)
visit diffs_project_merge_request_path(project, merge_request)
@@ -72,7 +78,7 @@ describe 'Merge request > User sees diff', :js do
end
context 'as user who needs to fork' do
- it 'shows fork/cancel confirmation' do
+ it 'shows fork/cancel confirmation', :sidekiq_might_not_need_inline do
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index dd5662d83f2..abf159949db 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -67,13 +67,13 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- it 'sees the latest detached merge request pipeline as the head pipeline' do
+ it 'sees the latest detached merge request pipeline as the head pipeline', :sidekiq_might_not_need_inline do
page.within('.ci-widget-content') do
expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
- context 'when a user updated a merge request in the parent project' do
+ context 'when a user updated a merge request in the parent project', :sidekiq_might_not_need_inline do
let!(:push_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
.execute(:push)
@@ -133,7 +133,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- context 'when a user merges a merge request in the parent project' do
+ context 'when a user merges a merge request in the parent project', :sidekiq_might_not_need_inline do
before do
click_button 'Merge when pipeline succeeds'
@@ -196,7 +196,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- it 'sees the latest branch pipeline as the head pipeline' do
+ it 'sees the latest branch pipeline as the head pipeline', :sidekiq_might_not_need_inline do
page.within('.ci-widget-content') do
expect(page).to have_content("##{push_pipeline.id}")
end
@@ -204,7 +204,7 @@ describe 'Merge request > User sees pipelines triggered by merge request', :js d
end
end
- context 'when a user created a merge request from a forked project to the parent project' do
+ context 'when a user created a merge request from a forked project to the parent project', :sidekiq_might_not_need_inline do
let(:merge_request) do
create(:merge_request,
source_project: forked_project,
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 6b6226ad1c5..098f41f120d 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Merge request > User sees merge widget', :js do
include ProjectForksHelper
include TestReportsHelper
+ include ReactiveCachingHelpers
let(:project) { create(:project, :repository) }
let(:project_only_mwps) { create(:project, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -43,7 +44,7 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request' do
let!(:environment) { create(:environment, project: project) }
let(:sha) { project.commit(merge_request.source_branch).sha }
- let(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+ let(:pipeline) { create(:ci_pipeline, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
let!(:deployment) do
@@ -75,7 +76,7 @@ describe 'Merge request > User sees merge widget', :js do
expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
- it 'allows me to merge, see cherry-pick modal and load branches list' do
+ it 'allows me to merge, see cherry-pick modal and load branches list', :sidekiq_might_not_need_inline do
wait_for_requests
click_button 'Merge'
@@ -190,7 +191,7 @@ describe 'Merge request > User sees merge widget', :js do
end
shared_examples 'pipeline widget' do
- it 'shows head pipeline information' do
+ it 'shows head pipeline information', :sidekiq_might_not_need_inline do
within '.ci-widget-content' do
expect(page).to have_content("Detached merge request pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
end
@@ -229,7 +230,7 @@ describe 'Merge request > User sees merge widget', :js do
end
shared_examples 'pipeline widget' do
- it 'shows head pipeline information' do
+ it 'shows head pipeline information', :sidekiq_might_not_need_inline do
within '.ci-widget-content' do
expect(page).to have_content("Merged result pipeline ##{pipeline.id} pending for #{pipeline.short_sha}")
end
@@ -370,7 +371,7 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'updates the MR widget' do
+ it 'updates the MR widget', :sidekiq_might_not_need_inline do
click_button 'Merge'
page.within('.mr-widget-body') do
@@ -416,7 +417,7 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project, merge_request)
end
- it 'user cannot remove source branch' do
+ it 'user cannot remove source branch', :sidekiq_might_not_need_inline do
expect(page).not_to have_field('remove-source-branch-input')
expect(page).to have_content('Deletes source branch')
end
@@ -435,6 +436,54 @@ describe 'Merge request > User sees merge widget', :js do
end
end
+ context 'exposed artifacts' do
+ subject { visit project_merge_request_path(project, merge_request) }
+
+ context 'when merge request has exposed artifacts' do
+ let(:merge_request) { create(:merge_request, :with_exposed_artifacts, source_project: project) }
+ let(:job) { merge_request.head_pipeline.builds.last }
+ let!(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+ context 'when result has not been parsed yet' do
+ it 'shows parsing status' do
+ subject
+
+ expect(page).to have_content('Loading artifacts')
+ end
+ end
+
+ context 'when result has been parsed' do
+ before do
+ allow_any_instance_of(MergeRequest).to receive(:find_exposed_artifacts).and_return(
+ status: :parsed, data: [
+ {
+ text: "the artifact",
+ url: "/namespace1/project1/-/jobs/1/artifacts/file/ci_artifacts.txt",
+ job_path: "/namespace1/project1/-/jobs/1",
+ job_name: "test"
+ }
+ ])
+ end
+
+ it 'shows the parsed results' do
+ subject
+
+ expect(page).to have_content('View exposed artifact')
+ end
+ end
+ end
+
+ context 'when merge request does not have exposed artifacts' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'does not show parsing status' do
+ subject
+
+ expect(page).not_to have_content('Loading artifacts')
+ end
+ end
+ end
+
context 'when merge request has test reports' do
let!(:head_pipeline) do
create(:ci_pipeline,
@@ -696,7 +745,7 @@ describe 'Merge request > User sees merge widget', :js do
context 'when MR has pipeline but user does not have permission' do
let(:sha) { project.commit(merge_request.source_branch).sha }
- let!(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+ let!(:pipeline) { create(:ci_pipeline, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
before do
project.update(
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index db0d632cdf2..3d25611e1ea 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -11,11 +11,14 @@ describe 'Merge request > User sees MR with deleted source branch', :js do
let(:user) { project.creator }
before do
+ stub_feature_flags(single_mr_diff_view: false)
merge_request.update!(source_branch: 'this-branch-does-not-exist')
sign_in(user)
visit project_merge_request_path(project, merge_request)
end
+ it_behaves_like 'rendering a single diff version'
+
it 'shows a message about missing source branch' do
expect(page).to have_content('Source branch does not exist.')
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 0391794649c..9c9e0dacb87 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
@@ -21,7 +21,7 @@ describe 'Merge request > User sees notes from forked project', :js do
sign_in(user)
end
- it 'user can reply to the comment' do
+ it 'user can reply to the comment', :sidekiq_might_not_need_inline do
visit project_merge_request_path(project, merge_request)
expect(page).to have_content('A commit comment')
diff --git a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
index 3e15a9c136b..d258b98f4a9 100644
--- a/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_from_forked_project_spec.rb
@@ -28,7 +28,7 @@ describe 'Merge request > User sees pipelines from forked project', :js do
visit project_merge_request_path(target_project, merge_request)
end
- it 'user visits a pipelines page' do
+ it 'user visits a pipelines page', :sidekiq_might_not_need_inline do
page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('.ci-table') do
diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb
index 7a8b938486a..f3d8f2b42f8 100644
--- a/spec/features/merge_request/user_sees_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_pipelines_spec.rb
@@ -124,7 +124,7 @@ describe 'Merge request > User sees pipelines', :js do
threads.each { |thr| thr.join }
end
- it 'user sees pipeline in merge request widget' do
+ it 'user sees pipeline in merge request widget', :sidekiq_might_not_need_inline do
visit project_merge_request_path(project, @merge_request)
expect(page.find(".ci-widget")).to have_content(TestEnv::BRANCH_SHA['feature'])
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index 62abcff7bda..c3fce9761df 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -16,11 +16,15 @@ describe 'Merge request > User sees versions', :js do
let!(:params) { {} }
before do
+ stub_feature_flags(single_mr_diff_view: false)
+
project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request, params)
end
+ it_behaves_like 'rendering a single diff version'
+
shared_examples 'allows commenting' do |file_id:, line_code:, comment:|
it do
diff_file_selector = ".diff-file[id='#{file_id}']"
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index 3d26ff3ed94..e2bcdfd1e2b 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -25,12 +25,15 @@ describe 'User comments on a diff', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit(diffs_project_merge_request_path(project, merge_request))
end
+ it_behaves_like 'rendering a single diff version'
+
context 'single suggestion note' do
it 'hides suggestion popover' do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
diff --git a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
index 4db067a4e41..5e59bc87e68 100644
--- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
+++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb
@@ -8,6 +8,7 @@ describe 'Merge request > User toggles whitespace changes', :js do
let(:user) { project.creator }
before do
+ stub_feature_flags(single_mr_diff_view: false)
project.add_maintainer(user)
sign_in(user)
visit diffs_project_merge_request_path(project, merge_request)
@@ -15,6 +16,8 @@ describe 'Merge request > User toggles whitespace changes', :js do
find('.js-show-diff-settings').click
end
+ it_behaves_like 'rendering a single diff version'
+
it 'has a button to toggle whitespace changes' do
expect(page).to have_content 'Show whitespace changes'
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 2d1eb260236..5a29477e597 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -9,6 +9,7 @@ describe 'User views diffs', :js do
let(:project) { create(:project, :public, :repository) }
before do
+ stub_feature_flags(single_mr_diff_view: false)
visit(diffs_project_merge_request_path(project, merge_request))
wait_for_requests
@@ -16,6 +17,8 @@ describe 'User views diffs', :js do
find('.js-toggle-tree-list').click
end
+ it_behaves_like 'rendering a single diff version'
+
shared_examples 'unfold diffs' do
it 'unfolds diffs upwards' do
first('.js-unfold').click
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
index 4fc8c71e47e..a9b96c5bbf5 100644
--- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
@@ -10,7 +10,7 @@ describe 'User squashes a merge request', :js do
let!(:original_head) { project.repository.commit('master') }
shared_examples 'squash' do
- it 'squashes the commits into a single commit, and adds a merge commit' do
+ it 'squashes the commits into a single commit, and adds a merge commit', :sidekiq_might_not_need_inline do
expect(page).to have_content('Merged')
latest_master_commits = project.repository.commits_between(original_head.sha, 'master').map(&:raw)
@@ -31,7 +31,7 @@ describe 'User squashes a merge request', :js do
end
shared_examples 'no squash' do
- it 'accepts the merge request without squashing' do
+ it 'accepts the merge request without squashing', :sidekiq_might_not_need_inline do
expect(page).to have_content('Merged')
expect(project.repository).to be_merged_to_root_ref(source_branch)
end
@@ -47,7 +47,9 @@ describe 'User squashes a merge request', :js do
before do
# Prevent source branch from being removed so we can use be_merged_to_root_ref
# method to check if squash was performed or not
- allow_any_instance_of(MergeRequest).to receive(:force_remove_source_branch?).and_return(false)
+ allow_next_instance_of(MergeRequest) do |instance|
+ allow(instance).to receive(:force_remove_source_branch?).and_return(false)
+ end
project.add_maintainer(user)
sign_in user
diff --git a/spec/features/milestones/user_views_milestones_spec.rb b/spec/features/milestones/user_views_milestones_spec.rb
index 0b51ca12997..09378cab5e3 100644
--- a/spec/features/milestones/user_views_milestones_spec.rb
+++ b/spec/features/milestones/user_views_milestones_spec.rb
@@ -34,4 +34,31 @@ describe "User views milestones" do
.and have_content(closed_issue.title)
end
end
+
+ context "with associated releases" do
+ set(:first_release) { create(:release, project: project, name: "The first release", milestones: [milestone], released_at: Time.zone.parse('2019-10-07')) }
+
+ context "with a single associated release" do
+ it "shows the associated release" do
+ expect(page).to have_content("Release #{first_release.name}")
+ expect(page).to have_link(first_release.name, href: project_releases_path(project, anchor: first_release.tag))
+ end
+ end
+
+ context "with lots of associated releases" do
+ set(:second_release) { create(:release, project: project, name: "The second release", milestones: [milestone], released_at: first_release.released_at + 1.day) }
+ set(:third_release) { create(:release, project: project, name: "The third release", milestones: [milestone], released_at: second_release.released_at + 1.day) }
+ set(:fourth_release) { create(:release, project: project, name: "The fourth release", milestones: [milestone], released_at: third_release.released_at + 1.day) }
+ set(:fifth_release) { create(:release, project: project, name: "The fifth release", milestones: [milestone], released_at: fourth_release.released_at + 1.day) }
+
+ it "shows the associated releases and the truncation text" do
+ expect(page).to have_content("Releases #{fifth_release.name} • #{fourth_release.name} • #{third_release.name} • 2 more releases")
+
+ expect(page).to have_link(fifth_release.name, href: project_releases_path(project, anchor: fifth_release.tag))
+ expect(page).to have_link(fourth_release.name, href: project_releases_path(project, anchor: fourth_release.tag))
+ expect(page).to have_link(third_release.name, href: project_releases_path(project, anchor: third_release.tag))
+ expect(page).to have_link("2 more releases", href: project_releases_path(project))
+ end
+ end
+ end
end
diff --git a/spec/features/populate_new_pipeline_vars_with_params_spec.rb b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
new file mode 100644
index 00000000000..5fe80e73e38
--- /dev/null
+++ b/spec/features/populate_new_pipeline_vars_with_params_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe "Populate new pipeline CI variables with url params", :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:page_path) { new_project_pipeline_path(project) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+
+ visit "#{page_path}?var[key1]=value1&file_var[key2]=value2"
+ end
+
+ it "var[key1]=value1 populates env_var variable correctly" do
+ page.within('.ci-variable-list .js-row:nth-child(1)') do
+ expect(find('.js-ci-variable-input-variable-type').value).to eq('env_var')
+ expect(find('.js-ci-variable-input-key').value).to eq('key1')
+ expect(find('.js-ci-variable-input-value').text).to eq('value1')
+ end
+ end
+
+ it "file_var[key2]=value2 populates file variable correctly" do
+ page.within('.ci-variable-list .js-row:nth-child(2)') do
+ expect(find('.js-ci-variable-input-variable-type').value).to eq('file')
+ expect(find('.js-ci-variable-input-key').value).to eq('key2')
+ expect(find('.js-ci-variable-input-value').text).to eq('value2')
+ end
+ end
+end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index e80a3cd32cc..0147963c0a3 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -22,7 +22,7 @@ describe 'Profile account page', :js do
expect(User.exists?(user.id)).to be_truthy
end
- it 'deletes user', :js do
+ it 'deletes user', :js, :sidekiq_might_not_need_inline do
click_button 'Delete account'
fill_in 'password', with: '12345678'
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 0905ab0aef8..9839b3d6c80 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -23,6 +23,7 @@ describe 'User edit profile' do
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
fill_in 'user_organization', with: 'GitLab'
+ select 'Data Analyst', from: 'user_role'
submit_settings
expect(user.reload).to have_attributes(
@@ -31,7 +32,8 @@ describe 'User edit profile' do
twitter: 'testtwitter',
website_url: 'testurl',
bio: 'I <3 GitLab',
- organization: 'GitLab'
+ organization: 'GitLab',
+ role: 'data_analyst'
)
expect(find('#user_location').value).to eq 'Ukraine'
diff --git a/spec/features/project_group_variables_spec.rb b/spec/features/project_group_variables_spec.rb
new file mode 100644
index 00000000000..c1f1c442937
--- /dev/null
+++ b/spec/features/project_group_variables_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Project group variables', :js do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subgroup_nested) { create(:group, parent: subgroup) }
+ let(:project) { create(:project, group: group) }
+ let(:project2) { create(:project, group: subgroup) }
+ let(:project3) { create(:project, group: subgroup_nested) }
+ let(:key1) { 'test_key' }
+ let(:key2) { 'test_key2' }
+ let(:key3) { 'test_key3' }
+ let!(:ci_variable) { create(:ci_group_variable, group: group, key: key1) }
+ let!(:ci_variable2) { create(:ci_group_variable, group: subgroup, key: key2) }
+ let!(:ci_variable3) { create(:ci_group_variable, group: subgroup_nested, key: key3) }
+ let(:project_path) { project_settings_ci_cd_path(project) }
+ let(:project2_path) { project_settings_ci_cd_path(project2) }
+ let(:project3_path) { project_settings_ci_cd_path(project3) }
+
+ before do
+ sign_in(user)
+ project.add_maintainer(user)
+ group.add_owner(user)
+ end
+
+ it 'project in group shows inherited vars from ancestor group' do
+ visit project_path
+ expect(page).to have_content(key1)
+ expect(page).to have_content(group.name)
+ end
+
+ it 'project in subgroup shows inherited vars from all ancestor groups' do
+ visit project2_path
+ expect(page).to have_content(key1)
+ expect(page).to have_content(key2)
+ expect(page).to have_content(group.name)
+ expect(page).to have_content(subgroup.name)
+ end
+
+ it 'project in nested subgroup shows inherited vars from all ancestor groups' do
+ visit project3_path
+ expect(page).to have_content(key1)
+ expect(page).to have_content(key2)
+ expect(page).to have_content(key3)
+ expect(page).to have_content(group.name)
+ expect(page).to have_content(subgroup.name)
+ expect(page).to have_content(subgroup_nested.name)
+ end
+
+ it 'project origin keys link to ancestor groups ci_cd settings' do
+ visit project_path
+ find('.group-origin-link').click
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ expect(find('.js-ci-variable-input-key').value).to eq(key1)
+ end
+ end
+end
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index f2c57d702a5..af936c80886 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -22,7 +22,7 @@ describe 'Pipeline Badge' do
let!(:job) { create(:ci_build, pipeline: pipeline) }
context 'when the pipeline was successful' do
- it 'displays so on the badge' do
+ it 'displays so on the badge', :sidekiq_might_not_need_inline do
job.success
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
@@ -33,7 +33,7 @@ describe 'Pipeline Badge' do
end
context 'when the pipeline failed' do
- it 'shows displays so on the badge' do
+ it 'shows displays so on the badge', :sidekiq_might_not_need_inline do
job.drop
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
@@ -52,7 +52,7 @@ describe 'Pipeline Badge' do
allow(job).to receive(:prerequisites).and_return([double])
end
- it 'displays the preparing badge' do
+ it 'displays the preparing badge', :sidekiq_might_not_need_inline do
job.enqueue
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
@@ -63,7 +63,7 @@ describe 'Pipeline Badge' do
end
context 'when the pipeline is running' do
- it 'shows displays so on the badge' do
+ it 'shows displays so on the badge', :sidekiq_might_not_need_inline do
create(:ci_build, pipeline: pipeline, name: 'second build', status_event: 'run')
visit pipeline_project_badges_path(project, ref: ref, format: :svg)
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 3b32d213754..0a5bc64b429 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -12,9 +12,11 @@ describe 'Editing file blob', :js do
let(:readme_file_path) { 'README.md' }
before do
- stub_feature_flags(web_ide_default: false)
+ stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
end
+ it_behaves_like 'rendering a single diff version'
+
context 'as a developer' do
let(:user) { create(:user) }
let(:role) { :developer }
@@ -27,14 +29,14 @@ describe 'Editing file blob', :js do
def edit_and_commit(commit_changes: true)
wait_for_requests
find('.js-edit-blob').click
- fill_editor(content: "class NextFeature\\nend\\n")
+ fill_editor(content: 'class NextFeature\\nend\\n')
if commit_changes
click_button 'Commit changes'
end
end
- def fill_editor(content: "class NextFeature\\nend\\n")
+ def fill_editor(content: 'class NextFeature\\nend\\n')
wait_for_requests
find('#editor')
execute_script("ace.edit('editor').setValue('#{content}')")
@@ -60,6 +62,13 @@ describe 'Editing file blob', :js do
expect(page).to have_content 'NextFeature'
end
+ it 'editing a template file in a sub directory does not change path' do
+ project.repository.create_file(user, 'ci/.gitlab-ci.yml', 'test', message: 'testing', branch_name: branch)
+ visit project_edit_blob_path(project, tree_join(branch, 'ci/.gitlab-ci.yml'))
+
+ expect(find_by_id('file_path').value).to eq('ci/.gitlab-ci.yml')
+ end
+
context 'from blob file path' do
before do
visit project_blob_path(project, tree_join(branch, file_path))
@@ -88,13 +97,13 @@ describe 'Editing file blob', :js do
context 'when rendering the preview' do
it 'renders content with CommonMark' do
visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
- fill_editor(content: "1. one\\n - sublist\\n")
+ fill_editor(content: '1. one\\n - sublist\\n')
click_link 'Preview'
wait_for_requests
# the above generates two separate lists (not embedded) in CommonMark
- expect(page).to have_content("sublist")
- expect(page).not_to have_xpath("//ol//li//ul")
+ expect(page).to have_content('sublist')
+ expect(page).not_to have_xpath('//ol//li//ul')
end
end
end
diff --git a/spec/features/projects/clusters/eks_spec.rb b/spec/features/projects/clusters/eks_spec.rb
index 758dccd6e49..e0ebccd85ac 100644
--- a/spec/features/projects/clusters/eks_spec.rb
+++ b/spec/features/projects/clusters/eks_spec.rb
@@ -10,6 +10,7 @@ describe 'AWS EKS Cluster', :js do
project.add_maintainer(user)
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ stub_application_setting(eks_integration_enabled: true)
end
context 'when user does not have a cluster and visits cluster index page' do
@@ -27,7 +28,7 @@ describe 'AWS EKS Cluster', :js do
end
it 'user sees a form to create an EKS cluster' do
- expect(page).to have_selector(:css, '.js-create-eks-cluster')
+ expect(page).to have_content('Create new Cluster on EKS')
end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index b5ab9faa14b..bdc946a9c98 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -67,17 +67,17 @@ describe 'Gcp Cluster', :js do
it 'user sees a cluster details page and creation status' do
subject
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Kubernetes cluster is being created...')
Clusters::Cluster.last.provider.make_created!
- expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
+ expect(page).to have_content('Kubernetes cluster was successfully created')
end
it 'user sees a error if something wrong during creation' do
subject
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ expect(page).to have_content('Kubernetes cluster is being created...')
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 84f2e3e09ae..bdaeda83926 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -13,8 +13,12 @@ describe 'User Cluster', :js do
gitlab_sign_in(user)
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
- allow_any_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
- allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
+ allow_next_instance_of(Clusters::Kubernetes::CreateOrUpdateNamespaceService) do |instance|
+ allow(instance).to receive(:execute)
+ end
+ allow_next_instance_of(Clusters::Cluster) do |instance|
+ allow(instance).to receive(:retrieve_connection_status).and_return(:connected)
+ end
end
context 'when user does not have a cluster and visits cluster index page' do
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 46a6f62ba14..34b15aeaa25 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -55,12 +55,16 @@ describe 'Cherry-pick Commits' do
end
end
- context "I cherry-pick a commit in a new merge request" do
+ context "I cherry-pick a commit in a new merge request", :js do
it do
+ find('.header-action-buttons a.dropdown-toggle').click
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
end
+
+ wait_for_requests
+
expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.")
expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 131d9097f48..b22715a44f0 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -56,8 +56,6 @@ describe 'User browses commits' do
project.enable_ci
create(:ci_build, pipeline: pipeline)
-
- allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return('')
end
it 'renders commit ci info' do
@@ -94,8 +92,12 @@ describe 'User browses commits' do
let(:commit) { create(:commit, project: project) }
it 'renders successfully' do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:binary?).and_return(true)
+ allow_next_instance_of(Gitlab::Diff::File) do |instance|
+ allow(instance).to receive(:blob).and_return(nil)
+ end
+ allow_next_instance_of(Gitlab::Diff::File) do |instance|
+ allow(instance).to receive(:binary?).and_return(true)
+ end
visit(project_commit_path(project, commit))
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 34bde29c8da..df5cec80ae4 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -107,7 +107,9 @@ describe "Compare", :js do
visit project_compare_index_path(project, from: "feature", to: "master")
allow(Commit).to receive(:max_diff_options).and_return(max_files: 3)
- allow_any_instance_of(DiffHelper).to receive(:render_overflow_warning?).and_return(true)
+ allow_next_instance_of(DiffHelper) do |instance|
+ allow(instance).to receive(:render_overflow_warning?).and_return(true)
+ end
click_button('Compare')
@@ -136,7 +138,7 @@ describe "Compare", :js do
def select_using_dropdown(dropdown_type, selection, commit: false)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
- # find input before using to wait for the inputs visiblity
+ # find input before using to wait for the inputs visibility
dropdown.find('.dropdown-menu')
dropdown.fill_in("Filter by Git revision", with: selection)
wait_for_requests
@@ -144,7 +146,7 @@ describe "Compare", :js do
if commit
dropdown.find('input[type="search"]').send_keys(:return)
else
- # find before all to wait for the items visiblity
+ # find before all to wait for the items visibility
dropdown.find("a[data-ref=\"#{selection}\"]", match: :first)
dropdown.all("a[data-ref=\"#{selection}\"]").last.click
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index dd690699ff6..3eab13cb820 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -175,8 +175,9 @@ describe 'Environment' do
#
# In EE we have to stub EE::Environment since it overwrites
# the "terminals" method.
- allow_any_instance_of(Gitlab.ee? ? EE::Environment : Environment)
- .to receive(:terminals) { nil }
+ allow_next_instance_of(Gitlab.ee? ? EE::Environment : Environment) do |instance|
+ allow(instance).to receive(:terminals) { nil }
+ end
visit terminal_project_environment_path(project, environment)
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 1a2302b3d0c..74c2758c30f 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -71,7 +71,9 @@ describe 'Environments page', :js do
let!(:application_prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
before do
- allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ allow_next_instance_of(Kubeclient::Client) do |instance|
+ allow(instance).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
end
it 'shows one environment without error' do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 9ec61743a11..5553e496e7a 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -42,7 +42,9 @@ describe 'Edit Project Settings' do
context 'When external issue tracker is enabled and issues enabled on project settings' do
it 'does not hide issues tab' do
- allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new)
+ end
visit project_path(project)
@@ -54,7 +56,9 @@ describe 'Edit Project Settings' do
it 'hides issues tab' do
project.issues_enabled = false
project.save!
- allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:external_issue_tracker).and_return(JiraService.new)
+ end
visit project_path(project)
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index 0e43f2fd26b..622764487d8 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -7,13 +7,11 @@ describe 'Projects > Files > User views files page' do
let(:user) { project.owner }
before do
- stub_feature_flags(vue_file_list: false)
-
sign_in user
visit project_tree_path(project, project.repository.root_ref)
end
- it 'user sees folders and submodules sorted together, followed by files' do
+ it 'user sees folders and submodules sorted together, followed by files', :js do
rows = all('td.tree-item-file-name').map(&:text)
tree = project.repository.tree
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 943c6e0e959..9fccb3441d6 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -7,7 +7,6 @@ describe 'Projects > Files > Project owner creates a license file', :js do
let(:project_maintainer) { project.owner }
before do
- stub_feature_flags(vue_file_list: false)
project.repository.delete_file(project_maintainer, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
sign_in(project_maintainer)
@@ -39,7 +38,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
end
it 'project maintainer creates a license file from the "Add license" link' do
- click_link 'Add license'
+ click_link 'Add LICENSE'
expect(page).to have_content('New file')
expect(current_path).to eq(
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 9f63b312146..ad6c565c8f9 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -12,7 +12,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file
it 'project maintainer creates a license file from a template' do
visit project_path(project)
- click_on 'Add license'
+ click_on 'Add LICENSE'
expect(page).to have_content('New file')
expect(current_path).to eq(
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 0b3f905b5de..10672bbec68 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -13,23 +13,22 @@ describe "User browses files" do
let(:user) { project.owner }
before do
- stub_feature_flags(vue_file_list: false)
sign_in(user)
end
- it "shows last commit for current directory" do
+ it "shows last commit for current directory", :js do
visit(tree_path_root_ref)
click_link("files")
last_commit = project.repository.last_commit_for_path(project.default_branch, "files")
- page.within(".blob-commit-info") do
+ page.within(".commit-detail") do
expect(page).to have_content(last_commit.short_id).and have_content(last_commit.author_name)
end
end
- context "when browsing the master branch" do
+ context "when browsing the master branch", :js do
before do
visit(tree_path_root_ref)
end
@@ -124,8 +123,7 @@ describe "User browses files" do
expect(current_path).to eq(project_tree_path(project, "markdown/doc/raketasks"))
expect(page).to have_content("backup_restore.md").and have_content("maintenance.md")
- click_link("shop")
- click_link("Maintenance")
+ click_link("maintenance.md")
expect(current_path).to eq(project_blob_path(project, "markdown/doc/raketasks/maintenance.md"))
expect(page).to have_content("bundle exec rake gitlab:env:info RAILS_ENV=production")
@@ -144,7 +142,7 @@ describe "User browses files" do
# rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
- find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown/d")
+ find("a", text: "..")["href"] == project_tree_url(project, "markdown/d")
# rubocop:enable Lint/Void
page.within(".tree-table") do
@@ -168,7 +166,7 @@ describe "User browses files" do
end
end
- context "when browsing a specific ref" do
+ context "when browsing a specific ref", :js do
let(:ref) { project_tree_path(project, "6d39438") }
before do
@@ -180,7 +178,7 @@ describe "User browses files" do
expect(page).to have_content(".gitignore").and have_content("LICENSE")
end
- it "shows files from a repository with apostroph in its name", :js do
+ it "shows files from a repository with apostroph in its name" do
first(".js-project-refs-dropdown").click
page.within(".project-refs-form") do
@@ -191,10 +189,10 @@ describe "User browses files" do
visit(project_tree_path(project, "'test'"))
- expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
+ expect(page).not_to have_selector(".tree-commit .animation-container")
end
- it "shows the code with a leading dot in the directory", :js do
+ it "shows the code with a leading dot in the directory" do
first(".js-project-refs-dropdown").click
page.within(".project-refs-form") do
@@ -203,7 +201,7 @@ describe "User browses files" do
visit(project_tree_path(project, "fix/.testdir"))
- expect(page).to have_css(".tree-commit-link").and have_no_content("Loading commit data...")
+ expect(page).not_to have_selector(".tree-commit .animation-container")
end
it "does not show the permalink link" do
@@ -221,7 +219,7 @@ describe "User browses files" do
click_link(".gitignore")
end
- it "shows a file content", :js do
+ it "shows a file content" do
expect(page).to have_content("*.rbc")
end
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index 08ebeed2cdd..618290416bd 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -7,8 +7,6 @@ describe 'Projects > Files > User browses LFS files' do
let(:user) { project.owner }
before do
- stub_feature_flags(vue_file_list: false)
-
sign_in(user)
end
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 19d95c87c6c..b8765066217 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -13,8 +13,6 @@ describe 'Projects > Files > User creates a directory', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(vue_file_list: false)
-
project.add_developer(user)
sign_in(user)
visit project_tree_path(project, 'master')
@@ -71,7 +69,7 @@ describe 'Projects > Files > User creates a directory', :js do
visit(project2_tree_path_root_ref)
end
- it 'creates a directory in a forked project' do
+ it 'creates a directory in a forked project', :sidekiq_might_not_need_inline do
find('.add-to-tree').click
click_link('New directory')
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index 74c037641cd..eb9a4d8cb09 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'Projects > Files > User creates files' do
+describe 'Projects > Files > User creates files', :js do
let(:fork_message) do
"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."
@@ -14,7 +14,6 @@ describe 'Projects > Files > User creates files' do
let(:user) { create(:user) }
before do
- stub_feature_flags(vue_file_list: false)
stub_feature_flags(web_ide_default: false)
project.add_maintainer(user)
@@ -42,7 +41,7 @@ describe 'Projects > Files > User creates files' do
visit(project2_tree_path_root_ref)
end
- it 'opens new file page on a forked project' do
+ it 'opens new file page on a forked project', :sidekiq_might_not_need_inline do
find('.add-to-tree').click
click_link('New file')
@@ -68,8 +67,7 @@ describe 'Projects > Files > User creates files' do
file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md'
- file_content = find('#file-content', visible: false)
- file_content.set options[:file_content] || 'Some content'
+ find('.ace_text-input', visible: false).send_keys.native.send_keys options[:file_content] || 'Some content'
click_button 'Commit changes'
end
@@ -89,7 +87,7 @@ describe 'Projects > Files > User creates files' do
expect(page).to have_content 'Path cannot include directory traversal'
end
- it 'creates and commit a new file', :js do
+ it 'creates and commit a new file' do
find('#editor')
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:file_name, with: 'not_a_file.md')
@@ -105,7 +103,7 @@ describe 'Projects > Files > User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file with new lines at the end of file', :js do
+ it 'creates and commit a new file with new lines at the end of file' do
find('#editor')
execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
fill_in(:file_name, with: 'not_a_file.md')
@@ -122,7 +120,7 @@ describe 'Projects > Files > User creates files' do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n")
end
- it 'creates and commit a new file with a directory name', :js do
+ it 'creates and commit a new file with a directory name' do
fill_in(:file_name, with: 'foo/bar/baz.txt')
expect(page).to have_selector('.file-editor')
@@ -139,7 +137,7 @@ describe 'Projects > Files > User creates files' do
expect(page).to have_content('*.rbca')
end
- it 'creates and commit a new file specifying a new branch', :js do
+ it 'creates and commit a new file specifying a new branch' do
expect(page).to have_selector('.file-editor')
find('#editor')
@@ -159,7 +157,7 @@ describe 'Projects > Files > User creates files' do
end
end
- context 'when an user does not have write access' do
+ context 'when an user does not have write access', :sidekiq_might_not_need_inline do
before do
project2.add_reporter(user)
visit(project2_tree_path_root_ref)
@@ -174,7 +172,7 @@ describe 'Projects > Files > User creates files' do
expect(page).to have_content(message)
end
- it 'creates and commit new file in forked project', :js do
+ it 'creates and commit new file in forked project' do
expect(page).to have_selector('.file-editor')
find('#editor')
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index fd4783cfb6b..0f543e47631 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -14,8 +14,6 @@ describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(vue_file_list: false)
-
sign_in(user)
end
@@ -47,7 +45,7 @@ describe 'Projects > Files > User deletes files', :js do
wait_for_requests
end
- it 'deletes the file in a forked project', :js do
+ it 'deletes the file in a forked project', :js, :sidekiq_might_not_need_inline do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 56430721ed6..374a7fb7936 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -12,7 +12,6 @@ describe 'Projects > Files > User edits files', :js do
before do
stub_feature_flags(web_ide_default: false)
- stub_feature_flags(vue_file_list: false)
sign_in(user)
end
@@ -136,7 +135,7 @@ describe 'Projects > Files > User edits files', :js do
)
end
- it 'inserts a content of a file in a forked project' do
+ it 'inserts a content of a file in a forked project', :sidekiq_might_not_need_inline do
click_link('.gitignore')
click_button('Edit')
@@ -154,7 +153,7 @@ describe 'Projects > Files > User edits files', :js do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
- it 'opens the Web IDE in a forked project' do
+ it 'opens the Web IDE in a forked project', :sidekiq_might_not_need_inline do
click_link('.gitignore')
click_button('Web IDE')
@@ -168,7 +167,7 @@ describe 'Projects > Files > User edits files', :js do
expect(page).to have_css('.ide .multi-file-tab', text: '.gitignore')
end
- it 'commits an edited file in a forked project' do
+ it 'commits an edited file in a forked project', :sidekiq_might_not_need_inline do
click_link('.gitignore')
find('.js-edit-blob').click
@@ -199,7 +198,7 @@ describe 'Projects > Files > User edits files', :js do
wait_for_requests
end
- it 'links to the forked project for editing' do
+ it 'links to the forked project for editing', :sidekiq_might_not_need_inline do
click_link('.gitignore')
find('.js-edit-blob').click
diff --git a/spec/features/projects/files/user_reads_pipeline_status_spec.rb b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
index 15f8fa7438d..9d38c44b6ef 100644
--- a/spec/features/projects/files/user_reads_pipeline_status_spec.rb
+++ b/spec/features/projects/files/user_reads_pipeline_status_spec.rb
@@ -9,8 +9,6 @@ describe 'user reads pipeline status', :js do
let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
before do
- stub_feature_flags(vue_file_list: false)
-
project.add_maintainer(user)
project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
@@ -25,7 +23,7 @@ describe 'user reads pipeline status', :js do
visit project_tree_path(project, expected_pipeline.ref)
wait_for_requests
- page.within('.blob-commit-info') do
+ page.within('.commit-detail') do
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
end
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index d50bc0a7d18..4c54bbdcd67 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -16,8 +16,6 @@ describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(vue_file_list: false)
-
sign_in(user)
end
@@ -55,7 +53,7 @@ describe 'Projects > Files > User replaces files', :js do
wait_for_requests
end
- it 'replaces an existed file with a new one in a forked project' do
+ it 'replaces an existed file with a new one in a forked project', :sidekiq_might_not_need_inline do
click_link('.gitignore')
expect(page).to have_content('.gitignore')
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 74b5d7c5041..35a3835ff12 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -16,8 +16,6 @@ describe 'Projects > Files > User uploads files' do
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
before do
- stub_feature_flags(vue_file_list: false)
-
project.add_maintainer(user)
sign_in(user)
end
@@ -76,7 +74,7 @@ describe 'Projects > Files > User uploads files' do
visit(project2_tree_path_root_ref)
end
- it 'uploads and commit a new file to a forked project', :js do
+ it 'uploads and commit a new file to a forked project', :js, :sidekiq_might_not_need_inline do
find('.add-to-tree').click
click_link('Upload file')
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 6792a6e2af0..0f97032eefa 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -27,7 +27,7 @@ describe 'Project fork' do
expect(page).to have_css('a.disabled', text: 'Fork')
end
- it 'forks the project' do
+ it 'forks the project', :sidekiq_might_not_need_inline do
visit project_path(project)
click_link 'Fork'
@@ -174,7 +174,7 @@ describe 'Project fork' do
expect(page).to have_css('.fork-thumbnail.disabled')
end
- it 'links to the fork if the project was already forked within that namespace' do
+ it 'links to the fork if the project was already forked within that namespace', :sidekiq_might_not_need_inline do
forked_project = fork_project(project, user, namespace: group, repository: true)
visit new_project_fork_path(project)
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
index 2dbe3d90bad..3b63d9a4c2d 100644
--- a/spec/features/projects/forks/fork_list_spec.rb
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -15,7 +15,7 @@ describe 'listing forks of a project' do
sign_in(user)
end
- it 'shows the forked project in the list with commit as description' do
+ it 'shows the forked project in the list with commit as description', :sidekiq_might_not_need_inline do
visit project_forks_path(source)
page.within('li.project-row') do
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 6082eb03374..5dabaf20952 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -29,12 +29,6 @@ describe 'Project Graph', :js do
end
end
- it 'renders graphs' do
- visit project_graph_path(project, 'master')
-
- expect(page).to have_selector('.stat-graph', visible: false)
- end
-
context 'commits graph' do
before do
visit commits_project_graph_path(project, 'master')
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 7618a2bdea3..c15a3250221 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -26,7 +26,9 @@ describe 'Import/Export - project export integration test', :js do
let(:project) { setup_project }
before do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ allow_next_instance_of(Gitlab::ImportExport) do |instance|
+ allow(instance).to receive(:storage_path).and_return(export_path)
+ end
end
after do
@@ -38,7 +40,7 @@ describe 'Import/Export - project export integration test', :js do
sign_in(user)
end
- it 'exports a project successfully' do
+ it 'exports a project successfully', :sidekiq_might_not_need_inline do
visit edit_project_path(project)
expect(page).to have_content('Export project')
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 6f96da60a31..33c7182c084 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -11,7 +11,9 @@ describe 'Import/Export - project import integration test', :js do
before do
stub_uploads_object_storage(FileUploader)
- allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ allow_next_instance_of(Gitlab::ImportExport) do |instance|
+ allow(instance).to receive(:storage_path).and_return(export_path)
+ end
gitlab_sign_in(user)
end
@@ -27,7 +29,7 @@ describe 'Import/Export - project import integration test', :js do
let(:project_path) { 'test-project-name' + randomHex }
context 'prefilled the path' do
- it 'user imports an exported project successfully' do
+ it 'user imports an exported project successfully', :sidekiq_might_not_need_inline do
visit new_project_path
fill_in :project_name, with: project_name, visible: true
@@ -53,7 +55,7 @@ describe 'Import/Export - project import integration test', :js do
end
context 'path is not prefilled' do
- it 'user imports an exported project successfully' do
+ it 'user imports an exported project successfully', :sidekiq_might_not_need_inline do
visit new_project_path
click_import_project_tab
click_link 'GitLab export'
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index f5d5bc7f5b9..c9568dbb7ce 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -166,7 +166,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
let(:source_project) { fork_project(project, user, repository: true) }
let(:target_project) { project }
- it 'shows merge request iid and source branch' do
+ it 'shows merge request iid and source branch', :sidekiq_might_not_need_inline do
visit project_job_path(source_project, job)
within '.js-pipeline-info' do
@@ -214,7 +214,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
let(:source_project) { fork_project(project, user, repository: true) }
let(:target_project) { project }
- it 'shows merge request iid and source branch' do
+ it 'shows merge request iid and source branch', :sidekiq_might_not_need_inline do
visit project_job_path(source_project, job)
within '.js-pipeline-info' do
diff --git a/spec/features/projects/labels/search_labels_spec.rb b/spec/features/projects/labels/search_labels_spec.rb
index 2d5a138c3cc..e2eec7400ff 100644
--- a/spec/features/projects/labels/search_labels_spec.rb
+++ b/spec/features/projects/labels/search_labels_spec.rb
@@ -68,7 +68,7 @@ describe 'Search for labels', :js do
find('#label-search').native.send_keys(:enter)
page.within('.prioritized-labels') do
- expect(page).to have_content('No prioritised labels with such name or description')
+ expect(page).to have_content('No prioritized labels with such name or description')
end
page.within('.other-labels') do
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index fb1165838c7..cb7a405e821 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -20,7 +20,7 @@ describe 'Projects > Members > Member leaves project' do
expect(project.users.exists?(user.id)).to be_falsey
end
- it 'user leaves project by url param', :js do
+ it 'user leaves project by url param', :js, :quarantine do
visit project_path(project, leave: 1)
page.accept_confirm
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index a77f0bdcbe9..7e7faca9741 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -26,7 +26,6 @@ describe 'Projects > Members > User requests access', :js do
expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project"
expect(project.requesters.exists?(user_id: user)).to be_truthy
- expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
expect(page).not_to have_content 'Leave Project'
@@ -64,7 +63,6 @@ describe 'Projects > Members > User requests access', :js do
accept_confirm { click_link 'Withdraw Access Request' }
- expect(page).to have_content 'Your access request to the project has been withdrawn.'
expect(page).not_to have_content 'Withdraw Access Request'
expect(page).to have_content 'Request Access'
end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index 5e94b2f721e..fb9667cd67d 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -7,6 +7,18 @@ describe 'Project milestone' do
let(:project) { create(:project, name: 'test', namespace: user.namespace) }
let(:milestone) { create(:milestone, project: project) }
+ def toggle_sidebar
+ find('.milestone-sidebar .gutter-toggle').click
+ end
+
+ def sidebar_release_block
+ find('.milestone-sidebar .block.releases')
+ end
+
+ def sidebar_release_block_collapsed_icon
+ find('.milestone-sidebar .block.releases .sidebar-collapsed-icon')
+ end
+
before do
sign_in(user)
end
@@ -39,15 +51,16 @@ describe 'Project milestone' do
context 'when project has disabled issues' do
before do
+ create(:issue, project: project, milestone: milestone)
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
visit project_milestone_path(project, milestone)
end
- it 'hides issues tab' do
+ it 'does not show any issues under the issues tab' do
within('#content-body') do
- expect(page).not_to have_link 'Issues', href: '#tab-issues'
- expect(page).to have_selector '.nav-links li a.active', count: 1
- expect(find('.nav-links li a.active')).to have_content 'Merge Requests'
+ expect(find('.nav-links li a.active')).to have_content 'Issues'
+ expect(page).not_to have_selector '.issuable-row'
end
end
@@ -75,17 +88,96 @@ describe 'Project milestone' do
describe 'the collapsed sidebar' do
before do
- find('.milestone-sidebar .gutter-toggle').click
+ toggle_sidebar
end
it 'shows the total MR and issue counts' do
find('.milestone-sidebar .block', match: :first)
aggregate_failures 'MR and issue blocks' do
- expect(find('.milestone-sidebar .block.issues')).to have_content 1
- expect(find('.milestone-sidebar .block.merge-requests')).to have_content 0
+ expect(find('.milestone-sidebar .block.issues')).to have_content '1'
+ expect(find('.milestone-sidebar .block.merge-requests')).to have_content '0'
end
end
end
end
+
+ context 'when the milestone is not associated with a release' do
+ before do
+ visit project_milestone_path(project, milestone)
+ end
+
+ it 'shows "None" in the "Releases" section' do
+ expect(sidebar_release_block).to have_content 'Releases None'
+ end
+
+ describe 'when the sidebar is collapsed' do
+ before do
+ toggle_sidebar
+ end
+
+ it 'shows "0" in the "Releases" section' do
+ expect(sidebar_release_block).to have_content '0'
+ end
+
+ it 'has a tooltip that reads "Releases"' do
+ expect(sidebar_release_block_collapsed_icon['title']).to eq 'Releases'
+ end
+ end
+ end
+
+ context 'when the milestone is associated with one release' do
+ before do
+ create(:release, project: project, name: 'Version 5', milestones: [milestone])
+
+ visit project_milestone_path(project, milestone)
+ end
+
+ it 'shows "Version 5" in the "Release" section' do
+ expect(sidebar_release_block).to have_content 'Release Version 5'
+ end
+
+ describe 'when the sidebar is collapsed' do
+ before do
+ toggle_sidebar
+ end
+
+ it 'shows "1" in the "Releases" section' do
+ expect(sidebar_release_block).to have_content '1'
+ end
+
+ it 'has a tooltip that reads "1 release"' do
+ expect(sidebar_release_block_collapsed_icon['title']).to eq '1 release'
+ end
+ end
+ end
+
+ context 'when the milestone is associated with multiple releases' do
+ before do
+ (5..10).each do |num|
+ released_at = Time.zone.parse('2019-10-04') + num.months
+ create(:release, project: project, name: "Version #{num}", milestones: [milestone], released_at: released_at)
+ end
+
+ visit project_milestone_path(project, milestone)
+ end
+
+ it 'shows a shortened list of releases in the "Releases" section' do
+ expect(sidebar_release_block).to have_content 'Releases Version 10 • Version 9 • Version 8 • 3 more releases'
+ end
+
+ describe 'when the sidebar is collapsed' do
+ before do
+ toggle_sidebar
+ end
+
+ it 'shows "6" in the "Releases" section' do
+ expect(sidebar_release_block).to have_content '6'
+ end
+
+ it 'has a tooltip that reads "6 releases"' do
+ expect(sidebar_release_block_collapsed_icon['title']).to eq '6 releases'
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages_lets_encrypt_spec.rb
index 8b5964b2eee..d09014e915d 100644
--- a/spec/features/projects/pages_lets_encrypt_spec.rb
+++ b/spec/features/projects/pages_lets_encrypt_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe "Pages with Let's Encrypt", :https_pages_enabled do
include LetsEncryptHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, pages_https_only: false) }
let(:user) { create(:user) }
let(:role) { :maintainer }
let(:certificate_pem) { attributes_for(:pages_domain)[:certificate] }
@@ -18,7 +18,21 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do
project.add_role(user, role)
sign_in(user)
project.namespace.update(owner: user)
- allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:pages_deployed?) { true }
+ end
+ end
+
+ it "creates new domain with Let's Encrypt enabled by default" do
+ visit new_project_pages_domain_path(project)
+
+ fill_in 'Domain', with: 'my.test.domain.com'
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
+ click_button 'Create New Domain'
+
+ expect(page).to have_content('my.test.domain.com')
+ expect(PagesDomain.find_by_domain('my.test.domain.com').auto_ssl_enabled).to eq(true)
end
context 'when the auto SSL management is initially disabled' do
@@ -32,14 +46,14 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do
expect(domain.auto_ssl_enabled).to eq false
expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
- expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
- expect(page).to have_field 'Key (PEM)', type: 'textarea'
+ expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_text domain.subject
find('.js-auto-ssl-toggle-container .project-feature-toggle').click
expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
- expect(page).not_to have_field 'Certificate (PEM)', type: 'textarea'
- expect(page).not_to have_field 'Key (PEM)', type: 'textarea'
+ expect(page).not_to have_selector '.card-header', text: 'Certificate'
+ expect(page).not_to have_text domain.subject
click_on 'Save Changes'
@@ -65,9 +79,6 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do
expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
expect(page).to have_field 'Key (PEM)', type: 'textarea'
- fill_in 'Certificate (PEM)', with: certificate_pem
- fill_in 'Key (PEM)', with: certificate_key
-
click_on 'Save Changes'
expect(domain.reload.auto_ssl_enabled).to eq false
@@ -79,7 +90,8 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do
it 'user do not see private key' do
visit edit_project_pages_domain_path(project, domain)
- expect(find_field('Key (PEM)', visible: :all, disabled: :all).value).to be_blank
+ expect(page).not_to have_selector '.card-header', text: 'Certificate'
+ expect(page).not_to have_text domain.subject
end
end
@@ -96,12 +108,23 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do
end
context 'when certificate is provided by user' do
- let(:domain) { create(:pages_domain, project: project) }
+ let(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) }
+
+ it 'user sees certificate subject' do
+ visit edit_project_pages_domain_path(project, domain)
+
+ expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_text domain.subject
+ end
- it 'user sees private key' do
+ it 'user can delete the certificate', :js do
visit edit_project_pages_domain_path(project, domain)
- expect(find_field('Key (PEM)').value).not_to be_blank
+ expect(page).to have_selector '.card-header', text: 'Certificate'
+ expect(page).to have_text domain.subject
+ within('.card') { accept_confirm { click_on 'Remove' } }
+ expect(page).to have_field 'Certificate (PEM)', with: ''
+ expect(page).to have_field 'Key (PEM)', with: ''
end
end
end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index d55e9d12801..3c4b5b2c4ca 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
shared_examples 'pages settings editing' do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project, pages_https_only: false) }
let(:user) { create(:user) }
let(:role) { :maintainer }
@@ -30,12 +30,52 @@ shared_examples 'pages settings editing' do
expect(page).to have_content('Access pages')
end
+ context 'when pages are disabled in the project settings' do
+ it 'renders disabled warning' do
+ project.project_feature.update!(pages_access_level: ProjectFeature::DISABLED)
+
+ visit project_pages_path(project)
+
+ expect(page).to have_content('GitLab Pages are disabled for this project')
+ end
+ end
+
it 'renders first deployment warning' do
visit project_pages_path(project)
expect(page).to have_content('It may take up to 30 minutes before the site is available after the first deployment.')
end
+ shared_examples 'does not render access control warning' do
+ it 'does not render access control warning' do
+ visit project_pages_path(project)
+
+ expect(page).not_to have_content('Access Control is enabled for this Pages website')
+ end
+ end
+
+ include_examples 'does not render access control warning'
+
+ context 'when access control is enabled in gitlab settings' do
+ before do
+ stub_pages_setting(access_control: true)
+ end
+
+ it 'renders access control warning' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Access Control is enabled for this Pages website')
+ end
+
+ context 'when pages are public' do
+ before do
+ project.project_feature.update!(pages_access_level: ProjectFeature::PUBLIC)
+ end
+
+ include_examples 'does not render access control warning'
+ end
+ end
+
context 'when support for external domains is disabled' do
it 'renders message that support is disabled' do
visit project_pages_path(project)
@@ -93,7 +133,7 @@ shared_examples 'pages settings editing' do
end
end
- context 'when pages are exposed on external HTTPS address', :https_pages_enabled do
+ context 'when pages are exposed on external HTTPS address', :https_pages_enabled, :js do
let(:certificate_pem) do
<<~PEM
-----BEGIN CERTIFICATE-----
@@ -138,6 +178,11 @@ shared_examples 'pages settings editing' do
visit new_project_pages_domain_path(project)
fill_in 'Domain', with: 'my.test.domain.com'
+
+ if ::Gitlab::LetsEncrypt.enabled?
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+ end
+
fill_in 'Certificate (PEM)', with: certificate_pem
fill_in 'Key (PEM)', with: certificate_key
click_button 'Create New Domain'
@@ -145,27 +190,49 @@ shared_examples 'pages settings editing' do
expect(page).to have_content('my.test.domain.com')
end
+ describe 'with dns verification enabled' do
+ before do
+ stub_application_setting(pages_domain_verification_enabled: true)
+ end
+
+ it 'shows the DNS verification record' do
+ domain = create(:pages_domain, project: project)
+
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+ expect(page).to have_field :domain_verification, with: "#{domain.verification_domain} TXT #{domain.keyed_verification_code}"
+ end
+ end
+
describe 'updating the certificate for an existing domain' do
let!(:domain) do
- create(:pages_domain, project: project)
+ create(:pages_domain, project: project, auto_ssl_enabled: false)
end
it 'allows the certificate to be updated' do
visit project_pages_path(project)
- within('#content-body') { click_link 'Details' }
- click_link 'Edit'
+ within('#content-body') { click_link 'Edit' }
click_button 'Save Changes'
expect(page).to have_content('Domain was updated')
end
context 'when the certificate is invalid' do
+ let!(:domain) do
+ create(:pages_domain, :without_certificate, :without_key, project: project)
+ end
+
it 'tells the user what the problem is' do
visit project_pages_path(project)
- within('#content-body') { click_link 'Details' }
- click_link 'Edit'
+ within('#content-body') { click_link 'Edit' }
+
+ if ::Gitlab::LetsEncrypt.enabled?
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+ end
+
fill_in 'Certificate (PEM)', with: 'invalid data'
click_button 'Save Changes'
@@ -174,6 +241,27 @@ shared_examples 'pages settings editing' do
expect(page).to have_content("Key doesn't match the certificate")
end
end
+
+ it 'allows the certificate to be removed', :js do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+
+ accept_confirm { click_link 'Remove' }
+
+ expect(page).to have_field('Certificate (PEM)', with: '')
+ expect(page).to have_field('Key (PEM)', with: '')
+ domain.reload
+ expect(domain.certificate).to be_nil
+ expect(domain.key).to be_nil
+ end
+
+ it 'shows the DNS CNAME record' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Edit' }
+ expect(page).to have_field :domain_dns, with: "#{domain.domain} CNAME #{domain.project.pages_subdomain}.#{Settings.pages.host}."
+ end
end
end
end
@@ -210,7 +298,7 @@ shared_examples 'pages settings editing' do
end
end
- describe 'HTTPS settings', :js, :https_pages_enabled do
+ describe 'HTTPS settings', :https_pages_enabled do
before do
project.namespace.update(owner: user)
@@ -318,18 +406,21 @@ shared_examples 'pages settings editing' do
expect(page).to have_link('Remove pages')
- click_link 'Remove pages'
+ accept_confirm { click_link 'Remove pages' }
- expect(project.pages_deployed?).to be_falsey
+ expect(page).to have_content('Pages were removed')
+ expect(project.reload.pages_deployed?).to be_falsey
end
end
end
end
-describe 'Pages' do
+describe 'Pages', :js do
include LetsEncryptHelpers
- include_examples 'pages settings editing'
+ context 'when editing normally' do
+ include_examples 'pages settings editing'
+ end
context 'when letsencrypt support is enabled' do
before do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 04adb1ec6af..94fac9a2eb5 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -128,7 +128,7 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the running build and shows retry button' do
+ it 'cancels the running build and shows retry button', :sidekiq_might_not_need_inline do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
@@ -146,7 +146,7 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the preparing build and shows retry button' do
+ it 'cancels the preparing build and shows retry button', :sidekiq_might_not_need_inline do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
@@ -186,7 +186,7 @@ describe 'Pipeline', :js do
end
end
- it 'unschedules the delayed job and shows play button as a manual job' do
+ it 'unschedules the delayed job and shows play button as a manual job', :sidekiq_might_not_need_inline do
find('#ci-badge-delayed-job .ci-action-icon-container').click
page.within('#ci-badge-delayed-job') do
@@ -305,7 +305,9 @@ describe 'Pipeline', :js do
find('.js-retry-button').click
end
- it { expect(page).not_to have_content('Retry') }
+ it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Retry')
+ end
end
end
@@ -321,7 +323,9 @@ describe 'Pipeline', :js do
click_on 'Cancel running'
end
- it { expect(page).not_to have_content('Cancel running') }
+ it 'does not show a "Cancel running" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Cancel running')
+ end
end
end
@@ -400,7 +404,7 @@ describe 'Pipeline', :js do
visit project_pipeline_path(source_project, pipeline)
end
- it 'shows the pipeline information' do
+ it 'shows the pipeline information', :sidekiq_might_not_need_inline do
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -473,7 +477,7 @@ describe 'Pipeline', :js do
visit project_pipeline_path(source_project, pipeline)
end
- it 'shows the pipeline information' do
+ it 'shows the pipeline information', :sidekiq_might_not_need_inline do
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -651,7 +655,9 @@ describe 'Pipeline', :js do
find('.js-retry-button').click
end
- it { expect(page).not_to have_content('Retry') }
+ it 'does not show a "Retry" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Retry')
+ end
end
end
@@ -663,7 +669,9 @@ describe 'Pipeline', :js do
click_on 'Cancel running'
end
- it { expect(page).not_to have_content('Cancel running') }
+ it 'does not show a "Cancel running" button', :sidekiq_might_not_need_inline do
+ expect(page).not_to have_content('Cancel running')
+ end
end
end
@@ -778,10 +786,10 @@ describe 'Pipeline', :js do
expect(page).to have_content(failed_build.stage)
end
- it 'does not show trace' do
+ it 'does not show log' do
subject
- expect(page).to have_content('No job trace')
+ expect(page).to have_content('No job log')
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 76d8ad1638b..f6eeb8d7065 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -133,14 +133,14 @@ describe 'Pipelines', :js do
wait_for_requests
end
- it 'indicated that pipelines was canceled' do
+ it 'indicated that pipelines was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
end
- context 'when pipeline is retryable' do
+ context 'when pipeline is retryable', :sidekiq_might_not_need_inline do
let!(:build) do
create(:ci_build, pipeline: pipeline,
stage: 'test')
@@ -185,33 +185,29 @@ describe 'Pipelines', :js do
visit project_pipelines_path(source_project)
end
- shared_examples_for 'showing detached merge request pipeline information' do
- it 'shows detached tag for the pipeline' do
+ shared_examples_for 'detached merge request pipeline' do
+ it 'shows pipeline information without pipeline ref', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).to have_content('detached')
end
- end
- it 'shows the link of the merge request' do
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
- end
- it 'does not show the ref of the pipeline' do
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
end
end
- it_behaves_like 'showing detached merge request pipeline information'
+ it_behaves_like 'detached merge request pipeline'
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
- it_behaves_like 'showing detached merge request pipeline information'
+ it_behaves_like 'detached merge request pipeline'
end
end
@@ -233,20 +229,16 @@ describe 'Pipelines', :js do
end
shared_examples_for 'Correct merge request pipeline information' do
- it 'does not show detached tag for the pipeline' do
+ it 'does not show detached tag for the pipeline, and shows the link of the merge request, and does not show the ref of the pipeline', :sidekiq_might_not_need_inline do
within '.pipeline-tags' do
expect(page).not_to have_content('detached')
end
- end
- it 'shows the link of the merge request' do
within '.branch-commit' do
expect(page).to have_link(merge_request.iid,
href: project_merge_request_path(project, merge_request))
end
- end
- it 'does not show the ref of the pipeline' do
within '.branch-commit' do
expect(page).not_to have_link(pipeline.ref)
end
@@ -429,7 +421,7 @@ describe 'Pipelines', :js do
find('.js-modal-primary-action').click
end
- it 'indicates that pipeline was canceled' do
+ it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do
expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
@@ -452,7 +444,7 @@ describe 'Pipelines', :js do
expect(page).not_to have_selector('.js-pipelines-retry-button')
end
- it 'has failed pipeline' do
+ it 'has failed pipeline', :sidekiq_might_not_need_inline do
expect(page).to have_selector('.ci-failed')
end
end
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index d96e243d96b..9bbeb0eb260 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -102,5 +102,30 @@ describe 'Projects > Settings > For a forked project', :js do
end
end
end
+
+ context 'grafana integration settings form' do
+ it 'successfully fills and completes the form' do
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+
+ within '.js-grafana-integration' do
+ click_button('Expand')
+ end
+
+ expect(page).to have_content('Grafana URL')
+ expect(page).to have_content('API Token')
+ expect(page).to have_button('Save Changes')
+
+ fill_in('grafana-url', with: 'http://gitlab-test.grafana.net')
+ fill_in('grafana-token', with: 'token')
+
+ click_button('Save Changes')
+
+ wait_for_requests
+
+ assert_text('Your changes have been saved')
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 9f09c5c4501..c0089e3c28c 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -107,4 +107,27 @@ describe 'Projects > Settings > User manages merge request settings' do
expect(project.printing_merge_request_link_enabled).to be(false)
end
end
+
+ describe 'Checkbox to remove source branch after merge', :js do
+ it 'is initially checked' do
+ checkbox = find_field('project_remove_source_branch_after_merge')
+ expect(checkbox).to be_checked
+ end
+
+ it 'when unchecked sets :remove_source_branch_after_merge to false' do
+ uncheck('project_remove_source_branch_after_merge')
+ within('.merge-request-settings-form') do
+ find('.qa-save-merge-request-changes')
+ click_on('Save changes')
+ end
+
+ find('.flash-notice')
+ checkbox = find_field('project_remove_source_branch_after_merge')
+
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.remove_source_branch_after_merge).to be(false)
+ end
+ end
end
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index bbb3a066ed5..ff133b58f89 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -2,12 +2,11 @@
require 'spec_helper'
-describe 'Projects > Show > Collaboration links' do
+describe 'Projects > Show > Collaboration links', :js do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
- stub_feature_flags(vue_file_list: false)
project.add_developer(user)
sign_in(user)
end
@@ -17,15 +16,21 @@ describe 'Projects > Show > Collaboration links' do
# The navigation bar
page.within('.header-new') do
+ find('.qa-new-menu-toggle').click
+
aggregate_failures 'dropdown links in the navigation bar' do
expect(page).to have_link('New issue')
expect(page).to have_link('New merge request')
expect(page).to have_link('New snippet', href: new_project_snippet_path(project))
end
+
+ find('.qa-new-menu-toggle').click
end
# The dropdown above the tree
page.within('.repo-breadcrumb') do
+ find('.qa-add-to-tree').click
+
aggregate_failures 'dropdown links above the repo tree' do
expect(page).to have_link('New file')
expect(page).to have_link('Upload file')
@@ -45,23 +50,19 @@ describe 'Projects > Show > Collaboration links' do
visit project_path(project)
page.within('.header-new') do
+ find('.qa-new-menu-toggle').click
+
aggregate_failures 'dropdown links' do
expect(page).not_to have_link('New issue')
expect(page).not_to have_link('New merge request')
expect(page).not_to have_link('New snippet', href: new_project_snippet_path(project))
end
- end
- page.within('.repo-breadcrumb') do
- aggregate_failures 'dropdown links' do
- expect(page).not_to have_link('New file')
- expect(page).not_to have_link('Upload file')
- expect(page).not_to have_link('New directory')
- expect(page).not_to have_link('New branch')
- expect(page).not_to have_link('New tag')
- end
+ find('.qa-new-menu-toggle').click
end
+ expect(page).not_to have_selector('.qa-add-to-tree')
+
expect(page).not_to have_link('Web IDE')
end
end
diff --git a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
index fdc238d55cf..cf1a679102c 100644
--- a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
+++ b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb
@@ -5,10 +5,6 @@ require 'spec_helper'
describe 'Projects > Show > User sees last commit CI status' do
set(:project) { create(:project, :repository, :public) }
- before do
- stub_feature_flags(vue_file_list: false)
- end
-
it 'shows the project README', :js do
project.enable_ci
pipeline = create(:ci_pipeline, project: project, sha: project.commit.sha, ref: 'master')
@@ -16,9 +12,9 @@ describe 'Projects > Show > User sees last commit CI status' do
visit project_path(project)
- page.within '.blob-commit-info' do
+ page.within '.commit-detail' do
expect(page).to have_content(project.commit.sha[0..6])
- expect(page).to have_link('Pipeline: skipped')
+ expect(page).to have_selector('[aria-label="Commit: skipped"]')
end
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index c136d7607fd..41c3c6b5770 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -59,8 +59,8 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
end
it '"Add license" button linked to new file populated for a license' do
- page.within('.project-stats') do
- expect(page).to have_link('Add license', href: presenter.add_license_path)
+ page.within('.project-buttons') do
+ expect(page).to have_link('Add LICENSE', href: presenter.add_license_path)
end
end
end
@@ -175,7 +175,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.license_blob).not_to be_nil
page.within('.project-buttons') do
- expect(page).not_to have_link('Add license')
+ expect(page).not_to have_link('Add LICENSE')
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index ca616be341d..180ffac4d4d 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -10,7 +10,6 @@ describe 'Projects tree', :js do
let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' }
before do
- stub_feature_flags(vue_file_list: false)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
index beb32104809..832985f1a30 100644
--- a/spec/features/projects/view_on_env_spec.rb
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -9,9 +9,13 @@ describe 'View on environment', :js do
let(:user) { project.creator }
before do
+ stub_feature_flags(single_mr_diff_view: false)
+
project.add_maintainer(user)
end
+ it_behaves_like 'rendering a single diff version'
+
context 'when the branch has a route map' do
let(:route_map) do
<<-MAP.strip_heredoc
@@ -26,7 +30,7 @@ describe 'View on environment', :js do
user,
start_branch: branch_name,
branch_name: branch_name,
- commit_message: "Add .gitlab/route-map.yml",
+ commit_message: 'Add .gitlab/route-map.yml',
file_path: '.gitlab/route-map.yml',
file_content: route_map
).execute
@@ -37,9 +41,9 @@ describe 'View on environment', :js do
user,
start_branch: branch_name,
branch_name: branch_name,
- commit_message: "Update feature",
+ commit_message: 'Update feature',
file_path: file_path,
- file_content: "# Noop"
+ file_content: '# Noop'
).execute
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 67ae26d8d1e..90e48f3c230 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -6,10 +6,6 @@ describe 'Project' do
include ProjectForksHelper
include MobileHelpers
- before do
- stub_feature_flags(vue_file_list: false)
- end
-
describe 'creating from template' do
let(:user) { create(:user) }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
@@ -190,7 +186,7 @@ describe 'Project' do
sign_in user
end
- it 'shows a link to the source project when it is available' do
+ it 'shows a link to the source project when it is available', :sidekiq_might_not_need_inline do
visit project_path(forked_project)
expect(page).to have_content('Forked from')
@@ -206,7 +202,7 @@ describe 'Project' do
expect(page).not_to have_content('Forked from')
end
- it 'shows the name of the deleted project when the source was deleted' do
+ it 'shows the name of the deleted project when the source was deleted', :sidekiq_might_not_need_inline do
forked_project
Projects::DestroyService.new(base_project, base_project.owner).execute
@@ -218,7 +214,7 @@ describe 'Project' do
context 'a fork of a fork' do
let(:fork_of_fork) { fork_project(forked_project, user, repository: true) }
- it 'links to the base project if the source project is removed' do
+ it 'links to the base project if the source project is removed', :sidekiq_might_not_need_inline do
fork_of_fork
Projects::DestroyService.new(forked_project, user).execute
@@ -263,7 +259,7 @@ describe 'Project' do
expect(page).to have_selector '#confirm_name_input:focus'
end
- it 'removes a project' do
+ it 'removes a project', :sidekiq_might_not_need_inline do
expect { remove_with_confirm('Remove project', project.path) }.to change { Project.count }.by(-1)
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(Project.all.count).to be_zero
@@ -272,7 +268,7 @@ describe 'Project' do
end
end
- describe 'tree view (default view is set to Files)' do
+ describe 'tree view (default view is set to Files)', :js do
let(:user) { create(:user, project_view: 'files') }
let(:project) { create(:forked_project_with_submodules) }
@@ -285,19 +281,19 @@ describe 'Project' do
it 'has working links to files' do
click_link('PROCESS.md')
- expect(page.status_code).to eq(200)
+ expect(page).to have_selector('.file-holder')
end
it 'has working links to directories' do
click_link('encoding')
- expect(page.status_code).to eq(200)
+ expect(page).to have_selector('.breadcrumb-item', text: 'encoding')
end
it 'has working links to submodules' do
click_link('645f6c4c')
- expect(page.status_code).to eq(200)
+ expect(page).to have_selector('.qa-branches-select', text: '645f6c4c82fd3f5e06f67134450a570b795e55a6')
end
context 'for signed commit on default branch', :js do
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
deleted file mode 100644
index 38699f0cc1b..00000000000
--- a/spec/features/raven_js_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'RavenJS' do
- let(:raven_path) { '/raven.chunk.js' }
-
- it 'does not load raven if sentry is disabled' do
- visit new_user_session_path
-
- expect(has_requested_raven).to eq(false)
- end
-
- it 'loads raven if sentry is enabled' do
- stub_sentry_settings
-
- visit new_user_session_path
-
- expect(has_requested_raven).to eq(true)
- end
-
- def has_requested_raven
- page.all('script', visible: false).one? do |elm|
- elm[:src] =~ /#{raven_path}$/
- end
- end
-end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 7e7c09e4a13..7b969aea547 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -26,10 +26,20 @@ describe 'User uses header search field', :js do
end
end
+ context 'when using the keyboard shortcut' do
+ before do
+ find('#search.js-autocomplete-disabled')
+ find('body').native.send_keys('s')
+ end
+
+ it 'shows the category search dropdown' do
+ expect(page).to have_selector('.dropdown-header', text: /#{scope_name}/i)
+ end
+ end
+
context 'when clicking the search field' do
before do
- page.find('#search').click
- wait_for_all_requests
+ page.find('#search.js-autocomplete-disabled').click
end
it 'shows category search dropdown' do
@@ -78,15 +88,21 @@ describe 'User uses header search field', :js do
end
context 'when entering text into the search field' do
- before do
+ it 'does not display the category search dropdown' do
page.within('.search-input-wrap') do
fill_in('search', with: scope_name.first(4))
end
- end
- it 'does not display the category search dropdown' do
expect(page).not_to have_selector('.dropdown-header', text: /#{scope_name}/i)
end
+
+ it 'hides the dropdown when there are no results' do
+ page.within('.search-input-wrap') do
+ fill_in('search', with: 'a_search_term_with_no_results')
+ end
+
+ expect(page).not_to have_selector('.dropdown-menu')
+ end
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 768b883a90e..9c1c81918fa 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -264,7 +264,9 @@ describe "Internal Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:branches).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:branches).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
@@ -283,7 +285,9 @@ describe "Internal Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:tags).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:tags).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index c2d44c05a22..dbaf97bc3fd 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -236,7 +236,9 @@ describe "Private Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:branches).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:branches).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
@@ -255,7 +257,9 @@ describe "Private Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:tags).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:tags).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 19f01257713..35cbc195f4f 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -477,7 +477,9 @@ describe "Public Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:branches).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:branches).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
@@ -496,7 +498,9 @@ describe "Public Project Access" do
before do
# Speed increase
- allow_any_instance_of(Project).to receive(:tags).and_return([])
+ allow_next_instance_of(Project) do |instance|
+ allow(instance).to receive(:tags).and_return([])
+ end
end
it { is_expected.to be_allowed_for(:admin) }
diff --git a/spec/features/sentry_js_spec.rb b/spec/features/sentry_js_spec.rb
new file mode 100644
index 00000000000..b39c4f0a0ae
--- /dev/null
+++ b/spec/features/sentry_js_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Sentry' do
+ let(:sentry_path) { '/sentry.chunk.js' }
+
+ it 'does not load sentry if sentry is disabled' do
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+ visit new_user_session_path
+
+ expect(has_requested_sentry).to eq(false)
+ end
+
+ it 'loads sentry if sentry is enabled' do
+ stub_sentry_settings
+
+ visit new_user_session_path
+
+ expect(has_requested_sentry).to eq(true)
+ end
+
+ def has_requested_sentry
+ page.all('script', visible: false).one? do |elm|
+ elm[:src] =~ /#{sentry_path}$/
+ end
+ end
+end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index 70e6978a7b6..f56bd055224 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'GPG signed commits' do
let(:project) { create(:project, :public, :repository) }
- it 'changes from unverified to verified when the user changes his email to match the gpg key' do
+ it 'changes from unverified to verified when the user changes his email to match the gpg key', :sidekiq_might_not_need_inline do
ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA
user = create(:user, email: 'unrelated.user@example.org')
@@ -30,7 +30,7 @@ describe 'GPG signed commits' do
expect(page).to have_button 'Verified'
end
- it 'changes from unverified to verified when the user adds the missing gpg key' do
+ it 'changes from unverified to verified when the user adds the missing gpg key', :sidekiq_might_not_need_inline do
ref = GpgHelpers::SIGNED_AND_AUTHORED_SHA
user = create(:user, email: GpgHelpers::User1.emails.first)
@@ -152,4 +152,26 @@ describe 'GPG signed commits' do
end
end
end
+
+ context 'view signed commit on the tree view', :js do
+ shared_examples 'a commit with a signature' do
+ before do
+ visit project_tree_path(project, 'signed-commits')
+ end
+
+ it 'displays commit signature' do
+ expect(page).to have_button 'Unverified'
+
+ click_on 'Unverified'
+
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with an unverified signature'
+ end
+ end
+ end
+
+ context 'with vue tree view enabled' do
+ it_behaves_like 'a commit with a signature'
+ end
+ end
end
diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb
index 82b416c3a7f..0fc62a578f9 100644
--- a/spec/features/tags/developer_deletes_tag_spec.rb
+++ b/spec/features/tags/developer_deletes_tag_spec.rb
@@ -39,8 +39,10 @@ describe 'Developer deletes tag' do
context 'when pre-receive hook fails', :js do
before do
- allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
- .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: Do not delete tags')
+ allow_next_instance_of(Gitlab::GitalyClient::OperationService) do |instance|
+ allow(instance).to receive(:rm_tag)
+ .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: Do not delete tags')
+ end
end
it 'shows the error message' do
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index 2f8b715289c..cf30776786b 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'Unsubscribe links' do
+describe 'Unsubscribe links', :sidekiq_might_not_need_inline do
include Warden::Test::Helpers
let(:recipient) { create(:user) }
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index 24b4f8dd4aa..c0cffe885de 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'Merge request > User sees revert modal', :js do
+describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not_need_inline do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/features/users/anonymous_sessions_spec.rb b/spec/features/users/anonymous_sessions_spec.rb
new file mode 100644
index 00000000000..e87ee39a3f4
--- /dev/null
+++ b/spec/features/users/anonymous_sessions_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Session TTLs', :clean_gitlab_redis_shared_state do
+ it 'creates a session with a short TTL when login fails' do
+ visit new_user_session_path
+ # The session key only gets created after a post
+ fill_in 'user_login', with: 'non-existant@gitlab.org'
+ fill_in 'user_password', with: '12345678'
+ click_button 'Sign in'
+
+ expect(page).to have_content('Invalid Login or password')
+
+ expect_single_session_with_expiration(Settings.gitlab['unauthenticated_session_expire_delay'])
+ end
+
+ it 'increases the TTL when the login succeeds' do
+ user = create(:user)
+ gitlab_sign_in(user)
+
+ expect(page).to have_content(user.name)
+
+ expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
+ end
+
+ def expect_single_session_with_expiration(expiration)
+ session_keys = get_session_keys
+
+ expect(session_keys.size).to eq(1)
+ expect(get_ttl(session_keys.first)).to eq expiration
+ end
+
+ def get_session_keys
+ Gitlab::Redis::SharedState.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a }
+ end
+
+ def get_ttl(key)
+ Gitlab::Redis::SharedState.with { |redis| redis.ttl(key) }
+ end
+end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index d1f3b3f4076..b7c54bb6de8 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -806,7 +806,7 @@ describe 'Login' do
gitlab_sign_in(user)
expect(current_path).to eq root_path
- expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address.")
+ expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address and unlock the power of CI/CD.")
end
context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 562d6fcab1b..3b19bd423a4 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -222,7 +222,7 @@ shared_examples 'Signup' do
expect(current_path).to eq users_sign_up_welcome_path
else
expect(current_path).to eq dashboard_projects_path
- expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.")
+ expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
end
@@ -379,7 +379,9 @@ shared_examples 'Signup' do
before do
InvisibleCaptcha.timestamp_enabled = true
stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(RegistrationsController).to receive(:verify_recaptcha).and_return(false)
+ allow_next_instance_of(RegistrationsController) do |instance|
+ allow(instance).to receive(:verify_recaptcha).and_return(false)
+ end
end
after do
@@ -413,6 +415,7 @@ end
describe 'With original flow' do
before do
stub_experiment(signup_flow: false)
+ stub_experiment_for_user(signup_flow: false)
end
it_behaves_like 'Signup'
@@ -421,6 +424,7 @@ end
describe 'With experimental flow' do
before do
stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: true)
end
it_behaves_like 'Signup'
@@ -439,11 +443,13 @@ describe 'With experimental flow' do
fill_in 'user_name', with: 'New name'
select 'Software Developer', from: 'user_role'
+ choose 'user_setup_for_company_true'
click_button 'Get started!'
new_user = User.find_by_username(new_user.username)
expect(new_user.name).to eq 'New name'
expect(new_user.software_developer_role?).to be_truthy
+ expect(new_user.setup_for_company).to be_truthy
expect(page).to have_current_path(new_project_path)
end
end
diff --git a/spec/finders/abuse_reports_finder_spec.rb b/spec/finders/abuse_reports_finder_spec.rb
new file mode 100644
index 00000000000..c84a645ca08
--- /dev/null
+++ b/spec/finders/abuse_reports_finder_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AbuseReportsFinder, '#execute' do
+ let(:params) { {} }
+ let!(:user1) { create(:user) }
+ let!(:user2) { create(:user) }
+ let!(:abuse_report_1) { create(:abuse_report, user: user1) }
+ let!(:abuse_report_2) { create(:abuse_report, user: user2) }
+
+ subject { described_class.new(params).execute }
+
+ context 'empty params' do
+ it 'returns all abuse reports' do
+ expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ end
+ end
+
+ context 'params[:user_id] is present' do
+ let(:params) { { user_id: user2 } }
+
+ it 'returns abuse reports for the specified user' do
+ expect(subject).to match_array([abuse_report_2])
+ end
+ end
+end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 1a33bdf11d7..70b5da0cc3c 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -73,58 +73,76 @@ describe BranchesFinder do
expect(result.count).to eq(3)
expect(result.map(&:name)).to eq(%w{csv fix lfs})
end
- end
- context 'filter and sort' do
- it 'filters branches by name and sorts by recently_updated' do
- params = { sort: 'updated_desc', search: 'feat' }
+ it 'filters branches by name that begins with' do
+ params = { search: '^feature_' }
branches_finder = described_class.new(repository, params)
result = branches_finder.execute
expect(result.first.name).to eq('feature_conflict')
- expect(result.count).to eq(2)
+ expect(result.count).to eq(1)
end
- it 'filters branches by name and sorts by recently_updated, with exact matches first' do
- params = { sort: 'updated_desc', search: 'feature' }
+ it 'filters branches by name that ends with' do
+ params = { search: 'feature$' }
branches_finder = described_class.new(repository, params)
result = branches_finder.execute
expect(result.first.name).to eq('feature')
- expect(result.second.name).to eq('feature_conflict')
- expect(result.count).to eq(2)
+ expect(result.count).to eq(1)
end
- it 'filters branches by name and sorts by last_updated' do
- params = { sort: 'updated_asc', search: 'feature' }
+ it 'filters branches by nonexistent name that begins with' do
+ params = { search: '^nope' }
branches_finder = described_class.new(repository, params)
result = branches_finder.execute
- expect(result.first.name).to eq('feature')
- expect(result.count).to eq(2)
+ expect(result.count).to eq(0)
end
- it 'filters branches by name that begins with' do
- params = { search: '^feature_' }
+ it 'filters branches by nonexistent name that ends with' do
+ params = { search: 'nope$' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.count).to eq(0)
+ end
+ end
+
+ context 'filter and sort' do
+ it 'filters branches by name and sorts by recently_updated' do
+ params = { sort: 'updated_desc', search: 'feat' }
branches_finder = described_class.new(repository, params)
result = branches_finder.execute
expect(result.first.name).to eq('feature_conflict')
- expect(result.count).to eq(1)
+ expect(result.count).to eq(2)
end
- it 'filters branches by name that ends with' do
- params = { search: 'feature$' }
+ it 'filters branches by name and sorts by recently_updated, with exact matches first' do
+ params = { sort: 'updated_desc', search: 'feature' }
branches_finder = described_class.new(repository, params)
result = branches_finder.execute
expect(result.first.name).to eq('feature')
- expect(result.count).to eq(1)
+ expect(result.second.name).to eq('feature_conflict')
+ expect(result.count).to eq(2)
+ end
+
+ it 'filters branches by name and sorts by last_updated' do
+ params = { sort: 'updated_asc', search: 'feature' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature')
+ expect(result.count).to eq(2)
end
end
end
diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb
index deec62d6598..08c241186d6 100644
--- a/spec/finders/container_repositories_finder_spec.rb
+++ b/spec/finders/container_repositories_finder_spec.rb
@@ -3,42 +3,50 @@
require 'spec_helper'
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!(:project_repository) { create(:container_repository, project: project) }
+
+ before do
+ group.add_reporter(reporter)
+ project.add_reporter(reporter)
+ end
describe '#execute' do
- let(:id) { nil }
+ context 'with authorized user' do
+ subject { described_class.new(user: reporter, subject: subject_object).execute }
- subject { described_class.new(id: id, container_type: container_type).execute }
+ context 'when subject_type is group' do
+ let(:subject_object) { group }
+ let(:other_project) { create(:project, group: group) }
- context 'when container_type is group' do
- let(:other_project) { create(:project, group: group) }
+ let(:other_repository) do
+ create(:container_repository, name: 'test_repository2', project: other_project)
+ end
- let(:other_repository) do
- create(:container_repository, name: 'test_repository2', project: other_project)
+ it { is_expected.to match_array([project_repository, other_repository]) }
end
- let(:container_type) { :group }
- let(:id) { group.id }
+ context 'when subject_type is project' do
+ let(:subject_object) { project }
- it { is_expected.to match_array([project_repository, other_repository]) }
- end
+ it { is_expected.to match_array([project_repository]) }
+ end
- context 'when container_type is project' do
- let(:container_type) { :project }
- let(:id) { project.id }
+ context 'with invalid subject_type' do
+ let(:subject_object) { "invalid type" }
- it { is_expected.to match_array([project_repository]) }
+ it { expect { subject }.to raise_exception('invalid subject_type') }
+ end
end
- context 'with invalid id' do
- let(:container_type) { :project }
- let(:id) { 123456789 }
+ context 'with unauthorized user' do
+ subject { described_class.new(user: guest, subject: group).execute }
- it 'raises an error' do
- expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound)
- end
+ it { is_expected.to be nil }
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index c27ce263bf0..6c10a617279 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -163,6 +163,20 @@ describe IssuesFinder do
end
end
+ context 'filtering by nonexistent author ID and issue term using CTE for search' do
+ let(:params) do
+ {
+ author_id: 'does-not-exist',
+ search: 'git',
+ attempt_group_search_optimizations: true
+ }
+ end
+
+ it 'returns no results' do
+ expect(issues).to be_empty
+ end
+ end
+
context 'filtering by milestone' do
let(:params) { { milestone_title: milestone.title } }
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index a396284f1e9..bc85a622119 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -23,6 +23,18 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
+ it 'filters by nonexistent author ID and MR term using CTE for search' do
+ params = {
+ author_id: 'does-not-exist',
+ search: 'git',
+ attempt_group_search_optimizations: true
+ }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to be_empty
+ end
+
it 'filters by projects' do
params = { projects: [project2.id, project3.id] }
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 4ec12b5a7f7..a9344cd593a 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-describe ProjectsFinder do
+describe ProjectsFinder, :do_not_mock_admin_mode do
+ include AdminModeHelper
+
describe '#execute' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
@@ -56,6 +58,31 @@ describe ProjectsFinder do
it { is_expected.to eq([internal_project]) }
end
+ describe 'with id_after' do
+ context 'only returns projects with a project id greater than given' do
+ let(:params) { { id_after: internal_project.id }}
+
+ it { is_expected.to eq([public_project]) }
+ end
+ end
+
+ describe 'with id_before' do
+ context 'only returns projects with a project id less than given' do
+ let(:params) { { id_before: public_project.id }}
+
+ it { is_expected.to eq([internal_project]) }
+ end
+ end
+
+ describe 'with both id_before and id_after' do
+ context 'only returns projects with a project id less than given' do
+ let!(:projects) { create_list(:project, 5, :public) }
+ let(:params) { { id_after: projects.first.id, id_before: projects.last.id }}
+
+ it { is_expected.to contain_exactly(*projects[1..-2]) }
+ end
+ end
+
describe 'filter by visibility_level' do
before do
private_project.add_maintainer(user)
@@ -188,5 +215,21 @@ describe ProjectsFinder do
it { is_expected.to eq([internal_project, public_project]) }
end
+
+ describe 'with admin user' do
+ let(:user) { create(:admin) }
+
+ context 'admin mode enabled' do
+ before do
+ enable_admin_mode!(current_user)
+ end
+
+ it { is_expected.to match_array([public_project, internal_project, private_project, shared_project]) }
+ end
+
+ context 'admin mode disabled' do
+ it { is_expected.to match_array([public_project, internal_project]) }
+ end
+ end
end
end
diff --git a/spec/finders/prometheus_metrics_finder_spec.rb b/spec/finders/prometheus_metrics_finder_spec.rb
new file mode 100644
index 00000000000..41b2e700e1e
--- /dev/null
+++ b/spec/finders/prometheus_metrics_finder_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PrometheusMetricsFinder do
+ describe '#execute' do
+ let(:finder) { described_class.new(params) }
+ let(:params) { {} }
+
+ subject { finder.execute }
+
+ context 'with params' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_metric) { create(:prometheus_metric, project: project) }
+ let_it_be(:common_metric) { create(:prometheus_metric, :common) }
+ let_it_be(:unique_metric) do
+ create(
+ :prometheus_metric,
+ :common,
+ title: 'Unique title',
+ y_label: 'Unique y_label',
+ group: :kubernetes,
+ identifier: 'identifier',
+ created_at: 5.minutes.ago
+ )
+ end
+
+ context 'with appropriate indexes' do
+ before do
+ allow_any_instance_of(described_class).to receive(:appropriate_index?).and_return(true)
+ end
+
+ context 'with project' do
+ let(:params) { { project: project } }
+
+ it { is_expected.to eq([project_metric]) }
+ end
+
+ context 'with group' do
+ let(:params) { { group: project_metric.group } }
+
+ it { is_expected.to contain_exactly(common_metric, project_metric) }
+ end
+
+ context 'with title' do
+ let(:params) { { title: project_metric.title } }
+
+ it { is_expected.to contain_exactly(project_metric, common_metric) }
+ end
+
+ context 'with y_label' do
+ let(:params) { { y_label: project_metric.y_label } }
+
+ it { is_expected.to contain_exactly(project_metric, common_metric) }
+ end
+
+ context 'with common' do
+ let(:params) { { common: true } }
+
+ it { is_expected.to contain_exactly(common_metric, unique_metric) }
+ end
+
+ context 'with ordered' do
+ let(:params) { { ordered: true } }
+
+ it { is_expected.to eq([unique_metric, project_metric, common_metric]) }
+ end
+
+ context 'with indentifier' do
+ let(:params) { { identifier: unique_metric.identifier } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ ArgumentError,
+ ':identifier must be scoped to a :project or :common'
+ )
+ end
+
+ context 'with common' do
+ let(:params) { { identifier: unique_metric.identifier, common: true } }
+
+ it { is_expected.to contain_exactly(unique_metric) }
+ end
+
+ context 'with id' do
+ let(:params) { { id: 14, identifier: 'string' } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ ArgumentError,
+ 'Only one of :identifier, :id is permitted'
+ )
+ end
+ end
+ end
+
+ context 'with id' do
+ let(:params) { { id: common_metric.id } }
+
+ it { is_expected.to contain_exactly(common_metric) }
+ end
+
+ context 'with multiple params' do
+ let(:params) do
+ {
+ group: project_metric.group,
+ title: project_metric.title,
+ y_label: project_metric.y_label,
+ common: true,
+ ordered: true
+ }
+ end
+
+ it { is_expected.to contain_exactly(common_metric) }
+ end
+ end
+
+ context 'without an appropriate index' do
+ let(:params) do
+ {
+ title: project_metric.title,
+ ordered: true
+ }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ ArgumentError,
+ 'An index should exist for params: [:title]'
+ )
+ end
+ end
+ end
+
+ context 'without params' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ ArgumentError,
+ 'Please provide one or more of: [:project, :group, :title, :y_label, :identifier, :id, :common, :ordered]'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb
index 5ffb8c74bf5..b9c67361f45 100644
--- a/spec/finders/releases_finder_spec.rb
+++ b/spec/finders/releases_finder_spec.rb
@@ -8,8 +8,7 @@ describe ReleasesFinder do
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') }
-
- subject { described_class.new(project, user)}
+ let(:finder) { described_class.new(project, user) }
before do
v1_0_0.update_attribute(:released_at, 2.days.ago)
@@ -17,11 +16,13 @@ describe ReleasesFinder do
end
describe '#execute' do
+ subject { finder.execute(**args) }
+
+ let(:args) { {} }
+
context 'when the user is not part of the project' do
it 'returns no releases' do
- releases = subject.execute
-
- expect(releases).to be_empty
+ is_expected.to be_empty
end
end
@@ -31,11 +32,25 @@ describe ReleasesFinder do
end
it 'sorts by release date' do
- releases = subject.execute
+ is_expected.to be_present
+ expect(subject.size).to eq(2)
+ expect(subject).to eq([v1_1_0, v1_0_0])
+ end
+
+ it 'preloads associations' do
+ expect(Release).to receive(:preloaded).once.and_call_original
+
+ subject
+ end
+
+ context 'when preload is false' do
+ let(:args) { { preload: false } }
+
+ it 'does not preload associations' do
+ expect(Release).not_to receive(:preloaded)
- expect(releases).to be_present
- expect(releases.size).to eq(2)
- expect(releases).to eq([v1_1_0, v1_0_0])
+ subject
+ end
end
end
end
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 85f970b71c4..e9f29ab2441 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -54,6 +54,44 @@ describe TagsFinder do
expect(result.count).to eq(0)
end
+
+ it 'filters tags by name that begins with' do
+ params = { search: '^v1.0' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.0.0')
+ expect(result.count).to eq(1)
+ end
+
+ it 'filters tags by name that ends with' do
+ params = { search: '0.0$' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.first.name).to eq('v1.0.0')
+ expect(result.count).to eq(1)
+ end
+
+ it 'filters tags by nonexistent name that begins with' do
+ params = { search: '^nope' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.count).to eq(0)
+ end
+
+ it 'filters tags by nonexistent name that ends with' do
+ params = { search: 'nope$' }
+ tags_finder = described_class.new(repository, params)
+
+ result = tags_finder.execute
+
+ expect(result.count).to eq(0)
+ end
end
context 'filter and sort' do
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 044e135fa0b..a837e7af251 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -36,10 +36,18 @@ describe TodosFinder do
expect(todos).to match_array([todo1, todo2])
end
- it 'returns correct todos when filtered by a type' do
- todos = finder.new(user, { type: 'Issue' }).execute
+ context 'when filtering by type' do
+ it 'returns correct todos when filtered by a type' do
+ todos = finder.new(user, { type: 'Issue' }).execute
- expect(todos).to match_array([todo1])
+ expect(todos).to match_array([todo1])
+ end
+
+ it 'returns the correct todos when filtering for multiple types' do
+ todos = finder.new(user, { type: %w[Issue MergeRequest] }).execute
+
+ expect(todos).to match_array([todo1, todo2])
+ end
end
context 'when filtering for actions' do
@@ -53,12 +61,10 @@ describe TodosFinder do
expect(todos).to match_array([todo2])
end
- context 'multiple actions' do
- it 'returns the expected todos' do
- todos = finder.new(user, { action_id: [Todo::DIRECTLY_ADDRESSED, Todo::ASSIGNED] }).execute
+ it 'returns the expected todos when filtering for multiple action ids' do
+ todos = finder.new(user, { action_id: [Todo::DIRECTLY_ADDRESSED, Todo::ASSIGNED] }).execute
- expect(todos).to match_array([todo2, todo1])
- end
+ expect(todos).to match_array([todo2, todo1])
end
end
@@ -69,12 +75,10 @@ describe TodosFinder do
expect(todos).to match_array([todo2])
end
- context 'multiple actions' do
- it 'returns the expected todos' do
- todos = finder.new(user, { action: [:directly_addressed, :assigned] }).execute
+ it 'returns the expected todos when filtering for multiple action names' do
+ todos = finder.new(user, { action: [:directly_addressed, :assigned] }).execute
- expect(todos).to match_array([todo2, todo1])
- end
+ expect(todos).to match_array([todo2, todo1])
end
end
end
@@ -136,6 +140,51 @@ describe TodosFinder do
end
end
end
+
+ context 'by state' do
+ let!(:todo1) { create(:todo, user: user, group: group, target: issue, state: :done) }
+ let!(:todo2) { create(:todo, user: user, group: group, target: issue, state: :pending) }
+
+ it 'returns the expected items when no state is provided' do
+ todos = finder.new(user, {}).execute
+
+ expect(todos).to match_array([todo2])
+ end
+
+ it 'returns the expected items when a state is provided' do
+ todos = finder.new(user, { state: :done }).execute
+
+ expect(todos).to match_array([todo1])
+ end
+
+ it 'returns the expected items when multiple states are provided' do
+ todos = finder.new(user, { state: [:pending, :done] }).execute
+
+ expect(todos).to match_array([todo1, todo2])
+ end
+ end
+
+ context 'by project' do
+ let_it_be(:project1) { create(:project) }
+ let_it_be(:project2) { create(:project) }
+ let_it_be(:project3) { create(:project) }
+
+ let!(:todo1) { create(:todo, user: user, project: project1, state: :pending) }
+ let!(:todo2) { create(:todo, user: user, project: project2, state: :pending) }
+ let!(:todo3) { create(:todo, user: user, project: project3, state: :pending) }
+
+ it 'returns the expected todos for one project' do
+ todos = finder.new(user, { project_id: project2.id }).execute
+
+ expect(todos).to match_array([todo2])
+ end
+
+ it 'returns the expected todos for many projects' do
+ todos = finder.new(user, { project_id: [project2.id, project1.id] }).execute
+
+ expect(todos).to match_array([todo2, todo1])
+ end
+ end
end
context 'external authorization' do
@@ -207,6 +256,19 @@ describe TodosFinder do
end
end
+ describe '.todo_types' do
+ it 'returns the expected types' do
+ expected_result =
+ if Gitlab.ee?
+ %w[Epic Issue MergeRequest]
+ else
+ %w[Issue MergeRequest]
+ end
+
+ expect(described_class.todo_types).to contain_exactly(*expected_result)
+ end
+ end
+
describe '#any_for_target?' do
it 'returns true if there are any todos for the given target' do
todo = create(:todo, :pending)
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 695175689b9..f978baa2026 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -35,7 +35,9 @@
"external_ip": { "type": ["string", "null"] },
"external_hostname": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
+ "kibana_hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] },
+ "stack": { "type": ["string", "null"] },
"update_available": { "type": ["boolean", "null"] },
"can_uninstall": { "type": "boolean" }
},
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar_extras.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar_extras.json
index 682e345d5f5..11076ec73de 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar_extras.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar_extras.json
@@ -3,6 +3,8 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
+ "project_emails_disabled": { "type": "boolean" },
+ "subscribe_disabled_description": { "type": "string" },
"subscribed": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
diff --git a/spec/fixtures/api/schemas/error_tracking/error.json b/spec/fixtures/api/schemas/error_tracking/error.json
index df2c02d7d5d..3f65105681e 100644
--- a/spec/fixtures/api/schemas/error_tracking/error.json
+++ b/spec/fixtures/api/schemas/error_tracking/error.json
@@ -4,7 +4,14 @@
"external_url",
"last_seen",
"message",
- "type"
+ "type",
+ "title",
+ "project_id",
+ "project_name",
+ "project_slug",
+ "short_id",
+ "status",
+ "frequency"
],
"properties" : {
"id": { "type": "string"},
@@ -15,7 +22,14 @@
"culprit": { "type": "string" },
"count": { "type": "integer"},
"external_url": { "type": "string" },
- "user_count": { "type": "integer"}
+ "user_count": { "type": "integer"},
+ "title": { "type": "string"},
+ "project_id": { "type": "string"},
+ "project_name": { "type": "string"},
+ "project_slug": { "type": "string"},
+ "short_id": { "type": "string"},
+ "status": { "type": "string"},
+ "frequency": { "type": "array"}
},
- "additionalProperties": true
+ "additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/error_tracking/error_detailed.json b/spec/fixtures/api/schemas/error_tracking/error_detailed.json
new file mode 100644
index 00000000000..40d6773f0e6
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/error_detailed.json
@@ -0,0 +1,45 @@
+{
+ "type": "object",
+ "required" : [
+ "external_url",
+ "external_base_url",
+ "last_seen",
+ "message",
+ "type",
+ "title",
+ "project_id",
+ "project_name",
+ "project_slug",
+ "short_id",
+ "status",
+ "frequency",
+ "first_release_last_commit",
+ "last_release_last_commit",
+ "first_release_short_version",
+ "last_release_short_version"
+ ],
+ "properties" : {
+ "id": { "type": "string"},
+ "first_seen": { "type": "string", "format": "date-time" },
+ "last_seen": { "type": "string", "format": "date-time" },
+ "type": { "type": "string" },
+ "message": { "type": "string" },
+ "culprit": { "type": "string" },
+ "count": { "type": "integer"},
+ "external_url": { "type": "string" },
+ "external_base_url": { "type": "string" },
+ "user_count": { "type": "integer"},
+ "title": { "type": "string"},
+ "project_id": { "type": "string"},
+ "project_name": { "type": "string"},
+ "project_slug": { "type": "string"},
+ "short_id": { "type": "string"},
+ "status": { "type": "string"},
+ "frequency": { "type": "array"},
+ "first_release_last_commit": { "type": ["string", "null"] },
+ "last_release_last_commit": { "type": ["string", "null"] },
+ "first_release_short_version": { "type": ["string", "null"] },
+ "last_release_short_version": { "type": ["string", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/error_tracking/error_stack_trace.json b/spec/fixtures/api/schemas/error_tracking/error_stack_trace.json
new file mode 100644
index 00000000000..a684dd0496a
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/error_stack_trace.json
@@ -0,0 +1,14 @@
+{
+ "type": "object",
+ "required": [
+ "issue_id",
+ "stack_trace_entries",
+ "date_received"
+ ],
+ "properties": {
+ "issue_id": { "type": ["string", "integer"] },
+ "stack_trace_entries": { "type": "object" },
+ "date_received": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/error_tracking/issue_detailed.json b/spec/fixtures/api/schemas/error_tracking/issue_detailed.json
new file mode 100644
index 00000000000..b5adea6fc62
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/issue_detailed.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required": [
+ "error"
+ ],
+ "properties": {
+ "error": { "$ref": "error_detailed.json" }
+ },
+ "additionalProperties": false
+}
+
diff --git a/spec/fixtures/api/schemas/error_tracking/issue_stack_trace.json b/spec/fixtures/api/schemas/error_tracking/issue_stack_trace.json
new file mode 100644
index 00000000000..7ec1ae63609
--- /dev/null
+++ b/spec/fixtures/api/schemas/error_tracking/issue_stack_trace.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required": [
+ "error"
+ ],
+ "properties": {
+ "error": { "$ref": "error_stack_trace.json" }
+ },
+ "additionalProperties": false
+}
+
diff --git a/spec/fixtures/api/schemas/public_api/v4/blobs.json b/spec/fixtures/api/schemas/public_api/v4/blobs.json
index a812815838f..5dcefb42367 100644
--- a/spec/fixtures/api/schemas/public_api/v4/blobs.json
+++ b/spec/fixtures/api/schemas/public_api/v4/blobs.json
@@ -5,6 +5,7 @@
"properties" : {
"basename": { "type": "string" },
"data": { "type": "string" },
+ "path": { "type": ["string"] },
"filename": { "type": ["string"] },
"id": { "type": ["string", "null"] },
"project_id": { "type": "integer" },
@@ -12,7 +13,7 @@
"startline": { "type": "integer" }
},
"required": [
- "basename", "data", "filename", "id", "ref", "startline", "project_id"
+ "basename", "data", "path", "filename", "id", "ref", "startline", "project_id"
],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
index ed8ed9085c0..721b8d4641f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json
@@ -7,6 +7,7 @@
"verified": { "type": "boolean" },
"verification_code": { "type": ["string", "null"] },
"enabled_until": { "type": ["date", "null"] },
+ "auto_ssl_enabled": { "type": "boolean" },
"certificate_expiration": {
"type": "object",
"properties": {
@@ -17,6 +18,6 @@
"additionalProperties": false
}
},
- "required": ["domain", "url", "project_id", "verified", "verification_code", "enabled_until"],
+ "required": ["domain", "url", "project_id", "verified", "verification_code", "enabled_until", "auto_ssl_enabled"],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
index b57d544f896..3dd80a6f11b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json
@@ -6,6 +6,7 @@
"verified": { "type": "boolean" },
"verification_code": { "type": ["string", "null"] },
"enabled_until": { "type": ["date", "null"] },
+ "auto_ssl_enabled": { "type": "boolean" },
"certificate": {
"type": "object",
"properties": {
@@ -18,6 +19,6 @@
"additionalProperties": false
}
},
- "required": ["domain", "url", "verified", "verification_code", "enabled_until"],
+ "required": ["domain", "url", "verified", "verification_code", "enabled_until", "auto_ssl_enabled"],
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json
index 2bdc8bc711c..c83eefeb7ed 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -38,10 +38,11 @@
"additionalProperties": false
},
"_links": {
- "required": ["merge_requests_url", "issues_url"],
+ "required": ["merge_requests_url", "issues_url", "edit_url"],
"properties": {
"merge_requests_url": { "type": "string" },
- "issues_url": { "type": "string" }
+ "issues_url": { "type": "string" },
+ "edit_url": { "type": "string"}
}
}
},
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
index bce74892059..dd65a4c7cdb 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
@@ -26,10 +26,11 @@
"additionalProperties": false
},
"_links": {
- "required": ["merge_requests_url", "issues_url"],
+ "required": ["merge_requests_url", "issues_url", "edit_url"],
"properties": {
"merge_requests_url": { "type": "string" },
- "issues_url": { "type": "string" }
+ "issues_url": { "type": "string" },
+ "edit_url": { "type": "string"}
}
}
},
diff --git a/spec/fixtures/api/schemas/release.json b/spec/fixtures/api/schemas/release.json
index 86f0f27606c..b0296e5e62d 100644
--- a/spec/fixtures/api/schemas/release.json
+++ b/spec/fixtures/api/schemas/release.json
@@ -1,9 +1,10 @@
{
"type": "object",
- "required": ["name", "tag_name"],
+ "required": ["tag_name", "description"],
"properties": {
"name": { "type": "string" },
"tag_name": { "type": "string" },
+ "ref": { "type": "string "},
"description": { "type": "string" },
"description_html": { "type": "string" },
"created_at": { "type": "date" },
diff --git a/spec/fixtures/grafana/dashboard_response.json b/spec/fixtures/grafana/dashboard_response.json
new file mode 100644
index 00000000000..c0dd77e2fdc
--- /dev/null
+++ b/spec/fixtures/grafana/dashboard_response.json
@@ -0,0 +1,764 @@
+{
+ "meta": {
+ "type": "db",
+ "canSave": true,
+ "canEdit": true,
+ "canAdmin": true,
+ "canStar": true,
+ "slug": "gitlab-omnibus-redis",
+ "url": "/-/grafana/d/XDaNK6amz/gitlab-omnibus-redis",
+ "expires": "0001-01-01T00:00:00Z",
+ "created": "2019-10-04T13:43:20Z",
+ "updated": "2019-10-04T13:43:20Z",
+ "updatedBy": "Anonymous",
+ "createdBy": "Anonymous",
+ "version": 1,
+ "hasAcl": false,
+ "isFolder": false,
+ "folderId": 1,
+ "folderTitle": "GitLab Omnibus",
+ "folderUrl": "/-/grafana/dashboards/f/l2EpNh2Zk/gitlab-omnibus",
+ "provisioned": true,
+ "provisionedExternalId": "redis.json"
+ },
+ "dashboard": {
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations \u0026 Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "description": "GitLab Omnibus dashboard for Redis servers",
+ "editable": true,
+ "gnetId": 763,
+ "graphTooltip": 0,
+ "id": 3,
+ "iteration": 1556027798221,
+ "links": [],
+ "panels": [
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+ "datasource": "GitLab Omnibus",
+ "decimals": 0,
+ "editable": true,
+ "error": false,
+ "format": "dtdurations",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 3, "w": 4, "x": 0, "y": 0 },
+ "id": 9,
+ "interval": null,
+ "isNew": true,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": false
+ },
+ "tableColumn": "addr",
+ "targets": [
+ {
+ "expr": "avg(time() - redis_start_time_seconds{instance=~\"$instance\"})",
+ "format": "time_series",
+ "instant": true,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 1800
+ }
+ ],
+ "thresholds": "",
+ "title": "Uptime",
+ "type": "singlestat",
+ "valueFontSize": "70%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+ "datasource": "GitLab Omnibus",
+ "decimals": 0,
+ "editable": true,
+ "error": false,
+ "format": "none",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 3, "w": 4, "x": 4, "y": 0 },
+ "hideTimeOverride": true,
+ "id": 12,
+ "interval": null,
+ "isNew": true,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": true
+ },
+ "tableColumn": "",
+ "targets": [
+ {
+ "expr": "sum(\n avg_over_time(redis_connected_clients{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 2
+ }
+ ],
+ "thresholds": "",
+ "timeFrom": "1m",
+ "timeShift": null,
+ "title": "Clients",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "avg"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 },
+ "id": 2,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": false,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_commands_processed_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "A",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Commands Executed",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "reqps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "decimals": 2,
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 },
+ "id": 1,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": false,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": true,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_keyspace_hits_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "hide": false,
+ "interval": "1m",
+ "intervalFactor": 1,
+ "legendFormat": "hits",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(\n rate(redis_keyspace_misses_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "hide": false,
+ "interval": "1m",
+ "intervalFactor": 1,
+ "legendFormat": "misses",
+ "metric": "",
+ "refId": "B",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Hits, Misses per Second",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": { "max": "#BF1B00" },
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 10, "w": 8, "x": 0, "y": 3 },
+ "id": 7,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [{ "alias": "/max - .*/", "dashes": true }],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "redis_memory_used_bytes{instance=~\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "used - {{instance}}",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "redis_config_maxmemory{instance=~\"$instance\"} \u003e 0",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "max - {{instance}}",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Memory Usage",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {
+ "evicts": "#890F02",
+ "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02",
+ "reclaims": "#3F6833"
+ },
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 8, "y": 6 },
+ "id": 8,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [{ "alias": "reclaims", "yaxis": 2 }],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(rate(redis_expired_keys_total{instance=~\"$instance\"}[$__interval]))",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "expired - {{ test_attribute }}",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(rate(redis_evicted_keys_total{instance=~\"$instance\"}[$__interval]))",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "evicted",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Expired / Evicted",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 16, "y": 6 },
+ "id": 10,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "In",
+ "refId": "A",
+ "step": 240
+ },
+ {
+ "expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "Out",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Network I/O",
+ "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 8,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 16, "x": 0, "y": 13 },
+ "id": 14,
+ "isNew": true,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum without (instance) (\n rate(redis_commands_total{instance=~\"$instance\"}[$__interval])\n) \u003e 0",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "{{ cmd }}",
+ "metric": "redis_command_calls_total",
+ "refId": "A",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Command Calls / sec",
+ "tooltip": { "msResolution": true, "shared": true, "sort": 2, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 7,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 16, "y": 13 },
+ "id": 13,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(redis_db_keys{instance=~\"$instance\"} - redis_db_keys_expiring{instance=~\"$instance\"}) ",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "not expiring",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(redis_db_keys_expiring{instance=~\"$instance\"})",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "expiring",
+ "metric": "",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Expiring vs Not-Expiring Keys",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 7,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 16, "x": 0, "y": 20 },
+ "id": 5,
+ "isNew": true,
+ "legend": {
+ "alignAsTable": true,
+ "avg": false,
+ "current": true,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (db) (\n redis_db_keys{instance=~\"$instance\"}\n)",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "{{ db }} ",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Items per DB",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "none", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ }
+ ],
+ "refresh": "1m",
+ "schemaVersion": 18,
+ "style": "dark",
+ "tags": ["redis"],
+ "templating": {
+ "list": [
+ {
+ "allValue": null,
+ "current": { "tags": [], "text": "All", "value": "$__all" },
+ "datasource": "GitLab Omnibus",
+ "definition": "",
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": false,
+ "name": "instance",
+ "options": [],
+ "query": "label_values(up{job=\"redis\"}, instance)",
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ }
+ ]
+ },
+ "time": { "from": "now-24h", "to": "now" },
+ "timepicker": {
+ "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
+ "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+ },
+ "timezone": "",
+ "title": "GitLab Omnibus - Redis",
+ "uid": "XDaNK6amz",
+ "version": 1
+ }
+}
diff --git a/spec/fixtures/grafana/datasource_response.json b/spec/fixtures/grafana/datasource_response.json
new file mode 100644
index 00000000000..07c075beb35
--- /dev/null
+++ b/spec/fixtures/grafana/datasource_response.json
@@ -0,0 +1,21 @@
+{
+ "id": 1,
+ "orgId": 1,
+ "name": "GitLab Omnibus",
+ "type": "prometheus",
+ "typeLogoUrl": "",
+ "access": "proxy",
+ "url": "http://localhost:9090",
+ "password": "",
+ "user": "",
+ "database": "",
+ "basicAuth": false,
+ "basicAuthUser": "",
+ "basicAuthPassword": "",
+ "withCredentials": false,
+ "isDefault": true,
+ "jsonData": {},
+ "secureJsonFields": {},
+ "version": 1,
+ "readOnly": true
+}
diff --git a/spec/fixtures/grafana/expected_grafana_embed.json b/spec/fixtures/grafana/expected_grafana_embed.json
new file mode 100644
index 00000000000..72fb5477b9e
--- /dev/null
+++ b/spec/fixtures/grafana/expected_grafana_embed.json
@@ -0,0 +1,27 @@
+{
+ "panel_groups": [
+ {
+ "panels": [
+ {
+ "title": "Network I/O",
+ "type": "area-chart",
+ "y_label": "",
+ "metrics": [
+ {
+ "id": "In_0",
+ "query_range": "sum( rate(redis_net_input_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+ "label": "In",
+ "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_input_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ },
+ {
+ "id": "Out_1",
+ "query_range": "sum( rate(redis_net_output_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+ "label": "Out",
+ "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_output_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/fixtures/grafana/proxy_response.json b/spec/fixtures/grafana/proxy_response.json
new file mode 100644
index 00000000000..b9f34abcaaf
--- /dev/null
+++ b/spec/fixtures/grafana/proxy_response.json
@@ -0,0 +1,459 @@
+{
+ "status": "success",
+ "data": {
+ "resultType": "matrix",
+ "result": [
+ {
+ "metric": {
+ "test_attribute": "test-attribute-value"
+ },
+ "values": [
+ [1570768177, "54"],
+ [1570768237, "54"],
+ [1570768297, "54"],
+ [1570768357, "54"],
+ [1570768417, "54"],
+ [1570768477, "54"],
+ [1570768537, "54"],
+ [1570768597, "54"],
+ [1570768657, "54"],
+ [1570768717, "54"],
+ [1570768777, "54"],
+ [1570768837, "54"],
+ [1570768897, "54"],
+ [1570768957, "54"],
+ [1570769017, "54"],
+ [1570769077, "54"],
+ [1570769377, "54"],
+ [1570769437, "54"],
+ [1570769497, "54"],
+ [1570769557, "54"],
+ [1570769617, "54"],
+ [1570769677, "54"],
+ [1570769737, "54"],
+ [1570769797, "54"],
+ [1570769857, "54"],
+ [1570769917, "54"],
+ [1570769977, "54"],
+ [1570770037, "54"],
+ [1570770097, "54"],
+ [1570770157, "54"],
+ [1570770217, "54"],
+ [1570770277, "54"],
+ [1570770337, "54"],
+ [1570770397, "54"],
+ [1570770457, "54"],
+ [1570770517, "54"],
+ [1570770577, "54"],
+ [1570770637, "54"],
+ [1570770697, "54"],
+ [1570770757, "54"],
+ [1570770817, "54"],
+ [1570770877, "54"],
+ [1570770937, "54"],
+ [1570770997, "54"],
+ [1570771057, "54"],
+ [1570771117, "54"],
+ [1570771177, "54"],
+ [1570771237, "54"],
+ [1570771297, "54"],
+ [1570771357, "54"],
+ [1570771417, "54"],
+ [1570771477, "54"],
+ [1570771537, "54"],
+ [1570771597, "54"],
+ [1570771657, "54"],
+ [1570771717, "54"],
+ [1570771777, "54"],
+ [1570771837, "54"],
+ [1570771897, "54"],
+ [1570771957, "54"],
+ [1570772017, "54"],
+ [1570772077, "54"],
+ [1570772137, "54"],
+ [1570772197, "54"],
+ [1570772257, "54"],
+ [1570772317, "54"],
+ [1570772377, "54"],
+ [1570772437, "54"],
+ [1570772497, "54"],
+ [1570772557, "54"],
+ [1570772617, "54"],
+ [1570772677, "54"],
+ [1570772737, "54"],
+ [1570772797, "54"],
+ [1570772857, "54"],
+ [1570772917, "54"],
+ [1570772977, "54"],
+ [1570773037, "54"],
+ [1570773097, "54"],
+ [1570773157, "54"],
+ [1570773217, "54"],
+ [1570773277, "54"],
+ [1570773337, "54"],
+ [1570773397, "54"],
+ [1570773457, "54"],
+ [1570773517, "54"],
+ [1570773577, "54"],
+ [1570773637, "54"],
+ [1570773697, "54"],
+ [1570773757, "54"],
+ [1570773817, "54"],
+ [1570773877, "54"],
+ [1570773937, "54"],
+ [1570773997, "54"],
+ [1570774057, "54"],
+ [1570774117, "54"],
+ [1570774177, "54"],
+ [1570774237, "54"],
+ [1570774297, "54"],
+ [1570774357, "54"],
+ [1570774417, "54"],
+ [1570774477, "54"],
+ [1570774537, "54"],
+ [1570774597, "54"],
+ [1570774657, "54"],
+ [1570774717, "54"],
+ [1570774777, "54"],
+ [1570774837, "54"],
+ [1570774897, "54"],
+ [1570774957, "54"],
+ [1570775017, "54"],
+ [1570775077, "54"],
+ [1570775137, "54"],
+ [1570776937, "54"],
+ [1570776997, "54"],
+ [1570777057, "54"],
+ [1570777117, "54"],
+ [1570777177, "54"],
+ [1570777237, "54"],
+ [1570777297, "54"],
+ [1570777357, "54"],
+ [1570777417, "54"],
+ [1570777477, "54"],
+ [1570777537, "54"],
+ [1570777597, "54"],
+ [1570777657, "54"],
+ [1570777717, "54"],
+ [1570778017, "54"],
+ [1570778077, "54"],
+ [1570778137, "54"],
+ [1570778197, "54"],
+ [1570778257, "54"],
+ [1570778317, "54"],
+ [1570778377, "54"],
+ [1570778437, "54"],
+ [1570778497, "54"],
+ [1570778557, "54"],
+ [1570778617, "54"],
+ [1570778677, "54"],
+ [1570778737, "54"],
+ [1570778797, "54"],
+ [1570778857, "54"],
+ [1570778917, "54"],
+ [1570778977, "54"],
+ [1570779037, "54"],
+ [1570779097, "54"],
+ [1570779157, "54"],
+ [1570779217, "54"],
+ [1570779277, "54"],
+ [1570779337, "54"],
+ [1570779397, "54"],
+ [1570779457, "54"],
+ [1570779517, "54"],
+ [1570779577, "54"],
+ [1570779637, "54"],
+ [1570779697, "54"],
+ [1570779757, "54"],
+ [1570779817, "54"],
+ [1570779877, "54"],
+ [1570779937, "54"],
+ [1570779997, "54"],
+ [1570780057, "54"],
+ [1570780117, "54"],
+ [1570780177, "54"],
+ [1570780237, "54"],
+ [1570780297, "54"],
+ [1570780357, "54"],
+ [1570780417, "54"],
+ [1570780477, "54"],
+ [1570780537, "54"],
+ [1570780597, "54"],
+ [1570780657, "54"],
+ [1570780717, "54"],
+ [1570780777, "54"],
+ [1570780837, "54"],
+ [1570780897, "54"],
+ [1570780957, "54"],
+ [1570781017, "54"],
+ [1570781077, "54"],
+ [1570781137, "54"],
+ [1570781197, "54"],
+ [1570781257, "54"],
+ [1570781317, "54"],
+ [1570781377, "54"],
+ [1570781437, "54"],
+ [1570781497, "54"],
+ [1570781557, "54"],
+ [1570781617, "54"],
+ [1570781677, "54"],
+ [1570781737, "54"],
+ [1570781797, "54"],
+ [1570781857, "54"],
+ [1570781917, "54"],
+ [1570781977, "54"],
+ [1570782037, "54"],
+ [1570782097, "54"],
+ [1570782157, "54"],
+ [1570782217, "54"],
+ [1570782277, "54"],
+ [1570782337, "54"],
+ [1570782397, "54"],
+ [1570782457, "54"],
+ [1570782517, "54"],
+ [1570782577, "54"],
+ [1570782637, "54"],
+ [1570782697, "54"],
+ [1570782757, "54"],
+ [1570782817, "54"],
+ [1570782877, "54"],
+ [1570782937, "54"],
+ [1570782997, "54"],
+ [1570783057, "54"],
+ [1570783117, "54"],
+ [1570783177, "54"],
+ [1570783237, "54"],
+ [1570783297, "54"],
+ [1570783357, "54"],
+ [1570783417, "54"],
+ [1570783477, "54"],
+ [1570783537, "54"],
+ [1570783597, "54"],
+ [1570783657, "54"],
+ [1570783717, "54"],
+ [1570783777, "54"],
+ [1570783837, "54"],
+ [1570783897, "54"],
+ [1570783957, "54"],
+ [1570784017, "54"],
+ [1570784077, "54"],
+ [1570784137, "54"],
+ [1570784197, "54"],
+ [1570784257, "54"],
+ [1570784317, "54"],
+ [1570784377, "54"],
+ [1570784437, "54"],
+ [1570784497, "54"],
+ [1570784557, "54"],
+ [1570784617, "54"],
+ [1570784677, "54"],
+ [1570784737, "54"],
+ [1570784797, "54"],
+ [1570784857, "54"],
+ [1570784917, "54"],
+ [1570784977, "54"],
+ [1570785037, "54"],
+ [1570785097, "54"],
+ [1570785157, "54"],
+ [1570785217, "54"],
+ [1570785277, "54"],
+ [1570785337, "54"],
+ [1570785397, "54"],
+ [1570785457, "54"],
+ [1570785517, "54"],
+ [1570785577, "54"],
+ [1570785637, "54"],
+ [1570785697, "54"],
+ [1570785757, "54"],
+ [1570785817, "54"],
+ [1570785877, "54"],
+ [1570785937, "54"],
+ [1570785997, "54"],
+ [1570786057, "54"],
+ [1570786117, "54"],
+ [1570786177, "54"],
+ [1570786237, "54"],
+ [1570786297, "54"],
+ [1570786357, "54"],
+ [1570786417, "54"],
+ [1570786477, "54"],
+ [1570786537, "54"],
+ [1570786597, "54"],
+ [1570786657, "54"],
+ [1570786717, "54"],
+ [1570786777, "54"],
+ [1570786837, "54"],
+ [1570786897, "54"],
+ [1570786957, "53"],
+ [1570787017, "54"],
+ [1570787077, "54"],
+ [1570787137, "54"],
+ [1570787197, "54"],
+ [1570787257, "54"],
+ [1570787317, "54"],
+ [1570787377, "54"],
+ [1570787437, "54"],
+ [1570787497, "54"],
+ [1570787557, "54"],
+ [1570787617, "54"],
+ [1570787677, "54"],
+ [1570787737, "54"],
+ [1570787797, "54"],
+ [1570787857, "54"],
+ [1570787917, "54"],
+ [1570787977, "54"],
+ [1570788037, "54"],
+ [1570788097, "54"],
+ [1570788157, "54"],
+ [1570788217, "54"],
+ [1570788277, "54"],
+ [1570788337, "54"],
+ [1570788397, "54"],
+ [1570788457, "54"],
+ [1570788517, "54"],
+ [1570788577, "54"],
+ [1570788637, "54"],
+ [1570788697, "54"],
+ [1570788757, "54"],
+ [1570788817, "54"],
+ [1570788877, "54"],
+ [1570788937, "54"],
+ [1570788997, "54"],
+ [1570789057, "54"],
+ [1570789117, "54"],
+ [1570789177, "54"],
+ [1570789237, "54"],
+ [1570789297, "54"],
+ [1570789357, "54"],
+ [1570789417, "54"],
+ [1570789477, "54"],
+ [1570789537, "54"],
+ [1570789597, "54"],
+ [1570789657, "54"],
+ [1570789717, "54"],
+ [1570789777, "54"],
+ [1570789837, "54"],
+ [1570789897, "54"],
+ [1570789957, "54"],
+ [1570790017, "54"],
+ [1570790077, "54"],
+ [1570790137, "54"],
+ [1570790197, "54"],
+ [1570790257, "54"],
+ [1570790317, "54"],
+ [1570790377, "54"],
+ [1570790437, "54"],
+ [1570790497, "54"],
+ [1570790557, "54"],
+ [1570790617, "54"],
+ [1570790677, "54"],
+ [1570790737, "54"],
+ [1570790797, "54"],
+ [1570790857, "54"],
+ [1570790917, "54"],
+ [1570790977, "54"],
+ [1570791037, "54"],
+ [1570791097, "54"],
+ [1570791157, "54"],
+ [1570791217, "54"],
+ [1570791277, "54"],
+ [1570791337, "54"],
+ [1570791397, "54"],
+ [1570791457, "54"],
+ [1570791517, "54"],
+ [1570791577, "54"],
+ [1570791637, "54"],
+ [1570791697, "54"],
+ [1570791757, "54"],
+ [1570791817, "54"],
+ [1570791877, "54"],
+ [1570791937, "54"],
+ [1570791997, "54"],
+ [1570792057, "54"],
+ [1570792117, "54"],
+ [1570792177, "54"],
+ [1570792237, "54"],
+ [1570792297, "54"],
+ [1570792357, "54"],
+ [1570792417, "54"],
+ [1570792477, "54"],
+ [1570792537, "54"],
+ [1570792597, "54"],
+ [1570792657, "54"],
+ [1570792717, "54"],
+ [1570792777, "54"],
+ [1570792837, "54"],
+ [1570792897, "54"],
+ [1570792957, "54"],
+ [1570793017, "54"],
+ [1570793077, "54"],
+ [1570793137, "54"],
+ [1570793197, "54"],
+ [1570793257, "54"],
+ [1570793317, "54"],
+ [1570793377, "54"],
+ [1570793437, "54"],
+ [1570793497, "54"],
+ [1570793557, "54"],
+ [1570793617, "54"],
+ [1570793677, "54"],
+ [1570793737, "54"],
+ [1570793797, "54"],
+ [1570793857, "54"],
+ [1570793917, "54"],
+ [1570793977, "54"],
+ [1570794037, "54"],
+ [1570794097, "54"],
+ [1570794157, "54"],
+ [1570794217, "54"],
+ [1570794277, "54"],
+ [1570794337, "54"],
+ [1570794397, "54"],
+ [1570794457, "54"],
+ [1570794517, "54"],
+ [1570794577, "54"],
+ [1570794637, "54"],
+ [1570794697, "54"],
+ [1570794757, "54"],
+ [1570794817, "54"],
+ [1570794877, "54"],
+ [1570794937, "54"],
+ [1570794997, "54"],
+ [1570795057, "54"],
+ [1570795117, "54"],
+ [1570795177, "54"],
+ [1570795237, "54"],
+ [1570795297, "54"],
+ [1570795357, "54"],
+ [1570795417, "54"],
+ [1570795477, "54"],
+ [1570795537, "54"],
+ [1570795597, "54"],
+ [1570795657, "54"],
+ [1570795717, "54"],
+ [1570795777, "54"],
+ [1570795837, "54"],
+ [1570795897, "54"],
+ [1570795957, "54"],
+ [1570796017, "54"],
+ [1570796077, "54"],
+ [1570796137, "54"],
+ [1570796197, "54"],
+ [1570796257, "54"],
+ [1570796317, "54"],
+ [1570796377, "54"],
+ [1570796437, "55"],
+ [1570796497, "54"],
+ [1570796557, "54"],
+ [1570796617, "54"],
+ [1570796677, "54"],
+ [1570796737, "54"],
+ [1570796797, "54"],
+ [1570796857, "54"],
+ [1570796917, "54"],
+ [1570796977, "54"]
+ ]
+ }
+ ]
+ }
+}
diff --git a/spec/fixtures/grafana/simplified_dashboard_response.json b/spec/fixtures/grafana/simplified_dashboard_response.json
new file mode 100644
index 00000000000..b450fda082b
--- /dev/null
+++ b/spec/fixtures/grafana/simplified_dashboard_response.json
@@ -0,0 +1,40 @@
+{
+ "dashboard": {
+ "panels": [
+ {
+ "datasource": "GitLab Omnibus",
+ "id": 8,
+ "lines": true,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "legendFormat": "In",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"[[instance]]\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "legendFormat": "Out",
+ "refId": "B"
+ }
+ ],
+ "title": "Network I/O",
+ "type": "graph",
+ "yaxes": [{ "format": "Bps" }, { "format": "short" }]
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {
+ "value": "localhost:9121"
+ },
+ "name": "instance"
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/group_export.tar.gz b/spec/fixtures/group_export.tar.gz
new file mode 100644
index 00000000000..83e360d7cc2
--- /dev/null
+++ b/spec/fixtures/group_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index fbd752b7403..31805a54f2f 100644
--- a/spec/fixtures/lib/gitlab/import_export/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -80,6 +80,17 @@
"issue_id": 40
}
],
+ "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",
@@ -2249,7 +2260,41 @@
]
}
],
- "snippets": [],
+ "snippets": [
+ {
+ "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": []
+ }
+ ],
"releases": [],
"project_members": [
{
@@ -6669,6 +6714,25 @@
]
}
]
+ },
+ {
+ "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": [
+ ]
}
],
"triggers": [
diff --git a/spec/fixtures/lib/gitlab/import_export/project.group.json b/spec/fixtures/lib/gitlab/import_export/group/project.json
index 47faf271cca..47faf271cca 100644
--- a/spec/fixtures/lib/gitlab/import_export/project.group.json
+++ b/spec/fixtures/lib/gitlab/import_export/group/project.json
diff --git a/spec/fixtures/lib/gitlab/import_export/project.light.json b/spec/fixtures/lib/gitlab/import_export/light/project.json
index 2971ca0f0f8..2971ca0f0f8 100644
--- a/spec/fixtures/lib/gitlab/import_export/project.light.json
+++ b/spec/fixtures/lib/gitlab/import_export/light/project.json
diff --git a/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json b/spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json
index b028147b5eb..b028147b5eb 100644
--- a/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json
+++ b/spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
index 9c1be32645a..ac40f2dcd13 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -1,7 +1,6 @@
{
"type": "object",
"required": [
- "unit",
"label",
"prometheus_endpoint_path"
],
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
index 1548daacd64..a16f1ef592f 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -3,7 +3,6 @@
"required": [
"title",
"y_label",
- "weight",
"metrics"
],
"properties": {
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 62ba0d36982..cef50bf553c 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -467,6 +467,26 @@ describe('Api', () => {
});
});
+ describe('user projects', () => {
+ it('fetches all projects that belong to a particular user', done => {
+ const query = 'dummy query';
+ const options = { unused: 'option' };
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/projects`;
+ mock.onGet(expectedUrl).reply(200, [
+ {
+ name: 'test',
+ },
+ ]);
+
+ Api.userProjects(userId, query, options, response => {
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
+ done();
+ });
+ });
+ });
+
describe('commitPipelines', () => {
it('fetches pipelines for a given commit', done => {
const projectId = 'example/foobar';
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
new file mode 100644
index 00000000000..0a16dfbc009
--- /dev/null
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -0,0 +1,81 @@
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import boardsStore from '~/boards/stores/boards_store';
+import { shallowMount } from '@vue/test-utils';
+
+describe('Issue Time Estimate component', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ boardsStore.create();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when limitToHours is false', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = false;
+ wrapper = shallowMount(IssueTimeEstimate, {
+ propsData: {
+ estimate: 374460,
+ },
+ sync: false,
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(
+ wrapper
+ .find('time')
+ .text()
+ .trim(),
+ ).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
+ });
+
+ it('prevents tooltip xss', done => {
+ const alertSpy = jest.spyOn(window, 'alert');
+ wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
+ wrapper.vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(
+ wrapper
+ .find('time')
+ .text()
+ .trim(),
+ ).toEqual('0m');
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
+ done();
+ });
+ });
+ });
+
+ describe('when limitToHours is true', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = true;
+ wrapper = shallowMount(IssueTimeEstimate, {
+ propsData: {
+ estimate: 374460,
+ },
+ sync: false,
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(
+ wrapper
+ .find('time')
+ .text()
+ .trim(),
+ ).toEqual('104h 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute');
+ });
+ });
+});
diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js
new file mode 100644
index 00000000000..ebe97769ab7
--- /dev/null
+++ b/spec/frontend/boards/issue_card_spec.js
@@ -0,0 +1,307 @@
+/* global ListAssignee, ListLabel, ListIssue */
+import { mount } from '@vue/test-utils';
+import _ from 'underscore';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
+import { listObj } from '../../javascripts/boards/mock_data';
+import store from '~/boards/stores';
+
+describe('Issue card component', () => {
+ const user = new ListAssignee({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+
+ let wrapper;
+ let issue;
+ let list;
+
+ beforeEach(() => {
+ list = { ...listObj, type: 'label' };
+ issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [list.label],
+ assignees: [],
+ reference_path: '#1',
+ real_path: '/test/1',
+ weight: 1,
+ });
+ wrapper = mount(IssueCardInner, {
+ propsData: {
+ list,
+ issue,
+ issueLinkBase: '/test',
+ rootPath: '/',
+ },
+ store,
+ sync: false,
+ });
+ });
+
+ it('renders issue title', () => {
+ expect(wrapper.find('.board-card-title').text()).toContain(issue.title);
+ });
+
+ it('includes issue base in link', () => {
+ expect(wrapper.find('.board-card-title a').attributes('href')).toContain('/test');
+ });
+
+ it('includes issue title on link', () => {
+ expect(wrapper.find('.board-card-title a').attributes('title')).toBe(issue.title);
+ });
+
+ it('does not render confidential icon', () => {
+ expect(wrapper.find('.fa-eye-flash').exists()).toBe(false);
+ });
+
+ it('renders confidential icon', done => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ confidential: true,
+ },
+ });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ done();
+ });
+ });
+
+ it('renders issue ID with #', () => {
+ expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.id}`);
+ });
+
+ describe('assignee', () => {
+ it('does not render assignee', () => {
+ expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
+ });
+
+ describe('exists', () => {
+ beforeEach(done => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [user],
+ },
+ });
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('renders assignee', () => {
+ expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true);
+ });
+
+ it('sets title', () => {
+ expect(wrapper.find('.js-assignee-tooltip').text()).toContain(`${user.name}`);
+ });
+
+ it('sets users path', () => {
+ expect(wrapper.find('.board-card-assignee a').attributes('href')).toBe('/test');
+ });
+
+ it('renders avatar', () => {
+ expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
+ });
+ });
+
+ describe('assignee default avatar', () => {
+ beforeEach(done => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [
+ new ListAssignee(
+ {
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ },
+ 'default_avatar',
+ ),
+ ],
+ },
+ });
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('displays defaults avatar if users avatar is null', () => {
+ expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
+ expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
+ 'default_avatar?width=24',
+ );
+ });
+ });
+ });
+
+ describe('multiple assignees', () => {
+ beforeEach(done => {
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees: [
+ new ListAssignee({
+ id: 2,
+ name: 'user2',
+ username: 'user2',
+ avatar: 'test_image',
+ }),
+ new ListAssignee({
+ id: 3,
+ name: 'user3',
+ username: 'user3',
+ avatar: 'test_image',
+ }),
+ new ListAssignee({
+ id: 4,
+ name: 'user4',
+ username: 'user4',
+ avatar: 'test_image',
+ }),
+ ],
+ },
+ });
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('renders all three assignees', () => {
+ expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3);
+ });
+
+ describe('more than three assignees', () => {
+ beforeEach(done => {
+ const { assignees } = wrapper.props('issue');
+ assignees.push(
+ new ListAssignee({
+ id: 5,
+ name: 'user5',
+ username: 'user5',
+ avatar: 'test_image',
+ }),
+ );
+
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees,
+ },
+ });
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('renders more avatar counter', () => {
+ expect(
+ wrapper
+ .find('.board-card-assignee .avatar-counter')
+ .text()
+ .trim(),
+ ).toEqual('+2');
+ });
+
+ it('renders two assignees', () => {
+ expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
+ });
+
+ it('renders 99+ avatar counter', done => {
+ const assignees = [
+ ...wrapper.props('issue').assignees,
+ ..._.range(5, 103).map(
+ i =>
+ new ListAssignee({
+ id: i,
+ name: 'name',
+ username: 'username',
+ avatar: 'test_image',
+ }),
+ ),
+ ];
+ wrapper.setProps({
+ issue: {
+ ...wrapper.props('issue'),
+ assignees,
+ },
+ });
+
+ wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .find('.board-card-assignee .avatar-counter')
+ .text()
+ .trim(),
+ ).toEqual('99+');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('labels', () => {
+ beforeEach(done => {
+ issue.addLabel(label1);
+ wrapper.setProps({ issue: { ...issue } });
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('does not render list label but renders all other labels', () => {
+ expect(wrapper.findAll('.badge').length).toBe(1);
+ });
+
+ it('renders label', () => {
+ const nodes = wrapper
+ .findAll('.badge')
+ .wrappers.map(label => label.attributes('data-original-title'));
+
+ expect(nodes.includes(label1.description)).toBe(true);
+ });
+
+ it('sets label description as title', () => {
+ expect(wrapper.find('.badge').attributes('data-original-title')).toContain(
+ label1.description,
+ );
+ });
+
+ it('sets background color of button', () => {
+ const nodes = wrapper
+ .findAll('.badge')
+ .wrappers.map(label => label.element.style.backgroundColor);
+
+ expect(nodes.includes(label1.color)).toBe(true);
+ });
+
+ it('does not render label if label does not have an ID', done => {
+ issue.addLabel(
+ new ListLabel({
+ title: 'closed',
+ }),
+ );
+ wrapper.setProps({ issue: { ...issue } });
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.findAll('.badge').length).toBe(1);
+ expect(wrapper.text()).not.toContain('closed');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
new file mode 100644
index 00000000000..38b2333e679
--- /dev/null
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -0,0 +1,21 @@
+import getters from '~/boards/stores/getters';
+
+describe('Boards - Getters', () => {
+ describe('getLabelToggleState', () => {
+ it('should return "on" when isShowingLabels is true', () => {
+ const state = {
+ isShowingLabels: true,
+ };
+
+ expect(getters.getLabelToggleState(state)).toBe('on');
+ });
+
+ it('should return "off" when isShowingLabels is false', () => {
+ const state = {
+ isShowingLabels: false,
+ };
+
+ expect(getters.getLabelToggleState(state)).toBe('off');
+ });
+ });
+});
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 517d8781600..199e11401a9 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -10,8 +10,10 @@ import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery';
+import initProjectSelectDropdown from '~/project_select';
jest.mock('~/lib/utils/poll');
+jest.mock('~/project_select');
const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
@@ -44,6 +46,7 @@ describe('Clusters', () => {
afterEach(() => {
cluster.destroy();
mock.restore();
+ jest.clearAllMocks();
});
describe('class constructor', () => {
@@ -55,6 +58,10 @@ describe('Clusters', () => {
it('should call initPolling on construct', () => {
expect(cluster.initPolling).toHaveBeenCalled();
});
+
+ it('should call initProjectSelectDropdown on construct', () => {
+ expect(initProjectSelectDropdown).toHaveBeenCalled();
+ });
});
describe('toggle', () => {
@@ -279,16 +286,21 @@ describe('Clusters', () => {
});
describe('installApplication', () => {
- it.each(APPLICATIONS)('tries to install %s', applicationId => {
- jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
+ it.each(APPLICATIONS)('tries to install %s', (applicationId, done) => {
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValue();
cluster.store.state.applications[applicationId].status = INSTALLABLE;
- cluster.installApplication({ id: applicationId });
-
- expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
- expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, undefined);
+ // eslint-disable-next-line promise/valid-params
+ cluster
+ .installApplication({ id: applicationId })
+ .then(() => {
+ expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
+ expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, undefined);
+ done();
+ })
+ .catch();
});
it('sets error request status when the request fails', () => {
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index fbcab078993..49bda9539fd 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -6,6 +6,7 @@ import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
import eventHub from '~/clusters/event_hub';
import { shallowMount } from '@vue/test-utils';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
describe('Applications', () => {
let vm;
@@ -13,6 +14,10 @@ describe('Applications', () => {
beforeEach(() => {
Applications = Vue.extend(applications);
+
+ gon.features = gon.features || {};
+ gon.features.enableClusterApplicationElasticStack = true;
+ gon.features.enableClusterApplicationCrossplane = true;
});
afterEach(() => {
@@ -39,6 +44,10 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
+ it('renders a row for Crossplane', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ });
+
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
@@ -54,6 +63,10 @@ describe('Applications', () => {
it('renders a row for Knative', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
+
+ it('renders a row for Elastic Stack', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ });
});
describe('Group cluster applications', () => {
@@ -76,6 +89,10 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
+ it('renders a row for Crossplane', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ });
+
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
@@ -91,6 +108,10 @@ describe('Applications', () => {
it('renders a row for Knative', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
+
+ it('renders a row for Elastic Stack', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ });
});
describe('Instance cluster applications', () => {
@@ -113,6 +134,10 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-cert_manager')).not.toBeNull();
});
+ it('renders a row for Crossplane', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-crossplane')).not.toBeNull();
+ });
+
it('renders a row for Prometheus', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
@@ -128,6 +153,10 @@ describe('Applications', () => {
it('renders a row for Knative', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBeNull();
});
+
+ it('renders a row for Elastic Stack', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
+ });
});
describe('Ingress application', () => {
@@ -164,10 +193,12 @@ describe('Applications', () => {
},
helm: { title: 'Helm Tiller' },
cert_manager: { title: 'Cert-Manager' },
+ crossplane: { title: 'Crossplane', stack: '' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
+ elastic_stack: { title: 'Elastic Stack', kibana_hostname: '' },
},
});
@@ -260,7 +291,11 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
+ expect(
+ vm.$el
+ .querySelector('.js-cluster-application-row-jupyter .js-hostname')
+ .getAttribute('readonly'),
+ ).toEqual(null);
});
});
@@ -273,7 +308,9 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter .js-hostname')).toBe(
+ null,
+ );
});
});
@@ -287,7 +324,11 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
+ expect(
+ vm.$el
+ .querySelector('.js-cluster-application-row-jupyter .js-hostname')
+ .getAttribute('readonly'),
+ ).toEqual('readonly');
});
});
@@ -299,7 +340,9 @@ describe('Applications', () => {
});
it('does not render input', () => {
- expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+ expect(vm.$el.querySelector('.js-cluster-application-row-jupyter .js-hostname')).toBe(
+ null,
+ );
});
it('renders disabled install button', () => {
@@ -361,4 +404,110 @@ describe('Applications', () => {
});
});
});
+
+ describe('Crossplane application', () => {
+ const propsData = {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ crossplane: {
+ title: 'Crossplane',
+ stack: {
+ code: '',
+ },
+ },
+ },
+ };
+
+ let wrapper;
+ beforeEach(() => {
+ wrapper = shallowMount(Applications, { propsData });
+ });
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ it('renders the correct Component', () => {
+ const crossplane = wrapper.find(CrossplaneProviderStack);
+ expect(crossplane.exists()).toBe(true);
+ });
+ });
+
+ describe('Elastic Stack application', () => {
+ describe('with ingress installed with ip & elastic stack installable', () => {
+ it('renders hostname active input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ },
+ },
+ });
+
+ expect(
+ vm.$el
+ .querySelector('.js-cluster-application-row-elastic_stack .js-hostname')
+ .getAttribute('readonly'),
+ ).toEqual(null);
+ });
+ });
+
+ describe('with ingress installed without external ip', () => {
+ it('does not render hostname input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ ingress: { title: 'Ingress', status: 'installed' },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack .js-hostname')).toBe(
+ null,
+ );
+ });
+ });
+
+ describe('with ingress & elastic stack installed', () => {
+ it('renders readonly input', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ elastic_stack: { title: 'Elastic Stack', status: 'installed', kibana_hostname: '' },
+ },
+ });
+
+ expect(
+ vm.$el
+ .querySelector('.js-cluster-application-row-elastic_stack .js-hostname')
+ .getAttribute('readonly'),
+ ).toEqual('readonly');
+ });
+ });
+
+ describe('without ingress installed', () => {
+ beforeEach(() => {
+ vm = mountComponent(Applications, {
+ applications: APPLICATIONS_MOCK_STATE,
+ });
+ });
+
+ it('does not render input', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack .js-hostname')).toBe(
+ null,
+ );
+ });
+
+ it('renders disabled install button', () => {
+ expect(
+ vm.$el
+ .querySelector(
+ '.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
+ )
+ .getAttribute('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+ });
});
diff --git a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
new file mode 100644
index 00000000000..0d234822d7b
--- /dev/null
+++ b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
+
+describe('CrossplaneProviderStack component', () => {
+ let wrapper;
+
+ const defaultProps = {
+ stacks: [
+ {
+ name: 'Google Cloud Platform',
+ code: 'gcp',
+ },
+ {
+ name: 'Amazon Web Services',
+ code: 'aws',
+ },
+ ],
+ };
+
+ function createComponent(props = {}) {
+ const propsData = {
+ ...defaultProps,
+ ...props,
+ };
+
+ wrapper = shallowMount(CrossplaneProviderStack, {
+ propsData,
+ });
+ }
+
+ beforeEach(() => {
+ const crossplane = {
+ title: 'crossplane',
+ stack: '',
+ };
+ createComponent({ crossplane });
+ });
+
+ const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
+ const findFirstDropdownElement = () => findDropdownElements().at(0);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders all of the available stacks in the dropdown', () => {
+ const dropdownElements = findDropdownElements();
+
+ expect(dropdownElements.length).toBe(defaultProps.stacks.length);
+
+ defaultProps.stacks.forEach((stack, index) =>
+ expect(dropdownElements.at(index).text()).toEqual(stack.name),
+ );
+ });
+
+ it('displays the correct label for the first dropdown item if a stack is selected', () => {
+ const crossplane = {
+ title: 'crossplane',
+ stack: 'gcp',
+ };
+ createComponent({ crossplane });
+ expect(wrapper.vm.dropdownText).toBe('Google Cloud Platform');
+ });
+
+ it('emits the "set" event with the selected stack value', () => {
+ const crossplane = {
+ title: 'crossplane',
+ stack: 'gcp',
+ };
+ createComponent({ crossplane });
+ findFirstDropdownElement().vm.$emit('click');
+ expect(wrapper.emitted().set[0][0].code).toEqual('gcp');
+ });
+ it('it renders the correct dropdown text when no stack is selected', () => {
+ expect(wrapper.vm.dropdownText).toBe('Select Stack');
+ });
+});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index 41ad398e924..016f5a259b5 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -52,6 +52,18 @@ const CLUSTERS_MOCK_DATA = {
email: 'test@example.com',
can_uninstall: false,
},
+ {
+ name: 'crossplane',
+ status: APPLICATION_STATUS.ERROR,
+ status_reason: 'Cannot connect',
+ can_uninstall: false,
+ },
+ {
+ name: 'elastic_stack',
+ status: APPLICATION_STATUS.ERROR,
+ status_reason: 'Cannot connect',
+ can_uninstall: false,
+ },
],
},
},
@@ -98,6 +110,17 @@ const CLUSTERS_MOCK_DATA = {
status_reason: 'Cannot connect',
email: 'test@example.com',
},
+ {
+ name: 'crossplane',
+ status: APPLICATION_STATUS.ERROR,
+ status_reason: 'Cannot connect',
+ stack: 'gcp',
+ },
+ {
+ name: 'elastic_stack',
+ status: APPLICATION_STATUS.ERROR,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -105,11 +128,13 @@ const CLUSTERS_MOCK_DATA = {
POST: {
'/gitlab-org/gitlab-shell/clusters/1/applications/helm': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': {},
+ '/gitlab-org/gitlab-shell/clusters/1/applications/crossplane': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/cert_manager': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
+ '/gitlab-org/gitlab-shell/clusters/1/applications/elastic_stack': {},
},
};
@@ -126,11 +151,13 @@ const DEFAULT_APPLICATION_STATE = {
const APPLICATIONS_MOCK_STATE = {
helm: { title: 'Helm Tiller', status: 'installable' },
ingress: { title: 'Ingress', status: 'installable' },
+ crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
cert_manager: { title: 'Cert-Manager', status: 'installable' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
knative: { title: 'Knative ', status: 'installable', hostname: '' },
+ elastic_stack: { title: 'Elastic Stack', status: 'installable', kibana_hostname: '' },
};
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 5ee06eb44c9..71d4daceb75 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -71,6 +71,7 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
},
ingress: {
title: 'Ingress',
@@ -84,6 +85,7 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
},
runner: {
title: 'GitLab Runner',
@@ -100,6 +102,7 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
},
prometheus: {
title: 'Prometheus',
@@ -111,6 +114,7 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
},
jupyter: {
title: 'JupyterHub',
@@ -123,6 +127,7 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
},
knative: {
title: 'Knative',
@@ -140,6 +145,7 @@ describe('Clusters Store', () => {
uninstallFailed: false,
updateSuccessful: false,
updateFailed: false,
+ validationError: null,
},
cert_manager: {
title: 'Cert-Manager',
@@ -152,6 +158,32 @@ describe('Clusters Store', () => {
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
+ validationError: null,
+ },
+ elastic_stack: {
+ title: 'Elastic Stack',
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
+ statusReason: mockResponseData.applications[7].status_reason,
+ requestReason: null,
+ kibana_hostname: '',
+ installed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
+ validationError: null,
+ },
+ crossplane: {
+ title: 'Crossplane',
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
+ statusReason: mockResponseData.applications[8].status_reason,
+ requestReason: null,
+ installed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
+ validationError: null,
},
},
environments: [],
@@ -183,5 +215,16 @@ describe('Clusters Store', () => {
`jupyter.${store.state.applications.ingress.externalIp}.nip.io`,
);
});
+
+ it('sets default hostname for elastic stack when ingress has a ip address', () => {
+ const mockResponseData =
+ CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(store.state.applications.elastic_stack.kibana_hostname).toEqual(
+ `kibana.${store.state.applications.ingress.externalIp}.nip.io`,
+ );
+ });
});
});
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index 47bdc677068..3c603c7f573 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -26,7 +26,7 @@ exports[`Confidential merge request project form group component renders empty s
>
fork the project
</a>
- and set the forks visiblity to private.
+ and set the forks visibility to private.
</span>
<gllink-stub
@@ -76,7 +76,7 @@ exports[`Confidential merge request project form group component renders fork dr
>
fork the project
</a>
- and set the forks visiblity to private.
+ and set the forks visibility to private.
</span>
<gllink-stub
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
new file mode 100644
index 00000000000..b87afdd7eb4
--- /dev/null
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = `
+<div>
+ <div
+ class="contributors-charts"
+ >
+ <h4>
+ Commits to master
+ </h4>
+
+ <span>
+ Excluding merge commits. Limited to 6,000 commits.
+ </span>
+
+ <div>
+ <glareachart-stub
+ data="[object Object]"
+ height="264"
+ option="[object Object]"
+ />
+ </div>
+
+ <div
+ class="row"
+ >
+ <div
+ class="col-6"
+ >
+ <h4>
+ John
+ </h4>
+
+ <p>
+ 2 commits (jawnnypoo@gmail.com)
+ </p>
+
+ <glareachart-stub
+ data="[object Object]"
+ height="216"
+ option="[object Object]"
+ />
+ </div>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
new file mode 100644
index 00000000000..fdba09ed26c
--- /dev/null
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { createStore } from '~/contributors/stores';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import ContributorsCharts from '~/contributors/components/contributors.vue';
+
+const localVue = createLocalVue();
+let wrapper;
+let mock;
+let store;
+const Component = Vue.extend(ContributorsCharts);
+const endpoint = 'contributors';
+const branch = 'master';
+const chartData = [
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
+];
+
+function factory() {
+ mock = new MockAdapter(axios);
+ jest.spyOn(axios, 'get');
+ mock.onGet().reply(200, chartData);
+ store = createStore();
+
+ wrapper = shallowMount(Component, {
+ propsData: {
+ endpoint,
+ branch,
+ },
+ stubs: {
+ GlLoadingIcon: true,
+ GlAreaChart: true,
+ },
+ store,
+ });
+}
+
+describe('Contributors charts', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ it('should fetch chart data when mounted', () => {
+ expect(axios.get).toHaveBeenCalledWith(endpoint);
+ });
+
+ it('should display loader whiled loading data', () => {
+ wrapper.vm.$store.state.loading = true;
+ return localVue.nextTick(() => {
+ expect(wrapper.find('.contributors-loader').exists()).toBe(true);
+ });
+ });
+
+ it('should render charts when loading completed and there is chart data', () => {
+ wrapper.vm.$store.state.loading = false;
+ wrapper.vm.$store.state.chartData = chartData;
+ return localVue.nextTick(() => {
+ expect(wrapper.find('.contributors-loader').exists()).toBe(false);
+ expect(wrapper.find('.contributors-charts').exists()).toBe(true);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
new file mode 100644
index 00000000000..bb017e0ac0f
--- /dev/null
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -0,0 +1,60 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import flashError from '~/flash';
+import * as actions from '~/contributors/stores/actions';
+import * as types from '~/contributors/stores/mutation_types';
+
+jest.mock('~/flash.js');
+
+describe('Contributors store actions', () => {
+ describe('fetchChartData', () => {
+ let mock;
+ const endpoint = '/contributors';
+ const chartData = { '2017-11': 0, '2017-12': 2 };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ it('should commit SET_CHART_DATA with received response', done => {
+ mock.onGet().reply(200, chartData);
+
+ testAction(
+ actions.fetchChartData,
+ { endpoint },
+ {},
+ [
+ { type: types.SET_LOADING_STATE, payload: true },
+ { type: types.SET_CHART_DATA, payload: chartData },
+ { type: types.SET_LOADING_STATE, payload: false },
+ ],
+ [],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should show flash on API error', done => {
+ mock.onGet().reply(400, 'Not Found');
+
+ testAction(
+ actions.fetchChartData,
+ { endpoint },
+ {},
+ [{ type: types.SET_LOADING_STATE, payload: true }],
+ [],
+ () => {
+ expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+});
+
+// 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
new file mode 100644
index 00000000000..62ae9b36f87
--- /dev/null
+++ b/spec/frontend/contributors/store/getters_spec.js
@@ -0,0 +1,73 @@
+import * as getters from '~/contributors/stores/getters';
+
+describe('Contributors Store Getters', () => {
+ const state = {};
+
+ describe('showChart', () => {
+ it('should NOT show chart if loading', () => {
+ state.loading = true;
+
+ expect(getters.showChart(state)).toEqual(false);
+ });
+
+ it('should NOT show chart there is not data', () => {
+ state.loading = false;
+ state.chartData = null;
+
+ expect(getters.showChart(state)).toEqual(false);
+ });
+
+ it('should show the chart in case loading complated and there is data', () => {
+ state.loading = false;
+ state.chartData = true;
+
+ expect(getters.showChart(state)).toEqual(true);
+ });
+
+ describe('parsedData', () => {
+ let parsed;
+
+ beforeAll(() => {
+ state.chartData = [
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
+ { author_name: 'Carlson', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
+ { author_name: 'Carlson', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
+ { author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
+ ];
+ parsed = getters.parsedData(state);
+ });
+
+ it('should group contributions by date ', () => {
+ expect(parsed.total).toMatchObject({ '2019-05-05': 3, '2019-03-03': 2, '2019-04-04': 2 });
+ });
+
+ it('should group contributions by author ', () => {
+ expect(parsed.byAuthor).toMatchObject({
+ Carlson: {
+ email: 'jawnnypoo@gmail.com',
+ commits: 2,
+ dates: {
+ '2019-03-03': 1,
+ '2019-05-05': 1,
+ },
+ },
+ John: {
+ email: 'jawnnypoo@gmail.com',
+ commits: 5,
+ dates: {
+ '2019-03-03': 1,
+ '2019-04-04': 2,
+ '2019-05-05': 2,
+ },
+ },
+ });
+ });
+ });
+ });
+});
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/spec/frontend/contributors/store/mutations_spec.js b/spec/frontend/contributors/store/mutations_spec.js
new file mode 100644
index 00000000000..e9e756d4a65
--- /dev/null
+++ b/spec/frontend/contributors/store/mutations_spec.js
@@ -0,0 +1,40 @@
+import state from '~/contributors/stores/state';
+import mutations from '~/contributors/stores/mutations';
+import * as types from '~/contributors/stores/mutation_types';
+
+describe('Contributors mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_LOADING_STATE', () => {
+ it('should set loading flag', () => {
+ const loading = true;
+ mutations[types.SET_LOADING_STATE](stateCopy, loading);
+
+ expect(stateCopy.loading).toEqual(loading);
+ });
+ });
+
+ describe('SET_CHART_DATA', () => {
+ const chartData = { '2017-11': 0, '2017-12': 2 };
+
+ it('should set chart data', () => {
+ mutations[types.SET_CHART_DATA](stateCopy, chartData);
+
+ expect(stateCopy.chartData).toEqual(chartData);
+ });
+ });
+
+ describe('SET_ACTIVE_BRANCH', () => {
+ it('should set search query', () => {
+ const branch = 'feature-branch';
+
+ mutations[types.SET_ACTIVE_BRANCH](stateCopy, branch);
+
+ expect(stateCopy.branch).toEqual(branch);
+ });
+ });
+});
diff --git a/spec/frontend/contributors/utils_spec.js b/spec/frontend/contributors/utils_spec.js
new file mode 100644
index 00000000000..a2b9154329b
--- /dev/null
+++ b/spec/frontend/contributors/utils_spec.js
@@ -0,0 +1,21 @@
+import * as utils from '~/contributors/utils';
+
+describe('Contributors Util Functions', () => {
+ describe('xAxisLabelFormatter', () => {
+ it('should return year if the date is in January', () => {
+ expect(utils.xAxisLabelFormatter(new Date('01-12-2019'))).toEqual('2019');
+ });
+
+ it('should return month name otherwise', () => {
+ expect(utils.xAxisLabelFormatter(new Date('12-02-2019'))).toEqual('Dec');
+ expect(utils.xAxisLabelFormatter(new Date('07-12-2019'))).toEqual('Jul');
+ });
+ });
+
+ describe('dateFormatter', () => {
+ it('should format provided date to YYYY-MM-DD format', () => {
+ expect(utils.dateFormatter(new Date('December 17, 1995 03:24:00'))).toEqual('1995-12-17');
+ expect(utils.dateFormatter(new Date(1565308800000))).toEqual('2019-08-09');
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
index 366c2fc7b26..efbe2635fcc 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/cluster_form_dropdown_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
-import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { GlIcon } from '@gitlab/ui';
describe('ClusterFormDropdown', () => {
let vm;
@@ -41,24 +41,50 @@ describe('ClusterFormDropdown', () => {
.trigger('click');
});
- it('displays selected item label', () => {
- expect(vm.find(DropdownButton).props('toggleText')).toEqual(secondItem.name);
+ it('emits input event with selected item', () => {
+ expect(vm.emitted('input')[0]).toEqual([secondItem.value]);
+ });
+ });
+
+ describe('when multiple items are selected', () => {
+ const value = [1];
+
+ beforeEach(() => {
+ vm.setProps({ items, multiple: true, value });
+ vm.findAll('.js-dropdown-item')
+ .at(0)
+ .trigger('click');
+ vm.findAll('.js-dropdown-item')
+ .at(1)
+ .trigger('click');
+ });
+
+ it('emits input event with an array of selected items', () => {
+ expect(vm.emitted('input')[1]).toEqual([[firstItem.value, secondItem.value]]);
+ });
+ });
+
+ describe('when multiple items can be selected', () => {
+ beforeEach(() => {
+ vm.setProps({ items, multiple: true, value: firstItem.value });
});
- it('sets selected value to dropdown hidden input', () => {
- expect(vm.find(DropdownHiddenInput).props('value')).toEqual(secondItem.value);
+ it('displays a checked GlIcon next to the item', () => {
+ expect(vm.find(GlIcon).is('.invisible')).toBe(false);
+ expect(vm.find(GlIcon).props('name')).toBe('mobile-issue-close');
});
});
describe('when an item is selected and has a custom label property', () => {
it('displays selected item custom label', () => {
const labelProperty = 'customLabel';
- const selectedItem = { [labelProperty]: 'Name' };
+ const label = 'Name';
+ const currentValue = 1;
+ const customLabelItems = [{ [labelProperty]: label, value: currentValue }];
- vm.setProps({ labelProperty });
- vm.setData({ selectedItem });
+ vm.setProps({ labelProperty, items: customLabelItems, value: currentValue });
- expect(vm.find(DropdownButton).props('toggleText')).toEqual(selectedItem[labelProperty]);
+ expect(vm.find(DropdownButton).props('toggleText')).toEqual(label);
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
new file mode 100644
index 00000000000..4bf3ac430f5
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/components/create_eks_cluster_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import CreateEksCluster from '~/create_cluster/eks_cluster/components/create_eks_cluster.vue';
+import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
+import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('CreateEksCluster', () => {
+ let vm;
+ let state;
+ const gitlabManagedClusterHelpPath = 'gitlab-managed-cluster-help-path';
+ const accountAndExternalIdsHelpPath = 'account-and-external-id-help-path';
+ const createRoleArnHelpPath = 'role-arn-help-path';
+ const kubernetesIntegrationHelpPath = 'kubernetes-integration';
+ const externalLinkIcon = 'external-link';
+
+ beforeEach(() => {
+ state = { hasCredentials: false };
+ const store = new Vuex.Store({
+ state,
+ });
+
+ vm = shallowMount(CreateEksCluster, {
+ propsData: {
+ gitlabManagedClusterHelpPath,
+ accountAndExternalIdsHelpPath,
+ createRoleArnHelpPath,
+ externalLinkIcon,
+ kubernetesIntegrationHelpPath,
+ },
+ localVue,
+ store,
+ });
+ });
+ afterEach(() => vm.destroy());
+
+ describe('when credentials are provided', () => {
+ beforeEach(() => {
+ state.hasCredentials = true;
+ });
+
+ it('displays eks cluster configuration form when credentials are valid', () => {
+ expect(vm.find(EksClusterConfigurationForm).exists()).toBe(true);
+ });
+
+ describe('passes to the cluster configuration form', () => {
+ it('help url for kubernetes integration documentation', () => {
+ expect(vm.find(EksClusterConfigurationForm).props('gitlabManagedClusterHelpPath')).toBe(
+ gitlabManagedClusterHelpPath,
+ );
+ });
+
+ it('help url for gitlab managed cluster documentation', () => {
+ expect(vm.find(EksClusterConfigurationForm).props('kubernetesIntegrationHelpPath')).toBe(
+ kubernetesIntegrationHelpPath,
+ );
+ });
+ });
+ });
+
+ describe('when credentials are invalid', () => {
+ beforeEach(() => {
+ state.hasCredentials = false;
+ });
+
+ it('displays service credentials form', () => {
+ expect(vm.find(ServiceCredentialsForm).exists()).toBe(true);
+ });
+
+ describe('passes to the service credentials form', () => {
+ it('help url for account and external ids', () => {
+ expect(vm.find(ServiceCredentialsForm).props('accountAndExternalIdsHelpPath')).toBe(
+ accountAndExternalIdsHelpPath,
+ );
+ });
+
+ it('external link icon', () => {
+ expect(vm.find(ServiceCredentialsForm).props('externalLinkIcon')).toBe(externalLinkIcon);
+ });
+
+ it('help url to create a role ARN', () => {
+ expect(vm.find(ServiceCredentialsForm).props('createRoleArnHelpPath')).toBe(
+ createRoleArnHelpPath,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
index 69290f6dfa9..25d613d64ed 100644
--- a/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/components/eks_cluster_configuration_form_spec.js
@@ -4,7 +4,6 @@ import Vue from 'vue';
import { GlFormCheckbox } from '@gitlab/ui';
import EksClusterConfigurationForm from '~/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue';
-import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
import eksClusterFormState from '~/create_cluster/eks_cluster/store/state';
import clusterDropdownStoreState from '~/create_cluster/eks_cluster/store/cluster_dropdown/state';
@@ -21,17 +20,21 @@ describe('EksClusterConfigurationForm', () => {
let subnetsState;
let keyPairsState;
let securityGroupsState;
+ let instanceTypesState;
let vpcsActions;
let rolesActions;
let regionsActions;
let subnetsActions;
let keyPairsActions;
let securityGroupsActions;
+ let instanceTypesActions;
let vm;
beforeEach(() => {
state = eksClusterFormState();
actions = {
+ signOut: jest.fn(),
+ createCluster: jest.fn(),
setClusterName: jest.fn(),
setEnvironmentScope: jest.fn(),
setKubernetesVersion: jest.fn(),
@@ -41,6 +44,8 @@ describe('EksClusterConfigurationForm', () => {
setRole: jest.fn(),
setKeyPair: jest.fn(),
setSecurityGroup: jest.fn(),
+ setInstanceType: jest.fn(),
+ setNodeCount: jest.fn(),
setGitlabManagedCluster: jest.fn(),
};
regionsActions = {
@@ -61,6 +66,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsActions = {
fetchItems: jest.fn(),
};
+ instanceTypesActions = {
+ fetchItems: jest.fn(),
+ };
rolesState = {
...clusterDropdownStoreState(),
};
@@ -79,6 +87,9 @@ describe('EksClusterConfigurationForm', () => {
securityGroupsState = {
...clusterDropdownStoreState(),
};
+ instanceTypesState = {
+ ...clusterDropdownStoreState(),
+ };
store = new Vuex.Store({
state,
actions,
@@ -113,6 +124,11 @@ describe('EksClusterConfigurationForm', () => {
state: securityGroupsState,
actions: securityGroupsActions,
},
+ instanceTypes: {
+ namespaced: true,
+ state: instanceTypesState,
+ actions: instanceTypesActions,
+ },
},
});
});
@@ -124,6 +140,7 @@ describe('EksClusterConfigurationForm', () => {
propsData: {
gitlabManagedClusterHelpPath: '',
kubernetesIntegrationHelpPath: '',
+ externalLinkIcon: '',
},
});
});
@@ -132,15 +149,34 @@ describe('EksClusterConfigurationForm', () => {
vm.destroy();
});
+ const setAllConfigurationFields = () => {
+ store.replaceState({
+ ...state,
+ clusterName: 'cluster name',
+ environmentScope: '*',
+ selectedRegion: 'region',
+ selectedRole: 'role',
+ selectedKeyPair: 'key pair',
+ selectedVpc: 'vpc',
+ selectedSubnet: 'subnet',
+ selectedSecurityGroup: 'group',
+ selectedInstanceType: 'small-1',
+ });
+ };
+
+ const findSignOutButton = () => vm.find('.js-sign-out');
+ const findCreateClusterButton = () => vm.find('.js-create-cluster');
const findClusterNameInput = () => vm.find('[id=eks-cluster-name]');
const findEnvironmentScopeInput = () => vm.find('[id=eks-environment-scope]');
const findKubernetesVersionDropdown = () => vm.find('[field-id="eks-kubernetes-version"]');
- const findRegionDropdown = () => vm.find(RegionDropdown);
+ const findRegionDropdown = () => vm.find('[field-id="eks-region"]');
const findKeyPairDropdown = () => vm.find('[field-id="eks-key-pair"]');
const findVpcDropdown = () => vm.find('[field-id="eks-vpc"]');
const findSubnetDropdown = () => vm.find('[field-id="eks-subnet"]');
const findRoleDropdown = () => vm.find('[field-id="eks-role"]');
const findSecurityGroupDropdown = () => vm.find('[field-id="eks-security-group"]');
+ const findInstanceTypeDropdown = () => vm.find('[field-id="eks-instance-type"');
+ const findNodeCountInput = () => vm.find('[id="eks-node-count"]');
const findGitlabManagedClusterCheckbox = () => vm.find(GlFormCheckbox);
describe('when mounted', () => {
@@ -151,6 +187,15 @@ describe('EksClusterConfigurationForm', () => {
it('fetches available roles', () => {
expect(rolesActions.fetchItems).toHaveBeenCalled();
});
+
+ it('fetches available instance types', () => {
+ expect(instanceTypesActions.fetchItems).toHaveBeenCalled();
+ });
+ });
+
+ it('dispatches signOut action when sign out button is clicked', () => {
+ findSignOutButton().trigger('click');
+ expect(actions.signOut).toHaveBeenCalled();
});
it('sets isLoadingRoles to RoleDropdown loading property', () => {
@@ -180,11 +225,13 @@ describe('EksClusterConfigurationForm', () => {
});
it('sets regions to RegionDropdown regions property', () => {
- expect(findRegionDropdown().props('regions')).toBe(regionsState.items);
+ expect(findRegionDropdown().props('items')).toBe(regionsState.items);
});
it('sets loadingRegionsError to RegionDropdown error property', () => {
- expect(findRegionDropdown().props('error')).toBe(regionsState.loadingItemsError);
+ regionsState.loadingItemsError = new Error();
+
+ expect(findRegionDropdown().props('hasErrors')).toEqual(true);
});
it('disables KeyPairDropdown when no region is selected', () => {
@@ -329,6 +376,34 @@ describe('EksClusterConfigurationForm', () => {
undefined,
);
});
+
+ it('cleans selected vpc', () => {
+ expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc: null }, undefined);
+ });
+
+ it('cleans selected key pair', () => {
+ expect(actions.setKeyPair).toHaveBeenCalledWith(
+ expect.anything(),
+ { keyPair: null },
+ undefined,
+ );
+ });
+
+ it('cleans selected subnet', () => {
+ expect(actions.setSubnet).toHaveBeenCalledWith(
+ expect.anything(),
+ { subnet: null },
+ undefined,
+ );
+ });
+
+ it('cleans selected security group', () => {
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(
+ expect.anything(),
+ { securityGroup: null },
+ undefined,
+ );
+ });
});
it('dispatches setClusterName when cluster name input changes', () => {
@@ -381,8 +456,10 @@ describe('EksClusterConfigurationForm', () => {
describe('when vpc is selected', () => {
const vpc = { name: 'vpc-1' };
+ const region = 'east-1';
beforeEach(() => {
+ state.selectedRegion = region;
findVpcDropdown().vm.$emit('input', vpc);
});
@@ -390,14 +467,34 @@ describe('EksClusterConfigurationForm', () => {
expect(actions.setVpc).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
});
+ it('cleans selected subnet', () => {
+ expect(actions.setSubnet).toHaveBeenCalledWith(
+ expect.anything(),
+ { subnet: null },
+ undefined,
+ );
+ });
+
+ it('cleans selected security group', () => {
+ expect(actions.setSecurityGroup).toHaveBeenCalledWith(
+ expect.anything(),
+ { securityGroup: null },
+ undefined,
+ );
+ });
+
it('dispatches fetchSubnets action', () => {
- expect(subnetsActions.fetchItems).toHaveBeenCalledWith(expect.anything(), { vpc }, undefined);
+ expect(subnetsActions.fetchItems).toHaveBeenCalledWith(
+ expect.anything(),
+ { vpc, region },
+ undefined,
+ );
});
it('dispatches fetchSecurityGroups action', () => {
expect(securityGroupsActions.fetchItems).toHaveBeenCalledWith(
expect.anything(),
- { vpc },
+ { vpc, region },
undefined,
);
});
@@ -454,4 +551,76 @@ describe('EksClusterConfigurationForm', () => {
);
});
});
+
+ describe('when instance type is selected', () => {
+ const instanceType = 'small-1';
+
+ beforeEach(() => {
+ findInstanceTypeDropdown().vm.$emit('input', instanceType);
+ });
+
+ it('dispatches setInstanceType action', () => {
+ expect(actions.setInstanceType).toHaveBeenCalledWith(
+ expect.anything(),
+ { instanceType },
+ undefined,
+ );
+ });
+ });
+
+ it('dispatches setNodeCount when node count input changes', () => {
+ const nodeCount = 5;
+
+ findNodeCountInput().vm.$emit('input', nodeCount);
+
+ expect(actions.setNodeCount).toHaveBeenCalledWith(expect.anything(), { nodeCount }, undefined);
+ });
+
+ describe('when all cluster configuration fields are set', () => {
+ beforeEach(() => {
+ setAllConfigurationFields();
+ });
+
+ it('enables create cluster button', () => {
+ expect(findCreateClusterButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when at least one cluster configuration field is not set', () => {
+ beforeEach(() => {
+ setAllConfigurationFields();
+ store.replaceState({
+ ...state,
+ clusterName: '',
+ });
+ });
+
+ it('disables create cluster button', () => {
+ expect(findCreateClusterButton().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when isCreatingCluster', () => {
+ beforeEach(() => {
+ setAllConfigurationFields();
+ store.replaceState({
+ ...state,
+ isCreatingCluster: true,
+ });
+ });
+
+ it('sets create cluster button as loading', () => {
+ expect(findCreateClusterButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe('clicking create cluster button', () => {
+ beforeEach(() => {
+ findCreateClusterButton().vm.$emit('click');
+ });
+
+ it('dispatches createCluster action', () => {
+ expect(actions.createCluster).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js b/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
deleted file mode 100644
index 0ebb5026a4b..00000000000
--- a/spec/frontend/create_cluster/eks_cluster/components/region_dropdown_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
-import RegionDropdown from '~/create_cluster/eks_cluster/components/region_dropdown.vue';
-
-describe('RegionDropdown', () => {
- let vm;
-
- const getClusterFormDropdown = () => vm.find(ClusterFormDropdown);
-
- beforeEach(() => {
- vm = shallowMount(RegionDropdown);
- });
- afterEach(() => vm.destroy());
-
- it('renders a cluster-form-dropdown', () => {
- expect(getClusterFormDropdown().exists()).toBe(true);
- });
-
- it('sets regions to cluster-form-dropdown items property', () => {
- const regions = [{ name: 'basic' }];
-
- vm.setProps({ regions });
-
- expect(getClusterFormDropdown().props('items')).toEqual(regions);
- });
-
- it('sets a loading text', () => {
- expect(getClusterFormDropdown().props('loadingText')).toEqual('Loading Regions');
- });
-
- it('sets a placeholder', () => {
- expect(getClusterFormDropdown().props('placeholder')).toEqual('Select a region');
- });
-
- it('sets an empty results text', () => {
- expect(getClusterFormDropdown().props('emptyText')).toEqual('No region found');
- });
-
- it('sets a search field placeholder', () => {
- expect(getClusterFormDropdown().props('searchFieldPlaceholder')).toEqual('Search regions');
- });
-
- it('sets hasErrors property', () => {
- vm.setProps({ error: {} });
-
- expect(getClusterFormDropdown().props('hasErrors')).toEqual(true);
- });
-
- it('sets an error message', () => {
- expect(getClusterFormDropdown().props('errorMessage')).toEqual(
- 'Could not load regions from your AWS account',
- );
- });
-});
diff --git a/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
new file mode 100644
index 00000000000..0be723b48f0
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/components/service_credentials_form_spec.js
@@ -0,0 +1,117 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+
+import ServiceCredentialsForm from '~/create_cluster/eks_cluster/components/service_credentials_form.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+
+import eksClusterState from '~/create_cluster/eks_cluster/store/state';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ServiceCredentialsForm', () => {
+ let vm;
+ let state;
+ let createRoleAction;
+ const accountId = 'accountId';
+ const externalId = 'externalId';
+
+ beforeEach(() => {
+ state = Object.assign(eksClusterState(), {
+ accountId,
+ externalId,
+ });
+ createRoleAction = jest.fn();
+
+ const store = new Vuex.Store({
+ state,
+ actions: {
+ createRole: createRoleAction,
+ },
+ });
+ vm = shallowMount(ServiceCredentialsForm, {
+ propsData: {
+ accountAndExternalIdsHelpPath: '',
+ createRoleArnHelpPath: '',
+ externalLinkIcon: '',
+ },
+ localVue,
+ store,
+ });
+ });
+ afterEach(() => vm.destroy());
+
+ const findAccountIdInput = () => vm.find('#gitlab-account-id');
+ const findCopyAccountIdButton = () => vm.find('.js-copy-account-id-button');
+ const findExternalIdInput = () => vm.find('#eks-external-id');
+ const findCopyExternalIdButton = () => vm.find('.js-copy-external-id-button');
+ const findInvalidCredentials = () => vm.find('.js-invalid-credentials');
+ const findSubmitButton = () => vm.find(LoadingButton);
+ const findForm = () => vm.find('form[name="service-credentials-form"]');
+
+ it('displays provided account id', () => {
+ expect(findAccountIdInput().attributes('value')).toBe(accountId);
+ });
+
+ it('allows to copy account id', () => {
+ expect(findCopyAccountIdButton().props('text')).toBe(accountId);
+ });
+
+ it('displays provided external id', () => {
+ expect(findExternalIdInput().attributes('value')).toBe(externalId);
+ });
+
+ it('allows to copy external id', () => {
+ expect(findCopyExternalIdButton().props('text')).toBe(externalId);
+ });
+
+ it('disables submit button when role ARN is not provided', () => {
+ expect(findSubmitButton().attributes('disabled')).toBeTruthy();
+ });
+
+ it('enables submit button when role ARN is not provided', () => {
+ vm.setData({ roleArn: '123' });
+
+ expect(findSubmitButton().attributes('disabled')).toBeFalsy();
+ });
+
+ it('dispatches createRole action when form is submitted', () => {
+ findForm().trigger('submit');
+
+ expect(createRoleAction).toHaveBeenCalled();
+ });
+
+ describe('when is creating role', () => {
+ beforeEach(() => {
+ vm.setData({ roleArn: '123' }); // set role ARN to enable button
+
+ state.isCreatingRole = true;
+ });
+
+ it('disables submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
+ it('sets submit button as loading', () => {
+ expect(findSubmitButton().props('loading')).toBe(true);
+ });
+
+ it('displays Authenticating label on submit button', () => {
+ expect(findSubmitButton().props('label')).toBe('Authenticating');
+ });
+ });
+
+ describe('when role can’t be created', () => {
+ beforeEach(() => {
+ state.createRoleError = 'Invalid credentials';
+ });
+
+ it('displays invalid role warning banner', () => {
+ expect(findInvalidCredentials().exists()).toBe(true);
+ });
+
+ it('displays invalid role error message', () => {
+ expect(findInvalidCredentials().text()).toContain(state.createRoleError);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..25be858dcb3
--- /dev/null
+++ b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
@@ -0,0 +1,152 @@
+import awsServicesFacadeFactory from '~/create_cluster/eks_cluster/services/aws_services_facade';
+import axios from '~/lib/utils/axios_utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+
+describe('awsServicesFacade', () => {
+ let apiPaths;
+ let axiosMock;
+ let awsServices;
+ let region;
+ let vpc;
+
+ beforeEach(() => {
+ apiPaths = {
+ getKeyPairsPath: '/clusters/aws/api/key_pairs',
+ getRegionsPath: '/clusters/aws/api/regions',
+ getRolesPath: '/clusters/aws/api/roles',
+ getSecurityGroupsPath: '/clusters/aws/api/security_groups',
+ getSubnetsPath: '/clusters/aws/api/subnets',
+ getVpcsPath: '/clusters/aws/api/vpcs',
+ getInstanceTypesPath: '/clusters/aws/api/instance_types',
+ };
+ region = 'west-1';
+ vpc = 'vpc-2';
+ awsServices = awsServicesFacadeFactory(apiPaths);
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ describe('when fetchRegions succeeds', () => {
+ let regions;
+ let regionsOutput;
+
+ beforeEach(() => {
+ regions = [{ region_name: 'east-1' }, { region_name: 'west-2' }];
+ regionsOutput = regions.map(({ region_name: name }) => ({ name, value: name }));
+ axiosMock.onGet(apiPaths.getRegionsPath).reply(200, { regions });
+ });
+
+ it('return list of roles where each item has a name and value', () => {
+ expect(awsServices.fetchRegions()).resolves.toEqual(regionsOutput);
+ });
+ });
+
+ describe('when fetchRoles succeeds', () => {
+ let roles;
+ let rolesOutput;
+
+ beforeEach(() => {
+ roles = [
+ { role_name: 'admin', arn: 'aws::admin' },
+ { role_name: 'read-only', arn: 'aws::read-only' },
+ ];
+ rolesOutput = roles.map(({ role_name: name, arn: value }) => ({ name, value }));
+ axiosMock.onGet(apiPaths.getRolesPath).reply(200, { roles });
+ });
+
+ it('return list of regions where each item has a name and value', () => {
+ expect(awsServices.fetchRoles()).resolves.toEqual(rolesOutput);
+ });
+ });
+
+ describe('when fetchKeyPairs succeeds', () => {
+ let keyPairs;
+ let keyPairsOutput;
+
+ beforeEach(() => {
+ keyPairs = [{ key_pair: 'key-pair' }, { key_pair: 'key-pair-2' }];
+ keyPairsOutput = keyPairs.map(({ key_name: name }) => ({ name, value: name }));
+ axiosMock
+ .onGet(apiPaths.getKeyPairsPath, { params: { region } })
+ .reply(200, { key_pairs: keyPairs });
+ });
+
+ it('return list of key pairs where each item has a name and value', () => {
+ expect(awsServices.fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
+ });
+ });
+
+ describe('when fetchVpcs succeeds', () => {
+ let vpcs;
+ let vpcsOutput;
+
+ beforeEach(() => {
+ vpcs = [{ vpc_id: 'vpc-1' }, { vpc_id: 'vpc-2' }];
+ vpcsOutput = vpcs.map(({ vpc_id: name }) => ({ name, value: name }));
+ axiosMock.onGet(apiPaths.getVpcsPath, { params: { region } }).reply(200, { vpcs });
+ });
+
+ it('return list of vpcs where each item has a name and value', () => {
+ expect(awsServices.fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
+ });
+ });
+
+ describe('when fetchSubnets succeeds', () => {
+ let subnets;
+ let subnetsOutput;
+
+ beforeEach(() => {
+ subnets = [{ subnet_id: 'vpc-1' }, { subnet_id: 'vpc-2' }];
+ subnetsOutput = subnets.map(({ subnet_id }) => ({ name: subnet_id, value: subnet_id }));
+ axiosMock
+ .onGet(apiPaths.getSubnetsPath, { params: { region, vpc_id: vpc } })
+ .reply(200, { subnets });
+ });
+
+ it('return list of subnets where each item has a name and value', () => {
+ expect(awsServices.fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
+ });
+ });
+
+ describe('when fetchSecurityGroups succeeds', () => {
+ let securityGroups;
+ let securityGroupsOutput;
+
+ beforeEach(() => {
+ securityGroups = [
+ { group_name: 'admin group', group_id: 'group-1' },
+ { group_name: 'basic group', group_id: 'group-2' },
+ ];
+ securityGroupsOutput = securityGroups.map(({ group_id: value, group_name: name }) => ({
+ name,
+ value,
+ }));
+ axiosMock
+ .onGet(apiPaths.getSecurityGroupsPath, { params: { region, vpc_id: vpc } })
+ .reply(200, { security_groups: securityGroups });
+ });
+
+ it('return list of security groups where each item has a name and value', () => {
+ expect(awsServices.fetchSecurityGroups({ region, vpc })).resolves.toEqual(
+ securityGroupsOutput,
+ );
+ });
+ });
+
+ describe('when fetchInstanceTypes succeeds', () => {
+ let instanceTypes;
+ let instanceTypesOutput;
+
+ beforeEach(() => {
+ instanceTypes = [{ instance_type_name: 't2.small' }, { instance_type_name: 't2.medium' }];
+ instanceTypesOutput = instanceTypes.map(({ instance_type_name }) => ({
+ name: instance_type_name,
+ value: instance_type_name,
+ }));
+ axiosMock.onGet(apiPaths.getInstanceTypesPath).reply(200, { instance_types: instanceTypes });
+ });
+
+ it('return list of instance types where each item has a name and value', () => {
+ expect(awsServices.fetchInstanceTypes()).resolves.toEqual(instanceTypesOutput);
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 1ed7f806804..cf6c317a2df 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -13,7 +13,20 @@ import {
SET_ROLE,
SET_SECURITY_GROUP,
SET_GITLAB_MANAGED_CLUSTER,
+ SET_INSTANCE_TYPE,
+ SET_NODE_COUNT,
+ REQUEST_CREATE_ROLE,
+ CREATE_ROLE_SUCCESS,
+ CREATE_ROLE_ERROR,
+ REQUEST_CREATE_CLUSTER,
+ CREATE_CLUSTER_ERROR,
+ SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
describe('EKS Cluster Store Actions', () => {
let clusterName;
@@ -25,19 +38,43 @@ describe('EKS Cluster Store Actions', () => {
let role;
let keyPair;
let securityGroup;
+ let instanceType;
+ let nodeCount;
let gitlabManagedCluster;
+ let mock;
+ let state;
+ let newClusterUrl;
beforeEach(() => {
clusterName = 'my cluster';
environmentScope = 'production';
kubernetesVersion = '11.1';
- region = { name: 'regions-1' };
- vpc = { name: 'vpc-1' };
- subnet = { name: 'subnet-1' };
- role = { name: 'role-1' };
- keyPair = { name: 'key-pair-1' };
- securityGroup = { name: 'default group' };
+ region = 'regions-1';
+ vpc = 'vpc-1';
+ subnet = 'subnet-1';
+ role = 'role-1';
+ keyPair = 'key-pair-1';
+ securityGroup = 'default group';
+ instanceType = 'small-1';
+ nodeCount = '5';
gitlabManagedCluster = true;
+
+ newClusterUrl = '/clusters/1';
+
+ state = {
+ ...createState(),
+ createRolePath: '/clusters/roles/',
+ signOutPath: '/aws/signout',
+ createClusterPath: '/clusters/',
+ };
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it.each`
@@ -51,10 +88,207 @@ describe('EKS Cluster Store Actions', () => {
${'setVpc'} | ${SET_VPC} | ${{ vpc }} | ${'vpc'}
${'setSubnet'} | ${SET_SUBNET} | ${{ subnet }} | ${'subnet'}
${'setSecurityGroup'} | ${SET_SECURITY_GROUP} | ${{ securityGroup }} | ${'securityGroup'}
+ ${'setInstanceType'} | ${SET_INSTANCE_TYPE} | ${{ instanceType }} | ${'instance type'}
+ ${'setNodeCount'} | ${SET_NODE_COUNT} | ${{ nodeCount }} | ${'node count'}
${'setGitlabManagedCluster'} | ${SET_GITLAB_MANAGED_CLUSTER} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
`(`$action commits $mutation with $payloadDescription payload`, data => {
const { action, mutation, payload } = data;
- testAction(actions[action], payload, createState(), [{ type: mutation, payload }]);
+ testAction(actions[action], payload, state, [{ type: mutation, payload }]);
+ });
+
+ describe('createRole', () => {
+ const payload = {
+ roleArn: 'role_arn',
+ externalId: 'externalId',
+ };
+
+ describe('when request succeeds', () => {
+ beforeEach(() => {
+ mock
+ .onPost(state.createRolePath, {
+ role_arn: payload.roleArn,
+ role_external_id: payload.externalId,
+ })
+ .reply(201);
+ });
+
+ it('dispatches createRoleSuccess action', () =>
+ testAction(
+ actions.createRole,
+ payload,
+ state,
+ [],
+ [{ type: 'requestCreateRole' }, { type: 'createRoleSuccess' }],
+ ));
+ });
+
+ describe('when request fails', () => {
+ let error;
+
+ beforeEach(() => {
+ error = new Error('Request failed with status code 400');
+ mock
+ .onPost(state.createRolePath, {
+ role_arn: payload.roleArn,
+ role_external_id: payload.externalId,
+ })
+ .reply(400, error);
+ });
+
+ it('dispatches createRoleError action', () =>
+ testAction(
+ actions.createRole,
+ payload,
+ state,
+ [],
+ [{ type: 'requestCreateRole' }, { type: 'createRoleError', payload: { error } }],
+ ));
+ });
+ });
+
+ describe('requestCreateRole', () => {
+ it('commits requestCreaterole mutation', () => {
+ testAction(actions.requestCreateRole, null, state, [{ type: REQUEST_CREATE_ROLE }]);
+ });
+ });
+
+ describe('createRoleSuccess', () => {
+ it('commits createRoleSuccess mutation', () => {
+ testAction(actions.createRoleSuccess, null, state, [{ type: CREATE_ROLE_SUCCESS }]);
+ });
+ });
+
+ describe('createRoleError', () => {
+ it('commits createRoleError mutation', () => {
+ const payload = {
+ error: new Error(),
+ };
+
+ testAction(actions.createRoleError, payload, state, [{ type: CREATE_ROLE_ERROR, payload }]);
+ });
+ });
+
+ describe('createCluster', () => {
+ let requestPayload;
+
+ beforeEach(() => {
+ requestPayload = {
+ name: clusterName,
+ environment_scope: environmentScope,
+ managed: gitlabManagedCluster,
+ provider_aws_attributes: {
+ region,
+ vpc_id: vpc,
+ subnet_ids: subnet,
+ role_arn: role,
+ key_name: keyPair,
+ security_group_id: securityGroup,
+ instance_type: instanceType,
+ num_nodes: nodeCount,
+ },
+ };
+ state = Object.assign(createState(), {
+ clusterName,
+ environmentScope,
+ kubernetesVersion,
+ selectedRegion: region,
+ selectedVpc: vpc,
+ selectedSubnet: subnet,
+ selectedRole: role,
+ selectedKeyPair: keyPair,
+ selectedSecurityGroup: securityGroup,
+ selectedInstanceType: instanceType,
+ nodeCount,
+ gitlabManagedCluster,
+ });
+ });
+
+ describe('when request succeeds', () => {
+ beforeEach(() => {
+ mock.onPost(state.createClusterPath, requestPayload).reply(201, null, {
+ location: '/clusters/1',
+ });
+ });
+
+ it('dispatches createClusterSuccess action', () =>
+ testAction(
+ actions.createCluster,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestCreateCluster' },
+ { type: 'createClusterSuccess', payload: newClusterUrl },
+ ],
+ ));
+ });
+
+ describe('when request fails', () => {
+ let response;
+
+ beforeEach(() => {
+ response = 'Request failed with status code 400';
+ mock.onPost(state.createClusterPath, requestPayload).reply(400, response);
+ });
+
+ it('dispatches createRoleError action', () =>
+ testAction(
+ actions.createCluster,
+ null,
+ state,
+ [],
+ [{ type: 'requestCreateCluster' }, { type: 'createClusterError', payload: response }],
+ ));
+ });
+ });
+
+ describe('requestCreateCluster', () => {
+ it('commits requestCreateCluster mutation', () => {
+ testAction(actions.requestCreateCluster, null, state, [{ type: REQUEST_CREATE_CLUSTER }]);
+ });
+ });
+
+ describe('createClusterSuccess', () => {
+ beforeEach(() => {
+ jest.spyOn(window.location, 'assign').mockImplementation(() => {});
+ });
+ afterEach(() => {
+ window.location.assign.mockRestore();
+ });
+
+ it('redirects to the new cluster URL', () => {
+ actions.createClusterSuccess(null, newClusterUrl);
+
+ expect(window.location.assign).toHaveBeenCalledWith(newClusterUrl);
+ });
+ });
+
+ describe('createClusterError', () => {
+ let payload;
+
+ beforeEach(() => {
+ payload = { name: ['Create cluster failed'] };
+ });
+
+ it('commits createClusterError mutation', () => {
+ testAction(actions.createClusterError, payload, state, [
+ { type: CREATE_CLUSTER_ERROR, payload },
+ ]);
+ });
+
+ it('creates a flash that displays the create cluster error', () => {
+ expect(createFlash).toHaveBeenCalledWith(payload.name[0]);
+ });
+ });
+
+ describe('signOut', () => {
+ beforeEach(() => {
+ mock.onDelete(state.signOutPath).reply(200, null);
+ });
+
+ it('commits signOut mutation', () => {
+ testAction(actions.signOut, null, state, [{ type: SIGN_OUT }]);
+ });
});
});
diff --git a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
index 81b65180fb5..0fb392f5eea 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/mutations_spec.js
@@ -8,7 +8,15 @@ import {
SET_SUBNET,
SET_ROLE,
SET_SECURITY_GROUP,
+ SET_INSTANCE_TYPE,
+ SET_NODE_COUNT,
SET_GITLAB_MANAGED_CLUSTER,
+ REQUEST_CREATE_ROLE,
+ CREATE_ROLE_SUCCESS,
+ CREATE_ROLE_ERROR,
+ REQUEST_CREATE_CLUSTER,
+ CREATE_CLUSTER_ERROR,
+ SIGN_OUT,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import createState from '~/create_cluster/eks_cluster/store/state';
import mutations from '~/create_cluster/eks_cluster/store/mutations';
@@ -24,6 +32,8 @@ describe('Create EKS cluster store mutations', () => {
let role;
let keyPair;
let securityGroup;
+ let instanceType;
+ let nodeCount;
let gitlabManagedCluster;
beforeEach(() => {
@@ -36,6 +46,8 @@ describe('Create EKS cluster store mutations', () => {
role = { name: 'role-1' };
keyPair = { name: 'key pair' };
securityGroup = { name: 'default group' };
+ instanceType = 'small-1';
+ nodeCount = '5';
gitlabManagedCluster = false;
state = createState();
@@ -50,8 +62,10 @@ describe('Create EKS cluster store mutations', () => {
${SET_REGION} | ${'selectedRegion'} | ${{ region }} | ${region} | ${'selected region payload'}
${SET_KEY_PAIR} | ${'selectedKeyPair'} | ${{ keyPair }} | ${keyPair} | ${'selected key pair payload'}
${SET_VPC} | ${'selectedVpc'} | ${{ vpc }} | ${vpc} | ${'selected vpc payload'}
- ${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected sybnet payload'}
+ ${SET_SUBNET} | ${'selectedSubnet'} | ${{ subnet }} | ${subnet} | ${'selected subnet payload'}
${SET_SECURITY_GROUP} | ${'selectedSecurityGroup'} | ${{ securityGroup }} | ${securityGroup} | ${'selected security group payload'}
+ ${SET_INSTANCE_TYPE} | ${'selectedInstanceType'} | ${{ instanceType }} | ${instanceType} | ${'selected instance type payload'}
+ ${SET_NODE_COUNT} | ${'nodeCount'} | ${{ nodeCount }} | ${nodeCount} | ${'node count payload'}
${SET_GITLAB_MANAGED_CLUSTER} | ${'gitlabManagedCluster'} | ${{ gitlabManagedCluster }} | ${gitlabManagedCluster} | ${'gitlab managed cluster'}
`(`$mutation sets $mutatedProperty to $expectedValueDescription`, data => {
const { mutation, mutatedProperty, payload, expectedValue } = data;
@@ -59,4 +73,101 @@ describe('Create EKS cluster store mutations', () => {
mutations[mutation](state, payload);
expect(state[mutatedProperty]).toBe(expectedValue);
});
+
+ describe(`mutation ${REQUEST_CREATE_ROLE}`, () => {
+ beforeEach(() => {
+ mutations[REQUEST_CREATE_ROLE](state);
+ });
+
+ it('sets isCreatingRole to true', () => {
+ expect(state.isCreatingRole).toBe(true);
+ });
+
+ it('sets createRoleError to null', () => {
+ expect(state.createRoleError).toBe(null);
+ });
+
+ it('sets hasCredentials to false', () => {
+ expect(state.hasCredentials).toBe(false);
+ });
+ });
+
+ describe(`mutation ${CREATE_ROLE_SUCCESS}`, () => {
+ beforeEach(() => {
+ mutations[CREATE_ROLE_SUCCESS](state);
+ });
+
+ it('sets isCreatingRole to false', () => {
+ expect(state.isCreatingRole).toBe(false);
+ });
+
+ it('sets createRoleError to null', () => {
+ expect(state.createRoleError).toBe(null);
+ });
+
+ it('sets hasCredentials to false', () => {
+ expect(state.hasCredentials).toBe(true);
+ });
+ });
+
+ describe(`mutation ${CREATE_ROLE_ERROR}`, () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ mutations[CREATE_ROLE_ERROR](state, { error });
+ });
+
+ it('sets isCreatingRole to false', () => {
+ expect(state.isCreatingRole).toBe(false);
+ });
+
+ it('sets createRoleError to the error object', () => {
+ expect(state.createRoleError).toBe(error);
+ });
+
+ it('sets hasCredentials to false', () => {
+ expect(state.hasCredentials).toBe(false);
+ });
+ });
+
+ describe(`mutation ${REQUEST_CREATE_CLUSTER}`, () => {
+ beforeEach(() => {
+ mutations[REQUEST_CREATE_CLUSTER](state);
+ });
+
+ it('sets isCreatingCluster to true', () => {
+ expect(state.isCreatingCluster).toBe(true);
+ });
+
+ it('sets createClusterError to null', () => {
+ expect(state.createClusterError).toBe(null);
+ });
+ });
+
+ describe(`mutation ${CREATE_CLUSTER_ERROR}`, () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ mutations[CREATE_CLUSTER_ERROR](state, { error });
+ });
+
+ it('sets isCreatingRole to false', () => {
+ expect(state.isCreatingCluster).toBe(false);
+ });
+
+ it('sets createRoleError to the error object', () => {
+ expect(state.createClusterError).toBe(error);
+ });
+ });
+
+ describe(`mutation ${SIGN_OUT}`, () => {
+ beforeEach(() => {
+ state.hasCredentials = true;
+ mutations[SIGN_OUT](state);
+ });
+
+ it('sets hasCredentials to false', () => {
+ expect(state.hasCredentials).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js b/spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js
index 7b8df03d3c3..b1c25d8fff7 100644
--- a/spec/frontend/projects/gke_cluster_namespace/gke_cluster_namespace_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster_namespace/gke_cluster_namespace_spec.js
@@ -1,4 +1,4 @@
-import initGkeNamespace from '~/projects/gke_cluster_namespace';
+import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
describe('GKE cluster namespace', () => {
const changeEvent = new Event('change');
@@ -14,7 +14,7 @@ describe('GKE cluster namespace', () => {
<input class="js-gl-managed" type="checkbox" value="1" checked />
<div class="js-namespace">
<input type="text" />
- </div>
+ </div>
<div class="js-namespace-prefixed">
<input type="text" />
</div>
diff --git a/spec/frontend/create_cluster/init_create_cluster_spec.js b/spec/frontend/create_cluster/init_create_cluster_spec.js
new file mode 100644
index 00000000000..e7b9a7adde4
--- /dev/null
+++ b/spec/frontend/create_cluster/init_create_cluster_spec.js
@@ -0,0 +1,73 @@
+import initCreateCluster from '~/create_cluster/init_create_cluster';
+import initGkeDropdowns from '~/create_cluster/gke_cluster';
+import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
+import PersistentUserCallout from '~/persistent_user_callout';
+
+jest.mock('~/create_cluster/gke_cluster', () => jest.fn());
+jest.mock('~/create_cluster/gke_cluster_namespace', () => jest.fn());
+jest.mock('~/persistent_user_callout', () => ({
+ factory: jest.fn(),
+}));
+
+describe('initCreateCluster', () => {
+ let document;
+ let gon;
+
+ beforeEach(() => {
+ document = {
+ body: { dataset: {} },
+ querySelector: jest.fn(),
+ };
+ gon = { features: {} };
+ });
+ afterEach(() => {
+ initGkeDropdowns.mockReset();
+ initGkeNamespace.mockReset();
+ PersistentUserCallout.factory.mockReset();
+ });
+
+ describe.each`
+ pageSuffix | page
+ ${':clusters:new'} | ${'project:clusters:new'}
+ ${':clusters:create_gcp'} | ${'groups:clusters:create_gcp'}
+ ${':clusters:create_user'} | ${'admin:clusters:create_user'}
+ `('when cluster page ends in $pageSuffix', ({ page }) => {
+ beforeEach(() => {
+ document.body.dataset = { page };
+
+ initCreateCluster(document, gon);
+ });
+
+ it('initializes create GKE cluster app', () => {
+ expect(initGkeDropdowns).toHaveBeenCalled();
+ });
+
+ it('initializes gcp signup offer banner', () => {
+ expect(PersistentUserCallout.factory).toHaveBeenCalled();
+ });
+ });
+
+ describe('when creating a project level cluster', () => {
+ it('initializes gke namespace app', () => {
+ document.body.dataset.page = 'project:clusters:new';
+
+ initCreateCluster(document, gon);
+
+ expect(initGkeNamespace).toHaveBeenCalled();
+ });
+ });
+
+ describe.each`
+ clusterLevel | page
+ ${'group level'} | ${'groups:clusters:new'}
+ ${'instance level'} | ${'admin:clusters:create_gcp'}
+ `('when creating a $clusterLevel cluster', ({ page }) => {
+ it('does not initialize gke namespace app', () => {
+ document.body.dataset = { page };
+
+ initCreateCluster(document, gon);
+
+ expect(initGkeNamespace).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/cycle_analytics/stage_nav_item_spec.js b/spec/frontend/cycle_analytics/stage_nav_item_spec.js
index ff079082ca7..a7a1d563e1e 100644
--- a/spec/frontend/cycle_analytics/stage_nav_item_spec.js
+++ b/spec/frontend/cycle_analytics/stage_nav_item_spec.js
@@ -133,45 +133,19 @@ describe('StageNavItem', () => {
hasStageName();
});
- it('renders options menu', () => {
- expect(wrapper.find('.more-actions-toggle').exists()).toBe(true);
+ it('does not render options menu', () => {
+ expect(wrapper.find('.more-actions-toggle').exists()).toBe(false);
});
- describe('Default stages', () => {
- beforeEach(() => {
- wrapper = createComponent(
- { canEdit: true, isUserAllowed: true, isDefaultStage: true },
- false,
- );
- });
- it('can hide the stage', () => {
- expect(wrapper.text()).toContain('Hide stage');
- });
- it('can not edit the stage', () => {
- expect(wrapper.text()).not.toContain('Edit stage');
- });
- it('can not remove the stage', () => {
- expect(wrapper.text()).not.toContain('Remove stage');
- });
+ it('can not edit the stage', () => {
+ expect(wrapper.text()).not.toContain('Edit stage');
+ });
+ it('can not remove the stage', () => {
+ expect(wrapper.text()).not.toContain('Remove stage');
});
- describe('Custom stages', () => {
- beforeEach(() => {
- wrapper = createComponent(
- { canEdit: true, isUserAllowed: true, isDefaultStage: false },
- false,
- );
- });
- it('can edit the stage', () => {
- expect(wrapper.text()).toContain('Edit stage');
- });
- it('can remove the stage', () => {
- expect(wrapper.text()).toContain('Remove stage');
- });
-
- it('can not hide the stage', () => {
- expect(wrapper.text()).not.toContain('Hide stage');
- });
+ it('can not hide the stage', () => {
+ expect(wrapper.text()).not.toContain('Hide stage');
});
});
});
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 290c0e797cb..3c6553f3547 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -41,6 +41,12 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.fixturesBasePath = `${ROOT_PATH}/tmp/tests/frontend/fixtures${IS_EE ? '-ee' : ''}`;
this.global.staticFixturesBasePath = `${ROOT_PATH}/spec/frontend/fixtures`;
+ /**
+ * window.fetch() is required by the apollo-upload-client library otherwise
+ * a ReferenceError is generated: https://github.com/jaydenseric/apollo-upload-client/issues/100
+ */
+ this.global.fetch = () => {};
+
// Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317
this.global.document.createRange = () => ({
setStart: () => {},
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
new file mode 100644
index 00000000000..54e8b0848a2
--- /dev/null
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -0,0 +1,105 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlLoadingIcon, GlLink } from '@gitlab/ui';
+import Stacktrace from '~/error_tracking/components/stacktrace.vue';
+import ErrorDetails from '~/error_tracking/components/error_details.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ErrorDetails', () => {
+ let store;
+ let wrapper;
+ let actions;
+ let getters;
+
+ function mountComponent() {
+ wrapper = shallowMount(ErrorDetails, {
+ localVue,
+ store,
+ propsData: {
+ issueDetailsPath: '/123/details',
+ issueStackTracePath: '/stacktrace',
+ },
+ });
+ }
+
+ beforeEach(() => {
+ actions = {
+ startPollingDetails: () => {},
+ startPollingStacktrace: () => {},
+ };
+
+ getters = {
+ sentryUrl: () => 'sentry.io',
+ stacktrace: () => [{ context: [1, 2], lineNo: 53, filename: 'index.js' }],
+ };
+
+ const state = {
+ error: {},
+ loading: true,
+ stacktraceData: {},
+ loadingStacktrace: true,
+ };
+
+ store = new Vuex.Store({
+ modules: {
+ details: {
+ namespaced: true,
+ actions,
+ state,
+ getters,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('loading', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('should show spinner while loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find(GlLink).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ });
+ });
+
+ describe('Error details', () => {
+ it('should show Sentry error details without stacktrace', () => {
+ store.state.details.loading = false;
+ store.state.details.error.id = 1;
+ mountComponent();
+ expect(wrapper.find(GlLink).exists()).toBe(true);
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ });
+
+ describe('Stacktrace', () => {
+ it('should show stacktrace', () => {
+ store.state.details.loading = false;
+ store.state.details.error.id = 1;
+ store.state.details.loadingStacktrace = false;
+ mountComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(true);
+ });
+
+ it('should NOT show stacktrace if no entries', () => {
+ store.state.details.loading = false;
+ store.state.details.loadingStacktrace = false;
+ store.getters = { 'details/sentryUrl': () => 'sentry.io', 'details/stacktrace': () => [] };
+ mountComponent();
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.find(Stacktrace).exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index ce8b8908026..1bbf23cc602 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -34,7 +34,7 @@ describe('ErrorTrackingList', () => {
beforeEach(() => {
actions = {
- getErrorList: () => {},
+ getSentryData: () => {},
startPolling: () => {},
restartPolling: jest.fn().mockName('restartPolling'),
};
@@ -45,8 +45,13 @@ describe('ErrorTrackingList', () => {
};
store = new Vuex.Store({
- actions,
- state,
+ modules: {
+ list: {
+ namespaced: true,
+ actions,
+ state,
+ },
+ },
});
});
@@ -70,7 +75,7 @@ describe('ErrorTrackingList', () => {
describe('results', () => {
beforeEach(() => {
- store.state.loading = false;
+ store.state.list.loading = false;
mountComponent();
});
@@ -84,7 +89,7 @@ describe('ErrorTrackingList', () => {
describe('no results', () => {
beforeEach(() => {
- store.state.loading = false;
+ store.state.list.loading = false;
mountComponent();
});
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
new file mode 100644
index 00000000000..95958408770
--- /dev/null
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+describe('Stacktrace Entry', () => {
+ let wrapper;
+
+ function mountComponent(props) {
+ wrapper = shallowMount(StackTraceEntry, {
+ propsData: {
+ filePath: 'sidekiq/util.rb',
+ lines: [
+ [22, ' def safe_thread(name, \u0026block)\n'],
+ [23, ' Thread.new do\n'],
+ [24, " Thread.current['sidekiq_label'] = name\n"],
+ [25, ' watchdog(name, \u0026block)\n'],
+ ],
+ errorLine: 24,
+ ...props,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('should render stacktrace entry collapsed', () => {
+ expect(wrapper.find(StackTraceEntry).exists()).toBe(true);
+ expect(wrapper.find(ClipboardButton).exists()).toBe(true);
+ expect(wrapper.find(Icon).exists()).toBe(true);
+ expect(wrapper.find(FileIcon).exists()).toBe(true);
+ expect(wrapper.element.querySelectorAll('table').length).toBe(0);
+ });
+
+ it('should render stacktrace entry table expanded', () => {
+ mountComponent({ expanded: true });
+ expect(wrapper.element.querySelectorAll('tr.line_holder').length).toBe(4);
+ expect(wrapper.element.querySelectorAll('.line_content.old').length).toBe(1);
+ });
+});
diff --git a/spec/frontend/error_tracking/components/stacktrace_spec.js b/spec/frontend/error_tracking/components/stacktrace_spec.js
new file mode 100644
index 00000000000..4f4a60acba4
--- /dev/null
+++ b/spec/frontend/error_tracking/components/stacktrace_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+import Stacktrace from '~/error_tracking/components/stacktrace.vue';
+import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
+
+describe('ErrorDetails', () => {
+ let wrapper;
+
+ const stackTraceEntry = {
+ filename: 'sidekiq/util.rb',
+ context: [
+ [22, ' def safe_thread(name, \u0026block)\n'],
+ [23, ' Thread.new do\n'],
+ [24, " Thread.current['sidekiq_label'] = name\n"],
+ [25, ' watchdog(name, \u0026block)\n'],
+ ],
+ lineNo: 24,
+ };
+
+ function mountComponent(entries) {
+ wrapper = shallowMount(Stacktrace, {
+ propsData: {
+ entries,
+ },
+ });
+ }
+
+ describe('Stacktrace', () => {
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('should render single Stacktrace entry', () => {
+ mountComponent([stackTraceEntry]);
+ expect(wrapper.findAll(StackTraceEntry).length).toBe(1);
+ });
+
+ it('should render multiple Stacktrace entry', () => {
+ const entriesNum = 3;
+ mountComponent(new Array(entriesNum).fill(stackTraceEntry));
+ expect(wrapper.findAll(StackTraceEntry).length).toBe(entriesNum);
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
new file mode 100644
index 00000000000..f72cd1e413b
--- /dev/null
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -0,0 +1,94 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import createFlash from '~/flash';
+import * as actions from '~/error_tracking/store/details/actions';
+import * as types from '~/error_tracking/store/details/mutation_types';
+
+jest.mock('~/flash.js');
+let mock;
+
+describe('Sentry error details store actions', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ createFlash.mockClear();
+ });
+
+ describe('startPollingDetails', () => {
+ const endpoint = '123/details';
+ it('should commit SET_ERROR with received response', done => {
+ const payload = { error: { id: 1 } };
+ mock.onGet().reply(200, payload);
+ testAction(
+ actions.startPollingDetails,
+ { endpoint },
+ {},
+ [
+ { type: types.SET_ERROR, payload: payload.error },
+ { type: types.SET_LOADING, payload: false },
+ ],
+ [],
+ () => {
+ done();
+ },
+ );
+ });
+
+ it('should show flash on API error', done => {
+ mock.onGet().reply(400);
+
+ testAction(
+ actions.startPollingDetails,
+ { endpoint },
+ {},
+ [{ type: types.SET_LOADING, payload: false }],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ done();
+ },
+ );
+ });
+ });
+
+ describe('startPollingStacktrace', () => {
+ const endpoint = '123/stacktrace';
+ it('should commit SET_ERROR with received response', done => {
+ const payload = { error: [1, 2, 3] };
+ mock.onGet().reply(200, payload);
+ testAction(
+ actions.startPollingStacktrace,
+ { endpoint },
+ {},
+ [
+ { type: types.SET_STACKTRACE_DATA, payload: payload.error },
+ { type: types.SET_LOADING_STACKTRACE, payload: false },
+ ],
+ [],
+ () => {
+ done();
+ },
+ );
+ });
+
+ it('should show flash on API error', done => {
+ mock.onGet().reply(400);
+
+ testAction(
+ actions.startPollingStacktrace,
+ { endpoint },
+ {},
+ [{ type: types.SET_LOADING_STACKTRACE, payload: false }],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ done();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/details/getters_spec.js b/spec/frontend/error_tracking/store/details/getters_spec.js
new file mode 100644
index 00000000000..ea57de5872b
--- /dev/null
+++ b/spec/frontend/error_tracking/store/details/getters_spec.js
@@ -0,0 +1,13 @@
+import * as getters from '~/error_tracking/store/details/getters';
+
+describe('Sentry error details store getters', () => {
+ const state = {
+ stacktraceData: { stack_trace_entries: [1, 2] },
+ };
+
+ describe('stacktrace', () => {
+ it('should get stacktrace', () => {
+ expect(getters.stacktrace(state)).toEqual([2, 1]);
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/list/getters_spec.js b/spec/frontend/error_tracking/store/list/getters_spec.js
new file mode 100644
index 00000000000..3cd7fa37d44
--- /dev/null
+++ b/spec/frontend/error_tracking/store/list/getters_spec.js
@@ -0,0 +1,33 @@
+import * as getters from '~/error_tracking/store/list/getters';
+
+describe('Error Tracking getters', () => {
+ let state;
+
+ const mockErrors = [
+ { title: 'ActiveModel::MissingAttributeError: missing attribute: encrypted_password' },
+ { title: 'Grape::Exceptions::MethodNotAllowed: Grape::Exceptions::MethodNotAllowed' },
+ { title: 'NoMethodError: undefined method `sanitize_http_headers=' },
+ { title: 'NoMethodError: undefined method `pry' },
+ ];
+
+ beforeEach(() => {
+ state = {
+ errors: mockErrors,
+ };
+ });
+
+ describe('search results', () => {
+ it('should return errors filtered by words in title matching the query', () => {
+ const filteredErrors = getters.filterErrorsByTitle(state)('NoMethod');
+
+ expect(filteredErrors).not.toContainEqual(mockErrors[0]);
+ expect(filteredErrors.length).toBe(2);
+ });
+
+ it('should not return results if there is no matching query', () => {
+ const filteredErrors = getters.filterErrorsByTitle(state)('GitLab');
+
+ expect(filteredErrors.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/mutation_spec.js b/spec/frontend/error_tracking/store/list/mutation_spec.js
index 8117104bdbc..6e021185b4d 100644
--- a/spec/frontend/error_tracking/store/mutation_spec.js
+++ b/spec/frontend/error_tracking/store/list/mutation_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/error_tracking/store/mutations';
-import * as types from '~/error_tracking/store/mutation_types';
+import mutations from '~/error_tracking/store/list/mutations';
+import * as types from '~/error_tracking/store/list/mutation_types';
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index 23e57c4bbf1..bff8ad0877a 100644
--- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -1,7 +1,9 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlButton, GlFormInput } from '@gitlab/ui';
+import { GlFormInput } from '@gitlab/ui';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
+import createStore from '~/error_tracking_settings/store';
import { defaultProps } from '../mock';
const localVue = createLocalVue();
@@ -9,15 +11,18 @@ localVue.use(Vuex);
describe('error tracking settings form', () => {
let wrapper;
+ let store;
function mountComponent() {
wrapper = shallowMount(ErrorTrackingForm, {
localVue,
+ store,
propsData: defaultProps,
});
}
beforeEach(() => {
+ store = createStore();
mountComponent();
});
@@ -38,7 +43,7 @@ describe('error tracking settings form', () => {
.attributes('id'),
).toBe('error-tracking-token');
- expect(wrapper.findAll(GlButton).exists()).toBe(true);
+ expect(wrapper.findAll(LoadingButton).exists()).toBe(true);
});
it('is rendered with labels and placeholders', () => {
@@ -59,9 +64,21 @@ describe('error tracking settings form', () => {
});
});
+ describe('loading projects', () => {
+ beforeEach(() => {
+ store.state.isLoadingProjects = true;
+ });
+
+ it('shows loading spinner', () => {
+ const { label, loading } = wrapper.find(LoadingButton).props();
+ expect(loading).toBe(true);
+ expect(label).toBe('Connecting');
+ });
+ });
+
describe('after a successful connection', () => {
beforeEach(() => {
- wrapper.setProps({ connectSuccessful: true });
+ store.state.connectSuccessful = true;
});
it('shows the success checkmark', () => {
@@ -77,7 +94,7 @@ describe('error tracking settings form', () => {
describe('after an unsuccessful connection', () => {
beforeEach(() => {
- wrapper.setProps({ connectError: true });
+ store.state.connectError = true;
});
it('does not show the check mark', () => {
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index 1eab0f7470b..e12c4e20f58 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -69,7 +69,14 @@ describe('error tracking settings actions', () => {
});
it('should request projects correctly', done => {
- testAction(actions.requestProjects, null, state, [{ type: types.RESET_CONNECT }], [], done);
+ testAction(
+ actions.requestProjects,
+ null,
+ state,
+ [{ type: types.SET_PROJECTS_LOADING, payload: true }, { type: types.RESET_CONNECT }],
+ [],
+ done,
+ );
});
it('should receive projects correctly', done => {
@@ -81,6 +88,7 @@ describe('error tracking settings actions', () => {
[
{ type: types.UPDATE_CONNECT_SUCCESS },
{ type: types.RECEIVE_PROJECTS, payload: testPayload },
+ { type: types.SET_PROJECTS_LOADING, payload: false },
],
[],
done,
@@ -93,7 +101,11 @@ describe('error tracking settings actions', () => {
actions.receiveProjectsError,
testPayload,
state,
- [{ type: types.UPDATE_CONNECT_ERROR }, { type: types.CLEAR_PROJECTS }],
+ [
+ { type: types.UPDATE_CONNECT_ERROR },
+ { type: types.CLEAR_PROJECTS },
+ { type: types.SET_PROJECTS_LOADING, payload: false },
+ ],
[],
done,
);
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 8fbdb534b3d..f20c0aa3540 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -8,7 +8,23 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') }
- let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
+
+ # rubocop: disable Layout/TrailingWhitespace
+ let(:merge_request) do
+ create(
+ :merge_request,
+ :with_diffs,
+ source_project: project,
+ target_project: project,
+ description: <<~MARKDOWN.strip_heredoc
+ - [ ] Task List Item
+ - [ ]
+ - [ ] Task List Item 2
+ MARKDOWN
+ )
+ end
+ # rubocop: enable Layout/TrailingWhitespace
+
let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) }
let(:pipeline) do
create(
diff --git a/spec/frontend/fixtures/static/environments_logs.html b/spec/frontend/fixtures/static/environments_logs.html
index ccf9c364154..88bb0a3ed41 100644
--- a/spec/frontend/fixtures/static/environments_logs.html
+++ b/spec/frontend/fixtures/static/environments_logs.html
@@ -2,8 +2,8 @@
class="js-kubernetes-logs"
data-current-environment-name="production"
data-environments-path="/root/my-project/environments.json"
- data-logs-page="/root/my-project/environments/1/logs"
- data-logs-path="/root/my-project/environments/1/logs.json"
+ data-project-full-path="root/my-project"
+ data-environment-id=1
>
<div class="build-page-pod-logs">
<div class="build-trace-container prepend-top-default">
diff --git a/spec/frontend/fixtures/static/signin_tabs.html b/spec/frontend/fixtures/static/signin_tabs.html
index 7e66ab9394b..247a6b03054 100644
--- a/spec/frontend/fixtures/static/signin_tabs.html
+++ b/spec/frontend/fixtures/static/signin_tabs.html
@@ -5,4 +5,7 @@
<li>
<a href="#login-pane">Standard</a>
</li>
+<li>
+<a href="#register-pane">Register</a>
+</li>
</ul>
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
index dded6ce6380..9710fbbc181 100644
--- a/spec/frontend/fixtures/u2f.rb
+++ b/spec/frontend/fixtures/u2f.rb
@@ -34,7 +34,9 @@ context 'U2F' do
before do
sign_in(user)
- allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
+ allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
+ allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
+ end
end
it 'u2f/register.html' do
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
new file mode 100644
index 00000000000..69ad71a1efb
--- /dev/null
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`grafana integration component default state to match the default snapshot 1`] = `
+<section
+ class="settings no-animate js-grafana-integration"
+ id="grafana"
+>
+ <div
+ class="settings-header"
+ >
+ <h4
+ class="js-section-header"
+ >
+
+ Grafana Authentication
+
+ </h4>
+
+ <glbutton-stub
+ class="js-settings-toggle"
+ >
+ Expand
+ </glbutton-stub>
+
+ <p
+ class="js-section-sub-header"
+ >
+
+ Embed Grafana charts in GitLab issues.
+
+ </p>
+ </div>
+
+ <div
+ class="settings-content"
+ >
+ <form>
+ <glformcheckbox-stub
+ class="mb-4"
+ id="grafana-integration-enabled"
+ >
+
+ Active
+
+ </glformcheckbox-stub>
+
+ <glformgroup-stub
+ description="Enter the base URL of the Grafana instance."
+ label="Grafana URL"
+ label-for="grafana-url"
+ >
+ <glforminput-stub
+ id="grafana-url"
+ placeholder="https://my-url.grafana.net/"
+ value="http://test.host"
+ />
+ </glformgroup-stub>
+
+ <glformgroup-stub
+ label="API Token"
+ label-for="grafana-token"
+ >
+ <glforminput-stub
+ id="grafana-token"
+ value="someToken"
+ />
+
+ <p
+ class="form-text text-muted"
+ >
+
+ Enter the Grafana API Token.
+
+ <a
+ href="https://grafana.com/docs/http_api/auth/#create-api-token"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+
+ More information
+
+ <icon-stub
+ class="vertical-align-middle"
+ name="external-link"
+ size="16"
+ />
+ </a>
+ </p>
+ </glformgroup-stub>
+
+ <glbutton-stub
+ variant="success"
+ >
+
+ Save Changes
+
+ </glbutton-stub>
+ </form>
+ </div>
+</section>
+`;
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
new file mode 100644
index 00000000000..c098ada0519
--- /dev/null
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -0,0 +1,125 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
+import { createStore } from '~/grafana_integration/store';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import { TEST_HOST } from 'helpers/test_constants';
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
+
+describe('grafana integration component', () => {
+ let wrapper;
+ let store;
+ const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
+ const grafanaIntegrationUrl = `${TEST_HOST}`;
+ const grafanaIntegrationToken = 'someToken';
+
+ beforeEach(() => {
+ store = createStore({
+ operationsSettingsEndpoint,
+ grafanaIntegrationUrl,
+ grafanaIntegrationToken,
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ createFlash.mockReset();
+ refreshCurrentPage.mockReset();
+ }
+ });
+
+ describe('default state', () => {
+ it('to match the default snapshot', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders header text', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.find('.js-section-header').text()).toBe('Grafana Authentication');
+ });
+
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
+ describe('sub-header', () => {
+ it('renders descriptive text', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.find('.js-section-sub-header').text()).toContain(
+ 'Embed Grafana charts in GitLab issues.',
+ );
+ });
+ });
+
+ describe('form', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'patch').mockImplementation();
+ });
+
+ afterEach(() => {
+ axios.patch.mockReset();
+ });
+
+ describe('submit button', () => {
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+
+ const endpointRequest = [
+ operationsSettingsEndpoint,
+ {
+ project: {
+ grafana_integration_attributes: {
+ grafana_url: grafanaIntegrationUrl,
+ token: grafanaIntegrationToken,
+ enabled: false,
+ },
+ },
+ },
+ ];
+
+ it('submits form on click', () => {
+ wrapper = mount(GrafanaIntegration, { store });
+ axios.patch.mockResolvedValue();
+
+ findSubmitButton(wrapper).trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+ return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ });
+
+ it('creates flash banner on error', () => {
+ const message = 'mockErrorMessage';
+ wrapper = mount(GrafanaIntegration, { store });
+ axios.patch.mockRejectedValue({ response: { data: { message } } });
+
+ findSubmitButton().trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+ return wrapper.vm
+ .$nextTick()
+ .then(jest.runAllTicks)
+ .then(() =>
+ expect(createFlash).toHaveBeenCalledWith(
+ `There was an error saving your changes. ${message}`,
+ 'alert',
+ ),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/grafana_integration/store/mutations_spec.js b/spec/frontend/grafana_integration/store/mutations_spec.js
new file mode 100644
index 00000000000..18e87394189
--- /dev/null
+++ b/spec/frontend/grafana_integration/store/mutations_spec.js
@@ -0,0 +1,35 @@
+import mutations from '~/grafana_integration/store/mutations';
+import createState from '~/grafana_integration/store/state';
+
+describe('grafana integration mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_GRAFANA_URL', () => {
+ it('sets grafanaUrl', () => {
+ const mockUrl = 'mockUrl';
+ mutations.SET_GRAFANA_URL(localState, mockUrl);
+
+ expect(localState.grafanaUrl).toBe(mockUrl);
+ });
+ });
+
+ describe('SET_GRAFANA_TOKEN', () => {
+ it('sets grafanaToken', () => {
+ const mockToken = 'mockToken';
+ mutations.SET_GRAFANA_TOKEN(localState, mockToken);
+
+ expect(localState.grafanaToken).toBe(mockToken);
+ });
+ });
+ describe('SET_GRAFANA_ENABLED', () => {
+ it('updates grafanaEnabled for integration', () => {
+ mutations.SET_GRAFANA_ENABLED(localState, true);
+
+ expect(localState.grafanaEnabled).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js
index 2e8bff298c4..0798ca580e2 100644
--- a/spec/frontend/helpers/monitor_helper_spec.js
+++ b/spec/frontend/helpers/monitor_helper_spec.js
@@ -41,5 +41,87 @@ describe('monitor helper', () => {
),
).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]);
});
+
+ it('updates series name from templates', () => {
+ const config = {
+ ...defaultConfig,
+ name: '{{cmd}}',
+ };
+
+ const [result] = monitorHelper.makeDataSeries(
+ [{ metric: { cmd: 'brpop' }, values: series }],
+ config,
+ );
+
+ expect(result.name).toEqual('brpop');
+ });
+
+ it('supports space-padded template expressions', () => {
+ const config = {
+ ...defaultConfig,
+ name: 'backend: {{ backend }}',
+ };
+
+ const [result] = monitorHelper.makeDataSeries(
+ [{ metric: { backend: 'HA Server' }, values: series }],
+ config,
+ );
+
+ expect(result.name).toEqual('backend: HA Server');
+ });
+
+ it('supports repeated template variables', () => {
+ const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' };
+
+ const [result] = monitorHelper.makeDataSeries(
+ [{ metric: { cmd: 'brpop' }, values: series }],
+ config,
+ );
+
+ expect(result.name).toEqual('brpop, brpop');
+ });
+
+ it('supports hyphenated template variables', () => {
+ const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
+
+ const [result] = monitorHelper.makeDataSeries(
+ [{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
+ config,
+ );
+
+ expect(result.name).toEqual('expired - test-attribute-value');
+ });
+
+ it('updates multiple series names from templates', () => {
+ const config = {
+ ...defaultConfig,
+ name: '{{job}}: {{cmd}}',
+ };
+
+ const [result] = monitorHelper.makeDataSeries(
+ [{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
+ config,
+ );
+
+ expect(result.name).toEqual('redis: brpop');
+ });
+
+ it('updates name for each series', () => {
+ const config = {
+ ...defaultConfig,
+ name: '{{cmd}}',
+ };
+
+ const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
+ [
+ { metric: { cmd: 'brpop' }, values: series },
+ { metric: { cmd: 'zrangebyscore' }, values: series },
+ ],
+ config,
+ );
+
+ expect(firstSeries.name).toEqual('brpop');
+ expect(secondSeries.name).toEqual('zrangebyscore');
+ });
});
});
diff --git a/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
new file mode 100644
index 00000000000..5d6c31f01d9
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/__snapshots__/stage_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IDE pipeline stage renders stage details & icon 1`] = `
+<div
+ class="ide-stage card prepend-top-default"
+>
+ <div
+ class="card-header"
+ >
+ <ciicon-stub
+ cssclasses=""
+ size="24"
+ status="[object Object]"
+ />
+
+ <strong
+ class="prepend-left-8 ide-stage-title"
+ data-container="body"
+ data-original-title=""
+ title=""
+ >
+
+ build
+
+ </strong>
+
+ <div
+ class="append-right-8 prepend-left-4"
+ >
+ <span
+ class="badge badge-pill"
+ >
+ 4
+ </span>
+ </div>
+
+ <icon-stub
+ class="ide-stage-collapse-icon"
+ name="angle-down"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="card-body"
+ >
+ <item-stub
+ job="[object Object]"
+ />
+ <item-stub
+ job="[object Object]"
+ />
+ <item-stub
+ job="[object Object]"
+ />
+ <item-stub
+ job="[object Object]"
+ />
+ </div>
+</div>
+`;
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
new file mode 100644
index 00000000000..2e42ab26d27
--- /dev/null
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Stage from '~/ide/components/jobs/stage.vue';
+import Item from '~/ide/components/jobs/item.vue';
+import { stages, jobs } from '../../mock_data';
+
+describe('IDE pipeline stage', () => {
+ let wrapper;
+ const defaultProps = {
+ stage: {
+ ...stages[0],
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ jobs: [...jobs],
+ isLoading: false,
+ isCollapsed: false,
+ },
+ };
+
+ const findHeader = () => wrapper.find({ ref: 'cardHeader' });
+ const findJobList = () => wrapper.find({ ref: 'jobList' });
+
+ const createComponent = props => {
+ wrapper = shallowMount(Stage, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('emits fetch event when mounted', () => {
+ createComponent();
+ expect(wrapper.emitted().fetch).toBeDefined();
+ });
+
+ it('renders loading icon when no jobs and isLoading is true', () => {
+ createComponent({
+ stage: { ...defaultProps.stage, isLoading: true, jobs: [] },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('emits toggleCollaped event with stage id when clicking header', () => {
+ const id = 5;
+ createComponent({ stage: { ...defaultProps.stage, id } });
+ findHeader().trigger('click');
+ expect(wrapper.emitted().toggleCollapsed[0][0]).toBe(id);
+ });
+
+ it('emits clickViewLog entity with job', () => {
+ const [job] = defaultProps.stage.jobs;
+ createComponent();
+ wrapper
+ .findAll(Item)
+ .at(0)
+ .vm.$emit('clickViewLog', job);
+ expect(wrapper.emitted().clickViewLog[0][0]).toBe(job);
+ });
+
+ it('renders stage details & icon', () => {
+ createComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('when collapsed', () => {
+ beforeEach(() => {
+ createComponent({ stage: { ...defaultProps.stage, isCollapsed: true } });
+ });
+
+ it('does not render job list', () => {
+ expect(findJobList().isVisible()).toBe(false);
+ });
+
+ it('sets border bottom class', () => {
+ expect(findHeader().classes('border-bottom-0')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index dfc76628d0c..6a33f4998c5 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -24,6 +24,9 @@ describe('IDE clientside preview', () => {
getFileData: jest.fn().mockReturnValue(Promise.resolve({})),
getRawFileData: jest.fn().mockReturnValue(Promise.resolve('')),
};
+ const storeClientsideActions = {
+ pingUsage: jest.fn().mockReturnValue(Promise.resolve({})),
+ };
const waitForCalls = () => new Promise(setImmediate);
@@ -42,6 +45,12 @@ describe('IDE clientside preview', () => {
...getters,
},
actions: storeActions,
+ modules: {
+ clientside: {
+ namespaced: true,
+ actions: storeClientsideActions,
+ },
+ },
});
wrapper = shallowMount(Clientside, {
@@ -76,7 +85,8 @@ describe('IDE clientside preview', () => {
describe('with main entry', () => {
beforeEach(() => {
createComponent({ getters: { packageJson: dummyPackageJson } });
- return wrapper.vm.initPreview();
+
+ return waitForCalls();
});
it('creates sandpack manager', () => {
@@ -95,6 +105,10 @@ describe('IDE clientside preview', () => {
},
);
});
+
+ it('pings usage', () => {
+ expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(1);
+ });
});
describe('computed', () => {
@@ -178,13 +192,13 @@ describe('IDE clientside preview', () => {
});
describe('showOpenInCodeSandbox', () => {
- it('returns true when visiblity is public', () => {
+ it('returns true when visibility is public', () => {
createComponent({ getters: { currentProject: () => ({ visibility: 'public' }) } });
expect(wrapper.vm.showOpenInCodeSandbox).toBe(true);
});
- it('returns false when visiblity is private', () => {
+ it('returns false when visibility is private', () => {
createComponent({ getters: { currentProject: () => ({ visibility: 'private' }) } });
expect(wrapper.vm.showOpenInCodeSandbox).toBe(false);
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 3d5ed4b5c0c..bb0d20bed91 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -1,11 +1,18 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
import services from '~/ide/services';
import Api from '~/api';
+import { escapeFileUrl } from '~/ide/stores/utils';
jest.mock('~/api');
const TEST_PROJECT_ID = 'alice/wonderland';
const TEST_BRANCH = 'master-patch-123';
const TEST_COMMIT_SHA = '123456789';
+const TEST_FILE_PATH = 'README2.md';
+const TEST_FILE_OLD_PATH = 'OLD_README2.md';
+const TEST_FILE_PATH_SPECIAL = 'READM?ME/abc';
+const TEST_FILE_CONTENTS = 'raw file content';
describe('IDE services', () => {
describe('commit', () => {
@@ -28,4 +35,80 @@ describe('IDE services', () => {
expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload);
});
});
+
+ describe('getBaseRawFileData', () => {
+ let file;
+ let mock;
+
+ beforeEach(() => {
+ file = {
+ mrChange: null,
+ projectId: TEST_PROJECT_ID,
+ path: TEST_FILE_PATH,
+ };
+
+ jest.spyOn(axios, 'get');
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('gives back file.baseRaw for files with that property present', () => {
+ file.baseRaw = TEST_FILE_CONTENTS;
+
+ return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ expect(content).toEqual(TEST_FILE_CONTENTS);
+ });
+ });
+
+ it('gives back file.baseRaw for files for temp files', () => {
+ file.tempFile = true;
+ file.baseRaw = TEST_FILE_CONTENTS;
+
+ return services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ expect(content).toEqual(TEST_FILE_CONTENTS);
+ });
+ });
+
+ describe.each`
+ relativeUrlRoot | filePath | isRenamed
+ ${''} | ${TEST_FILE_PATH} | ${false}
+ ${''} | ${TEST_FILE_OLD_PATH} | ${true}
+ ${''} | ${TEST_FILE_PATH_SPECIAL} | ${false}
+ ${''} | ${TEST_FILE_PATH_SPECIAL} | ${true}
+ ${'gitlab'} | ${TEST_FILE_OLD_PATH} | ${true}
+ `(
+ 'with relativeUrlRoot ($relativeUrlRoot) and filePath ($filePath) and isRenamed ($isRenamed)',
+ ({ relativeUrlRoot, filePath, isRenamed }) => {
+ beforeEach(() => {
+ if (isRenamed) {
+ file.mrChange = {
+ renamed_file: true,
+ old_path: filePath,
+ };
+ } else {
+ file.path = filePath;
+ }
+
+ gon.relative_url_root = relativeUrlRoot;
+
+ mock
+ .onGet(
+ `${relativeUrlRoot}/${TEST_PROJECT_ID}/raw/${TEST_COMMIT_SHA}/${escapeFileUrl(
+ filePath,
+ )}`,
+ )
+ .reply(200, TEST_FILE_CONTENTS);
+ });
+
+ it('fetches file content', () =>
+ services.getBaseRawFileData(file, TEST_COMMIT_SHA).then(content => {
+ expect(content).toEqual(TEST_FILE_CONTENTS);
+ }));
+ },
+ );
+ });
});
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
new file mode 100644
index 00000000000..a47bc0bd711
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
@@ -0,0 +1,39 @@
+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 * as actions from '~/ide/stores/modules/clientside/actions';
+
+const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
+const TEST_USAGE_URL = `${TEST_PROJECT_URL}/usage_ping/web_ide_clientside_preview`;
+
+describe('IDE store module clientside actions', () => {
+ let rootGetters;
+ let mock;
+
+ beforeEach(() => {
+ rootGetters = {
+ currentProject: {
+ web_url: TEST_PROJECT_URL,
+ },
+ };
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('pingUsage', () => {
+ it('posts to usage endpoint', done => {
+ const usageSpy = jest.fn(() => [200]);
+
+ mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
+
+ testAction(actions.pingUsage, null, rootGetters, [], [], () => {
+ expect(usageSpy).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
new file mode 100644
index 00000000000..f57391a6b0d
--- /dev/null
+++ b/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = `
+<glemptystate-stub
+ description="The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
+ svgpath="/emptySvg"
+ title="There are no issues to show"
+/>
+`;
+
+exports[`Issuables list component with empty issues response with closed state should display a message "There are no closed issues" if there are no closed issues 1`] = `"There are no closed issues"`;
+
+exports[`Issuables list component with empty issues response with empty query should display the message "There are no open issues" 1`] = `"There are no open issues"`;
+
+exports[`Issuables list component with empty issues response with query in window location should display "Sorry, your filter produced no results" if filters are too specific 1`] = `"Sorry, your filter produced no results"`;
diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js
new file mode 100644
index 00000000000..6148f3c68f2
--- /dev/null
+++ b/spec/frontend/issuables_list/components/issuable_spec.js
@@ -0,0 +1,345 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import { trimText } from 'helpers/text_helper';
+import initUserPopovers from '~/user_popovers';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import Issuable from '~/issuables_list/components/issuable.vue';
+import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
+import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
+
+jest.mock('~/user_popovers');
+
+const TEST_NOW = '2019-08-28T20:03:04.713Z';
+const TEST_MONTH_AGO = '2019-07-28';
+const TEST_MONTH_LATER = '2019-09-30';
+const DATE_FORMAT = 'mmm d, yyyy';
+const TEST_USER_NAME = 'Tyler Durden';
+const TEST_BASE_URL = `${TEST_HOST}/issues`;
+const TEST_TASK_STATUS = '50 of 100 tasks completed';
+const TEST_MILESTONE = {
+ title: 'Milestone title',
+ web_url: `${TEST_HOST}/milestone/1`,
+};
+const TEXT_CLOSED = 'CLOSED';
+const TEST_META_COUNT = 100;
+
+// Use FixedDate so that time sensitive info in snapshots don't fail
+class FixedDate extends Date {
+ constructor(date = TEST_NOW) {
+ super(date);
+ }
+}
+
+describe('Issuable component', () => {
+ let issuable;
+ let DateOrig;
+ let wrapper;
+
+ const factory = (props = {}) => {
+ wrapper = shallowMount(Issuable, {
+ propsData: {
+ issuable: simpleIssue,
+ baseUrl: TEST_BASE_URL,
+ ...props,
+ },
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ issuable = { ...simpleIssue };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeAll(() => {
+ DateOrig = window.Date;
+ window.Date = FixedDate;
+ });
+
+ afterAll(() => {
+ window.Date = DateOrig;
+ });
+
+ const findConfidentialIcon = () => wrapper.find('.fa-eye-slash');
+ const findTaskStatus = () => wrapper.find('.task-status');
+ const findOpenedAgoContainer = () => wrapper.find({ ref: 'openedAgoByContainer' });
+ const findMilestone = () => wrapper.find('.js-milestone');
+ const findMilestoneTooltip = () => findMilestone().attributes('data-original-title');
+ const findDueDate = () => wrapper.find('.js-due-date');
+ const findLabelContainer = () => wrapper.find('.js-labels');
+ const findLabelLinks = () => findLabelContainer().findAll(GlLink);
+ const findWeight = () => wrapper.find('.js-weight');
+ const findAssignees = () => wrapper.find(IssueAssignees);
+ const findMergeRequestsCount = () => wrapper.find('.js-merge-requests');
+ const findUpvotes = () => wrapper.find('.js-upvotes');
+ const findDownvotes = () => wrapper.find('.js-downvotes');
+ const findNotes = () => wrapper.find('.js-notes');
+ const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
+
+ describe('when mounted', () => {
+ it('initializes user popovers', () => {
+ expect(initUserPopovers).not.toHaveBeenCalled();
+
+ factory();
+
+ expect(initUserPopovers).toHaveBeenCalledWith([findOpenedAgoContainer().find('a').element]);
+ });
+ });
+
+ describe('with simple issuable', () => {
+ beforeEach(() => {
+ Object.assign(issuable, {
+ has_tasks: false,
+ task_status: TEST_TASK_STATUS,
+ created_at: TEST_MONTH_AGO,
+ author: {
+ ...issuable.author,
+ name: TEST_USER_NAME,
+ },
+ labels: [],
+ });
+
+ factory({ issuable });
+ });
+
+ it.each`
+ desc | finder
+ ${'bulk editing checkbox'} | ${findBulkCheckbox}
+ ${'confidential icon'} | ${findConfidentialIcon}
+ ${'task status'} | ${findTaskStatus}
+ ${'milestone'} | ${findMilestone}
+ ${'due date'} | ${findDueDate}
+ ${'labels'} | ${findLabelContainer}
+ ${'weight'} | ${findWeight}
+ ${'merge request count'} | ${findMergeRequestsCount}
+ ${'upvotes'} | ${findUpvotes}
+ ${'downvotes'} | ${findDownvotes}
+ `('does not render $desc', ({ finder }) => {
+ expect(finder().exists()).toBe(false);
+ });
+
+ it('does not have closed text', () => {
+ expect(wrapper.text()).not.toContain(TEXT_CLOSED);
+ });
+
+ it('does not have closed class', () => {
+ expect(wrapper.classes('closed')).toBe(false);
+ });
+
+ it('renders fuzzy opened date and author', () => {
+ expect(trimText(findOpenedAgoContainer().text())).toEqual(
+ `opened 1 month ago by ${TEST_USER_NAME}`,
+ );
+ });
+
+ it('renders no comments', () => {
+ expect(findNotes().classes('no-comments')).toBe(true);
+ });
+ });
+
+ describe('with confidential issuable', () => {
+ beforeEach(() => {
+ issuable.confidential = true;
+
+ factory({ issuable });
+ });
+
+ it('renders the confidential icon', () => {
+ expect(findConfidentialIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('with task status', () => {
+ beforeEach(() => {
+ Object.assign(issuable, {
+ has_tasks: true,
+ task_status: TEST_TASK_STATUS,
+ });
+
+ factory({ issuable });
+ });
+
+ it('renders task status', () => {
+ expect(findTaskStatus().exists()).toBe(true);
+ expect(findTaskStatus().text()).toBe(TEST_TASK_STATUS);
+ });
+ });
+
+ describe.each`
+ desc | dueDate | expectedTooltipPart
+ ${'past due'} | ${TEST_MONTH_AGO} | ${'Past due'}
+ ${'future due'} | ${TEST_MONTH_LATER} | ${'1 month remaining'}
+ `('with milestone with $desc', ({ dueDate, expectedTooltipPart }) => {
+ beforeEach(() => {
+ issuable.milestone = { ...TEST_MILESTONE, due_date: dueDate };
+
+ factory({ issuable });
+ });
+
+ it('renders milestone', () => {
+ expect(findMilestone().exists()).toBe(true);
+ expect(
+ findMilestone()
+ .find('.fa-clock-o')
+ .exists(),
+ ).toBe(true);
+ expect(findMilestone().text()).toEqual(TEST_MILESTONE.title);
+ });
+
+ it('renders tooltip', () => {
+ expect(findMilestoneTooltip()).toBe(
+ `${formatDate(dueDate, DATE_FORMAT)} (${expectedTooltipPart})`,
+ );
+ });
+
+ it('renders milestone with the correct href', () => {
+ const { title } = issuable.milestone;
+ const expected = mergeUrlParams({ milestone_title: title }, TEST_BASE_URL);
+
+ expect(findMilestone().attributes('href')).toBe(expected);
+ });
+ });
+
+ describe.each`
+ dueDate | hasClass | desc
+ ${TEST_MONTH_LATER} | ${false} | ${'with future due date'}
+ ${TEST_MONTH_AGO} | ${true} | ${'with past due date'}
+ `('$desc', ({ dueDate, hasClass }) => {
+ beforeEach(() => {
+ issuable.due_date = dueDate;
+
+ factory({ issuable });
+ });
+
+ it('renders due date', () => {
+ expect(findDueDate().exists()).toBe(true);
+ expect(findDueDate().text()).toBe(formatDate(dueDate, DATE_FORMAT));
+ });
+
+ it(hasClass ? 'has cred class' : 'does not have cred class', () => {
+ expect(findDueDate().classes('cred')).toEqual(hasClass);
+ });
+ });
+
+ describe('with labels', () => {
+ beforeEach(() => {
+ issuable.labels = [...testLabels];
+
+ factory({ issuable });
+ });
+
+ it('renders labels', () => {
+ factory({ issuable });
+
+ const labels = findLabelLinks().wrappers.map(label => ({
+ href: label.attributes('href'),
+ text: label.text(),
+ tooltip: label.find('span').attributes('data-original-title'),
+ }));
+
+ const expected = testLabels.map(label => ({
+ href: mergeUrlParams({ 'label_name[]': label.name }, TEST_BASE_URL),
+ text: label.name,
+ tooltip: label.description,
+ }));
+
+ expect(labels).toEqual(expected);
+ });
+ });
+
+ describe.each`
+ weight
+ ${0}
+ ${10}
+ ${12345}
+ `('with weight $weight', ({ weight }) => {
+ beforeEach(() => {
+ issuable.weight = weight;
+
+ factory({ issuable });
+ });
+
+ it('renders weight', () => {
+ expect(findWeight().exists()).toBe(true);
+ expect(findWeight().text()).toEqual(weight.toString());
+ });
+ });
+
+ describe('with closed state', () => {
+ beforeEach(() => {
+ issuable.state = 'closed';
+
+ factory({ issuable });
+ });
+
+ it('renders closed text', () => {
+ expect(wrapper.text()).toContain(TEXT_CLOSED);
+ });
+
+ it('has closed class', () => {
+ expect(wrapper.classes('closed')).toBe(true);
+ });
+ });
+
+ describe('with assignees', () => {
+ beforeEach(() => {
+ issuable.assignees = testAssignees;
+
+ factory({ issuable });
+ });
+
+ it('renders assignees', () => {
+ expect(findAssignees().exists()).toBe(true);
+ expect(findAssignees().props('assignees')).toEqual(testAssignees);
+ });
+ });
+
+ describe.each`
+ desc | key | finder
+ ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
+ ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
+ ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
+ ${'with notes count'} | ${'user_notes_count'} | ${findNotes}
+ `('$desc', ({ key, finder }) => {
+ beforeEach(() => {
+ issuable[key] = TEST_META_COUNT;
+
+ factory({ issuable });
+ });
+
+ it('renders merge requests count', () => {
+ expect(finder().exists()).toBe(true);
+ expect(finder().text()).toBe(TEST_META_COUNT.toString());
+ expect(finder().classes('no-comments')).toBe(false);
+ });
+ });
+
+ describe('with bulk editing', () => {
+ describe.each`
+ selected | desc
+ ${true} | ${'when selected'}
+ ${false} | ${'when unselected'}
+ `('$desc', ({ selected }) => {
+ beforeEach(() => {
+ factory({ isBulkEditing: true, selected });
+ });
+
+ it(`renders checked is ${selected}`, () => {
+ expect(findBulkCheckbox().element.checked).toBe(selected);
+ });
+
+ it('emits select when clicked', () => {
+ expect(wrapper.emitted().select).toBeUndefined();
+
+ findBulkCheckbox().trigger('click');
+
+ expect(wrapper.emitted().select).toEqual([[{ issuable, selected: !selected }]]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
new file mode 100644
index 00000000000..e598a9c5a5d
--- /dev/null
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -0,0 +1,410 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
+import flash from '~/flash';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'helpers/test_constants';
+import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue';
+import Issuable from '~/issuables_list/components/issuable.vue';
+import issueablesEventBus from '~/issuables_list/eventhub';
+import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants';
+
+jest.mock('~/flash', () => jest.fn());
+jest.mock('~/issuables_list/eventhub');
+
+const TEST_LOCATION = `${TEST_HOST}/issues`;
+const TEST_ENDPOINT = '/issues';
+const TEST_CREATE_ISSUES_PATH = '/createIssue';
+const TEST_EMPTY_SVG_PATH = '/emptySvg';
+
+const localVue = createLocalVue();
+
+const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
+ .fill(0)
+ .map((_, i) => ({
+ id: i,
+ web_url: `url${i}`,
+ }));
+
+describe('Issuables list component', () => {
+ let oldLocation;
+ let mockAxios;
+ let wrapper;
+ let apiSpy;
+
+ const setupApiMock = cb => {
+ apiSpy = jest.fn(cb);
+
+ mockAxios.onGet(TEST_ENDPOINT).reply(cfg => apiSpy(cfg));
+ };
+
+ const factory = (props = { sortKey: 'priority' }) => {
+ wrapper = shallowMount(localVue.extend(IssuablesListApp), {
+ propsData: {
+ endpoint: TEST_ENDPOINT,
+ createIssuePath: TEST_CREATE_ISSUES_PATH,
+ emptySvgPath: TEST_EMPTY_SVG_PATH,
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ const findLoading = () => wrapper.find(GlSkeletonLoading);
+ const findIssuables = () => wrapper.findAll(Issuable);
+ const findFirstIssuable = () => findIssuables().wrappers[0];
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+
+ oldLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: { href: '', search: '' },
+ });
+ window.location.href = TEST_LOCATION;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mockAxios.restore();
+ jest.clearAllMocks();
+ window.location = oldLocation;
+ });
+
+ describe('with failed issues response', () => {
+ beforeEach(() => {
+ setupApiMock(() => [500]);
+
+ factory();
+
+ return waitForPromises();
+ });
+
+ it('does not show loading', () => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+
+ it('flashes an error', () => {
+ expect(flash).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('with successful issues response', () => {
+ beforeEach(() => {
+ setupApiMock(() => [
+ 200,
+ MOCK_ISSUES.slice(0, PAGE_SIZE),
+ {
+ 'x-total': 100,
+ 'x-page': 2,
+ },
+ ]);
+ });
+
+ it('has default props and data', () => {
+ factory();
+ expect(wrapper.vm).toMatchObject({
+ // Props
+ canBulkEdit: false,
+ createIssuePath: TEST_CREATE_ISSUES_PATH,
+ emptySvgPath: TEST_EMPTY_SVG_PATH,
+
+ // Data
+ filters: {
+ state: 'opened',
+ },
+ isBulkEditing: false,
+ issuables: [],
+ loading: true,
+ page: 1,
+ selection: {},
+ totalItems: 0,
+ });
+ });
+
+ it('does not call API until mounted', () => {
+ expect(apiSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('calls API', () => {
+ expect(apiSpy).toHaveBeenCalled();
+ });
+
+ it('shows loading', () => {
+ expect(findLoading().exists()).toBe(true);
+ expect(findIssuables().length).toBe(0);
+ expect(findEmptyState().exists()).toBe(false);
+ });
+ });
+
+ describe('when finished loading', () => {
+ beforeEach(() => {
+ factory();
+
+ return waitForPromises();
+ });
+
+ it('does not display empty state', () => {
+ expect(wrapper.vm.issuables.length).toBeGreaterThan(0);
+ expect(wrapper.vm.emptyState).toEqual({});
+ expect(wrapper.contains(GlEmptyState)).toBe(false);
+ });
+
+ it('sets the proper page and total items', () => {
+ expect(wrapper.vm.totalItems).toBe(100);
+ expect(wrapper.vm.page).toBe(2);
+ });
+
+ it('renders one page of issuables and pagination', () => {
+ expect(findIssuables().length).toBe(PAGE_SIZE);
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('with bulk editing enabled', () => {
+ beforeEach(() => {
+ issueablesEventBus.$on.mockReset();
+ issueablesEventBus.$emit.mockReset();
+
+ setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
+ factory({ canBulkEdit: true });
+
+ return waitForPromises();
+ });
+
+ it('is not enabled by default', () => {
+ expect(wrapper.vm.isBulkEditing).toBe(false);
+ });
+
+ it('does not select issues by default', () => {
+ expect(wrapper.vm.selection).toEqual({});
+ });
+
+ it('"Select All" checkbox toggles all visible issuables"', () => {
+ wrapper.vm.onSelectAll();
+ expect(wrapper.vm.selection).toEqual(
+ wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
+ );
+
+ wrapper.vm.onSelectAll();
+ expect(wrapper.vm.selection).toEqual({});
+ });
+
+ it('"Select All checkbox" selects all issuables if only some are selected"', () => {
+ wrapper.vm.selection = { [wrapper.vm.issuables[0].id]: true };
+ wrapper.vm.onSelectAll();
+ expect(wrapper.vm.selection).toEqual(
+ wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}),
+ );
+ });
+
+ it('selects and deselects issuables', () => {
+ const [i0, i1, i2] = wrapper.vm.issuables;
+
+ expect(wrapper.vm.selection).toEqual({});
+ wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
+ expect(wrapper.vm.selection).toEqual({});
+ wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
+ expect(wrapper.vm.selection).toEqual({ '1': true });
+ wrapper.vm.onSelectIssuable({ issuable: i0, selected: true });
+ expect(wrapper.vm.selection).toEqual({ '1': true, '0': true });
+ wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
+ expect(wrapper.vm.selection).toEqual({ '1': true, '0': true, '2': true });
+ wrapper.vm.onSelectIssuable({ issuable: i2, selected: true });
+ expect(wrapper.vm.selection).toEqual({ '1': true, '0': true, '2': true });
+ wrapper.vm.onSelectIssuable({ issuable: i0, selected: false });
+ expect(wrapper.vm.selection).toEqual({ '1': true, '2': true });
+ });
+
+ it('broadcasts a message to the bulk edit sidebar when a value is added to selection', () => {
+ issueablesEventBus.$emit.mockReset();
+ const i1 = wrapper.vm.issuables[1];
+
+ wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(issueablesEventBus.$emit).toHaveBeenCalledTimes(1);
+ expect(issueablesEventBus.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
+ });
+ });
+
+ it('does not broadcast a message to the bulk edit sidebar when a value is not added to selection', () => {
+ issueablesEventBus.$emit.mockReset();
+
+ return wrapper.vm
+ .$nextTick()
+ .then(waitForPromises)
+ .then(() => {
+ const i1 = wrapper.vm.issuables[1];
+
+ wrapper.vm.onSelectIssuable({ issuable: i1, selected: false });
+ })
+ .then(wrapper.vm.$nextTick)
+ .then(() => {
+ expect(issueablesEventBus.$emit).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ it('listens to a message to toggle bulk editing', () => {
+ expect(wrapper.vm.isBulkEditing).toBe(false);
+ expect(issueablesEventBus.$on.mock.calls[0][0]).toBe('issuables:toggleBulkEdit');
+ issueablesEventBus.$on.mock.calls[0][1](true); // Call the message handler
+
+ return waitForPromises()
+ .then(() => {
+ expect(wrapper.vm.isBulkEditing).toBe(true);
+ issueablesEventBus.$on.mock.calls[0][1](false);
+ })
+ .then(() => {
+ expect(wrapper.vm.isBulkEditing).toBe(false);
+ });
+ });
+ });
+
+ describe('with query params in window.location', () => {
+ const query =
+ '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&utf8=%E2%9C%93&weight=0';
+ const expectedFilters = {
+ assignee_username: 'root',
+ author_username: 'root',
+ confidential: 'yes',
+ my_reaction_emoji: 'airplane',
+ scope: 'all',
+ state: 'opened',
+ utf8: '✓',
+ weight: '0',
+ milestone: 'v3.0',
+ labels: 'Aquapod,Astro',
+ order_by: 'milestone_due',
+ sort: 'desc',
+ };
+
+ beforeEach(() => {
+ window.location.href = `${TEST_LOCATION}${query}`;
+ window.location.search = query;
+ setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
+ factory({ sortKey: 'milestone_due_desc' });
+ return waitForPromises();
+ });
+
+ it('applies filters and sorts', () => {
+ expect(wrapper.vm.hasFilters).toBe(true);
+ expect(wrapper.vm.filters).toEqual(expectedFilters);
+
+ expect(apiSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ params: {
+ ...expectedFilters,
+ with_labels_details: true,
+ page: 1,
+ per_page: PAGE_SIZE,
+ },
+ }),
+ );
+ });
+
+ it('passes the base url to issuable', () => {
+ expect(findFirstIssuable().props('baseUrl')).toEqual(TEST_LOCATION);
+ });
+ });
+
+ describe('with hash in window.location', () => {
+ beforeEach(() => {
+ window.location.href = `${TEST_LOCATION}#stuff`;
+ setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
+ factory();
+ return waitForPromises();
+ });
+
+ it('passes the base url to issuable', () => {
+ expect(findFirstIssuable().props('baseUrl')).toEqual(TEST_LOCATION);
+ });
+ });
+
+ describe('with manual sort', () => {
+ beforeEach(() => {
+ setupApiMock(() => [200, MOCK_ISSUES.slice(0)]);
+ factory({ sortKey: RELATIVE_POSITION });
+ });
+
+ it('uses manual page size', () => {
+ expect(apiSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ params: expect.objectContaining({
+ per_page: PAGE_SIZE_MANUAL,
+ }),
+ }),
+ );
+ });
+ });
+
+ describe('with empty issues response', () => {
+ beforeEach(() => {
+ setupApiMock(() => [200, []]);
+ });
+
+ describe('with query in window location', () => {
+ beforeEach(() => {
+ window.location.search = '?weight=Any';
+
+ factory();
+
+ return waitForPromises().then(() => wrapper.vm.$nextTick());
+ });
+
+ it('should display "Sorry, your filter produced no results" if filters are too specific', () => {
+ expect(findEmptyState().props('title')).toMatchSnapshot();
+ });
+ });
+
+ describe('with closed state', () => {
+ beforeEach(() => {
+ window.location.search = '?state=closed';
+
+ factory();
+
+ return waitForPromises().then(() => wrapper.vm.$nextTick());
+ });
+
+ it('should display a message "There are no closed issues" if there are no closed issues', () => {
+ expect(findEmptyState().props('title')).toMatchSnapshot();
+ });
+ });
+
+ describe('with all state', () => {
+ beforeEach(() => {
+ window.location.search = '?state=all';
+
+ factory();
+
+ return waitForPromises().then(() => wrapper.vm.$nextTick());
+ });
+
+ it('should display a catch-all if there are no issues to show', () => {
+ expect(findEmptyState().element).toMatchSnapshot();
+ });
+ });
+
+ describe('with empty query', () => {
+ beforeEach(() => {
+ factory();
+
+ return wrapper.vm.$nextTick().then(waitForPromises);
+ });
+
+ it('should display the message "There are no open issues"', () => {
+ expect(findEmptyState().props('title')).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuables_list/issuable_list_test_data.js b/spec/frontend/issuables_list/issuable_list_test_data.js
new file mode 100644
index 00000000000..617780fd736
--- /dev/null
+++ b/spec/frontend/issuables_list/issuable_list_test_data.js
@@ -0,0 +1,72 @@
+export const simpleIssue = {
+ id: 442,
+ iid: 31,
+ title: 'Dismiss Cipher with no integrity',
+ state: 'opened',
+ created_at: '2019-08-26T19:06:32.667Z',
+ updated_at: '2019-08-28T19:53:58.314Z',
+ labels: [],
+ milestone: null,
+ assignees: [],
+ author: {
+ id: 3,
+ name: 'Elnora Bernhard',
+ username: 'treva.lesch',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/a8c0d9c2882406cf2a9b71494625a796?s=80&d=identicon',
+ web_url: 'http://localhost:3001/treva.lesch',
+ },
+ assignee: null,
+ user_notes_count: 0,
+ merge_requests_count: 0,
+ upvotes: 0,
+ downvotes: 0,
+ due_date: null,
+ confidential: false,
+ web_url: 'http://localhost:3001/h5bp/html5-boilerplate/issues/31',
+ has_tasks: false,
+ weight: null,
+};
+
+export const testLabels = [
+ {
+ id: 1,
+ name: 'Tanuki',
+ description: 'A cute animal',
+ color: '#ff0000',
+ text_color: '#ffffff',
+ },
+ {
+ id: 2,
+ name: 'Octocat',
+ description: 'A grotesque mish-mash of whiskers and tentacles',
+ color: '#333333',
+ text_color: '#000000',
+ },
+ {
+ id: 3,
+ name: 'scoped::label',
+ description: 'A scoped label',
+ color: '#00ff00',
+ text_color: '#ffffff',
+ },
+];
+
+export const testAssignees = [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'http://localhost:3001/root',
+ },
+ {
+ id: 22,
+ name: 'User 0',
+ username: 'user0',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon',
+ web_url: 'http://localhost:3001/user0',
+ },
+];
diff --git a/spec/frontend/issue_show/helpers.js b/spec/frontend/issue_show/helpers.js
new file mode 100644
index 00000000000..5d2ced98ae4
--- /dev/null
+++ b/spec/frontend/issue_show/helpers.js
@@ -0,0 +1,10 @@
+// eslint-disable-next-line import/prefer-default-export
+export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => {
+ const e = new CustomEvent('keydown');
+
+ e.keyCode = code;
+ e.metaKey = metaKey;
+ e.ctrlKey = ctrlKey;
+
+ return e;
+};
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index cc334009982..7c834542a9a 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -60,8 +60,8 @@ describe('Job Log', () => {
expect(wrapper.find('.collapsible-line').attributes('role')).toBe('button');
});
- it('renders an icon with the closed state', () => {
- expect(wrapper.find('.collapsible-line svg').classes()).toContain('ic-angle-right');
+ it('renders an icon with the open state', () => {
+ expect(wrapper.find('.collapsible-line svg').classes()).toContain('ic-angle-down');
});
describe('on click header section', () => {
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 43dacfe622c..8819f39dee0 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -26,7 +26,7 @@ describe('Jobs Store Utils', () => {
const parsedHeaderLine = parseHeaderLine(headerLine, 2);
expect(parsedHeaderLine).toEqual({
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
...headerLine,
@@ -57,7 +57,7 @@ describe('Jobs Store Utils', () => {
it('adds the section duration to the correct header', () => {
const parsed = [
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
section: 'prepare-script',
@@ -66,7 +66,7 @@ describe('Jobs Store Utils', () => {
lines: [],
},
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
section: 'foo-bar',
@@ -85,7 +85,7 @@ describe('Jobs Store Utils', () => {
it('does not add the section duration when the headers do not match', () => {
const parsed = [
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
section: 'bar-foo',
@@ -94,7 +94,7 @@ describe('Jobs Store Utils', () => {
lines: [],
},
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
section: 'foo-bar',
@@ -183,7 +183,7 @@ describe('Jobs Store Utils', () => {
describe('collpasible section', () => {
it('adds a `isClosed` property', () => {
- expect(result[1].isClosed).toEqual(true);
+ expect(result[1].isClosed).toEqual(false);
});
it('adds a `isHeader` property', () => {
@@ -213,7 +213,7 @@ describe('Jobs Store Utils', () => {
const existingLog = [
{
isHeader: true,
- isClosed: true,
+ isClosed: false,
line: { content: [{ text: 'bar' }], offset: 10, lineNumber: 1 },
},
];
@@ -263,7 +263,7 @@ describe('Jobs Store Utils', () => {
const existingLog = [
{
isHeader: true,
- isClosed: true,
+ isClosed: false,
lines: [{ offset: 101, content: [{ text: 'foobar' }], lineNumber: 2 }],
line: {
offset: 10,
@@ -435,7 +435,7 @@ describe('Jobs Store Utils', () => {
expect(result).toEqual([
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
offset: 1,
@@ -461,7 +461,7 @@ describe('Jobs Store Utils', () => {
expect(result).toEqual([
{
- isClosed: true,
+ isClosed: false,
isHeader: true,
line: {
offset: 1,
diff --git a/spec/frontend/lib/utils/chart_utils_spec.js b/spec/frontend/lib/utils/chart_utils_spec.js
new file mode 100644
index 00000000000..e811b8405fb
--- /dev/null
+++ b/spec/frontend/lib/utils/chart_utils_spec.js
@@ -0,0 +1,11 @@
+import { firstAndLastY } from '~/lib/utils/chart_utils';
+
+describe('Chart utils', () => {
+ describe('firstAndLastY', () => {
+ it('returns the first and last y-values of a given data set as an array', () => {
+ const data = [['', 1], ['', 2], ['', 3]];
+
+ expect(firstAndLastY(data)).toEqual([1, 3]);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index e2e71229320..ee27789b6b9 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -428,16 +428,57 @@ describe('newDate', () => {
});
describe('getDateInPast', () => {
- const date = new Date(1563235200000); // 2019-07-16T00:00:00.000Z;
+ const date = new Date('2019-07-16T00:00:00.000Z');
const daysInPast = 90;
it('returns the correct date in the past', () => {
const dateInPast = datetimeUtility.getDateInPast(date, daysInPast);
- expect(dateInPast).toBe('2019-04-17T00:00:00.000Z');
+ const expectedDateInPast = new Date('2019-04-17T00:00:00.000Z');
+
+ expect(dateInPast).toStrictEqual(expectedDateInPast);
});
it('does not modifiy the original date', () => {
datetimeUtility.getDateInPast(date, daysInPast);
- expect(date).toStrictEqual(new Date(1563235200000));
+ expect(date).toStrictEqual(new Date('2019-07-16T00:00:00.000Z'));
+ });
+});
+
+describe('getDatesInRange', () => {
+ it('returns an empty array if 1st or 2nd argument is not a Date object', () => {
+ const d1 = new Date('2019-01-01');
+ const d2 = 90;
+ const range = datetimeUtility.getDatesInRange(d1, d2);
+
+ expect(range).toEqual([]);
+ });
+
+ it('returns a range of dates between two given dates', () => {
+ const d1 = new Date('2019-01-01');
+ const d2 = new Date('2019-01-31');
+
+ const range = datetimeUtility.getDatesInRange(d1, d2);
+
+ expect(range.length).toEqual(31);
+ });
+
+ it('applies mapper function if provided fro each item in range', () => {
+ const d1 = new Date('2019-01-01');
+ const d2 = new Date('2019-01-31');
+ const formatter = date => date.getDate();
+
+ const range = datetimeUtility.getDatesInRange(d1, d2, formatter);
+
+ range.forEach((formattedItem, index) => {
+ expect(formattedItem).toEqual(index + 1);
+ });
+ });
+});
+
+describe('secondsToMilliseconds', () => {
+ it('converts seconds to milliseconds correctly', () => {
+ expect(datetimeUtility.secondsToMilliseconds(0)).toBe(0);
+ expect(datetimeUtility.secondsToMilliseconds(60)).toBe(60000);
+ expect(datetimeUtility.secondsToMilliseconds(123)).toBe(123000);
});
});
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index 381d7c6f8d9..2f8f1092612 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -7,6 +7,8 @@ import {
sum,
isOdd,
median,
+ changeInPercent,
+ formattedChangeInPercent,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
@@ -122,4 +124,42 @@ describe('Number Utils', () => {
expect(median(items)).toBe(14.5);
});
});
+
+ describe('changeInPercent', () => {
+ it.each`
+ firstValue | secondValue | expectedOutput
+ ${99} | ${100} | ${1}
+ ${100} | ${99} | ${-1}
+ ${0} | ${99} | ${Infinity}
+ ${2} | ${2} | ${0}
+ ${-100} | ${-99} | ${1}
+ `(
+ 'computes the change between $firstValue and $secondValue in percent',
+ ({ firstValue, secondValue, expectedOutput }) => {
+ expect(changeInPercent(firstValue, secondValue)).toBe(expectedOutput);
+ },
+ );
+ });
+
+ describe('formattedChangeInPercent', () => {
+ it('prepends "%" to the output', () => {
+ expect(formattedChangeInPercent(1, 2)).toMatch(/%$/);
+ });
+
+ it('indicates if the change was a decrease', () => {
+ expect(formattedChangeInPercent(100, 99)).toContain('-1');
+ });
+
+ it('indicates if the change was an increase', () => {
+ expect(formattedChangeInPercent(99, 100)).toContain('+1');
+ });
+
+ it('shows "-" per default if the change can not be expressed in an integer', () => {
+ expect(formattedChangeInPercent(0, 1)).toBe('-');
+ });
+
+ it('shows the given fallback if the change can not be expressed in an integer', () => {
+ expect(formattedChangeInPercent(0, 1, { nonFiniteResult: '*' })).toBe('*');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index b6f1aef9ce4..deb6dab772e 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -90,6 +90,19 @@ describe('text_utility', () => {
});
});
+ describe('convertToSnakeCase', () => {
+ it.each`
+ txt | result
+ ${'snakeCase'} | ${'snake_case'}
+ ${'snake Case'} | ${'snake_case'}
+ ${'snake case'} | ${'snake_case'}
+ ${'snake_case'} | ${'snake_case'}
+ ${'snakeCasesnake Case'} | ${'snake_casesnake_case'}
+ `('converts string $txt to $result string', ({ txt, result }) => {
+ expect(textUtils.convertToSnakeCase(txt)).toEqual(result);
+ });
+ });
+
describe('convertToSentenceCase', () => {
it('converts Sentence Case to Sentence case', () => {
expect(textUtils.convertToSentenceCase('Hello World')).toBe('Hello world');
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/frontend/monitoring/charts/time_series_spec.js
index 5c718135b90..554535418fe 100644
--- a/spec/javascripts/monitoring/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/charts/time_series_spec.js
@@ -1,55 +1,77 @@
import { shallowMount } from '@vue/test-utils';
+import { setTestTimeout } from 'helpers/timeout';
import { createStore } from '~/monitoring/stores';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
-import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
+import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types';
-import { TEST_HOST } from 'spec/test_constants';
-import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data';
+import {
+ deploymentData,
+ metricsGroupsAPIResponse,
+ mockedQueryResultPayload,
+ mockProjectDir,
+ mockHost,
+} from '../mock_data';
+
+import * as iconUtils from '~/lib/utils/icon_utils';
+
+const mockSvgPathContent = 'mockSvgPathContent';
+const mockWidgets = 'mockWidgets';
+
+jest.mock('~/lib/utils/icon_utils', () => ({
+ getSvgIconPathContent: jest.fn().mockImplementation(
+ () =>
+ new Promise(resolve => {
+ resolve(mockSvgPathContent);
+ }),
+ ),
+}));
describe('Time series component', () => {
- const mockSha = 'mockSha';
- const mockWidgets = 'mockWidgets';
- const mockSvgPathContent = 'mockSvgPathContent';
- const projectPath = `${TEST_HOST}${mockProjectPath}`;
- const commitUrl = `${projectPath}/commit/${mockSha}`;
let mockGraphData;
let makeTimeSeriesChart;
- let spriteSpy;
let store;
beforeEach(() => {
+ setTestTimeout(1000);
+
store = createStore();
- store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
+
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ metricsGroupsAPIResponse,
+ );
+
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
- [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
+
+ // Mock data contains 2 panels, pick the first one
+ store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload);
+
+ [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics;
makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, {
propsData: {
graphData: { ...graphData, type },
- containerWidth: 0,
deploymentData: store.state.monitoringDashboard.deploymentData,
- projectPath,
+ projectPath: `${mockHost}${mockProjectDir}`,
},
slots: {
default: mockWidgets,
},
sync: false,
store,
+ attachToDocument: true,
});
-
- spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake(
- () => new Promise(resolve => resolve(mockSvgPathContent)),
- );
});
describe('general functions', () => {
let timeSeriesChart;
- beforeEach(() => {
+ beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ timeSeriesChart.vm.$nextTick(done);
});
it('renders chart title', () => {
@@ -74,18 +96,24 @@ describe('Time series component', () => {
describe('methods', () => {
describe('formatTooltipText', () => {
- const mockDate = deploymentData[0].created_at;
- const mockCommitUrl = deploymentData[0].commitUrl;
- const generateSeriesData = type => ({
- seriesData: [
- {
- seriesName: timeSeriesChart.vm.chartData[0].name,
- componentSubType: type,
- value: [mockDate, 5.55555],
- seriesIndex: 0,
- },
- ],
- value: mockDate,
+ let mockDate;
+ let mockCommitUrl;
+ let generateSeriesData;
+
+ beforeEach(() => {
+ mockDate = deploymentData[0].created_at;
+ mockCommitUrl = deploymentData[0].commitUrl;
+ generateSeriesData = type => ({
+ seriesData: [
+ {
+ seriesName: timeSeriesChart.vm.chartData[0].name,
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ dataIndex: 0,
+ },
+ ],
+ value: mockDate,
+ });
});
describe('when series is of line type', () => {
@@ -95,17 +123,21 @@ describe('Time series component', () => {
});
it('formats tooltip title', () => {
- expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip content', () => {
- const name = 'Core Usage';
+ const name = 'Pod average';
const value = '5.556';
+ const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
expect(seriesLabel.vm.color).toBe('');
expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
- expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([
+ { name, value, dataIndex, color: undefined },
+ ]);
+
expect(
shallowWrapperContainsSlotText(
timeSeriesChart.find(GlAreaChart),
@@ -116,13 +148,13 @@ describe('Time series component', () => {
});
});
- describe('when series is of scatter type', () => {
+ describe('when series is of scatter type, for deployments', () => {
beforeEach(() => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
it('formats tooltip title', () => {
- expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip sha', () => {
@@ -144,7 +176,7 @@ describe('Time series component', () => {
});
it('gets svg path content', () => {
- expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
+ expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName);
});
it('sets svg path content', () => {
@@ -168,7 +200,7 @@ describe('Time series component', () => {
const mockWidth = 233;
beforeEach(() => {
- spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
width: mockWidth,
}));
timeSeriesChart.vm.onResize();
@@ -212,6 +244,39 @@ describe('Time series component', () => {
});
describe('chartOptions', () => {
+ describe('are extended by `option`', () => {
+ const mockSeriesName = 'Extra series 1';
+ const mockOption = {
+ option1: 'option1',
+ option2: 'option2',
+ };
+
+ it('arbitrary options', () => {
+ timeSeriesChart.setProps({
+ option: mockOption,
+ });
+
+ expect(timeSeriesChart.vm.chartOptions).toEqual(expect.objectContaining(mockOption));
+ });
+
+ it('additional series', () => {
+ timeSeriesChart.setProps({
+ option: {
+ series: [
+ {
+ name: mockSeriesName,
+ },
+ ],
+ },
+ });
+
+ const optionSeries = timeSeriesChart.vm.chartOptions.series;
+
+ expect(optionSeries.length).toEqual(2);
+ expect(optionSeries[0].name).toEqual(mockSeriesName);
+ });
+ });
+
describe('yAxis formatter', () => {
let format;
@@ -228,9 +293,9 @@ describe('Time series component', () => {
describe('scatterSeries', () => {
it('utilizes deployment data', () => {
expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
- ['2017-05-31T21:23:37.881Z', 0],
- ['2017-05-30T20:08:04.629Z', 0],
- ['2017-05-30T17:42:38.409Z', 0],
+ ['2019-07-16T10:14:25.589Z', 0],
+ ['2019-07-16T11:14:25.589Z', 0],
+ ['2019-07-16T12:14:25.589Z', 0],
]);
expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
@@ -239,7 +304,7 @@ describe('Time series component', () => {
describe('yAxisLabel', () => {
it('constructs a label for the chart y-axis', () => {
- expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
+ expect(timeSeriesChart.vm.yAxisLabel).toBe('Memory Used per Pod');
});
});
});
@@ -272,6 +337,10 @@ describe('Time series component', () => {
timeSeriesAreaChart.vm.$nextTick(done);
});
+ afterEach(() => {
+ timeSeriesAreaChart.destroy();
+ });
+
it('is a Vue instance', () => {
expect(glChart.exists()).toBe(true);
expect(glChart.isVueInstance()).toBe(true);
@@ -297,6 +366,9 @@ describe('Time series component', () => {
});
describe('when tooltip is showing deployment data', () => {
+ const mockSha = 'mockSha';
+ const commitUrl = `${mockProjectDir}/commit/${mockSha}`;
+
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.vm.$nextTick(done);
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
new file mode 100644
index 00000000000..6707d0b1fe8
--- /dev/null
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -0,0 +1,303 @@
+import Anomaly from '~/monitoring/components/charts/anomaly.vue';
+
+import { shallowMount } from '@vue/test-utils';
+import { colorValues } from '~/monitoring/constants';
+import {
+ anomalyDeploymentData,
+ mockProjectDir,
+ anomalyMockGraphData,
+ anomalyMockResultValues,
+} from '../../mock_data';
+import { TEST_HOST } from 'helpers/test_constants';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+
+const mockWidgets = 'mockWidgets';
+const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
+
+jest.mock('~/lib/utils/icon_utils'); // mock getSvgIconPathContent
+
+const makeAnomalyGraphData = (datasetName, template = anomalyMockGraphData) => {
+ const queries = anomalyMockResultValues[datasetName].map((values, index) => ({
+ ...template.queries[index],
+ result: [
+ {
+ metrics: {},
+ values,
+ },
+ ],
+ }));
+ return { ...template, queries };
+};
+
+describe('Anomaly chart component', () => {
+ let wrapper;
+
+ const setupAnomalyChart = props => {
+ wrapper = shallowMount(Anomaly, {
+ propsData: { ...props },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ });
+ };
+ const findTimeSeries = () => wrapper.find(MonitorTimeSeriesChart);
+ const getTimeSeriesProps = () => findTimeSeries().props();
+
+ describe('wrapped monitor-time-series-chart component', () => {
+ const dataSetName = 'noAnomaly';
+ const dataSet = anomalyMockResultValues[dataSetName];
+ const inputThresholds = ['some threshold'];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ thresholds: inputThresholds,
+ projectPath: mockProjectPath,
+ });
+ });
+
+ it('is a Vue instance', () => {
+ expect(findTimeSeries().exists()).toBe(true);
+ expect(findTimeSeries().isVueInstance()).toBe(true);
+ });
+
+ describe('receives props correctly', () => {
+ describe('graph-data', () => {
+ it('receives a single "metric" series', () => {
+ const { graphData } = getTimeSeriesProps();
+ expect(graphData.queries.length).toBe(1);
+ });
+
+ it('receives "metric" with all data', () => {
+ const { graphData } = getTimeSeriesProps();
+ const query = graphData.queries[0];
+ const expectedQuery = makeAnomalyGraphData(dataSetName).queries[0];
+ expect(query).toEqual(expectedQuery);
+ });
+
+ it('receives the "metric" results', () => {
+ const { graphData } = getTimeSeriesProps();
+ const { result } = graphData.queries[0];
+ const { values } = result[0];
+ const [metricDataset] = dataSet;
+ expect(values).toEqual(expect.any(Array));
+
+ values.forEach(([, y], index) => {
+ expect(y).toBeCloseTo(metricDataset[index][1]);
+ });
+ });
+ });
+
+ describe('option', () => {
+ let option;
+ let series;
+
+ beforeEach(() => {
+ ({ option } = getTimeSeriesProps());
+ ({ series } = option);
+ });
+
+ it('contains a boundary band', () => {
+ expect(series).toEqual(expect.any(Array));
+ expect(series.length).toEqual(2); // 1 upper + 1 lower boundaries
+ expect(series[0].stack).toEqual(series[1].stack);
+
+ series.forEach(s => {
+ expect(s.type).toBe('line');
+ expect(s.lineStyle.width).toBe(0);
+ expect(s.lineStyle.color).toMatch(/rgba\(.+\)/);
+ expect(s.lineStyle.color).toMatch(s.color);
+ expect(s.symbol).toEqual('none');
+ });
+ });
+
+ it('upper boundary values are stacked on top of lower boundary', () => {
+ const [lowerSeries, upperSeries] = series;
+ const [, upperDataset, lowerDataset] = dataSet;
+
+ lowerSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(lowerDataset[i][1]);
+ });
+
+ upperSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ });
+ });
+ });
+
+ describe('series-config', () => {
+ let seriesConfig;
+
+ beforeEach(() => {
+ ({ seriesConfig } = getTimeSeriesProps());
+ });
+
+ it('display symbols is enabled', () => {
+ expect(seriesConfig).toEqual(
+ expect.objectContaining({
+ type: 'line',
+ symbol: 'circle',
+ showSymbol: true,
+ symbolSize: expect.any(Function),
+ itemStyle: {
+ color: expect.any(Function),
+ },
+ }),
+ );
+ });
+ it('does not display anomalies', () => {
+ const { symbolSize, itemStyle } = seriesConfig;
+ const [metricDataset] = dataSet;
+
+ metricDataset.forEach((v, dataIndex) => {
+ const size = symbolSize(null, { dataIndex });
+ const color = itemStyle.color({ dataIndex });
+
+ // normal color and small size
+ expect(size).toBeCloseTo(0);
+ expect(color).toBe(colorValues.primaryColor);
+ });
+ });
+
+ it('can format y values (to use in tooltips)', () => {
+ expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
+ expect(parseFloat(wrapper.vm.yValueFormatted(1, 0))).toEqual(dataSet[1][0][1]);
+ expect(parseFloat(wrapper.vm.yValueFormatted(2, 0))).toEqual(dataSet[2][0][1]);
+ });
+ });
+
+ describe('inherited properties', () => {
+ it('"deployment-data" keeps the same value', () => {
+ const { deploymentData } = getTimeSeriesProps();
+ expect(deploymentData).toEqual(anomalyDeploymentData);
+ });
+ it('"thresholds" keeps the same value', () => {
+ const { thresholds } = getTimeSeriesProps();
+ expect(thresholds).toEqual(inputThresholds);
+ });
+ it('"projectPath" keeps the same value', () => {
+ const { projectPath } = getTimeSeriesProps();
+ expect(projectPath).toEqual(mockProjectPath);
+ });
+ });
+ });
+ });
+
+ describe('with no boundary data', () => {
+ const dataSetName = 'noBoundary';
+ const dataSet = anomalyMockResultValues[dataSetName];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('option', () => {
+ let option;
+ let series;
+
+ beforeEach(() => {
+ ({ option } = getTimeSeriesProps());
+ ({ series } = option);
+ });
+
+ it('does not display a boundary band', () => {
+ expect(series).toEqual(expect.any(Array));
+ expect(series.length).toEqual(0); // no boundaries
+ });
+
+ it('can format y values (to use in tooltips)', () => {
+ expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(dataSet[0][0][1]);
+ expect(wrapper.vm.yValueFormatted(1, 0)).toBe(''); // missing boundary
+ expect(wrapper.vm.yValueFormatted(2, 0)).toBe(''); // missing boundary
+ });
+ });
+ });
+
+ describe('with one anomaly', () => {
+ const dataSetName = 'oneAnomaly';
+ const dataSet = anomalyMockResultValues[dataSetName];
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('series-config', () => {
+ it('displays one anomaly', () => {
+ const { seriesConfig } = getTimeSeriesProps();
+ const { symbolSize, itemStyle } = seriesConfig;
+ const [metricDataset] = dataSet;
+
+ const bigDots = metricDataset.filter((v, dataIndex) => {
+ const size = symbolSize(null, { dataIndex });
+ return size > 0.1;
+ });
+ const redDots = metricDataset.filter((v, dataIndex) => {
+ const color = itemStyle.color({ dataIndex });
+ return color === colorValues.anomalySymbol;
+ });
+
+ expect(bigDots.length).toBe(1);
+ expect(redDots.length).toBe(1);
+ });
+ });
+ });
+
+ describe('with offset', () => {
+ const dataSetName = 'negativeBoundary';
+ const dataSet = anomalyMockResultValues[dataSetName];
+ const expectedOffset = 4; // Lowst point in mock data is -3.70, it gets rounded
+
+ beforeEach(() => {
+ setupAnomalyChart({
+ graphData: makeAnomalyGraphData(dataSetName),
+ deploymentData: anomalyDeploymentData,
+ });
+ });
+
+ describe('receives props correctly', () => {
+ describe('graph-data', () => {
+ it('receives a single "metric" series', () => {
+ const { graphData } = getTimeSeriesProps();
+ expect(graphData.queries.length).toBe(1);
+ });
+
+ it('receives "metric" results and applies the offset to them', () => {
+ const { graphData } = getTimeSeriesProps();
+ const { result } = graphData.queries[0];
+ const { values } = result[0];
+ const [metricDataset] = dataSet;
+ expect(values).toEqual(expect.any(Array));
+
+ values.forEach(([, y], index) => {
+ expect(y).toBeCloseTo(metricDataset[index][1] + expectedOffset);
+ });
+ });
+ });
+ });
+
+ describe('option', () => {
+ it('upper boundary values are stacked on top of lower boundary, plus the offset', () => {
+ const { option } = getTimeSeriesProps();
+ const { series } = option;
+ const [lowerSeries, upperSeries] = series;
+ const [, upperDataset, lowerDataset] = dataSet;
+
+ lowerSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(lowerDataset[i][1] + expectedOffset);
+ });
+
+ upperSeries.data.forEach(([, y], i) => {
+ expect(y).toBeCloseTo(upperDataset[i][1] - lowerDataset[i][1]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
index be544435671..ca05461c8cf 100644
--- a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js
@@ -51,6 +51,16 @@ describe('DateTimePicker', () => {
});
});
+ it('renders dropdown without a selectedTimeWindow set', done => {
+ createComponent({
+ selectedTimeWindow: {},
+ });
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dateTimePicker.findAll('input').length).toBe(2);
+ done();
+ });
+ });
+
it('renders inputs with h/m/s truncated if its all 0s', done => {
createComponent({
selectedTimeWindow: {
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
index 5de1a7c4c3b..3e22b0858e6 100644
--- a/spec/frontend/monitoring/embed/embed_spec.js
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -61,8 +61,8 @@ describe('Embed', () => {
describe('metrics are available', () => {
beforeEach(() => {
- store.state.monitoringDashboard.groups = groups;
- store.state.monitoringDashboard.groups[0].metrics = metricsData;
+ store.state.monitoringDashboard.dashboard.panel_groups = groups;
+ store.state.monitoringDashboard.dashboard.panel_groups[0].metrics = metricsData;
store.state.monitoringDashboard.metricsWithData = metricsWithData;
mountComponent();
diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js
index df4acb82e95..1685021fd4b 100644
--- a/spec/frontend/monitoring/embed/mock_data.js
+++ b/spec/frontend/monitoring/embed/mock_data.js
@@ -81,7 +81,9 @@ export const metricsData = [
export const initialState = {
monitoringDashboard: {},
- groups: [],
+ dashboard: {
+ panel_groups: [],
+ },
metricsWithData: [],
useDashboardEndpoint: true,
};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
new file mode 100644
index 00000000000..c42366ab484
--- /dev/null
+++ b/spec/frontend/monitoring/mock_data.js
@@ -0,0 +1,465 @@
+export const mockHost = 'http://test.host';
+export const mockProjectDir = '/frontend-fixtures/environments-project';
+
+export const anomalyDeploymentData = [
+ {
+ id: 111,
+ iid: 3,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-08-19T22:00:00.000Z',
+ deployed_at: '2019-08-19T22:01:00.000Z',
+ tag: false,
+ 'last?': true,
+ },
+ {
+ id: 110,
+ iid: 2,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-08-19T23:00:00.000Z',
+ deployed_at: '2019-08-19T23:00:00.000Z',
+ tag: false,
+ 'last?': false,
+ },
+];
+
+export const anomalyMockResultValues = {
+ noAnomaly: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 1.45],
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ['2019-08-19T22:00:00.000Z', 1.48],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ['2019-08-19T22:00:00.000Z', 3.0],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', 0.45],
+ ['2019-08-19T20:00:00.000Z', 0.65],
+ ['2019-08-19T21:00:00.000Z', 0.7],
+ ['2019-08-19T22:00:00.000Z', 0.8],
+ ],
+ ],
+ noBoundary: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 1.45],
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ['2019-08-19T22:00:00.000Z', 1.48],
+ ],
+ [
+ // empty upper boundary
+ ],
+ [
+ // empty lower boundary
+ ],
+ ],
+ oneAnomaly: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', 0.45],
+ ['2019-08-19T20:00:00.000Z', 0.65],
+ ['2019-08-19T21:00:00.000Z', 0.7],
+ ],
+ ],
+ negativeBoundary: [
+ [
+ ['2019-08-19T19:00:00.000Z', 1.25],
+ ['2019-08-19T20:00:00.000Z', 3.45], // anomaly
+ ['2019-08-19T21:00:00.000Z', 1.55],
+ ],
+ [
+ // upper boundary
+ ['2019-08-19T19:00:00.000Z', 2],
+ ['2019-08-19T20:00:00.000Z', 2.55],
+ ['2019-08-19T21:00:00.000Z', 2.65],
+ ],
+ [
+ // lower boundary
+ ['2019-08-19T19:00:00.000Z', -1.25],
+ ['2019-08-19T20:00:00.000Z', -2.65],
+ ['2019-08-19T21:00:00.000Z', -3.7], // lowest point
+ ],
+ ],
+};
+
+export const anomalyMockGraphData = {
+ title: 'Requests Per Second Mock Data',
+ type: 'anomaly-chart',
+ weight: 3,
+ metrics: [
+ // Not used
+ ],
+ queries: [
+ {
+ metricId: '90',
+ id: 'metric',
+ query_range: 'MOCK_PROMETHEUS_METRIC_QUERY_RANGE',
+ unit: 'RPS',
+ label: 'Metrics RPS',
+ metric_id: 90,
+ prometheus_endpoint_path: 'MOCK_METRIC_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ {
+ metricId: '91',
+ id: 'upper',
+ query_range: '...',
+ unit: 'RPS',
+ label: 'Upper Limit Metrics RPS',
+ metric_id: 91,
+ prometheus_endpoint_path: 'MOCK_UPPER_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ {
+ metricId: '92',
+ id: 'lower',
+ query_range: '...',
+ unit: 'RPS',
+ label: 'Lower Limit Metrics RPS',
+ metric_id: 92,
+ prometheus_endpoint_path: 'MOCK_LOWER_PEP',
+ result: [
+ {
+ metric: {},
+ values: [['2019-08-19T19:00:00.000Z', 0]],
+ },
+ ],
+ },
+ ],
+};
+
+export const deploymentData = [
+ {
+ id: 111,
+ iid: 3,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl:
+ 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-07-16T10:14:25.589Z',
+ tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
+ 'last?': true,
+ },
+ {
+ id: 110,
+ iid: 2,
+ sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl:
+ 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ ref: {
+ name: 'master',
+ },
+ created_at: '2019-07-16T11:14:25.589Z',
+ tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
+ 'last?': false,
+ },
+ {
+ id: 109,
+ iid: 1,
+ sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
+ commitUrl:
+ 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
+ ref: {
+ name: 'update2-readme',
+ },
+ created_at: '2019-07-16T12:14:25.589Z',
+ tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
+ 'last?': false,
+ },
+];
+
+export const metricsNewGroupsAPIResponse = [
+ {
+ group: 'System metrics (Kubernetes)',
+ priority: 5,
+ panels: [
+ {
+ title: 'Memory Usage (Pod average)',
+ type: 'area-chart',
+ y_label: 'Memory Used per Pod',
+ 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',
+ label: 'Pod average',
+ unit: 'MB',
+ metric_id: 17,
+ prometheus_endpoint_path:
+ '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
+ appearance: {
+ line: {
+ width: 2,
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const mockedQueryResultPayload = {
+ metricId: '17_system_metrics_kubernetes_container_memory_average',
+ result: [
+ {
+ metric: {},
+ values: [
+ [1563272065.589, '10.396484375'],
+ [1563272125.589, '10.333984375'],
+ [1563272185.589, '10.333984375'],
+ [1563272245.589, '10.333984375'],
+ [1563272305.589, '10.333984375'],
+ [1563272365.589, '10.333984375'],
+ [1563272425.589, '10.38671875'],
+ [1563272485.589, '10.333984375'],
+ [1563272545.589, '10.333984375'],
+ [1563272605.589, '10.333984375'],
+ [1563272665.589, '10.333984375'],
+ [1563272725.589, '10.333984375'],
+ [1563272785.589, '10.396484375'],
+ [1563272845.589, '10.333984375'],
+ [1563272905.589, '10.333984375'],
+ [1563272965.589, '10.3984375'],
+ [1563273025.589, '10.337890625'],
+ [1563273085.589, '10.34765625'],
+ [1563273145.589, '10.337890625'],
+ [1563273205.589, '10.337890625'],
+ [1563273265.589, '10.337890625'],
+ [1563273325.589, '10.337890625'],
+ [1563273385.589, '10.337890625'],
+ [1563273445.589, '10.337890625'],
+ [1563273505.589, '10.337890625'],
+ [1563273565.589, '10.337890625'],
+ [1563273625.589, '10.337890625'],
+ [1563273685.589, '10.337890625'],
+ [1563273745.589, '10.337890625'],
+ [1563273805.589, '10.337890625'],
+ [1563273865.589, '10.390625'],
+ [1563273925.589, '10.390625'],
+ ],
+ },
+ ],
+};
+
+export const metricsGroupsAPIResponse = [
+ {
+ group: 'System metrics (Kubernetes)',
+ priority: 5,
+ panels: [
+ {
+ title: 'Memory Usage (Pod average)',
+ type: 'area-chart',
+ y_label: 'Memory Used per Pod',
+ 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',
+ label: 'Pod average',
+ unit: 'MB',
+ metric_id: 17,
+ prometheus_endpoint_path:
+ '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
+ appearance: {
+ line: {
+ width: 2,
+ },
+ },
+ },
+ ],
+ },
+ {
+ title: 'Core Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Cores',
+ 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)',
+ label: 'Total',
+ unit: 'cores',
+ metric_id: 13,
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const environmentData = [
+ {
+ id: 34,
+ name: 'production',
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
+ environment_type: null,
+ stop_action: false,
+ metrics_path: '/root/hello-prometheus/environments/34/metrics',
+ environment_path: '/root/hello-prometheus/environments/34',
+ stop_path: '/root/hello-prometheus/environments/34/stop',
+ terminal_path: '/root/hello-prometheus/environments/34/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/production',
+ created_at: '2018-06-29T16:53:38.301Z',
+ updated_at: '2018-06-29T16:57:09.825Z',
+ last_deployment: {
+ id: 127,
+ },
+ },
+ {
+ id: 35,
+ name: 'review/noop-branch',
+ state: 'available',
+ external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
+ environment_type: 'review',
+ stop_action: true,
+ metrics_path: '/root/hello-prometheus/environments/35/metrics',
+ environment_path: '/root/hello-prometheus/environments/35',
+ stop_path: '/root/hello-prometheus/environments/35/stop',
+ terminal_path: '/root/hello-prometheus/environments/35/terminal',
+ folder_path: '/root/hello-prometheus/environments/folders/review',
+ created_at: '2018-07-03T18:39:41.702Z',
+ updated_at: '2018-07-03T18:44:54.010Z',
+ last_deployment: {
+ id: 128,
+ },
+ },
+ {
+ id: 36,
+ name: 'no-deployment/noop-branch',
+ state: 'available',
+ created_at: '2018-07-04T18:39:41.702Z',
+ updated_at: '2018-07-04T18:44:54.010Z',
+ },
+];
+
+export const metricsDashboardResponse = {
+ dashboard: {
+ dashboard: 'Environment metrics',
+ priority: 1,
+ panel_groups: [
+ {
+ group: 'System metrics (Kubernetes)',
+ priority: 5,
+ panels: [
+ {
+ title: 'Memory Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Memory Used',
+ 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',
+ label: 'Total',
+ unit: 'GB',
+ metric_id: 12,
+ prometheus_endpoint_path: 'http://test',
+ },
+ ],
+ },
+ {
+ title: 'Core Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Cores',
+ 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)',
+ label: 'Total',
+ unit: 'cores',
+ metric_id: 13,
+ },
+ ],
+ },
+ {
+ title: 'Memory Usage (Pod average)',
+ type: 'line-chart',
+ y_label: 'Memory Used per Pod',
+ 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}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
+ label: 'Pod average',
+ unit: 'MB',
+ metric_id: 14,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ status: 'success',
+};
+
+export const dashboardGitResponse = [
+ {
+ default: true,
+ display_name: 'Default',
+ can_edit: false,
+ project_blob_path: null,
+ path: 'config/prometheus/common_metrics.yml',
+ },
+ {
+ default: false,
+ display_name: 'Custom Dashboard 1',
+ can_edit: true,
+ project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_1.yml`,
+ path: '.gitlab/dashboards/dashboard_1.yml',
+ },
+ {
+ default: false,
+ display_name: 'Custom Dashboard 2',
+ can_edit: true,
+ project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_2.yml`,
+ path: '.gitlab/dashboards/dashboard_2.yml',
+ },
+];
diff --git a/spec/frontend/monitoring/panel_type_spec.js b/spec/frontend/monitoring/panel_type_spec.js
new file mode 100644
index 00000000000..54a63e7f61f
--- /dev/null
+++ b/spec/frontend/monitoring/panel_type_spec.js
@@ -0,0 +1,166 @@
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { setTestTimeout } from 'helpers/timeout';
+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 { graphDataPrometheusQueryRange } from '../../javascripts/monitoring/mock_data';
+import { anomalyMockGraphData } from '../../frontend/monitoring/mock_data';
+import { createStore } from '~/monitoring/stores';
+
+global.IS_EE = true;
+global.URL.createObjectURL = jest.fn();
+
+describe('Panel Type component', () => {
+ let axiosMock;
+ let store;
+ let panelType;
+ const dashboardWidth = 100;
+ const exampleText = 'example_text';
+
+ beforeEach(() => {
+ setTestTimeout(1000);
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ describe('When no graphData is available', () => {
+ let glEmptyChart;
+ // Deep clone object before modifying
+ const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange));
+ graphDataNoResult.queries[0].result = [];
+
+ beforeEach(() => {
+ panelType = shallowMount(PanelType, {
+ propsData: {
+ clipboardText: 'dashboard_link',
+ dashboardWidth,
+ graphData: graphDataNoResult,
+ },
+ sync: false,
+ attachToDocument: true,
+ });
+ });
+
+ afterEach(() => {
+ panelType.destroy();
+ });
+
+ describe('Empty Chart component', () => {
+ beforeEach(() => {
+ glEmptyChart = panelType.find(EmptyChart);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glEmptyChart.isVueInstance()).toBe(true);
+ });
+
+ it('it receives a graph title', () => {
+ const props = glEmptyChart.props();
+
+ expect(props.graphTitle).toBe(panelType.vm.graphData.title);
+ });
+ });
+ });
+
+ describe('when Graph data is available', () => {
+ const propsData = {
+ clipboardText: exampleText,
+ dashboardWidth,
+ graphData: graphDataPrometheusQueryRange,
+ };
+
+ beforeEach(done => {
+ store = createStore();
+ panelType = shallowMount(PanelType, {
+ propsData,
+ store,
+ sync: false,
+ attachToDocument: true,
+ });
+ panelType.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ panelType.destroy();
+ });
+
+ describe('Time Series Chart panel type', () => {
+ it('is rendered', () => {
+ expect(panelType.find(TimeSeriesChart).isVueInstance()).toBe(true);
+ expect(panelType.find(TimeSeriesChart).exists()).toBe(true);
+ });
+
+ it('sets clipboard text on the dropdown', () => {
+ const link = () => panelType.find('.js-chart-link');
+ const clipboardText = () => link().element.dataset.clipboardText;
+
+ expect(clipboardText()).toBe(exampleText);
+ });
+ });
+
+ describe('Anomaly Chart panel type', () => {
+ beforeEach(done => {
+ panelType.setProps({
+ graphData: anomalyMockGraphData,
+ });
+ panelType.vm.$nextTick(done);
+ });
+
+ it('is rendered with an anomaly chart', () => {
+ expect(panelType.find(AnomalyChart).isVueInstance()).toBe(true);
+ expect(panelType.find(AnomalyChart).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when downloading metrics data as CSV', () => {
+ beforeEach(done => {
+ graphDataPrometheusQueryRange.y_label = 'metric';
+ store = createStore();
+ panelType = shallowMount(PanelType, {
+ propsData: {
+ clipboardText: exampleText,
+ dashboardWidth,
+ graphData: graphDataPrometheusQueryRange,
+ },
+ store,
+ sync: false,
+ attachToDocument: true,
+ });
+ panelType.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ panelType.destroy();
+ });
+
+ describe('csvText', () => {
+ it('converts metrics data from json to csv', () => {
+ const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`;
+ const data = graphDataPrometheusQueryRange.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+ const secondRow = `${data[1][0]},${data[1][1]}`;
+
+ expect(panelType.vm.csvText).toBe(`${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: panelType.vm.csvText.length,
+ type: 'text/plain',
+ }),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 1bd74f59282..d4bc613ffea 100644
--- a/spec/javascripts/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,8 +1,14 @@
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+
import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ backOffRequest,
fetchDashboard,
receiveMetricsDashboardSuccess,
receiveMetricsDashboardFailure,
@@ -15,8 +21,6 @@ import {
setGettingStartedEmptyState,
} from '~/monitoring/stores/actions';
import storeState from '~/monitoring/stores/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { resetStore } from '../helpers';
import {
deploymentData,
environmentData,
@@ -25,55 +29,108 @@ import {
dashboardGitResponse,
} from '../mock_data';
-describe('Monitoring store actions', () => {
+jest.mock('~/lib/utils/common_utils');
+
+const resetStore = str => {
+ str.replaceState({
+ showEmptyState: true,
+ emptyState: 'loading',
+ groups: [],
+ });
+};
+
+const MAX_REQUESTS = 3;
+
+describe('Monitoring store helpers', () => {
let mock;
+ // Mock underlying `backOff` function to remove in-built delay.
+ backOff.mockImplementation(
+ callback =>
+ new Promise((resolve, reject) => {
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
+ const next = () => callback(next, stop);
+ callback(next, stop);
+ }),
+ );
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
- resetStore(store);
mock.restore();
});
+ describe('backOffRequest', () => {
+ it('returns immediately when recieving a 200 status code', () => {
+ mock.onGet(TEST_HOST).reply(200);
+
+ return backOffRequest(() => axios.get(TEST_HOST)).then(() => {
+ expect(mock.history.get.length).toBe(1);
+ });
+ });
+
+ it(`repeats the network call ${MAX_REQUESTS} times when receiving a 204 response`, done => {
+ mock.onGet(TEST_HOST).reply(statusCodes.NO_CONTENT, {});
+
+ backOffRequest(() => axios.get(TEST_HOST))
+ .then(done.fail)
+ .catch(() => {
+ expect(mock.history.get.length).toBe(MAX_REQUESTS);
+ done();
+ });
+ });
+ });
+});
+
+describe('Monitoring store actions', () => {
+ let mock;
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+ afterEach(() => {
+ resetStore(store);
+ mock.restore();
+ });
describe('requestMetricsData', () => {
it('sets emptyState to loading', () => {
- const commit = jasmine.createSpy();
+ const commit = jest.fn();
const { state } = store;
-
- requestMetricsData({ state, commit });
-
+ requestMetricsData({
+ state,
+ commit,
+ });
expect(commit).toHaveBeenCalledWith(types.REQUEST_METRICS_DATA);
});
});
-
describe('fetchDeploymentsData', () => {
it('commits RECEIVE_DEPLOYMENTS_DATA_SUCCESS on error', done => {
- const dispatch = jasmine.createSpy();
+ const dispatch = jest.fn();
const { state } = store;
state.deploymentsEndpoint = '/success';
-
mock.onGet(state.deploymentsEndpoint).reply(200, {
deployments: deploymentData,
});
-
- fetchDeploymentsData({ state, dispatch })
+ fetchDeploymentsData({
+ state,
+ dispatch,
+ })
.then(() => {
expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataSuccess', deploymentData);
done();
})
.catch(done.fail);
});
-
it('commits RECEIVE_DEPLOYMENTS_DATA_FAILURE on error', done => {
- const dispatch = jasmine.createSpy();
+ const dispatch = jest.fn();
const { state } = store;
state.deploymentsEndpoint = '/error';
-
mock.onGet(state.deploymentsEndpoint).reply(500);
-
- fetchDeploymentsData({ state, dispatch })
+ fetchDeploymentsData({
+ state,
+ dispatch,
+ })
.then(() => {
expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataFailure');
done();
@@ -81,33 +138,33 @@ describe('Monitoring store actions', () => {
.catch(done.fail);
});
});
-
describe('fetchEnvironmentsData', () => {
it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on error', done => {
- const dispatch = jasmine.createSpy();
+ const dispatch = jest.fn();
const { state } = store;
state.environmentsEndpoint = '/success';
-
mock.onGet(state.environmentsEndpoint).reply(200, {
environments: environmentData,
});
-
- fetchEnvironmentsData({ state, dispatch })
+ fetchEnvironmentsData({
+ state,
+ dispatch,
+ })
.then(() => {
expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataSuccess', environmentData);
done();
})
.catch(done.fail);
});
-
it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', done => {
- const dispatch = jasmine.createSpy();
+ const dispatch = jest.fn();
const { state } = store;
state.environmentsEndpoint = '/error';
-
mock.onGet(state.environmentsEndpoint).reply(500);
-
- fetchEnvironmentsData({ state, dispatch })
+ fetchEnvironmentsData({
+ state,
+ dispatch,
+ })
.then(() => {
expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataFailure');
done();
@@ -115,14 +172,11 @@ describe('Monitoring store actions', () => {
.catch(done.fail);
});
});
-
describe('Set endpoints', () => {
let mockedState;
-
beforeEach(() => {
mockedState = storeState();
});
-
it('should commit SET_ENDPOINTS mutation', done => {
testAction(
setEndpoints,
@@ -147,42 +201,45 @@ describe('Monitoring store actions', () => {
);
});
});
-
describe('Set empty states', () => {
let mockedState;
-
beforeEach(() => {
mockedState = storeState();
});
-
it('should commit SET_METRICS_ENDPOINT mutation', done => {
testAction(
setGettingStartedEmptyState,
null,
mockedState,
- [{ type: types.SET_GETTING_STARTED_EMPTY_STATE }],
+ [
+ {
+ type: types.SET_GETTING_STARTED_EMPTY_STATE,
+ },
+ ],
[],
done,
);
});
});
-
describe('fetchDashboard', () => {
let dispatch;
let state;
const response = metricsDashboardResponse;
-
beforeEach(() => {
- dispatch = jasmine.createSpy();
+ dispatch = jest.fn();
state = storeState();
state.dashboardEndpoint = '/dashboard';
});
-
it('dispatches receive and success actions', done => {
const params = {};
mock.onGet(state.dashboardEndpoint).reply(200, response);
-
- fetchDashboard({ state, dispatch }, params)
+ fetchDashboard(
+ {
+ state,
+ dispatch,
+ },
+ params,
+ )
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestMetricsDashboard');
expect(dispatch).toHaveBeenCalledWith('receiveMetricsDashboardSuccess', {
@@ -193,12 +250,16 @@ describe('Monitoring store actions', () => {
})
.catch(done.fail);
});
-
it('dispatches failure action', done => {
const params = {};
mock.onGet(state.dashboardEndpoint).reply(500);
-
- fetchDashboard({ state, dispatch }, params)
+ fetchDashboard(
+ {
+ state,
+ dispatch,
+ },
+ params,
+ )
.then(() => {
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
@@ -209,77 +270,92 @@ describe('Monitoring store actions', () => {
.catch(done.fail);
});
});
-
describe('receiveMetricsDashboardSuccess', () => {
let commit;
let dispatch;
let state;
-
beforeEach(() => {
- commit = jasmine.createSpy();
- dispatch = jasmine.createSpy();
+ commit = jest.fn();
+ dispatch = jest.fn();
state = storeState();
});
-
it('stores groups ', () => {
const params = {};
const response = metricsDashboardResponse;
-
- receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
-
+ receiveMetricsDashboardSuccess(
+ {
+ state,
+ commit,
+ dispatch,
+ },
+ {
+ response,
+ params,
+ },
+ );
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DATA_SUCCESS,
metricsDashboardResponse.dashboard.panel_groups,
);
-
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params);
});
-
it('sets the dashboards loaded from the repository', () => {
const params = {};
const response = metricsDashboardResponse;
-
response.all_dashboards = dashboardGitResponse;
- receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
-
+ receiveMetricsDashboardSuccess(
+ {
+ state,
+ commit,
+ dispatch,
+ },
+ {
+ response,
+ params,
+ },
+ );
expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
});
});
-
describe('receiveMetricsDashboardFailure', () => {
let commit;
-
beforeEach(() => {
- commit = jasmine.createSpy();
+ commit = jest.fn();
});
-
it('commits failure action', () => {
- receiveMetricsDashboardFailure({ commit });
-
+ receiveMetricsDashboardFailure({
+ commit,
+ });
expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, undefined);
});
-
it('commits failure action with error', () => {
- receiveMetricsDashboardFailure({ commit }, 'uh-oh');
-
+ receiveMetricsDashboardFailure(
+ {
+ commit,
+ },
+ 'uh-oh',
+ );
expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, 'uh-oh');
});
});
-
describe('fetchPrometheusMetrics', () => {
let commit;
let dispatch;
-
beforeEach(() => {
- commit = jasmine.createSpy();
- dispatch = jasmine.createSpy();
+ commit = jest.fn();
+ dispatch = jest.fn();
});
-
it('commits empty state when state.groups is empty', done => {
const state = storeState();
const params = {};
-
- fetchPrometheusMetrics({ state, commit, dispatch }, params)
+ fetchPrometheusMetrics(
+ {
+ state,
+ commit,
+ dispatch,
+ },
+ params,
+ )
.then(() => {
expect(commit).toHaveBeenCalledWith(types.SET_NO_DATA_EMPTY_STATE);
expect(dispatch).not.toHaveBeenCalled();
@@ -287,49 +363,54 @@ describe('Monitoring store actions', () => {
})
.catch(done.fail);
});
-
it('dispatches fetchPrometheusMetric for each panel query', done => {
const params = {};
const state = storeState();
- state.groups = metricsDashboardResponse.dashboard.panel_groups;
-
- const metric = state.groups[0].panels[0].metrics[0];
-
- fetchPrometheusMetrics({ state, commit, dispatch }, params)
+ state.dashboard.panel_groups = metricsDashboardResponse.dashboard.panel_groups;
+ const metric = state.dashboard.panel_groups[0].panels[0].metrics[0];
+ fetchPrometheusMetrics(
+ {
+ state,
+ commit,
+ dispatch,
+ },
+ params,
+ )
.then(() => {
- expect(dispatch.calls.count()).toEqual(3);
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, params });
+ expect(dispatch).toHaveBeenCalledTimes(3);
+ expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
+ metric,
+ params,
+ });
done();
})
.catch(done.fail);
-
done();
});
});
-
describe('fetchPrometheusMetric', () => {
it('commits prometheus query result', done => {
- const commit = jasmine.createSpy();
+ const commit = jest.fn();
const params = {
start: '2019-08-06T12:40:02.184Z',
end: '2019-08-06T20:40:02.184Z',
};
const metric = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics[0];
const state = storeState();
-
- const data = metricsGroupsAPIResponse.data[0].metrics[0].queries[0];
- const response = { data };
+ const data = metricsGroupsAPIResponse[0].panels[0].metrics[0];
+ const response = {
+ data,
+ };
mock.onGet('http://test').reply(200, response);
-
- fetchPrometheusMetric({ state, commit }, { metric, params });
-
- setTimeout(() => {
- expect(commit).toHaveBeenCalledWith(types.SET_QUERY_RESULT, {
- metricId: metric.metric_id,
- result: data.result,
- });
- done();
- });
+ fetchPrometheusMetric({ state, commit }, { metric, params })
+ .then(() => {
+ expect(commit).toHaveBeenCalledWith(types.SET_QUERY_RESULT, {
+ metricId: metric.metric_id,
+ result: data.result,
+ });
+ done();
+ })
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index bdddd83358c..fdad290a8d6 100644
--- a/spec/javascripts/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -11,104 +11,62 @@ import { uniqMetricsId } from '~/monitoring/stores/utils';
describe('Monitoring mutations', () => {
let stateCopy;
-
beforeEach(() => {
stateCopy = state();
});
-
- describe(types.RECEIVE_METRICS_DATA_SUCCESS, () => {
+ describe('RECEIVE_METRICS_DATA_SUCCESS', () => {
let groups;
-
beforeEach(() => {
- stateCopy.groups = [];
- groups = metricsGroupsAPIResponse.data;
+ stateCopy.dashboard.panel_groups = [];
+ groups = metricsGroupsAPIResponse;
});
-
- it('normalizes values', () => {
+ it('adds a key to the group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
-
- const expectedTimestamp = '2017-05-25T08:22:34.925Z';
- const expectedValue = 0.0010794445585559514;
- const [timestamp, value] = stateCopy.groups[0].metrics[0].queries[0].result[0].values[0];
-
- expect(timestamp).toEqual(expectedTimestamp);
- expect(value).toEqual(expectedValue);
+ expect(stateCopy.dashboard.panel_groups[0].key).toBe('system-metrics-kubernetes--0');
});
-
- it('contains two groups that contains, one of which has two queries sorted by priority', () => {
+ it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
-
- expect(stateCopy.groups).toBeDefined();
- expect(stateCopy.groups.length).toEqual(2);
- expect(stateCopy.groups[0].metrics.length).toEqual(2);
+ const expectedLabel = 'Pod average';
+ const { label, query_range } = stateCopy.dashboard.panel_groups[0].metrics[0].metrics[0];
+ expect(label).toEqual(expectedLabel);
+ expect(query_range.length).toBeGreaterThan(0);
});
-
- it('assigns queries a metric id', () => {
+ it('contains one group, which it has two panels and one metrics property', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
-
- expect(stateCopy.groups[1].metrics[0].queries[0].metricId).toEqual('100');
+ expect(stateCopy.dashboard.panel_groups).toBeDefined();
+ expect(stateCopy.dashboard.panel_groups.length).toEqual(1);
+ expect(stateCopy.dashboard.panel_groups[0].panels.length).toEqual(2);
+ expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics.length).toEqual(1);
+ expect(stateCopy.dashboard.panel_groups[0].panels[1].metrics.length).toEqual(1);
});
-
- it('removes the data if all the values from a query are not defined', () => {
+ it('assigns queries a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
-
- expect(stateCopy.groups[1].metrics[0].queries[0].result.length).toEqual(0);
- });
-
- it('assigns metric id of null if metric has no id', () => {
- stateCopy.groups = [];
- const noId = groups.map(group => ({
- ...group,
- ...{
- metrics: group.metrics.map(metric => {
- const { id, ...metricWithoutId } = metric;
-
- return metricWithoutId;
- }),
- },
- }));
-
- mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, noId);
-
- stateCopy.groups.forEach(group => {
- group.metrics.forEach(metric => {
- expect(metric.queries.every(query => query.metricId === null)).toBe(true);
- });
- });
+ expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].metricId).toEqual(
+ '17_system_metrics_kubernetes_container_memory_average',
+ );
});
-
- describe('dashboard endpoint enabled', () => {
+ describe('dashboard endpoint', () => {
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
-
- beforeEach(() => {
- stateCopy.useDashboardEndpoint = true;
- });
-
it('aliases group panels to metrics for backwards compatibility', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
-
- expect(stateCopy.groups[0].metrics[0]).toBeDefined();
+ expect(stateCopy.dashboard.panel_groups[0].metrics[0]).toBeDefined();
});
-
it('aliases panel metrics to queries for backwards compatibility', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
-
- expect(stateCopy.groups[0].metrics[0].queries).toBeDefined();
+ expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries).toBeDefined();
});
});
});
- describe(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, () => {
+ describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
it('stores the deployment data', () => {
stateCopy.deploymentData = [];
mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData);
-
expect(stateCopy.deploymentData).toBeDefined();
expect(stateCopy.deploymentData.length).toEqual(3);
expect(typeof stateCopy.deploymentData[0]).toEqual('object');
});
});
-
describe('SET_ENDPOINTS', () => {
it('should set all the endpoints', () => {
mutations[types.SET_ENDPOINTS](stateCopy, {
@@ -118,7 +76,6 @@ describe('Monitoring mutations', () => {
dashboardEndpoint: 'dashboard.json',
projectPath: '/gitlab-org/gitlab-foss',
});
-
expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
expect(stateCopy.environmentsEndpoint).toEqual('environments.json');
expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
@@ -126,51 +83,59 @@ describe('Monitoring mutations', () => {
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
});
});
-
describe('SET_QUERY_RESULT', () => {
const metricId = 12;
const id = 'system_metrics_kubernetes_container_memory_total';
- const result = [{ values: [[0, 1], [1, 1], [1, 3]] }];
-
+ const result = [
+ {
+ values: [[0, 1], [1, 1], [1, 3]],
+ },
+ ];
beforeEach(() => {
- stateCopy.useDashboardEndpoint = true;
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
-
it('clears empty state', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId,
result,
});
-
expect(stateCopy.showEmptyState).toBe(false);
});
-
it('sets metricsWithData value', () => {
- const uniqId = uniqMetricsId({ metric_id: metricId, id });
+ const uniqId = uniqMetricsId({
+ metric_id: metricId,
+ id,
+ });
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId: uniqId,
result,
});
-
expect(stateCopy.metricsWithData).toEqual([uniqId]);
});
-
it('does not store empty results', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId,
result: [],
});
-
expect(stateCopy.metricsWithData).toEqual([]);
});
});
-
describe('SET_ALL_DASHBOARDS', () => {
- it('stores the dashboards loaded from the git repository', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
+ it('stores `undefined` dashboards as an empty array', () => {
+ mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
+ expect(stateCopy.allDashboards).toEqual([]);
+ });
+
+ it('stores `null` dashboards as an empty array', () => {
+ mutations[types.SET_ALL_DASHBOARDS](stateCopy, null);
+
+ expect(stateCopy.allDashboards).toEqual([]);
+ });
+
+ it('stores dashboards loaded from the git repository', () => {
+ mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
});
});
diff --git a/spec/javascripts/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 98388ac19f8..98388ac19f8 100644
--- a/spec/javascripts/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
new file mode 100644
index 00000000000..45b99b71e06
--- /dev/null
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -0,0 +1,331 @@
+import $ from 'jquery';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import Autosize from 'autosize';
+import axios from '~/lib/utils/axios_utils';
+import createStore from '~/notes/stores';
+import CommentForm from '~/notes/components/comment_form.vue';
+import * as constants from '~/notes/constants';
+import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import { trimText } from 'helpers/text_helper';
+import { keyboardDownEvent } from '../../issue_show/helpers';
+import {
+ loggedOutnoteableData,
+ notesDataMock,
+ userDataMock,
+ noteableDataMock,
+} from '../../notes/mock_data';
+
+jest.mock('autosize');
+jest.mock('~/commons/nav/user_merge_requests');
+jest.mock('~/gl_form');
+
+describe('issue_comment_form component', () => {
+ let store;
+ let wrapper;
+ let axiosMock;
+
+ const setupStore = (userData, noteableData) => {
+ store.dispatch('setUserData', userData);
+ store.dispatch('setNoteableData', noteableData);
+ store.dispatch('setNotesData', notesDataMock);
+ };
+
+ const mountComponent = (noteableType = 'issue') => {
+ wrapper = mount(CommentForm, {
+ propsData: {
+ noteableType,
+ },
+ store,
+ sync: false,
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ store = createStore();
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ wrapper.destroy();
+ jest.clearAllMocks();
+ });
+
+ describe('user is logged in', () => {
+ beforeEach(() => {
+ setupStore(userDataMock, noteableDataMock);
+
+ mountComponent();
+ });
+
+ it('should render user avatar with link', () => {
+ expect(wrapper.find('.timeline-icon .user-avatar-link').attributes('href')).toEqual(
+ userDataMock.path,
+ );
+ });
+
+ describe('handleSave', () => {
+ it('should request to save note when note is entered', () => {
+ wrapper.vm.note = 'hello world';
+ jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
+ jest.spyOn(wrapper.vm, 'resizeTextarea');
+ jest.spyOn(wrapper.vm, 'stopPolling');
+
+ wrapper.vm.handleSave();
+
+ expect(wrapper.vm.isSubmitting).toEqual(true);
+ expect(wrapper.vm.note).toEqual('');
+ expect(wrapper.vm.saveNote).toHaveBeenCalled();
+ expect(wrapper.vm.stopPolling).toHaveBeenCalled();
+ expect(wrapper.vm.resizeTextarea).toHaveBeenCalled();
+ });
+
+ it('should toggle issue state when no note', () => {
+ jest.spyOn(wrapper.vm, 'toggleIssueState');
+
+ wrapper.vm.handleSave();
+
+ expect(wrapper.vm.toggleIssueState).toHaveBeenCalled();
+ });
+
+ it('should disable action button whilst submitting', done => {
+ const saveNotePromise = Promise.resolve();
+ wrapper.vm.note = 'hello world';
+ jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise);
+ jest.spyOn(wrapper.vm, 'stopPolling');
+
+ const actionButton = wrapper.find('.js-action-button');
+
+ wrapper.vm.handleSave();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(actionButton.vm.disabled).toBeTruthy();
+ })
+ .then(saveNotePromise)
+ .then(wrapper.vm.$nextTick)
+ .then(() => {
+ expect(actionButton.vm.disabled).toBeFalsy();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('textarea', () => {
+ it('should render textarea with placeholder', () => {
+ expect(wrapper.find('.js-main-target-form textarea').attributes('placeholder')).toEqual(
+ 'Write a comment or drag your files here…',
+ );
+ });
+
+ it('should make textarea disabled while requesting', done => {
+ const $submitButton = $(wrapper.find('.js-comment-submit-button').element);
+ wrapper.vm.note = 'hello world';
+ jest.spyOn(wrapper.vm, 'stopPolling');
+ jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(new Promise(() => {}));
+
+ wrapper.vm.$nextTick(() => {
+ // Wait for wrapper.vm.note change triggered. It should enable $submitButton.
+ $submitButton.trigger('click');
+
+ wrapper.vm.$nextTick(() => {
+ // Wait for wrapper.isSubmitting triggered. It should disable textarea.
+ expect(wrapper.find('.js-main-target-form textarea').attributes('disabled')).toBe(
+ 'disabled',
+ );
+ done();
+ });
+ });
+ });
+
+ it('should support quick actions', () => {
+ expect(
+ wrapper.find('.js-main-target-form textarea').attributes('data-supports-quick-actions'),
+ ).toBe('true');
+ });
+
+ it('should link to markdown docs', () => {
+ const { markdownDocsPath } = notesDataMock;
+
+ expect(
+ wrapper
+ .find(`a[href="${markdownDocsPath}"]`)
+ .text()
+ .trim(),
+ ).toEqual('Markdown');
+ });
+
+ it('should link to quick actions docs', () => {
+ const { quickActionsDocsPath } = notesDataMock;
+
+ expect(
+ wrapper
+ .find(`a[href="${quickActionsDocsPath}"]`)
+ .text()
+ .trim(),
+ ).toEqual('quick actions');
+ });
+
+ it('should resize textarea after note discarded', done => {
+ jest.spyOn(wrapper.vm, 'discard');
+
+ wrapper.vm.note = 'foo';
+ wrapper.vm.discard();
+
+ wrapper.vm.$nextTick(() => {
+ expect(Autosize.update).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ describe('edit mode', () => {
+ it('should enter edit mode when arrow up is pressed', () => {
+ jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
+ wrapper.find('.js-main-target-form textarea').value = 'Foo';
+ wrapper
+ .find('.js-main-target-form textarea')
+ .element.dispatchEvent(keyboardDownEvent(38, true));
+
+ expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
+ });
+
+ it('inits autosave', () => {
+ expect(wrapper.vm.autosave).toBeDefined();
+ expect(wrapper.vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`);
+ });
+ });
+
+ describe('event enter', () => {
+ it('should save note when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
+ wrapper.find('.js-main-target-form textarea').value = 'Foo';
+ wrapper
+ .find('.js-main-target-form textarea')
+ .element.dispatchEvent(keyboardDownEvent(13, true));
+
+ expect(wrapper.vm.handleSave).toHaveBeenCalled();
+ });
+
+ it('should save note when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
+ wrapper.find('.js-main-target-form textarea').value = 'Foo';
+ wrapper
+ .find('.js-main-target-form textarea')
+ .element.dispatchEvent(keyboardDownEvent(13, false, true));
+
+ expect(wrapper.vm.handleSave).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('actions', () => {
+ it('should be possible to close the issue', () => {
+ expect(
+ wrapper
+ .find('.btn-comment-and-close')
+ .text()
+ .trim(),
+ ).toEqual('Close issue');
+ });
+
+ it('should render comment button as disabled', () => {
+ expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toEqual(
+ 'disabled',
+ );
+ });
+
+ it('should enable comment button if it has note', done => {
+ wrapper.vm.note = 'Foo';
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.js-comment-submit-button').attributes('disabled')).toBeFalsy();
+ done();
+ });
+ });
+
+ it('should update buttons texts when it has note', done => {
+ wrapper.vm.note = 'Foo';
+ wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .find('.btn-comment-and-close')
+ .text()
+ .trim(),
+ ).toEqual('Comment & close issue');
+
+ done();
+ });
+ });
+
+ it('updates button text with noteable type', done => {
+ wrapper.setProps({ noteableType: constants.MERGE_REQUEST_NOTEABLE_TYPE });
+
+ wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .find('.btn-comment-and-close')
+ .text()
+ .trim(),
+ ).toEqual('Close merge request');
+ done();
+ });
+ });
+
+ describe('when clicking close/reopen button', () => {
+ it('should disable button and show a loading spinner', done => {
+ const toggleStateButton = wrapper.find('.js-action-button');
+
+ toggleStateButton.trigger('click');
+ wrapper.vm.$nextTick(() => {
+ expect(toggleStateButton.element.disabled).toEqual(true);
+ expect(toggleStateButton.find('.js-loading-button-icon').exists()).toBe(true);
+
+ done();
+ });
+ });
+ });
+
+ describe('when toggling state', () => {
+ it('should update MR count', done => {
+ jest.spyOn(wrapper.vm, 'closeIssue').mockResolvedValue();
+
+ wrapper.vm.toggleIssueState();
+
+ wrapper.vm.$nextTick(() => {
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('issue is confidential', () => {
+ it('shows information warning', done => {
+ store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.confidential-issue-warning')).toBeDefined();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('user is not logged in', () => {
+ beforeEach(() => {
+ setupStore(null, loggedOutnoteableData);
+
+ mountComponent();
+ });
+
+ it('should render signed out widget', () => {
+ expect(trimText(wrapper.text())).toEqual('Please register or sign in to reply');
+ });
+
+ it('should not render submission form', () => {
+ expect(wrapper.find('textarea').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
new file mode 100644
index 00000000000..f90147f9105
--- /dev/null
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -0,0 +1,141 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+
+import createStore from '~/notes/stores';
+import diffDiscussionHeader from '~/notes/components/diff_discussion_header.vue';
+
+import { discussionMock } from '../../../javascripts/notes/mock_data';
+import mockDiffFile from '../../diffs/mock_data/diff_discussions';
+
+const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
+
+describe('diff_discussion_header component', () => {
+ let store;
+ let wrapper;
+
+ preloadFixtures(discussionWithTwoUnresolvedNotes);
+
+ beforeEach(() => {
+ window.mrTabs = {};
+ store = createStore();
+
+ const localVue = createLocalVue();
+ wrapper = mount(diffDiscussionHeader, {
+ store,
+ propsData: { discussion: discussionMock },
+ localVue,
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render user avatar', () => {
+ const discussion = { ...discussionMock };
+ discussion.diff_file = mockDiffFile;
+ discussion.diff_discussion = true;
+
+ wrapper.setProps({ discussion });
+
+ expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
+ });
+
+ describe('action text', () => {
+ const commitId = 'razupaltuff';
+ const truncatedCommitId = commitId.substr(0, 8);
+ let commitElement;
+
+ beforeEach(done => {
+ store.state.diffs = {
+ projectPath: 'something',
+ };
+
+ wrapper.setProps({
+ discussion: {
+ ...discussionMock,
+ for_commit: true,
+ commit_id: commitId,
+ diff_discussion: true,
+ diff_file: {
+ ...mockDiffFile,
+ },
+ },
+ });
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ commitElement = wrapper.find('.commit-sha');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('for diff threads without a commit id', () => {
+ it('should show started a thread on the diff text', done => {
+ Object.assign(wrapper.vm.discussion, {
+ for_commit: false,
+ commit_id: null,
+ });
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain('started a thread on the diff');
+
+ done();
+ });
+ });
+
+ it('should show thread on older version text', done => {
+ Object.assign(wrapper.vm.discussion, {
+ for_commit: false,
+ commit_id: null,
+ active: false,
+ });
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain('started a thread on an old version of the diff');
+
+ done();
+ });
+ });
+ });
+
+ describe('for commit threads', () => {
+ it('should display a monospace started a thread on commit', () => {
+ expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
+ expect(commitElement.exists()).toBe(true);
+ expect(commitElement.text()).toContain(truncatedCommitId);
+ });
+ });
+
+ describe('for diff thread with a commit id', () => {
+ it('should display started thread on commit header', done => {
+ wrapper.vm.discussion.for_commit = false;
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
+
+ expect(commitElement).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('should display outdated change on commit header', done => {
+ wrapper.vm.discussion.for_commit = false;
+ wrapper.vm.discussion.active = false;
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.text()).toContain(
+ `started a thread on an outdated change in commit ${truncatedCommitId}`,
+ );
+
+ expect(commitElement).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index d3c8cf72376..91f9dab2530 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -1,6 +1,6 @@
import createStore from '~/notes/stores';
import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
-import { discussionMock } from '../../../javascripts/notes/mock_data';
+import { discussionMock } from '../../notes/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_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 58d367077e8..f77236b14bc 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -8,11 +8,7 @@ import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_sys
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import createStore from '~/notes/stores';
-import {
- noteableDataMock,
- discussionMock,
- notesDataMock,
-} from '../../../javascripts/notes/mock_data';
+import { noteableDataMock, discussionMock, notesDataMock } from '../../notes/mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index a8ec47fd44f..3716b349210 100644
--- a/spec/frontend/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -9,7 +9,8 @@ import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
import { setTestTimeout } from 'helpers/timeout';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
-import * as mockData from '../../../javascripts/notes/mock_data';
+import * as mockData from '../../notes/mock_data';
+import * as urlUtility from '~/lib/utils/url_utility';
setTestTimeout(1000);
@@ -54,7 +55,9 @@ describe('note_app', () => {
components: {
NotesApp,
},
- template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>',
+ template: `<div class="js-vue-notes-event">
+ <notes-app ref="notesApp" v-bind="$attrs" />
+ </div>`,
},
{
attachToDocument: true,
@@ -313,4 +316,23 @@ describe('note_app', () => {
});
});
});
+
+ describe('mounted', () => {
+ beforeEach(() => {
+ axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
+ wrapper = mountComponent();
+ return waitForDiscussionsRequest();
+ });
+
+ it('should listen hashchange event', () => {
+ const notesApp = wrapper.find(NotesApp);
+ const hash = 'some dummy hash';
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
+ const setTargetNoteHash = jest.spyOn(notesApp.vm, 'setTargetNoteHash');
+
+ window.dispatchEvent(new Event('hashchange'), hash);
+
+ expect(setTargetNoteHash).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
new file mode 100644
index 00000000000..01cb70d395c
--- /dev/null
+++ b/spec/frontend/notes/mock_data.js
@@ -0,0 +1,1255 @@
+// Copied to ee/spec/frontend/notes/mock_data.js
+
+export const notesDataMock = {
+ discussionsPath: '/gitlab-org/gitlab-foss/issues/26/discussions.json',
+ lastFetchedAt: 1501862675,
+ markdownDocsPath: '/help/user/markdown',
+ newSessionPath: '/users/sign_in?redirect_to_referer=yes',
+ notesPath: '/gitlab-org/gitlab-foss/noteable/issue/98/notes',
+ quickActionsDocsPath: '/help/user/project/quick_actions',
+ registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
+ prerenderedNotesCount: 1,
+ closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
+ reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
+ canAwardEmoji: true,
+};
+
+export const userDataMock = {
+ avatar_url: 'mock_path',
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+};
+
+export const noteableDataMock = {
+ assignees: [],
+ author_id: 1,
+ branch_name: null,
+ confidential: false,
+ create_note_path: '/gitlab-org/gitlab-foss/notes?target_id=98&target_type=issue',
+ created_at: '2017-02-07T10:11:18.395Z',
+ current_user: {
+ can_create_note: true,
+ can_update: true,
+ can_award_emoji: true,
+ },
+ description: '',
+ due_date: null,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ id: 98,
+ iid: 26,
+ labels: [],
+ lock_version: null,
+ milestone: null,
+ milestone_id: null,
+ moved_to_id: null,
+ preview_note_path: '/gitlab-org/gitlab-foss/preview_markdown?target_id=98&target_type=Issue',
+ project_id: 2,
+ state: 'opened',
+ time_estimate: 0,
+ title: '14',
+ total_time_spent: 0,
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ updated_at: '2017-08-04T09:53:01.226Z',
+ updated_by_id: 1,
+ web_url: '/gitlab-org/gitlab-foss/issues/26',
+ noteableType: 'issue',
+};
+
+export const lastFetchedAt = '1501862675';
+
+export const individualNote = {
+ expanded: true,
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ individual_note: true,
+ notes: [
+ {
+ id: '1390',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2017-08-01T17: 09: 33.762Z',
+ updated_at: '2017-08-01T17: 09: 33.762Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'sdfdsaf',
+ note_html: "<p dir='auto'>sdfdsaf</p>",
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ emoji_awardable: true,
+ award_emoji: [
+ { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
+ { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ note_url: '/group/project/merge_requests/1#note_1',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1390',
+ },
+ ],
+ reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+};
+
+export const note = {
+ id: '546',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2017-08-10T15:24:03.087Z',
+ updated_at: '2017-08-10T15:24:03.087Z',
+ system: false,
+ noteable_id: 67,
+ noteable_type: 'Issue',
+ noteable_iid: 7,
+ type: null,
+ human_access: 'Owner',
+ note: 'Vel id placeat reprehenderit sit numquam.',
+ note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0',
+ emoji_awardable: true,
+ award_emoji: [
+ {
+ name: 'baseball',
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+ },
+ {
+ name: 'bath_tone3',
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+ },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/546/toggle_award_emoji',
+ note_url: '/group/project/merge_requests/1#note_1',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/546',
+};
+
+export const discussionMock = {
+ id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ expanded: true,
+ notes: [
+ {
+ id: '1395',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:51:58.559Z',
+ updated_at: '2017-08-02T10:51:58.559Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'THIS IS A DICUSSSION!',
+ note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>",
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ can_resolve: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1395/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1395',
+ },
+ {
+ id: '1396',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:56:50.980Z',
+ updated_at: '2017-08-03T14:19:35.691Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'sadfasdsdgdsf',
+ note_html: "<p dir='auto'>sadfasdsdgdsf</p>",
+ last_edited_at: '2017-08-03T14:19:35.691Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ can_resolve: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1396/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1396',
+ },
+ {
+ id: '1437',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-03T18:11:18.780Z',
+ updated_at: '2017-08-04T09:52:31.062Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'adsfasf Should disappear',
+ note_html: "<p dir='auto'>adsfasf Should disappear</p>",
+ last_edited_at: '2017-08-04T09:52:31.062Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ can_resolve: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1437/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1437',
+ },
+ ],
+ individual_note: false,
+ resolvable: true,
+ active: true,
+};
+
+export const loggedOutnoteableData = {
+ id: '98',
+ iid: 26,
+ author_id: 1,
+ description: '',
+ lock_version: 1,
+ milestone_id: null,
+ state: 'opened',
+ title: 'asdsa',
+ updated_by_id: 1,
+ created_at: '2017-02-07T10:11:18.395Z',
+ updated_at: '2017-08-08T10:22:51.564Z',
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ milestone: null,
+ labels: [],
+ branch_name: null,
+ confidential: false,
+ assignees: [
+ {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ },
+ ],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 2,
+ web_url: '/gitlab-org/gitlab-foss/issues/26',
+ current_user: {
+ can_create_note: false,
+ can_update: false,
+ },
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ create_note_path: '/gitlab-org/gitlab-foss/notes?target_id=98&target_type=issue',
+ preview_note_path: '/gitlab-org/gitlab-foss/preview_markdown?target_id=98&target_type=Issue',
+};
+
+export const collapseNotesMock = [
+ {
+ expanded: true,
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ individual_note: true,
+ notes: [
+ {
+ id: '1390',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2018-02-26T18:07:41.071Z',
+ updated_at: '2018-02-26T18:07:41.071Z',
+ system: true,
+ system_note_icon_name: 'pencil',
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false },
+ discussion_id: 'b97fb7bda470a65b3e009377a9032edec0a4dd05',
+ emoji_awardable: false,
+ path: '/h5bp/html5-boilerplate/notes/1057',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
+ },
+ ],
+ },
+ {
+ expanded: true,
+ id: 'ffde43f25984ad7f2b4275135e0e2846875336c0',
+ individual_note: true,
+ notes: [
+ {
+ id: '1391',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2018-02-26T18:13:24.071Z',
+ updated_at: '2018-02-26T18:13:24.071Z',
+ system: true,
+ system_note_icon_name: 'pencil',
+ noteable_id: 99,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false },
+ discussion_id: '3eb958b4d81dec207ec3537a2f3bd8b9f271bb34',
+ emoji_awardable: false,
+ path: '/h5bp/html5-boilerplate/notes/1057',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
+ },
+ ],
+ },
+];
+
+export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
+ GET: {
+ '/gitlab-org/gitlab-foss/issues/26/discussions.json': [
+ {
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ expanded: true,
+ notes: [
+ {
+ id: '1390',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-01T17:09:33.762Z',
+ updated_at: '2017-08-01T17:09:33.762Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'sdfdsaf',
+ note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ emoji_awardable: true,
+ award_emoji: [
+ {
+ name: 'baseball',
+ user: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ },
+ },
+ {
+ name: 'art',
+ user: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ },
+ },
+ ],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1390',
+ },
+ ],
+ individual_note: true,
+ },
+ {
+ id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ reply_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ expanded: true,
+ notes: [
+ {
+ id: '1391',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:51:38.685Z',
+ updated_at: '2017-08-02T10:51:38.685Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'New note!',
+ note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+ emoji_awardable: true,
+ award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1391/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1391',
+ },
+ ],
+ individual_note: true,
+ },
+ ],
+ '/gitlab-org/gitlab-foss/noteable/issue/98/notes': {
+ last_fetched_at: 1512900838,
+ notes: [],
+ },
+ },
+ PUT: {
+ '/gitlab-org/gitlab-foss/notes/1471': {
+ commands_changes: null,
+ valid: true,
+ id: '1471',
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-08T16:53:00.666Z',
+ updated_at: '2017-12-10T11:03:21.876Z',
+ system: false,
+ noteable_id: 124,
+ noteable_type: 'Issue',
+ noteable_iid: 29,
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'Adding a comment',
+ note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+ last_edited_at: '2017-12-10T11:03:21.876Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ emoji_awardable: true,
+ award_emoji: [],
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1471',
+ },
+ },
+};
+
+export const DISCUSSION_NOTE_RESPONSE_MAP = {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP,
+ GET: {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
+ '/gitlab-org/gitlab-foss/issues/26/discussions.json': [
+ {
+ id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ reply_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ expanded: true,
+ notes: [
+ {
+ id: '1471',
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-08T16:53:00.666Z',
+ updated_at: '2017-08-08T16:53:00.666Z',
+ system: false,
+ noteable_id: 124,
+ noteable_type: 'Issue',
+ noteable_iid: 29,
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'Adding a comment',
+ note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+ current_user: {
+ can_edit: true,
+ can_award_emoji: true,
+ },
+ discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji',
+ noteable_note_url: '/group/project/merge_requests/1#note_1',
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+ path: '/gitlab-org/gitlab-foss/notes/1471',
+ },
+ ],
+ individual_note: false,
+ },
+ ],
+ },
+};
+
+export function getIndividualNoteResponse(config) {
+ return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+}
+
+export function getDiscussionNoteResponse(config) {
+ return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+}
+
+export const notesWithDescriptionChanges = [
+ {
+ id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ expanded: true,
+ notes: [
+ {
+ id: '901',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:36.117Z',
+ updated_at: '2018-05-29T12:05:36.117Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ note_html:
+ '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/901',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ expanded: true,
+ notes: [
+ {
+ id: '902',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:58.694Z',
+ updated_at: '2018-05-29T12:05:58.694Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
+ note_html:
+ '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/902',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ reply_id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ expanded: true,
+ notes: [
+ {
+ id: '903',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:05.772Z',
+ updated_at: '2018-05-29T12:06:05.772Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '7f1feda384083eb31763366e6392399fde6f3f31',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_903&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/903',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ expanded: true,
+ notes: [
+ {
+ id: '904',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:16.112Z',
+ updated_at: '2018-05-29T12:06:16.112Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'Ullamcorper eget nulla facilisi etiam',
+ note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/904',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ expanded: true,
+ notes: [
+ {
+ id: '905',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:28.851Z',
+ updated_at: '2018-05-29T12:06:28.851Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/905',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ expanded: true,
+ notes: [
+ {
+ id: '906',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:20:02.925Z',
+ updated_at: '2018-05-29T12:20:02.925Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/906',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+];
+
+export const collapsedSystemNotes = [
+ {
+ id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ expanded: true,
+ notes: [
+ {
+ id: '901',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:36.117Z',
+ updated_at: '2018-05-29T12:05:36.117Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+ note_html:
+ '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/901',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ expanded: true,
+ notes: [
+ {
+ id: '902',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:05:58.694Z',
+ updated_at: '2018-05-29T12:05:58.694Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note:
+ 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
+ note_html:
+ '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/902',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ expanded: true,
+ notes: [
+ {
+ id: '904',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:16.112Z',
+ updated_at: '2018-05-29T12:06:16.112Z',
+ system: false,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'Ullamcorper eget nulla facilisi etiam',
+ note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
+ current_user: { can_edit: true, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
+ emoji_awardable: true,
+ award_emoji: [],
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
+ human_access: 'Owner',
+ toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
+ path: '/gitlab-org/gitlab-shell/notes/904',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ expanded: true,
+ notes: [
+ {
+ id: '905',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:06:28.851Z',
+ updated_at: '2018-05-29T12:06:28.851Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ start_description_version_id: undefined,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/905',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+ {
+ id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ expanded: true,
+ notes: [
+ {
+ id: '906',
+ type: null,
+ attachment: null,
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ path: '/root',
+ },
+ created_at: '2018-05-29T12:20:02.925Z',
+ updated_at: '2018-05-29T12:20:02.925Z',
+ system: true,
+ noteable_id: 182,
+ noteable_type: 'Issue',
+ resolvable: false,
+ noteable_iid: 12,
+ note: 'changed the description',
+ note_html: '<p dir="auto">changed the description</p>',
+ current_user: { can_edit: false, can_award_emoji: true },
+ resolved: false,
+ resolved_by: null,
+ system_note_icon_name: 'pencil-square',
+ discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
+ emoji_awardable: false,
+ report_abuse_path:
+ '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
+ human_access: 'Owner',
+ path: '/gitlab-org/gitlab-shell/notes/906',
+ },
+ ],
+ individual_note: true,
+ resolvable: false,
+ resolved: false,
+ diff_discussion: false,
+ },
+];
+
+export const discussion1 = {
+ id: 'abc1',
+ resolvable: true,
+ resolved: false,
+ active: true,
+ diff_file: {
+ file_path: 'about.md',
+ },
+ position: {
+ new_line: 50,
+ old_line: null,
+ },
+ notes: [
+ {
+ created_at: '2018-07-04T16:25:41.749Z',
+ },
+ ],
+};
+
+export const resolvedDiscussion1 = {
+ id: 'abc1',
+ resolvable: true,
+ resolved: true,
+ diff_file: {
+ file_path: 'about.md',
+ },
+ position: {
+ new_line: 50,
+ old_line: null,
+ },
+ notes: [
+ {
+ created_at: '2018-07-04T16:25:41.749Z',
+ },
+ ],
+};
+
+export const discussion2 = {
+ id: 'abc2',
+ resolvable: true,
+ resolved: false,
+ active: true,
+ diff_file: {
+ file_path: 'README.md',
+ },
+ position: {
+ new_line: null,
+ old_line: 20,
+ },
+ notes: [
+ {
+ created_at: '2018-07-04T12:05:41.749Z',
+ },
+ ],
+};
+
+export const discussion3 = {
+ id: 'abc3',
+ resolvable: true,
+ active: true,
+ resolved: false,
+ diff_file: {
+ file_path: 'README.md',
+ },
+ position: {
+ new_line: 21,
+ old_line: null,
+ },
+ notes: [
+ {
+ created_at: '2018-07-05T17:25:41.749Z',
+ },
+ ],
+};
+
+export const unresolvableDiscussion = {
+ resolvable: false,
+};
+
+export const discussionFiltersMock = [
+ {
+ title: 'Show all activity',
+ value: 0,
+ },
+ {
+ title: 'Show comments only',
+ value: 1,
+ },
+ {
+ title: 'Show system notes only',
+ value: 2,
+ },
+];
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
new file mode 100644
index 00000000000..cef264f3915
--- /dev/null
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -0,0 +1,62 @@
+import AddRequest from '~/performance_bar/components/add_request.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('add request form', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(AddRequest);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('hides the input on load', () => {
+ expect(wrapper.find('input').exists()).toBe(false);
+ });
+
+ describe('when clicking the button', () => {
+ beforeEach(() => {
+ wrapper.find('button').trigger('click');
+ });
+
+ it('shows the form', () => {
+ expect(wrapper.find('input').exists()).toBe(true);
+ });
+
+ describe('when pressing escape', () => {
+ beforeEach(() => {
+ wrapper.find('input').trigger('keyup.esc');
+ });
+
+ it('hides the input', () => {
+ expect(wrapper.find('input').exists()).toBe(false);
+ });
+ });
+
+ describe('when submitting the form', () => {
+ beforeEach(() => {
+ wrapper.find('input').setValue('http://gitlab.example.com/users/root/calendar.json');
+ wrapper.find('input').trigger('keyup.enter');
+ });
+
+ it('emits an event to add the request', () => {
+ expect(wrapper.emitted()['add-request']).toBeTruthy();
+ expect(wrapper.emitted()['add-request'][0]).toEqual([
+ 'http://gitlab.example.com/users/root/calendar.json',
+ ]);
+ });
+
+ it('hides the input', () => {
+ expect(wrapper.find('input').exists()).toBe(false);
+ });
+
+ it('clears the value for next time', () => {
+ wrapper.find('button').trigger('click');
+
+ expect(wrapper.find('input').text()).toEqual('');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
new file mode 100644
index 00000000000..38ffe98c79b
--- /dev/null
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -0,0 +1,75 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import ActionComponent from '~/pipelines/components/graph/action_component.vue';
+
+describe('pipeline graph action component', () => {
+ let wrapper;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(200);
+
+ wrapper = mount(ActionComponent, {
+ propsData: {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionIcon: 'cancel',
+ },
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(wrapper.attributes('data-original-title')).toBe('bar');
+ });
+
+ it('should update bootstrap tooltip when title changes', done => {
+ wrapper.setProps({ tooltipText: 'changed' });
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.attributes('data-original-title')).toBe('changed');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should render an svg', () => {
+ expect(wrapper.find('.ci-action-icon-wrapper')).toBeDefined();
+ expect(wrapper.find('svg')).toBeDefined();
+ });
+
+ describe('on click', () => {
+ it('emits `pipelineActionRequestComplete` after a successful request', done => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.find('button').trigger('click');
+
+ waitForPromises()
+ .then(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('renders a loading icon while waiting for request', done => {
+ wrapper.find('button').trigger('click');
+
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 8cf290f2663..45ac278dd38 100644
--- a/spec/javascripts/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -17,6 +17,7 @@ describe('Pipelines Triggerer', () => {
const createComponent = () => {
wrapper = mount(pipelineTriggerer, {
propsData: mockData,
+ sync: false,
});
};
@@ -49,6 +50,8 @@ describe('Pipelines Triggerer', () => {
},
});
- expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ });
});
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index d47504d2f54..1c785ec6ffe 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -1,22 +1,21 @@
-import Vue from 'vue';
-import tableRowComp from '~/pipelines/components/pipelines_table_row.vue';
+import { mount } from '@vue/test-utils';
+import PipelinesTableRowComponent from '~/pipelines/components/pipelines_table_row.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipelines Table Row', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
- const buildComponent = pipeline => {
- const PipelinesTableRowComponent = Vue.extend(tableRowComp);
- return new PipelinesTableRowComponent({
- el: document.querySelector('.test-dom-element'),
+
+ const createWrapper = pipeline =>
+ mount(PipelinesTableRowComponent, {
propsData: {
pipeline,
autoDevopsHelpPath: 'foo',
viewType: 'root',
},
- }).$mount();
- };
+ sync: false,
+ });
- let component;
+ let wrapper;
let pipeline;
let pipelineWithoutAuthor;
let pipelineWithoutCommit;
@@ -32,28 +31,29 @@ describe('Pipelines Table Row', () => {
});
afterEach(() => {
- component.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
it('should render a table row', () => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
- expect(component.$el.getAttribute('class')).toContain('gl-responsive-table-row');
+ expect(wrapper.attributes('class')).toContain('gl-responsive-table-row');
});
describe('status column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
});
it('should render a pipeline link', () => {
- expect(
- component.$el.querySelector('.table-section.commit-link a').getAttribute('href'),
- ).toEqual(pipeline.path);
+ expect(wrapper.find('.table-section.commit-link a').attributes('href')).toEqual(
+ pipeline.path,
+ );
});
it('should render status text', () => {
- expect(component.$el.querySelector('.table-section.commit-link a').textContent).toContain(
+ expect(wrapper.find('.table-section.commit-link a').text()).toContain(
pipeline.details.status.text,
);
});
@@ -61,33 +61,32 @@ describe('Pipelines Table Row', () => {
describe('information column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
});
it('should render a pipeline link', () => {
- expect(
- component.$el.querySelector('.table-section:nth-child(2) a').getAttribute('href'),
- ).toEqual(pipeline.path);
+ expect(wrapper.find('.table-section:nth-child(2) a').attributes('href')).toEqual(
+ pipeline.path,
+ );
});
it('should render pipeline ID', () => {
- expect(
- component.$el.querySelector('.table-section:nth-child(2) a > span').textContent,
- ).toEqual(`#${pipeline.id}`);
+ expect(wrapper.find('.table-section:nth-child(2) a > span').text()).toEqual(
+ `#${pipeline.id}`,
+ );
});
describe('when a user is provided', () => {
it('should render user information', () => {
expect(
- component.$el
- .querySelector('.table-section:nth-child(3) .js-pipeline-url-user')
- .getAttribute('href'),
+ wrapper.find('.table-section:nth-child(3) .js-pipeline-url-user').attributes('href'),
).toEqual(pipeline.user.path);
expect(
- component.$el
- .querySelector('.table-section:nth-child(3) .js-user-avatar-image-toolip')
- .textContent.trim(),
+ wrapper
+ .find('.table-section:nth-child(3) .js-user-avatar-image-toolip')
+ .text()
+ .trim(),
).toEqual(pipeline.user.name);
});
});
@@ -95,40 +94,47 @@ describe('Pipelines Table Row', () => {
describe('commit column', () => {
it('should render link to commit', () => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
- const commitLink = component.$el.querySelector('.branch-commit .commit-sha');
+ const commitLink = wrapper.find('.branch-commit .commit-sha');
- expect(commitLink.getAttribute('href')).toEqual(pipeline.commit.commit_path);
+ expect(commitLink.attributes('href')).toEqual(pipeline.commit.commit_path);
});
const findElements = () => {
- const commitTitleElement = component.$el.querySelector('.branch-commit .commit-title');
- const commitAuthorElement = commitTitleElement.querySelector('a.avatar-image-container');
+ const commitTitleElement = wrapper.find('.branch-commit .commit-title');
+ const commitAuthorElement = commitTitleElement.find('a.avatar-image-container');
- if (!commitAuthorElement) {
- return { commitAuthorElement };
+ if (!commitAuthorElement.exists()) {
+ return {
+ commitAuthorElement,
+ };
}
- const commitAuthorLink = commitAuthorElement.getAttribute('href');
+ const commitAuthorLink = commitAuthorElement.attributes('href');
const commitAuthorName = commitAuthorElement
- .querySelector('.js-user-avatar-image-toolip')
- .textContent.trim();
-
- return { commitAuthorElement, commitAuthorLink, commitAuthorName };
+ .find('.js-user-avatar-image-toolip')
+ .text()
+ .trim();
+
+ return {
+ commitAuthorElement,
+ commitAuthorLink,
+ commitAuthorName,
+ };
};
it('renders nothing without commit', () => {
expect(pipelineWithoutCommit.commit).toBe(null);
- component = buildComponent(pipelineWithoutCommit);
+ wrapper = createWrapper(pipelineWithoutCommit);
const { commitAuthorElement } = findElements();
- expect(commitAuthorElement).toBe(null);
+ expect(commitAuthorElement.exists()).toBe(false);
});
it('renders commit author', () => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
const { commitAuthorLink, commitAuthorName } = findElements();
expect(commitAuthorLink).toEqual(pipeline.commit.author.path);
@@ -137,8 +143,8 @@ describe('Pipelines Table Row', () => {
it('renders commit with unregistered author', () => {
expect(pipelineWithoutAuthor.commit.author).toBe(null);
- component = buildComponent(pipelineWithoutAuthor);
+ wrapper = createWrapper(pipelineWithoutAuthor);
const { commitAuthorLink, commitAuthorName } = findElements();
expect(commitAuthorLink).toEqual(`mailto:${pipelineWithoutAuthor.commit.author_email}`);
@@ -148,13 +154,12 @@ describe('Pipelines Table Row', () => {
describe('stages column', () => {
beforeEach(() => {
- component = buildComponent(pipeline);
+ wrapper = createWrapper(pipeline);
});
it('should render an icon for each stage', () => {
expect(
- component.$el.querySelectorAll('.table-section:nth-child(4) .js-builds-dropdown-button')
- .length,
+ wrapper.findAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
).toEqual(pipeline.details.stages.length);
});
});
@@ -172,44 +177,49 @@ describe('Pipelines Table Row', () => {
withActions.cancel_path = '/cancel';
withActions.retry_path = '/retry';
- component = buildComponent(withActions);
+ wrapper = createWrapper(withActions);
});
it('should render the provided actions', () => {
- expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull();
- expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull();
- const dropdownMenu = component.$el.querySelectorAll('.dropdown-menu');
+ expect(wrapper.find('.js-pipelines-retry-button').exists()).toBe(true);
+ expect(wrapper.find('.js-pipelines-cancel-button').exists()).toBe(true);
+ const dropdownMenu = wrapper.find('.dropdown-menu');
- expect(dropdownMenu).toContainText(scheduledJobAction.name);
+ expect(dropdownMenu.text()).toContain(scheduledJobAction.name);
});
it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => {
eventHub.$on('retryPipeline', endpoint => {
- expect(endpoint).toEqual('/retry');
+ expect(endpoint).toBe('/retry');
});
- component.$el.querySelector('.js-pipelines-retry-button').click();
-
- expect(component.isRetrying).toEqual(true);
+ wrapper.find('.js-pipelines-retry-button').trigger('click');
+ expect(wrapper.vm.isRetrying).toBe(true);
});
it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
eventHub.$once('openConfirmationModal', data => {
const { id, ref, commit } = pipeline;
- expect(data.endpoint).toEqual('/cancel');
- expect(data.pipeline).toEqual(jasmine.objectContaining({ id, ref, commit }));
+ expect(data.endpoint).toBe('/cancel');
+ expect(data.pipeline).toEqual(
+ expect.objectContaining({
+ id,
+ ref,
+ commit,
+ }),
+ );
});
- component.$el.querySelector('.js-pipelines-cancel-button').click();
+ wrapper.find('.js-pipelines-cancel-button').trigger('click');
});
it('renders a loading icon when `cancelingPipeline` matches pipeline id', done => {
- component.cancelingPipeline = pipeline.id;
- component
+ wrapper.setProps({ cancelingPipeline: pipeline.id });
+ wrapper.vm
.$nextTick()
.then(() => {
- expect(component.isCancelling).toEqual(true);
+ expect(wrapper.vm.isCancelling).toBe(true);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/pipelines/test_reports/mock_data.js
new file mode 100644
index 00000000000..b0f22bc63fb
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/mock_data.js
@@ -0,0 +1,123 @@
+import { formatTime } from '~/lib/utils/datetime_utility';
+import { TestStatus } from '~/pipelines/constants';
+
+export const testCases = [
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000748,
+ name: 'Test#subtract when a is 1 and b is 2 raises an error',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.000064,
+ name: 'Test#subtract when a is 2 and b is 1 returns correct result',
+ stack_trace: null,
+ status: TestStatus.SUCCESS,
+ system_output: null,
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.009292,
+ name: 'Test#sum when a is 1 and b is 2 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ 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 <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0.00018,
+ name: 'Test#sum when a is 100 and b is 200 returns summary',
+ stack_trace: null,
+ status: TestStatus.FAILED,
+ system_output:
+ "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in <top (required)>'",
+ },
+ {
+ classname: 'spec.test_spec',
+ execution_time: 0,
+ name: 'Test#skipped text',
+ stack_trace: null,
+ status: TestStatus.SKIPPED,
+ system_output: null,
+ },
+];
+
+export const testCasesFormatted = [
+ {
+ ...testCases[2],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[0].execution_time * 1000),
+ },
+ {
+ ...testCases[3],
+ icon: 'status_failed_borderless',
+ formattedTime: formatTime(testCases[1].execution_time * 1000),
+ },
+ {
+ ...testCases[4],
+ icon: 'status_skipped_borderless',
+ formattedTime: formatTime(testCases[2].execution_time * 1000),
+ },
+ {
+ ...testCases[0],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[3].execution_time * 1000),
+ },
+ {
+ ...testCases[1],
+ icon: 'status_success_borderless',
+ formattedTime: formatTime(testCases[4].execution_time * 1000),
+ },
+];
+
+export const testSuites = [
+ {
+ error_count: 0,
+ failed_count: 2,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 2,
+ test_cases: testCases,
+ total_count: 4,
+ total_time: 60,
+ },
+ {
+ error_count: 0,
+ failed_count: 10,
+ name: 'rspec:osx',
+ skipped_count: 0,
+ success_count: 50,
+ test_cases: [],
+ total_count: 60,
+ total_time: 0.010284,
+ },
+];
+
+export const testSuitesFormatted = testSuites.map(x => ({
+ ...x,
+ formattedTime: formatTime(x.total_time * 1000),
+}));
+
+export const testReports = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: testSuites,
+ total_count: 4,
+ total_time: 0.010284,
+};
+
+export const testReportsWithNoSuites = {
+ error_count: 0,
+ failed_count: 2,
+ skipped_count: 0,
+ success_count: 2,
+ test_suites: [],
+ total_count: 4,
+ total_time: 0.010284,
+};
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
new file mode 100644
index 00000000000..c1721e12234
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -0,0 +1,109 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import { TEST_HOST } from '../../../helpers/test_constants';
+import testAction from '../../../helpers/vuex_action_helper';
+import createFlash from '~/flash';
+import { testReports } from '../mock_data';
+
+jest.mock('~/flash.js');
+
+describe('Actions TestReports Store', () => {
+ let mock;
+ let state;
+
+ const endpoint = `${TEST_HOST}/test_reports.json`;
+ const defaultState = {
+ endpoint,
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = defaultState;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('fetch reports', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {});
+ });
+
+ it('sets testReports and shows tests', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ state,
+ [{ type: types.SET_REPORTS, payload: testReports }],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchReports,
+ null,
+ {
+ endpoint: null,
+ },
+ [],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('set selected suite', () => {
+ const selectedSuite = testReports.test_suites[0];
+
+ it('sets selectedSuite', done => {
+ testAction(
+ actions.setSelectedSuite,
+ selectedSuite,
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: selectedSuite }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('remove selected suite', () => {
+ it('sets selectedSuite to {}', done => {
+ testAction(
+ actions.removeSelectedSuite,
+ {},
+ state,
+ [{ type: types.SET_SELECTED_SUITE, payload: {} }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('toggles loading', () => {
+ it('sets isLoading to true', done => {
+ testAction(actions.toggleLoading, {}, state, [{ type: types.TOGGLE_LOADING }], [], done);
+ });
+
+ it('toggles isLoading to false', done => {
+ testAction(
+ actions.toggleLoading,
+ {},
+ { ...state, isLoading: true },
+ [{ type: types.TOGGLE_LOADING }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
new file mode 100644
index 00000000000..e630a005409
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/getters_spec.js
@@ -0,0 +1,54 @@
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { testReports, testSuitesFormatted, testCasesFormatted } from '../mock_data';
+
+describe('Getters TestReports Store', () => {
+ let state;
+
+ const defaultState = {
+ testReports,
+ selectedSuite: testReports.test_suites[0],
+ };
+
+ const emptyState = {
+ testReports: {},
+ selectedSuite: {},
+ };
+
+ beforeEach(() => {
+ state = {
+ testReports,
+ };
+ });
+
+ const setupState = (testState = defaultState) => {
+ state = testState;
+ };
+
+ describe('getTestSuites', () => {
+ it('should return the test suites', () => {
+ setupState();
+
+ expect(getters.getTestSuites(state)).toEqual(testSuitesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getTestSuites(state)).toEqual([]);
+ });
+ });
+
+ describe('getSuiteTests', () => {
+ it('should return the test cases inside the suite', () => {
+ setupState();
+
+ expect(getters.getSuiteTests(state)).toEqual(testCasesFormatted);
+ });
+
+ it('should return an empty array when testReports is empty', () => {
+ setupState(emptyState);
+
+ expect(getters.getSuiteTests(state)).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
new file mode 100644
index 00000000000..ad5b7f91163
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -0,0 +1,63 @@
+import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import mutations from '~/pipelines/stores/test_reports/mutations';
+import { testReports, testSuites } from '../mock_data';
+
+describe('Mutations TestReports Store', () => {
+ let mockState;
+
+ const defaultState = {
+ endpoint: '',
+ testReports: {},
+ selectedSuite: {},
+ isLoading: false,
+ };
+
+ beforeEach(() => {
+ mockState = defaultState;
+ });
+
+ describe('set endpoint', () => {
+ it('should set endpoint', () => {
+ const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ mutations[types.SET_ENDPOINT](mockState, 'foo');
+
+ expect(mockState.endpoint).toEqual(expectedState.endpoint);
+ });
+ });
+
+ describe('set reports', () => {
+ it('should set testReports', () => {
+ const expectedState = Object.assign({}, mockState, { testReports });
+ mutations[types.SET_REPORTS](mockState, testReports);
+
+ expect(mockState.testReports).toEqual(expectedState.testReports);
+ });
+ });
+
+ describe('set selected suite', () => {
+ it('should set selectedSuite', () => {
+ const expectedState = Object.assign({}, mockState, { selectedSuite: testSuites[0] });
+ mutations[types.SET_SELECTED_SUITE](mockState, testSuites[0]);
+
+ expect(mockState.selectedSuite).toEqual(expectedState.selectedSuite);
+ });
+ });
+
+ describe('toggle loading', () => {
+ it('should set to true', () => {
+ const expectedState = Object.assign({}, 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 });
+ mockState.isLoading = true;
+
+ mutations[types.TOGGLE_LOADING](mockState);
+
+ expect(mockState.isLoading).toEqual(expectedState.isLoading);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
new file mode 100644
index 00000000000..4d6422745a9
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -0,0 +1,64 @@
+import Vuex from 'vuex';
+import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
+import { shallowMount } from '@vue/test-utils';
+import { testReports } from './mock_data';
+import * as actions from '~/pipelines/stores/test_reports/actions';
+
+describe('Test reports app', () => {
+ let wrapper;
+ let store;
+
+ const loadingSpinner = () => wrapper.find('.js-loading-spinner');
+ const testsDetail = () => wrapper.find('.js-tests-detail');
+ const noTestsToShow = () => wrapper.find('.js-no-tests-to-show');
+
+ const createComponent = (state = {}) => {
+ store = new Vuex.Store({
+ state: {
+ isLoading: false,
+ selectedSuite: {},
+ testReports,
+ ...state,
+ },
+ actions,
+ });
+
+ wrapper = shallowMount(TestReports, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => createComponent({ isLoading: true }));
+
+ it('shows the loading spinner', () => {
+ expect(noTestsToShow().exists()).toBe(false);
+ expect(testsDetail().exists()).toBe(false);
+ expect(loadingSpinner().exists()).toBe(true);
+ });
+ });
+
+ describe('when the api returns no data', () => {
+ beforeEach(() => createComponent({ testReports: {} }));
+
+ it('displays that there are no tests to show', () => {
+ const noTests = noTestsToShow();
+
+ expect(noTests.exists()).toBe(true);
+ expect(noTests.text()).toBe('There are no tests to show.');
+ });
+ });
+
+ describe('when the api returns data', () => {
+ beforeEach(() => createComponent());
+
+ it('sets testReports and shows tests', () => {
+ expect(wrapper.vm.testReports).toBeTruthy();
+ expect(wrapper.vm.showTests).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
new file mode 100644
index 00000000000..b4305719ea8
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -0,0 +1,77 @@
+import Vuex from 'vuex';
+import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { TestStatus } from '~/pipelines/constants';
+import { shallowMount } from '@vue/test-utils';
+import { testSuites, testCases } from './mock_data';
+
+describe('Test reports suite table', () => {
+ let wrapper;
+ let store;
+
+ const noCasesMessage = () => wrapper.find('.js-no-test-cases');
+ const allCaseRows = () => wrapper.findAll('.js-case-row');
+ const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
+ const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
+
+ const createComponent = (suite = testSuites[0]) => {
+ store = new Vuex.Store({
+ state: {
+ selectedSuite: suite,
+ },
+ getters,
+ });
+
+ wrapper = shallowMount(SuiteTable, {
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('should not render', () => {
+ beforeEach(() => createComponent([]));
+
+ it('a table when there are no test cases', () => {
+ expect(noCasesMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('when a test suite is supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(allCaseRows().length).toBe(testCases.length);
+ });
+
+ it('renders the failed tests first', () => {
+ const failedCaseNames = testCases
+ .filter(x => x.status === TestStatus.FAILED)
+ .map(x => x.name);
+
+ const skippedCaseNames = testCases
+ .filter(x => x.status === TestStatus.SKIPPED)
+ .map(x => x.name);
+
+ expect(findCaseRowAtIndex(0).text()).toContain(failedCaseNames[0]);
+ expect(findCaseRowAtIndex(1).text()).toContain(failedCaseNames[1]);
+ expect(findCaseRowAtIndex(2).text()).toContain(skippedCaseNames[0]);
+ });
+
+ it('renders the correct icon for each status', () => {
+ const failedTest = testCases.findIndex(x => x.status === TestStatus.FAILED);
+ const skippedTest = testCases.findIndex(x => x.status === TestStatus.SKIPPED);
+ const successTest = testCases.findIndex(x => x.status === TestStatus.SUCCESS);
+
+ const failedRow = findCaseRowAtIndex(failedTest);
+ const skippedRow = findCaseRowAtIndex(skippedTest);
+ const successRow = findCaseRowAtIndex(successTest);
+
+ expect(findIconForRow(failedRow, TestStatus.FAILED).exists()).toBe(true);
+ expect(findIconForRow(skippedRow, TestStatus.SKIPPED).exists()).toBe(true);
+ expect(findIconForRow(successRow, TestStatus.SUCCESS).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
new file mode 100644
index 00000000000..19a7755dbdc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -0,0 +1,78 @@
+import Summary from '~/pipelines/components/test_reports/test_summary.vue';
+import { mount } from '@vue/test-utils';
+import { testSuites } from './mock_data';
+
+describe('Test reports summary', () => {
+ let wrapper;
+
+ const backButton = () => wrapper.find('.js-back-button');
+ const totalTests = () => wrapper.find('.js-total-tests');
+ const failedTests = () => wrapper.find('.js-failed-tests');
+ const erroredTests = () => wrapper.find('.js-errored-tests');
+ const successRate = () => wrapper.find('.js-success-rate');
+ const duration = () => wrapper.find('.js-duration');
+
+ const defaultProps = {
+ report: testSuites[0],
+ showBack: false,
+ };
+
+ const createComponent = props => {
+ wrapper = mount(Summary, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('should not render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button by default', () => {
+ expect(backButton().exists()).toBe(false);
+ });
+ });
+
+ describe('should render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('a back button and emit on-back-click event', () => {
+ createComponent({
+ showBack: true,
+ });
+
+ expect(backButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when a report is supplied', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays the correct total', () => {
+ expect(totalTests().text()).toBe('4 jobs');
+ });
+
+ it('displays the correct failure count', () => {
+ expect(failedTests().text()).toBe('2 failures');
+ });
+
+ it('displays the correct error count', () => {
+ expect(erroredTests().text()).toBe('0 errors');
+ });
+
+ it('calculates and displays percentages correctly', () => {
+ expect(successRate().text()).toBe('50% success rate');
+ });
+
+ it('displays the correctly formatted duration', () => {
+ expect(duration().text()).toBe('00:01:00');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
new file mode 100644
index 00000000000..e7599d5cdbc
--- /dev/null
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -0,0 +1,54 @@
+import Vuex from 'vuex';
+import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
+import * as getters from '~/pipelines/stores/test_reports/getters';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { testReports, testReportsWithNoSuites } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Test reports summary table', () => {
+ let wrapper;
+ let store;
+
+ const allSuitesRows = () => wrapper.findAll('.js-suite-row');
+ const noSuitesToShow = () => wrapper.find('.js-no-tests-suites');
+
+ const defaultProps = {
+ testReports,
+ };
+
+ const createComponent = (reports = null) => {
+ store = new Vuex.Store({
+ state: {
+ testReports: reports || testReports,
+ },
+ getters,
+ });
+
+ wrapper = mount(SummaryTable, {
+ propsData: defaultProps,
+ store,
+ localVue,
+ });
+ };
+
+ describe('when test reports are supplied', () => {
+ beforeEach(() => createComponent());
+
+ it('renders the correct number of rows', () => {
+ expect(noSuitesToShow().exists()).toBe(false);
+ expect(allSuitesRows().length).toBe(testReports.test_suites.length);
+ });
+ });
+
+ describe('when there are no test suites', () => {
+ beforeEach(() => {
+ createComponent({ testReportsWithNoSuites });
+ });
+
+ it('displays the no suites to show message', () => {
+ expect(noSuitesToShow().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js
index 8102033139f..e60f9f62747 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/project_find_file_spec.js
@@ -3,6 +3,9 @@ import $ from 'jquery';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
import { TEST_HOST } from 'helpers/test_constants';
+import sanitize from 'sanitize-html';
+
+jest.mock('sanitize-html', () => jest.fn(val => val));
const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
@@ -38,31 +41,31 @@ describe('ProjectFindFile', () => {
href: el.querySelector('a').href,
}));
+ const files = [
+ 'fileA.txt',
+ 'fileB.txt',
+ 'fi#leC.txt',
+ 'folderA/fileD.txt',
+ 'folder#B/fileE.txt',
+ 'folde?rC/fil#F.txt',
+ ];
+
beforeEach(() => {
// Create a mock adapter for stubbing axios API requests
mock = new MockAdapter(axios);
element = $(TEMPLATE);
+ mock.onGet(FILE_FIND_URL).replyOnce(200, files);
+ getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
});
afterEach(() => {
// Reset the mock adapter
mock.restore();
+ sanitize.mockClear();
});
it('loads and renders elements from remote server', done => {
- const files = [
- 'fileA.txt',
- 'fileB.txt',
- 'fi#leC.txt',
- 'folderA/fileD.txt',
- 'folder#B/fileE.txt',
- 'folde?rC/fil#F.txt',
- ];
- mock.onGet(FILE_FIND_URL).replyOnce(200, files);
-
- getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
-
setImmediate(() => {
expect(findFiles()).toEqual(
files.map(text => ({
@@ -74,4 +77,14 @@ describe('ProjectFindFile', () => {
done();
});
});
+
+ it('sanitizes search text', done => {
+ const searchText = element.find('.file-finder-input').val();
+
+ setImmediate(() => {
+ expect(sanitize).toHaveBeenCalledTimes(1);
+ expect(sanitize).toHaveBeenCalledWith(searchText);
+ done();
+ });
+ });
});
diff --git a/spec/frontend/registry/components/collapsible_container_spec.js b/spec/frontend/registry/components/collapsible_container_spec.js
index f93ebab1a4d..d035055afd3 100644
--- a/spec/frontend/registry/components/collapsible_container_spec.js
+++ b/spec/frontend/registry/components/collapsible_container_spec.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import collapsibleComponent from '~/registry/components/collapsible_container.vue';
-import { repoPropsData } from '../mock_data';
import createFlash from '~/flash';
+import Tracking from '~/tracking';
+import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import * as getters from '~/registry/stores/getters';
+import { repoPropsData } from '../mock_data';
jest.mock('~/flash.js');
@@ -16,9 +17,10 @@ describe('collapsible registry container', () => {
let wrapper;
let store;
- const findDeleteBtn = w => w.find('.js-remove-repo');
- const findContainerImageTags = w => w.find('.container-image-tags');
- const findToggleRepos = w => w.findAll('.js-toggle-repo');
+ const findDeleteBtn = (w = wrapper) => w.find('.js-remove-repo');
+ const findContainerImageTags = (w = wrapper) => w.find('.container-image-tags');
+ const findToggleRepos = (w = wrapper) => w.findAll('.js-toggle-repo');
+ const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
const mountWithStore = config => mount(collapsibleComponent, { ...config, store, localVue });
@@ -124,4 +126,45 @@ describe('collapsible registry container', () => {
expect(deleteBtn.exists()).toBe(false);
});
});
+
+ describe('tracking', () => {
+ const category = 'mock_page';
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ wrapper.vm.deleteItem = jest.fn().mockResolvedValue();
+ wrapper.vm.fetchRepos = jest.fn();
+ wrapper.setData({
+ tracking: {
+ ...wrapper.vm.tracking,
+ category,
+ },
+ });
+ });
+
+ it('send an event when delete button is clicked', () => {
+ const deleteBtn = findDeleteBtn();
+ deleteBtn.trigger('click');
+ expect(Tracking.event).toHaveBeenCalledWith(category, 'click_button', {
+ label: 'registry_repository_delete',
+ category,
+ });
+ });
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ expect(Tracking.event).toHaveBeenCalledWith(category, 'cancel_delete', {
+ label: 'registry_repository_delete',
+ category,
+ });
+ });
+ it('send an event when confirm is clicked on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+
+ expect(Tracking.event).toHaveBeenCalledWith(category, 'confirm_delete', {
+ label: 'registry_repository_delete',
+ category,
+ });
+ });
+ });
});
diff --git a/spec/frontend/registry/components/table_registry_spec.js b/spec/frontend/registry/components/table_registry_spec.js
index 7cb7c012d9d..ab88caf44e1 100644
--- a/spec/frontend/registry/components/table_registry_spec.js
+++ b/spec/frontend/registry/components/table_registry_spec.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import tableRegistry from '~/registry/components/table_registry.vue';
import { mount, createLocalVue } from '@vue/test-utils';
+import createFlash from '~/flash';
+import Tracking from '~/tracking';
+import tableRegistry from '~/registry/components/table_registry.vue';
import { repoPropsData } from '../mock_data';
import * as getters from '~/registry/stores/getters';
+jest.mock('~/flash');
+
const [firstImage, secondImage] = repoPropsData.list;
const localVue = createLocalVue();
@@ -15,11 +19,12 @@ describe('table registry', () => {
let wrapper;
let store;
- const findSelectAllCheckbox = w => w.find('.js-select-all-checkbox > input');
- const findSelectCheckboxes = w => w.findAll('.js-select-checkbox > input');
- const findDeleteButton = w => w.find('.js-delete-registry');
- const findDeleteButtonsRow = w => w.findAll('.js-delete-registry-row');
- const findPagination = w => w.find('.js-registry-pagination');
+ const findSelectAllCheckbox = (w = wrapper) => w.find('.js-select-all-checkbox > input');
+ const findSelectCheckboxes = (w = wrapper) => w.findAll('.js-select-checkbox > input');
+ const findDeleteButton = (w = wrapper) => w.find({ ref: 'bulkDeleteButton' });
+ const findDeleteButtonsRow = (w = wrapper) => w.findAll('.js-delete-registry-row');
+ const findPagination = (w = wrapper) => w.find('.js-registry-pagination');
+ const findDeleteModal = (w = wrapper) => w.find({ ref: 'deleteModal' });
const bulkDeletePath = 'path';
const mountWithStore = config => mount(tableRegistry, { ...config, store, localVue });
@@ -139,7 +144,7 @@ describe('table registry', () => {
},
});
wrapper.vm.handleMultipleDelete();
- expect(wrapper.vm.showError).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalled();
});
});
@@ -169,6 +174,27 @@ describe('table registry', () => {
});
});
+ describe('modal event handlers', () => {
+ beforeEach(() => {
+ wrapper.vm.handleSingleDelete = jest.fn();
+ wrapper.vm.handleMultipleDelete = jest.fn();
+ });
+ it('on ok when one item is selected should call singleDelete', () => {
+ wrapper.setData({ itemsToBeDeleted: [0] });
+ wrapper.vm.onDeletionConfirmed();
+
+ expect(wrapper.vm.handleSingleDelete).toHaveBeenCalledWith(repoPropsData.list[0]);
+ expect(wrapper.vm.handleMultipleDelete).not.toHaveBeenCalled();
+ });
+ it('on ok when multiple items are selected should call muultiDelete', () => {
+ wrapper.setData({ itemsToBeDeleted: [0, 1, 2] });
+ wrapper.vm.onDeletionConfirmed();
+
+ expect(wrapper.vm.handleMultipleDelete).toHaveBeenCalled();
+ expect(wrapper.vm.handleSingleDelete).not.toHaveBeenCalled();
+ });
+ });
+
describe('pagination', () => {
const repo = {
repoPropsData,
@@ -265,4 +291,83 @@ describe('table registry', () => {
expect(deleteBtns.length).toBe(0);
});
});
+
+ describe('event tracking', () => {
+ const mockPageName = 'mock_page';
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ wrapper.vm.handleSingleDelete = jest.fn();
+ wrapper.vm.handleMultipleDelete = jest.fn();
+ document.body.dataset.page = mockPageName;
+ });
+
+ afterEach(() => {
+ document.body.dataset.page = null;
+ });
+
+ describe('single tag delete', () => {
+ beforeEach(() => {
+ wrapper.setData({ itemsToBeDeleted: [0] });
+ });
+
+ it('send an event when delete button is clicked', () => {
+ const deleteBtn = findDeleteButtonsRow();
+ deleteBtn.at(0).trigger('click');
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
+ label: 'registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
+ label: 'registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ it('send an event when confirm is clicked on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
+ label: 'registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ });
+ describe('bulk tag delete', () => {
+ beforeEach(() => {
+ const items = [0, 1, 2];
+ wrapper.setData({ itemsToBeDeleted: items, selectedItems: items });
+ });
+
+ it('send an event when delete button is clicked', () => {
+ const deleteBtn = findDeleteButton();
+ deleteBtn.vm.$emit('click');
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'click_button', {
+ label: 'bulk_registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'cancel_delete', {
+ label: 'bulk_registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ it('send an event when confirm is clicked on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+
+ expect(Tracking.event).toHaveBeenCalledWith(mockPageName, 'confirm_delete', {
+ label: 'bulk_registry_tag_delete',
+ property: 'foo',
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/releases/detail/components/app_spec.js b/spec/frontend/releases/detail/components/app_spec.js
index f8eb33a69a8..4726f18c8fa 100644
--- a/spec/frontend/releases/detail/components/app_spec.js
+++ b/spec/frontend/releases/detail/components/app_spec.js
@@ -8,15 +8,17 @@ describe('Release detail component', () => {
let wrapper;
let releaseClone;
let actions;
+ let state;
beforeEach(() => {
gon.api_version = 'v4';
releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release)));
- const state = {
+ state = {
release: releaseClone,
markdownDocsPath: 'path/to/markdown/docs',
+ updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
};
actions = {
@@ -46,6 +48,21 @@ describe('Release detail component', () => {
expect(wrapper.find('#git-ref').element.value).toBe(releaseClone.tagName);
});
+ it('renders the correct help text under the "Tag name" field', () => {
+ const helperText = wrapper.find('#tag-name-help');
+ const helperTextLink = helperText.find('a');
+ const helperTextLinkAttrs = helperTextLink.attributes();
+
+ expect(helperText.text()).toBe(
+ 'Changing a Release tag is only supported via Releases API. More information',
+ );
+ expect(helperTextLink.text()).toBe('More information');
+ expect(helperTextLinkAttrs.href).toBe(state.updateReleaseApiDocsPath);
+ expect(helperTextLinkAttrs.rel).toContain('noopener');
+ expect(helperTextLinkAttrs.rel).toContain('noreferrer');
+ expect(helperTextLinkAttrs.target).toBe('_blank');
+ });
+
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(releaseClone.name);
});
diff --git a/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap b/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
deleted file mode 100644
index 8f2c0427c83..00000000000
--- a/spec/frontend/releases/list/components/__snapshots__/release_block_spec.js.snap
+++ /dev/null
@@ -1,332 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Release block with default props matches the snapshot 1`] = `
-<div
- class="card release-block"
- id="v0.3"
->
- <div
- class="card-body"
- >
- <div
- class="d-flex align-items-start"
- >
- <h2
- class="card-title mt-0 mr-auto"
- >
-
- New release
-
- <!---->
- </h2>
-
- <a
- class="btn btn-default js-edit-button ml-2"
- data-original-title="Edit this release"
- href="http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit"
- title=""
- >
- <svg
- aria-hidden="true"
- class="s16 ic-pencil"
- >
- <use
- xlink:href="#pencil"
- />
- </svg>
- </a>
- </div>
-
- <div
- class="card-subtitle d-flex flex-wrap text-secondary"
- >
- <div
- class="append-right-8"
- >
- <svg
- aria-hidden="true"
- class="align-middle s16 ic-commit"
- >
- <use
- xlink:href="#commit"
- />
- </svg>
-
- <span
- data-original-title="Initial commit"
- title=""
- >
- c22b0728
- </span>
- </div>
-
- <div
- class="append-right-8"
- >
- <svg
- aria-hidden="true"
- class="align-middle s16 ic-tag"
- >
- <use
- xlink:href="#tag"
- />
- </svg>
-
- <span
- data-original-title="Tag"
- title=""
- >
- v0.3
- </span>
- </div>
-
- <div
- class="js-milestone-list-label"
- >
- <svg
- aria-hidden="true"
- class="align-middle s16 ic-flag"
- >
- <use
- xlink:href="#flag"
- />
- </svg>
-
- <span
- class="js-label-text"
- >
- Milestones
- </span>
- </div>
-
- <a
- class="append-right-4 prepend-left-4 js-milestone-link"
- data-original-title="The 13.6 milestone!"
- href="http://0.0.0.0:3001/root/release-test/-/milestones/2"
- title=""
- >
-
- 13.6
-
- </a>
-
- •
-
- <a
- class="append-right-4 prepend-left-4 js-milestone-link"
- data-original-title="The 13.5 milestone!"
- href="http://0.0.0.0:3001/root/release-test/-/milestones/1"
- title=""
- >
-
- 13.5
-
- </a>
-
- <!---->
-
- <div
- class="append-right-4"
- >
-
- •
-
- <span
- data-original-title="Aug 26, 2019 5:54pm GMT+0000"
- title=""
- >
-
- released 1 month ago
-
- </span>
- </div>
-
- <div
- class="d-flex"
- >
-
- by
-
- <a
- class="user-avatar-link prepend-left-4"
- href=""
- >
- <span>
- <img
- alt="root's avatar"
- class="avatar s20 "
- data-original-title=""
- data-src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
- height="20"
- src="https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
- title=""
- width="20"
- />
-
- <div
- aria-hidden="true"
- class="js-user-avatar-image-toolip d-none"
- style="display: none;"
- >
- <div>
- root
- </div>
- </div>
- </span>
- <!---->
- </a>
- </div>
- </div>
-
- <div
- class="card-text prepend-top-default"
- >
- <b>
-
- Assets
-
- <span
- class="js-assets-count badge badge-pill"
- >
- 5
- </span>
- </b>
-
- <ul
- class="pl-0 mb-0 prepend-top-8 list-unstyled js-assets-list"
- >
- <li
- class="append-bottom-8"
- >
- <a
- class=""
- data-original-title="Download asset"
- href="https://google.com"
- title=""
- >
- <svg
- aria-hidden="true"
- class="align-middle append-right-4 align-text-bottom s16 ic-package"
- >
- <use
- xlink:href="#package"
- />
- </svg>
-
- my link
-
- <span>
- (external source)
- </span>
- </a>
- </li>
- <li
- class="append-bottom-8"
- >
- <a
- class=""
- data-original-title="Download asset"
- href="https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50"
- title=""
- >
- <svg
- aria-hidden="true"
- class="align-middle append-right-4 align-text-bottom s16 ic-package"
- >
- <use
- xlink:href="#package"
- />
- </svg>
-
- my second link
-
- <!---->
- </a>
- </li>
- </ul>
-
- <div
- class="dropdown"
- >
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn btn-link"
- data-toggle="dropdown"
- type="button"
- >
- <svg
- aria-hidden="true"
- class="align-top append-right-4 s16 ic-doc-code"
- >
- <use
- xlink:href="#doc-code"
- />
- </svg>
-
- Source code
-
- <svg
- aria-hidden="true"
- class="s16 ic-arrow-down"
- >
- <use
- xlink:href="#arrow-down"
- />
- </svg>
- </button>
-
- <div
- class="js-sources-dropdown dropdown-menu"
- >
- <li>
- <a
- class=""
- href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip"
- >
- Download zip
- </a>
- </li>
- <li>
- <a
- class=""
- href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz"
- >
- Download tar.gz
- </a>
- </li>
- <li>
- <a
- class=""
- href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2"
- >
- Download tar.bz2
- </a>
- </li>
- <li>
- <a
- class=""
- href="http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar"
- >
- Download tar
- </a>
- </li>
- </div>
- </div>
- </div>
-
- <div
- class="card-text prepend-top-default"
- >
- <div>
- <p
- data-sourcepos="1:1-1:21"
- dir="auto"
- >
- A super nice release!
- </p>
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/releases/list/components/release_block_footer_spec.js b/spec/frontend/releases/list/components/release_block_footer_spec.js
new file mode 100644
index 00000000000..172147f1cc8
--- /dev/null
+++ b/spec/frontend/releases/list/components/release_block_footer_spec.js
@@ -0,0 +1,163 @@
+import { mount } from '@vue/test-utils';
+import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { GlLink } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
+import { release } from '../../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+jest.mock('~/vue_shared/mixins/timeago', () => ({
+ methods: {
+ timeFormated() {
+ return '7 fortnightes ago';
+ },
+ tooltipTitle() {
+ return 'February 30, 2401';
+ },
+ },
+}));
+
+describe('Release block footer', () => {
+ let wrapper;
+ let releaseClone;
+
+ const factory = (props = {}) => {
+ wrapper = mount(ReleaseBlockFooter, {
+ propsData: {
+ ...convertObjectPropsToCamelCase(releaseClone),
+ ...props,
+ },
+ sync: false,
+ });
+
+ return wrapper.vm.$nextTick();
+ };
+
+ beforeEach(() => {
+ releaseClone = JSON.parse(JSON.stringify(release));
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const commitInfoSection = () => wrapper.find('.js-commit-info');
+ const commitInfoSectionLink = () => commitInfoSection().find(GlLink);
+ const tagInfoSection = () => wrapper.find('.js-tag-info');
+ const tagInfoSectionLink = () => tagInfoSection().find(GlLink);
+ const authorDateInfoSection = () => wrapper.find('.js-author-date-info');
+
+ describe('with all props provided', () => {
+ beforeEach(() => factory());
+
+ it('renders the commit icon', () => {
+ const commitIcon = commitInfoSection().find(Icon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('commit');
+ });
+
+ it('renders the commit SHA with a link', () => {
+ const commitLink = commitInfoSectionLink();
+
+ expect(commitLink.exists()).toBe(true);
+ expect(commitLink.text()).toBe(releaseClone.commit.short_id);
+ expect(commitLink.attributes('href')).toBe(releaseClone.commit_path);
+ });
+
+ it('renders the tag icon', () => {
+ const commitIcon = tagInfoSection().find(Icon);
+
+ expect(commitIcon.exists()).toBe(true);
+ expect(commitIcon.props('name')).toBe('tag');
+ });
+
+ it('renders the tag name with a link', () => {
+ 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);
+ });
+
+ it('renders the author and creation time info', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(
+ `Created 7 fortnightes ago by ${releaseClone.author.username}`,
+ );
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+ });
+
+ describe('without any commit info', () => {
+ beforeEach(() => factory({ commit: undefined }));
+
+ it('does not render any commit info', () => {
+ expect(commitInfoSection().exists()).toBe(false);
+ });
+ });
+
+ describe('without a commit URL', () => {
+ beforeEach(() => factory({ commitPath: undefined }));
+
+ 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);
+ });
+ });
+
+ describe('without a tag name', () => {
+ beforeEach(() => factory({ tagName: undefined }));
+
+ it('does not render any tag info', () => {
+ expect(tagInfoSection().exists()).toBe(false);
+ });
+ });
+
+ describe('without a tag URL', () => {
+ beforeEach(() => factory({ tagPath: undefined }));
+
+ it('renders the tag name as plain text (instead of a link)', () => {
+ expect(tagInfoSectionLink().exists()).toBe(false);
+ expect(tagInfoSection().text()).toBe(releaseClone.tag_name);
+ });
+ });
+
+ describe('without any author info', () => {
+ beforeEach(() => factory({ author: undefined }));
+
+ it('renders the release date without the author name', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnightes ago');
+ });
+ });
+
+ describe('without a released at date', () => {
+ beforeEach(() => factory({ releasedAt: undefined }));
+
+ it('renders the author name without the release date', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(
+ `Created by ${releaseClone.author.username}`,
+ );
+ });
+ });
+
+ describe('without a release date or author info', () => {
+ beforeEach(() => factory({ author: undefined, releasedAt: undefined }));
+
+ it('does not render any author or release date info', () => {
+ expect(authorDateInfoSection().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/releases/list/components/release_block_spec.js b/spec/frontend/releases/list/components/release_block_spec.js
index ac51c3af11a..b63ef068d8e 100644
--- a/spec/frontend/releases/list/components/release_block_spec.js
+++ b/spec/frontend/releases/list/components/release_block_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import ReleaseBlock from '~/releases/list/components/release_block.vue';
+import ReleaseBlockFooter from '~/releases/list/components/release_block_footer.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { first } from 'underscore';
import { release } from '../../mock_data';
@@ -21,14 +22,16 @@ describe('Release block', () => {
let wrapper;
let releaseClone;
- const factory = (releaseProp, releaseEditPageFeatureFlag = true) => {
+ const factory = (releaseProp, featureFlags = {}) => {
wrapper = mount(ReleaseBlock, {
propsData: {
release: releaseProp,
},
provide: {
glFeatures: {
- releaseEditPage: releaseEditPageFeatureFlag,
+ releaseEditPage: true,
+ releaseIssueSummary: true,
+ ...featureFlags,
},
},
sync: false,
@@ -39,41 +42,25 @@ describe('Release block', () => {
const milestoneListLabel = () => wrapper.find('.js-milestone-list-label');
const editButton = () => wrapper.find('.js-edit-button');
- const RealDate = Date;
beforeEach(() => {
- // timeago.js calls Date(), so let's mock that case to avoid time-dependent test failures.
- const constantDate = new Date('2019-10-25T00:12:00');
-
- /* eslint no-global-assign:off */
- global.Date = jest.fn((...props) =>
- props.length ? new RealDate(...props) : new RealDate(constantDate),
- );
-
- Object.assign(Date, RealDate);
-
releaseClone = JSON.parse(JSON.stringify(release));
});
afterEach(() => {
wrapper.destroy();
- global.Date = RealDate;
});
describe('with default props', () => {
beforeEach(() => factory(release));
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it("renders the block with an id equal to the release's tag name", () => {
expect(wrapper.attributes().id).toBe('v0.3');
});
it('renders an edit button that links to the "Edit release" page', () => {
expect(editButton().exists()).toBe(true);
- expect(editButton().attributes('href')).toBe(release._links.edit);
+ expect(editButton().attributes('href')).toBe(release._links.edit_url);
});
it('renders release name', () => {
@@ -158,6 +145,10 @@ describe('Release block', () => {
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
});
+
+ it('renders the footer', () => {
+ expect(wrapper.find(ReleaseBlockFooter).exists()).toBe(true);
+ });
});
it('renders commit sha', () => {
@@ -180,7 +171,7 @@ describe('Release block', () => {
});
});
- it("does not render an edit button if release._links.edit isn't a string", () => {
+ it("does not render an edit button if release._links.edit_url isn't a string", () => {
delete releaseClone._links;
return factory(releaseClone).then(() => {
@@ -189,7 +180,7 @@ describe('Release block', () => {
});
it('does not render an edit button if the releaseEditPage feature flag is disabled', () =>
- factory(releaseClone, false).then(() => {
+ factory(releaseClone, { releaseEditPage: false }).then(() => {
expect(editButton().exists()).toBe(false);
}));
diff --git a/spec/frontend/releases/mock_data.js b/spec/frontend/releases/mock_data.js
index b2ebf1174d4..61d95b86b1c 100644
--- a/spec/frontend/releases/mock_data.js
+++ b/spec/frontend/releases/mock_data.js
@@ -30,6 +30,7 @@ export const milestones = [
export const release = {
name: 'New release',
tag_name: 'v0.3',
+ tag_path: '/root/release-test/-/tags/v0.3',
description: 'A super nice release!',
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
@@ -56,6 +57,7 @@ export const release = {
committer_email: 'admin@example.com',
committed_date: '2019-08-26T17:47:07.000Z',
},
+ commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false,
milestones,
assets: {
@@ -95,6 +97,6 @@ export const release = {
],
},
_links: {
- edit: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
+ edit_url: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
},
};
diff --git a/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap b/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap
new file mode 100644
index 00000000000..31a1cd23060
--- /dev/null
+++ b/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Repository directory download links component renders downloads links for path app 1`] = `
+<section
+ class="border-top pt-1 mt-1"
+>
+ <h5
+ class="m-0 dropdown-bold-header"
+ >
+ Download this directory
+ </h5>
+
+ <div
+ class="dropdown-menu-content"
+ >
+ <div
+ class="btn-group ml-0 w-100"
+ >
+ <gllink-stub
+ class="btn btn-xs btn-primary"
+ href="http://test.com/?path=app"
+ >
+
+ zip
+
+ </gllink-stub>
+ <gllink-stub
+ class="btn btn-xs"
+ href="http://test.com/?path=app"
+ >
+
+ tar
+
+ </gllink-stub>
+ </div>
+ </div>
+</section>
+`;
+
+exports[`Repository directory download links component renders downloads links for path app/assets 1`] = `
+<section
+ class="border-top pt-1 mt-1"
+>
+ <h5
+ class="m-0 dropdown-bold-header"
+ >
+ Download this directory
+ </h5>
+
+ <div
+ class="dropdown-menu-content"
+ >
+ <div
+ class="btn-group ml-0 w-100"
+ >
+ <gllink-stub
+ class="btn btn-xs btn-primary"
+ href="http://test.com/?path=app/assets"
+ >
+
+ zip
+
+ </gllink-stub>
+ <gllink-stub
+ class="btn btn-xs"
+ href="http://test.com/?path=app/assets"
+ >
+
+ tar
+
+ </gllink-stub>
+ </div>
+ </div>
+</section>
+`;
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 08173f4f0c4..706c26403c0 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -62,19 +62,23 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<!---->
- <gllink-stub
- class="js-commit-pipeline"
- data-original-title="Commit: failed"
- href="https://test.com/pipeline"
- title=""
+ <div
+ class="ci-status-link"
>
- <ciicon-stub
- aria-label="Commit: failed"
- cssclasses=""
- size="24"
- status="[object Object]"
- />
- </gllink-stub>
+ <gllink-stub
+ class="js-commit-pipeline"
+ data-original-title="Commit: failed"
+ href="https://test.com/pipeline"
+ title=""
+ >
+ <ciicon-stub
+ aria-label="Commit: failed"
+ cssclasses=""
+ size="24"
+ status="[object Object]"
+ />
+ </gllink-stub>
+ </div>
<div
class="commit-sha-group d-flex"
@@ -165,19 +169,23 @@ exports[`Repository last commit component renders the signature HTML as returned
</button>
</div>
- <gllink-stub
- class="js-commit-pipeline"
- data-original-title="Commit: failed"
- href="https://test.com/pipeline"
- title=""
+ <div
+ class="ci-status-link"
>
- <ciicon-stub
- aria-label="Commit: failed"
- cssclasses=""
- size="24"
- status="[object Object]"
- />
- </gllink-stub>
+ <gllink-stub
+ class="js-commit-pipeline"
+ data-original-title="Commit: failed"
+ href="https://test.com/pipeline"
+ title=""
+ >
+ <ciicon-stub
+ aria-label="Commit: failed"
+ cssclasses=""
+ size="24"
+ status="[object Object]"
+ />
+ </gllink-stub>
+ </div>
<div
class="commit-sha-group d-flex"
diff --git a/spec/frontend/repository/components/directory_download_links_spec.js b/spec/frontend/repository/components/directory_download_links_spec.js
new file mode 100644
index 00000000000..4d70b44de08
--- /dev/null
+++ b/spec/frontend/repository/components/directory_download_links_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import DirectoryDownloadLinks from '~/repository/components/directory_download_links.vue';
+
+let vm;
+
+function factory(currentPath) {
+ vm = shallowMount(DirectoryDownloadLinks, {
+ propsData: {
+ currentPath,
+ links: [{ text: 'zip', path: 'http://test.com/' }, { text: 'tar', path: 'http://test.com/' }],
+ },
+ });
+}
+
+describe('Repository directory download links component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path
+ ${'app'}
+ ${'app/assets'}
+ `('renders downloads links for path $path', ({ path }) => {
+ factory(path);
+
+ expect(vm.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 01b56d453e6..e07ad4cf46b 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -17,7 +17,7 @@ function createCommitData(data = {}) {
avatarUrl: 'https://test.com',
webUrl: 'https://test.com/test',
},
- latestPipeline: {
+ pipeline: {
detailedStatus: {
detailsPath: 'https://test.com/pipeline',
icon: 'failed',
@@ -74,7 +74,7 @@ describe('Repository last commit component', () => {
});
it('hides pipeline components when pipeline does not exist', () => {
- factory(createCommitData({ latestPipeline: null }));
+ factory(createCommitData({ pipeline: null }));
expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
new file mode 100644
index 00000000000..a5e3eb4bce1
--- /dev/null
+++ b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
@@ -0,0 +1,36 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Repository file preview component renders file HTML 1`] = `
+<article
+ class="file-holder limited-width-container readme-holder"
+>
+ <div
+ class="file-title"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-file-text-o fa-fw"
+ />
+
+ <gllink-stub
+ href="http://test.com"
+ >
+ <strong>
+ README.md
+ </strong>
+ </gllink-stub>
+ </div>
+
+ <div
+ class="blob-viewer"
+ >
+ <div>
+ <div
+ class="blob"
+ >
+ test
+ </div>
+ </div>
+ </div>
+</article>
+`;
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
new file mode 100644
index 00000000000..0112e6310f4
--- /dev/null
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Preview from '~/repository/components/preview/index.vue';
+
+let vm;
+let $apollo;
+
+function factory(blob) {
+ $apollo = {
+ query: jest.fn().mockReturnValue(Promise.resolve({})),
+ };
+
+ vm = shallowMount(Preview, {
+ propsData: {
+ blob,
+ },
+ mocks: {
+ $apollo,
+ },
+ });
+}
+
+describe('Repository file preview component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders file HTML', () => {
+ factory({
+ webUrl: 'http://test.com',
+ name: 'README.md',
+ });
+
+ vm.setData({ readme: { html: '<div class="blob">test</div>' } });
+
+ expect(vm.element).toMatchSnapshot();
+ });
+
+ it('renders loading icon', () => {
+ factory({
+ webUrl: 'http://test.com',
+ name: 'README.md',
+ });
+
+ vm.setData({ loading: 1 });
+
+ expect(vm.find(GlLoadingIcon).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index d55dc553031..f8e65a51297 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -25,6 +25,8 @@ exports[`Repository table row component renders table row 1`] = `
<!---->
<!---->
+
+ <!---->
</td>
<td
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 827927e6d9a..41450becabb 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -1,18 +1,36 @@
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSkeletonLoading } from '@gitlab/ui';
import Table from '~/repository/components/table/index.vue';
+import TableRow from '~/repository/components/table/row.vue';
let vm;
let $apollo;
-function factory(path, data = () => ({})) {
- $apollo = {
- query: jest.fn().mockReturnValue(Promise.resolve({ data: data() })),
- };
-
+const MOCK_BLOBS = [
+ {
+ id: '123abc',
+ sha: '123abc',
+ flatPath: 'blob',
+ name: 'blob.md',
+ type: 'blob',
+ webUrl: 'http://test.com',
+ },
+ {
+ id: '124abc',
+ sha: '124abc',
+ flatPath: 'blob2',
+ name: 'blob2.md',
+ type: 'blob',
+ webUrl: 'http://test.com',
+ },
+];
+
+function factory({ path, isLoading = false, entries = {} }) {
vm = shallowMount(Table, {
propsData: {
path,
+ isLoading,
+ entries,
},
mocks: {
$apollo,
@@ -31,50 +49,30 @@ describe('Repository table component', () => {
${'app/assets'} | ${'master'}
${'/'} | ${'test'}
`('renders table caption for $ref in $path', ({ path, ref }) => {
- factory(path);
+ factory({ path });
vm.setData({ ref });
- expect(vm.find('caption').text()).toEqual(
+ expect(vm.find('.table').attributes('aria-label')).toEqual(
`Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
);
});
it('shows loading icon', () => {
- factory('/');
-
- vm.setData({ isLoadingFiles: true });
+ factory({ path: '/', isLoading: true });
- expect(vm.find(GlLoadingIcon).isVisible()).toBe(true);
+ expect(vm.find(GlSkeletonLoading).exists()).toBe(true);
});
- describe('normalizeData', () => {
- it('normalizes edge nodes', () => {
- const output = vm.vm.normalizeData('blobs', [{ node: '1' }, { node: '2' }]);
-
- expect(output).toEqual(['1', '2']);
+ it('renders table rows', () => {
+ factory({
+ path: '/',
+ entries: {
+ blobs: MOCK_BLOBS,
+ },
});
- });
-
- describe('hasNextPage', () => {
- it('returns undefined when hasNextPage is false', () => {
- const output = vm.vm.hasNextPage({
- trees: { pageInfo: { hasNextPage: false } },
- submodules: { pageInfo: { hasNextPage: false } },
- blobs: { pageInfo: { hasNextPage: false } },
- });
- expect(output).toBe(undefined);
- });
-
- it('returns pageInfo object when hasNextPage is true', () => {
- const output = vm.vm.hasNextPage({
- trees: { pageInfo: { hasNextPage: false } },
- submodules: { pageInfo: { hasNextPage: false } },
- blobs: { pageInfo: { hasNextPage: true, nextCursor: 'test' } },
- });
-
- expect(output).toEqual({ hasNextPage: true, nextCursor: 'test' });
- });
+ expect(vm.find(TableRow).exists()).toBe(true);
+ expect(vm.findAll(TableRow).length).toBe(2);
});
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index e539c560975..aa0b9385f1a 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -2,6 +2,7 @@ import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import { GlBadge, GlLink } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import TableRow from '~/repository/components/table/row.vue';
+import Icon from '~/vue_shared/components/icon.vue';
jest.mock('~/lib/utils/url_utility');
@@ -40,6 +41,7 @@ describe('Repository table row component', () => {
it('renders table row', () => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type: 'file',
currentPath: '/',
@@ -56,6 +58,7 @@ describe('Repository table row component', () => {
`('renders a $componentName for type $type', ({ type, component }) => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type,
currentPath: '/',
@@ -72,6 +75,7 @@ describe('Repository table row component', () => {
`('pushes new router if type $type is tree', ({ type, pushes }) => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type,
currentPath: '/',
@@ -94,6 +98,7 @@ describe('Repository table row component', () => {
`('calls visitUrl if $type is not tree', ({ type, pushes }) => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type,
currentPath: '/',
@@ -104,13 +109,14 @@ describe('Repository table row component', () => {
if (pushes) {
expect(visitUrl).not.toHaveBeenCalled();
} else {
- expect(visitUrl).toHaveBeenCalledWith('https://test.com');
+ expect(visitUrl).toHaveBeenCalledWith('https://test.com', undefined);
}
});
it('renders commit ID for submodule', () => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type: 'commit',
currentPath: '/',
@@ -122,6 +128,7 @@ describe('Repository table row component', () => {
it('renders link with href', () => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type: 'blob',
url: 'https://test.com',
@@ -134,6 +141,7 @@ describe('Repository table row component', () => {
it('renders LFS badge', () => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type: 'commit',
currentPath: '/',
@@ -146,6 +154,7 @@ describe('Repository table row component', () => {
it('renders commit and web links with href for submodule', () => {
factory({
id: '1',
+ sha: '123',
path: 'test',
type: 'commit',
url: 'https://test.com',
@@ -156,4 +165,18 @@ describe('Repository table row component', () => {
expect(vm.find('a').attributes('href')).toEqual('https://test.com');
expect(vm.find(GlLink).attributes('href')).toEqual('https://test.com/commit');
});
+
+ it('renders lock icon', () => {
+ factory({
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ });
+
+ vm.setData({ commit: { lockLabel: 'Locked by Root', committedDate: '2019-01-01' } });
+
+ expect(vm.find(Icon).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
new file mode 100644
index 00000000000..148e307a5d4
--- /dev/null
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -0,0 +1,71 @@
+import { shallowMount } from '@vue/test-utils';
+import TreeContent from '~/repository/components/tree_content.vue';
+import FilePreview from '~/repository/components/preview/index.vue';
+
+let vm;
+let $apollo;
+
+function factory(path, data = () => ({})) {
+ $apollo = {
+ query: jest.fn().mockReturnValue(Promise.resolve({ data: data() })),
+ };
+
+ vm = shallowMount(TreeContent, {
+ propsData: {
+ path,
+ },
+ mocks: {
+ $apollo,
+ },
+ });
+}
+
+describe('Repository table component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders file preview', () => {
+ factory('/');
+
+ vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
+
+ expect(vm.find(FilePreview).exists()).toBe(true);
+ });
+
+ describe('normalizeData', () => {
+ it('normalizes edge nodes', () => {
+ factory('/');
+
+ const output = vm.vm.normalizeData('blobs', [{ node: '1' }, { node: '2' }]);
+
+ expect(output).toEqual(['1', '2']);
+ });
+ });
+
+ describe('hasNextPage', () => {
+ it('returns undefined when hasNextPage is false', () => {
+ factory('/');
+
+ const output = vm.vm.hasNextPage({
+ trees: { pageInfo: { hasNextPage: false } },
+ submodules: { pageInfo: { hasNextPage: false } },
+ blobs: { pageInfo: { hasNextPage: false } },
+ });
+
+ expect(output).toBe(undefined);
+ });
+
+ it('returns pageInfo object when hasNextPage is true', () => {
+ factory('/');
+
+ const output = vm.vm.hasNextPage({
+ trees: { pageInfo: { hasNextPage: false } },
+ submodules: { pageInfo: { hasNextPage: false } },
+ blobs: { pageInfo: { hasNextPage: true, nextCursor: 'test' } },
+ });
+
+ expect(output).toEqual({ hasNextPage: true, nextCursor: 'test' });
+ });
+ });
+});
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index a3a766eca41..9199c726680 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import { normalizeData, resolveCommit, fetchLogsTree } from '~/repository/log_tree';
+import { resolveCommit, fetchLogsTree } from '~/repository/log_tree';
const mockData = [
{
@@ -15,22 +15,6 @@ const mockData = [
},
];
-describe('normalizeData', () => {
- it('normalizes data into LogTreeCommit object', () => {
- expect(normalizeData(mockData)).toEqual([
- {
- sha: '123',
- message: 'testing message',
- committedDate: '2019-01-01',
- commitPath: 'https://test.com',
- fileName: 'index.js',
- type: 'blob',
- __typename: 'LogTreeCommit',
- },
- ]);
- });
-});
-
describe('resolveCommit', () => {
it('calls resolve when commit found', () => {
const resolver = {
@@ -57,7 +41,7 @@ describe('fetchLogsTree', () => {
jest.spyOn(axios, 'get');
- global.gon = { gitlab_url: 'https://test.com' };
+ global.gon = { relative_url_root: '' };
client = {
readQuery: () => ({
@@ -80,10 +64,9 @@ describe('fetchLogsTree', () => {
it('calls axios get', () =>
fetchLogsTree(client, '', '0', resolver).then(() => {
- expect(axios.get).toHaveBeenCalledWith(
- 'https://test.com/gitlab-org/gitlab-foss/refs/master/logs_tree',
- { params: { format: 'json', offset: '0' } },
- );
+ expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/refs/master/logs_tree/', {
+ params: { format: 'json', offset: '0' },
+ });
}));
it('calls axios get once', () =>
diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js
new file mode 100644
index 00000000000..c0afb7931b1
--- /dev/null
+++ b/spec/frontend/repository/pages/index_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+import IndexPage from '~/repository/pages/index.vue';
+import TreePage from '~/repository/pages/tree.vue';
+import { updateElementsVisibility } from '~/repository/utils/dom';
+
+jest.mock('~/repository/utils/dom');
+
+describe('Repository index page component', () => {
+ let wrapper;
+
+ function factory() {
+ wrapper = shallowMount(IndexPage);
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+
+ updateElementsVisibility.mockClear();
+ });
+
+ it('calls updateElementsVisibility on mounted', () => {
+ factory();
+
+ expect(updateElementsVisibility).toHaveBeenCalledWith('.js-show-on-project-root', true);
+ });
+
+ it('calls updateElementsVisibility after destroy', () => {
+ factory();
+ wrapper.destroy();
+
+ expect(updateElementsVisibility.mock.calls.pop()).toEqual(['.js-show-on-project-root', false]);
+ });
+
+ it('renders TreePage', () => {
+ factory();
+
+ const child = wrapper.find(TreePage);
+
+ expect(child.exists()).toBe(true);
+ expect(child.props()).toEqual({ path: '/' });
+ });
+});
diff --git a/spec/frontend/repository/pages/tree_spec.js b/spec/frontend/repository/pages/tree_spec.js
new file mode 100644
index 00000000000..36662696c91
--- /dev/null
+++ b/spec/frontend/repository/pages/tree_spec.js
@@ -0,0 +1,60 @@
+import { shallowMount } from '@vue/test-utils';
+import TreePage from '~/repository/pages/tree.vue';
+import { updateElementsVisibility } from '~/repository/utils/dom';
+
+jest.mock('~/repository/utils/dom');
+
+describe('Repository tree page component', () => {
+ let wrapper;
+
+ function factory(path) {
+ wrapper = shallowMount(TreePage, { propsData: { path } });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+
+ updateElementsVisibility.mockClear();
+ });
+
+ describe('when root path', () => {
+ beforeEach(() => {
+ factory('/');
+ });
+
+ it('shows root elements', () => {
+ expect(updateElementsVisibility.mock.calls).toEqual([
+ ['.js-show-on-root', true],
+ ['.js-hide-on-root', false],
+ ]);
+ });
+
+ describe('when changed', () => {
+ beforeEach(() => {
+ updateElementsVisibility.mockClear();
+
+ wrapper.setProps({ path: '/test' });
+ });
+
+ it('hides root elements', () => {
+ expect(updateElementsVisibility.mock.calls).toEqual([
+ ['.js-show-on-root', false],
+ ['.js-hide-on-root', true],
+ ]);
+ });
+ });
+ });
+
+ describe('when non-root path', () => {
+ beforeEach(() => {
+ factory('/test');
+ });
+
+ it('hides root elements', () => {
+ expect(updateElementsVisibility.mock.calls).toEqual([
+ ['.js-show-on-root', false],
+ ['.js-hide-on-root', true],
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js
new file mode 100644
index 00000000000..2d75358106c
--- /dev/null
+++ b/spec/frontend/repository/utils/commit_spec.js
@@ -0,0 +1,30 @@
+import { normalizeData } from '~/repository/utils/commit';
+
+const mockData = [
+ {
+ commit: {
+ id: '123',
+ message: 'testing message',
+ committed_date: '2019-01-01',
+ },
+ commit_path: `https://test.com`,
+ file_name: 'index.js',
+ type: 'blob',
+ },
+];
+
+describe('normalizeData', () => {
+ it('normalizes data into LogTreeCommit object', () => {
+ expect(normalizeData(mockData)).toEqual([
+ {
+ sha: '123',
+ message: 'testing message',
+ committedDate: '2019-01-01',
+ commitPath: 'https://test.com',
+ fileName: 'index.js',
+ type: 'blob',
+ __typename: 'LogTreeCommit',
+ },
+ ]);
+ });
+});
diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js
new file mode 100644
index 00000000000..678d444904d
--- /dev/null
+++ b/spec/frontend/repository/utils/dom_spec.js
@@ -0,0 +1,20 @@
+import { setHTMLFixture } from '../../helpers/fixtures';
+import { updateElementsVisibility } from '~/repository/utils/dom';
+
+describe('updateElementsVisibility', () => {
+ it('adds hidden class', () => {
+ setHTMLFixture('<div class="js-test"></div>');
+
+ updateElementsVisibility('.js-test', false);
+
+ expect(document.querySelector('.js-test').classList).toContain('hidden');
+ });
+
+ it('removes hidden class', () => {
+ setHTMLFixture('<div class="hidden js-test"></div>');
+
+ updateElementsVisibility('.js-test', true);
+
+ expect(document.querySelector('.js-test').classList).not.toContain('hidden');
+ });
+});
diff --git a/spec/frontend/repository/utils/readme_spec.js b/spec/frontend/repository/utils/readme_spec.js
new file mode 100644
index 00000000000..6b7876c8947
--- /dev/null
+++ b/spec/frontend/repository/utils/readme_spec.js
@@ -0,0 +1,33 @@
+import { readmeFile } from '~/repository/utils/readme';
+
+describe('readmeFile', () => {
+ describe('markdown files', () => {
+ it('returns markdown file', () => {
+ expect(readmeFile([{ name: 'README' }, { name: 'README.md' }])).toEqual({
+ name: 'README.md',
+ });
+
+ expect(readmeFile([{ name: 'README' }, { name: 'index.md' }])).toEqual({
+ name: 'index.md',
+ });
+ });
+ });
+
+ describe('plain files', () => {
+ it('returns plain file', () => {
+ expect(readmeFile([{ name: 'README' }, { name: 'TEST.md' }])).toEqual({
+ name: 'README',
+ });
+
+ expect(readmeFile([{ name: 'readme' }, { name: 'TEST.md' }])).toEqual({
+ name: 'readme',
+ });
+ });
+ });
+
+ describe('non-previewable file', () => {
+ it('returns undefined', () => {
+ expect(readmeFile([{ name: 'index.js' }, { name: 'TEST.md' }])).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/frontend/repository/utils/title_spec.js b/spec/frontend/repository/utils/title_spec.js
index c4879716fd7..63035933424 100644
--- a/spec/frontend/repository/utils/title_spec.js
+++ b/spec/frontend/repository/utils/title_spec.js
@@ -8,8 +8,8 @@ describe('setTitle', () => {
${'app/assets'} | ${'app/assets'}
${'app/assets/javascripts'} | ${'app/assets/javascripts'}
`('sets document title as $title for $path', ({ path, title }) => {
- setTitle(path, 'master', 'GitLab');
+ setTitle(path, 'master', 'GitLab Org / GitLab');
- expect(document.title).toEqual(`${title} · master · GitLab`);
+ expect(document.title).toEqual(`${title} · master · GitLab Org / GitLab · GitLab`);
});
});
diff --git a/spec/javascripts/raven/index_spec.js b/spec/frontend/sentry/index_spec.js
index 6b9fe923624..82b6c445d96 100644
--- a/spec/javascripts/raven/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -1,8 +1,8 @@
-import RavenConfig from '~/raven/raven_config';
-import index from '~/raven/index';
+import SentryConfig from '~/sentry/sentry_config';
+import index from '~/sentry/index';
-describe('RavenConfig options', () => {
- const sentryDsn = 'sentryDsn';
+describe('SentryConfig options', () => {
+ const dsn = 'https://123@sentry.gitlab.test/123';
const currentUserId = 'currentUserId';
const gitlabUrl = 'gitlabUrl';
const environment = 'test';
@@ -11,7 +11,7 @@ describe('RavenConfig options', () => {
beforeEach(() => {
window.gon = {
- sentry_dsn: sentryDsn,
+ sentry_dsn: dsn,
sentry_environment: environment,
current_user_id: currentUserId,
gitlab_url: gitlabUrl,
@@ -20,14 +20,14 @@ describe('RavenConfig options', () => {
process.env.HEAD_COMMIT_SHA = revision;
- spyOn(RavenConfig, 'init');
+ jest.spyOn(SentryConfig, 'init').mockImplementation();
indexReturnValue = index();
});
it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
- expect(RavenConfig.init).toHaveBeenCalledWith({
- sentryDsn,
+ expect(SentryConfig.init).toHaveBeenCalledWith({
+ dsn,
currentUserId,
whitelistUrls: [gitlabUrl, 'webpack-internal://'],
environment,
@@ -38,7 +38,7 @@ describe('RavenConfig options', () => {
});
});
- it('should return RavenConfig', () => {
- expect(indexReturnValue).toBe(RavenConfig);
+ it('should return SentryConfig', () => {
+ expect(indexReturnValue).toBe(SentryConfig);
});
});
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
new file mode 100644
index 00000000000..62b8bbd50a2
--- /dev/null
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -0,0 +1,214 @@
+import * as Sentry from '@sentry/browser';
+import SentryConfig from '~/sentry/sentry_config';
+
+describe('SentryConfig', () => {
+ describe('IGNORE_ERRORS', () => {
+ it('should be an array of strings', () => {
+ const areStrings = SentryConfig.IGNORE_ERRORS.every(error => typeof error === 'string');
+
+ expect(areStrings).toBe(true);
+ });
+ });
+
+ describe('BLACKLIST_URLS', () => {
+ it('should be an array of regexps', () => {
+ const areRegExps = SentryConfig.BLACKLIST_URLS.every(url => url instanceof RegExp);
+
+ expect(areRegExps).toBe(true);
+ });
+ });
+
+ describe('SAMPLE_RATE', () => {
+ it('should be a finite number', () => {
+ expect(typeof SentryConfig.SAMPLE_RATE).toEqual('number');
+ });
+ });
+
+ describe('init', () => {
+ const options = {
+ currentUserId: 1,
+ };
+
+ beforeEach(() => {
+ jest.spyOn(SentryConfig, 'configure');
+ jest.spyOn(SentryConfig, 'bindSentryErrors');
+ jest.spyOn(SentryConfig, 'setUser');
+
+ SentryConfig.init(options);
+ });
+
+ it('should set the options property', () => {
+ expect(SentryConfig.options).toEqual(options);
+ });
+
+ it('should call the configure method', () => {
+ expect(SentryConfig.configure).toHaveBeenCalled();
+ });
+
+ it('should call the error bindings method', () => {
+ expect(SentryConfig.bindSentryErrors).toHaveBeenCalled();
+ });
+
+ it('should call setUser', () => {
+ expect(SentryConfig.setUser).toHaveBeenCalled();
+ });
+
+ it('should not call setUser if there is no current user ID', () => {
+ jest.clearAllMocks();
+
+ options.currentUserId = undefined;
+
+ SentryConfig.init(options);
+
+ expect(SentryConfig.setUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('configure', () => {
+ const sentryConfig = {};
+ const options = {
+ dsn: 'https://123@sentry.gitlab.test/123',
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
+ release: 'revision',
+ tags: {
+ revision: 'revision',
+ },
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'init').mockImplementation();
+
+ sentryConfig.options = options;
+ sentryConfig.IGNORE_ERRORS = 'ignore_errors';
+ sentryConfig.BLACKLIST_URLS = 'blacklist_urls';
+
+ SentryConfig.configure.call(sentryConfig);
+ });
+
+ it('should call Sentry.init', () => {
+ expect(Sentry.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ tags: options.tags,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'test',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+
+ it('should set environment from options', () => {
+ sentryConfig.options.environment = 'development';
+
+ SentryConfig.configure.call(sentryConfig);
+
+ expect(Sentry.init).toHaveBeenCalledWith({
+ dsn: options.dsn,
+ release: options.release,
+ tags: options.tags,
+ sampleRate: 0.95,
+ whitelistUrls: options.whitelistUrls,
+ environment: 'development',
+ ignoreErrors: sentryConfig.IGNORE_ERRORS,
+ blacklistUrls: sentryConfig.BLACKLIST_URLS,
+ });
+ });
+ });
+
+ describe('setUser', () => {
+ let sentryConfig;
+
+ beforeEach(() => {
+ sentryConfig = { options: { currentUserId: 1 } };
+ jest.spyOn(Sentry, 'setUser');
+
+ SentryConfig.setUser.call(sentryConfig);
+ });
+
+ it('should call .setUser', () => {
+ expect(Sentry.setUser).toHaveBeenCalledWith({
+ id: sentryConfig.options.currentUserId,
+ });
+ });
+ });
+
+ describe('handleSentryErrors', () => {
+ let event;
+ let req;
+ let config;
+ let err;
+
+ beforeEach(() => {
+ event = {};
+ req = { status: 'status', responseText: 'Unknown response text', statusText: 'statusText' };
+ config = { type: 'type', url: 'url', data: 'data' };
+ err = {};
+
+ jest.spyOn(Sentry, 'captureMessage');
+
+ SentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should call Sentry.captureMessage', () => {
+ expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: err,
+ event,
+ },
+ });
+ });
+
+ describe('if no err is provided', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ SentryConfig.handleSentryErrors(event, req, config);
+ });
+
+ it('should use req.statusText as the error value', () => {
+ expect(Sentry.captureMessage).toHaveBeenCalledWith(req.statusText, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: req.responseText,
+ error: req.statusText,
+ event,
+ },
+ });
+ });
+ });
+
+ describe('if no req.responseText is provided', () => {
+ beforeEach(() => {
+ req.responseText = undefined;
+
+ jest.clearAllMocks();
+
+ SentryConfig.handleSentryErrors(event, req, config, err);
+ });
+
+ it('should use `Unknown response text` as the response', () => {
+ expect(Sentry.captureMessage).toHaveBeenCalledWith(err, {
+ extra: {
+ type: config.type,
+ url: config.url,
+ data: config.data,
+ status: req.status,
+ response: 'Unknown response text',
+ error: err,
+ event,
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 452d4cd07cc..d0d1af56872 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -24,6 +24,7 @@ describe('AssigneeAvatarLink component', () => {
};
wrapper = shallowMount(AssigneeAvatarLink, {
+ attachToDocument: true,
propsData,
sync: false,
});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index ff0c8d181b5..c88ae196875 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -16,6 +16,7 @@ describe('CollapsedAssigneeList component', () => {
};
wrapper = shallowMount(CollapsedAssigneeList, {
+ attachToDocument: true,
propsData,
sync: false,
});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index 6398351834c..1de21f30d21 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -18,6 +18,7 @@ describe('UncollapsedAssigneeList component', () => {
};
wrapper = mount(UncollapsedAssigneeList, {
+ attachToDocument: true,
sync: false,
propsData,
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
new file mode 100644
index 00000000000..95296de5a5d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SplitButton renders actionItems 1`] = `
+<gldropdown-stub
+ menu-class="dropdown-menu-selectable "
+ split="true"
+ text="professor"
+>
+ <gldropdownitem-stub
+ active="true"
+ active-class="is-active"
+ >
+ <strong>
+ professor
+ </strong>
+
+ <div>
+ very symphonic
+ </div>
+ </gldropdownitem-stub>
+
+ <gldropdowndivider-stub />
+ <gldropdownitem-stub
+ active-class="is-active"
+ >
+ <strong>
+ captain
+ </strong>
+
+ <div>
+ warp drive
+ </div>
+ </gldropdownitem-stub>
+
+ <!---->
+</gldropdown-stub>
+`;
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index f89627e727b..77d8e00cf00 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -1,22 +1,27 @@
-import Vue from 'vue';
-import commitComp from '~/vue_shared/components/commit.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import CommitComponent from '~/vue_shared/components/commit.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Commit component', () => {
let props;
- let component;
- let CommitComponent;
+ let wrapper;
- beforeEach(() => {
- CommitComponent = Vue.extend(commitComp);
- });
+ const findUserAvatar = () => wrapper.find(UserAvatarLink);
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(CommitComponent, {
+ propsData,
+ sync: false,
+ });
+ };
afterEach(() => {
- component.$destroy();
+ wrapper.destroy();
});
it('should render a fork icon if it does not represent a tag', () => {
- component = mountComponent(CommitComponent, {
+ createComponent({
tag: false,
commitRef: {
name: 'master',
@@ -34,7 +39,12 @@ describe('Commit component', () => {
},
});
- expect(component.$el.querySelector('.icon-container').children).toContain('svg');
+ expect(
+ wrapper
+ .find('.icon-container')
+ .find(Icon)
+ .exists(),
+ ).toBe(true);
});
describe('Given all the props', () => {
@@ -56,68 +66,51 @@ describe('Commit component', () => {
username: 'jschatz1',
},
};
-
- component = mountComponent(CommitComponent, props);
+ createComponent(props);
});
it('should render a tag icon if it represents a tag', () => {
- expect(component.$el.querySelector('.icon-container svg.ic-tag')).not.toBeNull();
+ expect(wrapper.find('icon-stub[name="tag"]').exists()).toBe(true);
});
it('should render a link to the ref url', () => {
- expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual(
- props.commitRef.ref_url,
- );
+ expect(wrapper.find('.ref-name').attributes('href')).toBe(props.commitRef.ref_url);
});
it('should render the ref name', () => {
- expect(component.$el.querySelector('.ref-name').textContent).toContain(props.commitRef.name);
+ expect(wrapper.find('.ref-name').text()).toContain(props.commitRef.name);
});
it('should render the commit short sha with a link to the commit url', () => {
- expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual(
- props.commitUrl,
- );
+ expect(wrapper.find('.commit-sha').attributes('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
+ expect(wrapper.find('.commit-sha').text()).toContain(props.shortSha);
});
it('should render icon for commit', () => {
- expect(
- component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'),
- ).toContain('commit');
+ expect(wrapper.find('icon-stub[name="commit"]').exists()).toBe(true);
});
describe('Given commit title and author props', () => {
it('should render a link to the author profile', () => {
- expect(
- component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
- ).toEqual(props.author.path);
+ const userAvatar = findUserAvatar();
+
+ expect(userAvatar.props('linkHref')).toBe(props.author.path);
});
it('Should render the author avatar with title and alt attributes', () => {
- expect(
- component.$el
- .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip')
- .textContent.trim(),
- ).toContain(props.author.username);
-
- expect(
- component.$el
- .querySelector('.commit-title .avatar-image-container img')
- .getAttribute('alt'),
- ).toContain(`${props.author.username}'s avatar`);
+ const userAvatar = findUserAvatar();
+
+ expect(userAvatar.exists()).toBe(true);
+
+ expect(userAvatar.props('imgAlt')).toBe(`${props.author.username}'s avatar`);
});
});
it('should render the commit title', () => {
- expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual(
- props.commitUrl,
- );
+ expect(wrapper.find('.commit-row-message').attributes('href')).toEqual(props.commitUrl);
- expect(component.$el.querySelector('a.commit-row-message').textContent).toContain(
- props.title,
- );
+ expect(wrapper.find('.commit-row-message').text()).toContain(props.title);
});
});
@@ -136,9 +129,9 @@ describe('Commit component', () => {
author: {},
};
- component = mountComponent(CommitComponent, props);
+ createComponent(props);
- expect(component.$el.querySelector('.commit-title span').textContent).toContain(
+ expect(wrapper.find('.commit-title span').text()).toContain(
"Can't find HEAD commit for this branch",
);
});
@@ -159,16 +152,16 @@ describe('Commit component', () => {
author: {},
};
- component = mountComponent(CommitComponent, props);
- const refEl = component.$el.querySelector('.ref-name');
+ createComponent(props);
+ const refEl = wrapper.find('.ref-name');
- expect(refEl.textContent).toContain('master');
+ expect(refEl.text()).toContain('master');
- expect(refEl.href).toBe(props.commitRef.ref_url);
+ expect(refEl.attributes('href')).toBe(props.commitRef.ref_url);
- expect(refEl.getAttribute('data-original-title')).toBe(props.commitRef.name);
+ expect(refEl.attributes('data-original-title')).toBe(props.commitRef.name);
- expect(component.$el.querySelector('.icon-container .ic-branch')).not.toBeNull();
+ expect(wrapper.find('icon-stub[name="branch"]').exists()).toBe(true);
});
});
@@ -192,16 +185,16 @@ describe('Commit component', () => {
author: {},
};
- component = mountComponent(CommitComponent, props);
- const refEl = component.$el.querySelector('.ref-name');
+ createComponent(props);
+ const refEl = wrapper.find('.ref-name');
- expect(refEl.textContent).toContain('1234');
+ expect(refEl.text()).toContain('1234');
- expect(refEl.href).toBe(props.mergeRequestRef.path);
+ expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path);
- expect(refEl.getAttribute('data-original-title')).toBe(props.mergeRequestRef.title);
+ expect(refEl.attributes('data-original-title')).toBe(props.mergeRequestRef.title);
- expect(component.$el.querySelector('.icon-container .ic-git-merge')).not.toBeNull();
+ expect(wrapper.find('icon-stub[name="git-merge"]').exists()).toBe(true);
});
});
@@ -226,9 +219,9 @@ describe('Commit component', () => {
showRefInfo: false,
};
- component = mountComponent(CommitComponent, props);
+ createComponent(props);
- expect(component.$el.querySelector('.ref-name')).toBeNull();
+ expect(wrapper.find('.ref-name').exists()).toBe(false);
});
});
});
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
new file mode 100644
index 00000000000..3ad8f3aec7c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+
+import ImageViewer from '~/vue_shared/components/content_viewer/viewers/image_viewer.vue';
+import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
+
+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,
+ });
+ 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(imageInfo.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index d1de98f4a15..9e6b5286899 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -1,114 +1,129 @@
-import Vue from 'vue';
-
+import { shallowMount } from '@vue/test-utils';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-
-import mountComponent from 'helpers/vue_mount_component_helper';
import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
-const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
- const Component = Vue.extend(IssueAssignees);
-
- return mountComponent(Component, {
- assignees,
- cssClass,
- });
-};
+const TEST_CSS_CLASSES = 'test-classes';
+const TEST_MAX_VISIBLE = 4;
+const TEST_ICON_SIZE = 16;
describe('IssueAssigneesComponent', () => {
+ let wrapper;
let vm;
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('data', () => {
- it('returns default data props', () => {
- expect(vm.maxVisibleAssignees).toBe(2);
- expect(vm.maxAssigneeAvatars).toBe(3);
- expect(vm.maxAssignees).toBe(99);
+ const factory = props => {
+ wrapper = shallowMount(IssueAssignees, {
+ propsData: {
+ assignees: mockAssigneesList,
+ ...props,
+ },
+ sync: false,
});
+ vm = wrapper.vm; // eslint-disable-line
+ };
+
+ const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
+ const findAvatars = () => wrapper.findAll(UserAvatarLink);
+ const findOverflowCounter = () => wrapper.find('.avatar-counter');
+
+ it('returns default data props', () => {
+ factory({ assignees: mockAssigneesList });
+ expect(vm.iconSize).toBe(24);
+ expect(vm.maxVisible).toBe(3);
+ expect(vm.maxAssignees).toBe(99);
});
- describe('computed', () => {
- describe('countOverLimit', () => {
- it('should return difference between assignees count and maxVisibleAssignees', () => {
- expect(vm.countOverLimit).toBe(mockAssigneesList.length - vm.maxVisibleAssignees);
- });
- });
-
- describe('assigneesToShow', () => {
- it('should return assignees containing only 2 items when count more than maxAssigneeAvatars', () => {
- expect(vm.assigneesToShow.length).toBe(2);
- });
-
- it('should return all assignees as it is when count less than maxAssigneeAvatars', () => {
- vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
-
- expect(vm.assigneesToShow.length).toBe(3);
- });
- });
-
- describe('assigneesCounterTooltip', () => {
- it('should return string containing count of remaining assignees when count more than maxAssigneeAvatars', () => {
- expect(vm.assigneesCounterTooltip).toBe('3 more assignees');
- });
- });
-
- describe('shouldRenderAssigneesCounter', () => {
- it('should return `false` when assignees count less than maxAssigneeAvatars', () => {
- vm.assignees = mockAssigneesList.slice(0, 3); // Set 3 Assignees
-
- expect(vm.shouldRenderAssigneesCounter).toBe(false);
- });
-
- it('should return `true` when assignees count more than maxAssigneeAvatars', () => {
- expect(vm.shouldRenderAssigneesCounter).toBe(true);
+ describe.each`
+ numAssignees | maxVisible | expectedShown | expectedHidden
+ ${0} | ${3} | ${0} | ${''}
+ ${1} | ${3} | ${1} | ${''}
+ ${2} | ${3} | ${2} | ${''}
+ ${3} | ${3} | ${3} | ${''}
+ ${4} | ${3} | ${2} | ${'+2'}
+ ${5} | ${2} | ${1} | ${'+4'}
+ ${1000} | ${5} | ${4} | ${'99+'}
+ `(
+ 'with assignees ($numAssignees) and maxVisible ($maxVisible)',
+ ({ numAssignees, maxVisible, expectedShown, expectedHidden }) => {
+ beforeEach(() => {
+ factory({ assignees: Array(numAssignees).fill({}), maxVisible });
});
- });
- describe('assigneeCounterLabel', () => {
- it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => {
- expect(vm.assigneeCounterLabel).toBe('+3');
+ if (expectedShown) {
+ it('shows assignee avatars', () => {
+ expect(findAvatars().length).toEqual(expectedShown);
+ });
+ } else {
+ it('does not show assignee avatars', () => {
+ expect(findAvatars().length).toEqual(0);
+ });
+ }
+
+ if (expectedHidden) {
+ it('shows overflow counter', () => {
+ const hiddenCount = numAssignees - expectedShown;
+
+ expect(findOverflowCounter().exists()).toBe(true);
+ expect(findOverflowCounter().text()).toEqual(expectedHidden.toString());
+ expect(findOverflowCounter().attributes('data-original-title')).toEqual(
+ `${hiddenCount} more assignees`,
+ );
+ });
+ } else {
+ it('does not show overflow counter', () => {
+ expect(findOverflowCounter().exists()).toBe(false);
+ });
+ }
+ },
+ );
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ factory({
+ imgCssClasses: TEST_CSS_CLASSES,
+ maxVisible: TEST_MAX_VISIBLE,
+ iconSize: TEST_ICON_SIZE,
});
});
- });
- describe('methods', () => {
- describe('avatarUrlTitle', () => {
- it('returns string containing alt text for assignee avatar', () => {
- expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham');
- });
+ it('computes alt text for assignee avatar', () => {
+ expect(vm.avatarUrlTitle(mockAssigneesList[0])).toBe('Avatar for Terrell Graham');
});
- });
- describe('template', () => {
it('renders component root element with class `issue-assignees`', () => {
- expect(vm.$el.classList.contains('issue-assignees')).toBe(true);
+ expect(wrapper.element.classList.contains('issue-assignees')).toBe(true);
});
- it('renders assignee avatars', () => {
- expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2);
+ it('renders assignee', () => {
+ const data = findAvatars().wrappers.map(x => ({
+ ...x.props(),
+ }));
+
+ const expected = mockAssigneesList.slice(0, TEST_MAX_VISIBLE - 1).map(x =>
+ expect.objectContaining({
+ linkHref: x.web_url,
+ imgAlt: `Avatar for ${x.name}`,
+ imgCssClasses: TEST_CSS_CLASSES,
+ imgSrc: x.avatar_url,
+ imgSize: TEST_ICON_SIZE,
+ }),
+ );
+
+ expect(data).toEqual(expected);
});
- it('renders assignee tooltips', () => {
- const tooltipText = vm.$el
- .querySelectorAll('.user-avatar-link')[0]
- .querySelector('.js-assignee-tooltip').innerText;
-
- expect(tooltipText).toContain('Assignee');
- expect(tooltipText).toContain('Terrell Graham');
- expect(tooltipText).toContain('@monserrate.gleichner');
- });
+ describe('assignee tooltips', () => {
+ it('renders "Assignee" header', () => {
+ expect(findTooltipText()).toContain('Assignee');
+ });
- it('renders additional assignees count', () => {
- const avatarCounterEl = vm.$el.querySelector('.avatar-counter');
+ it('renders assignee name', () => {
+ expect(findTooltipText()).toContain('Terrell Graham');
+ });
- expect(avatarCounterEl.innerText.trim()).toBe('+3');
- expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees');
+ it('renders assignee @username', () => {
+ expect(findTooltipText()).toContain('@monserrate.gleichner');
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index eafff7f681e..45f131194ca 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import createStore from '~/notes/stores';
-import { userDataMock } from '../../../../javascripts/notes/mock_data';
+import { userDataMock } from '../../../notes/mock_data';
describe('issue placeholder system note component', () => {
let store;
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index a65e3eb294a..c2e8359f78d 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -57,7 +57,7 @@ describe('system note component', () => {
// we need to strip them because they break layout of commit lists in system notes:
// https://gitlab.com/gitlab-org/gitlab-foss/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
it('removes wrapping paragraph from note HTML', () => {
- expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
+ expect(vm.$el.querySelector('.system-note-message').innerHTML).toContain('<span>closed</span>');
});
it('should initMRPopovers onMount', () => {
diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js
new file mode 100644
index 00000000000..cff955c05b2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/slot_switch_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+
+import SlotSwitch from '~/vue_shared/components/slot_switch';
+
+describe('SlotSwitch', () => {
+ const slots = {
+ first: '<a>AGP</a>',
+ second: '<p>PCI</p>',
+ };
+
+ let wrapper;
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(SlotSwitch, {
+ propsData,
+ slots,
+ sync: false,
+ });
+ };
+
+ const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map(c => c.html());
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ it('throws an error if activeSlotNames is missing', () => {
+ expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"');
+ });
+
+ it('renders no slots if activeSlotNames is empty', () => {
+ createComponent({
+ activeSlotNames: [],
+ });
+
+ expect(getChildrenHtml().length).toBe(0);
+ });
+
+ it('renders one slot if activeSlotNames contains single slot name', () => {
+ createComponent({
+ activeSlotNames: ['first'],
+ });
+
+ expect(getChildrenHtml()).toEqual([slots.first]);
+ });
+
+ it('renders multiple slots if activeSlotNames contains multiple slot names', () => {
+ createComponent({
+ activeSlotNames: Object.keys(slots),
+ });
+
+ expect(getChildrenHtml()).toEqual(Object.values(slots));
+ });
+});
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
new file mode 100644
index 00000000000..520abb02cf7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -0,0 +1,104 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import SplitButton from '~/vue_shared/components/split_button.vue';
+
+const mockActionItems = [
+ {
+ eventName: 'concert',
+ title: 'professor',
+ description: 'very symphonic',
+ },
+ {
+ eventName: 'apocalypse',
+ title: 'captain',
+ description: 'warp drive',
+ },
+];
+
+describe('SplitButton', () => {
+ let wrapper;
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(SplitButton, {
+ propsData,
+ sync: false,
+ });
+ };
+
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownItem = (index = 0) =>
+ findDropdown()
+ .findAll(GlDropdownItem)
+ .at(index);
+ const selectItem = index => {
+ findDropdownItem(index).vm.$emit('click');
+
+ return wrapper.vm.$nextTick();
+ };
+ const clickToggleButton = () => {
+ findDropdown().vm.$emit('click');
+
+ return wrapper.vm.$nextTick();
+ };
+
+ it('fails for empty actionItems', () => {
+ const actionItems = [];
+ expect(() => createComponent({ actionItems })).toThrow();
+ });
+
+ it('fails for single actionItems', () => {
+ const actionItems = [mockActionItems[0]];
+ expect(() => createComponent({ actionItems })).toThrow();
+ });
+
+ it('renders actionItems', () => {
+ createComponent({ actionItems: mockActionItems });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('toggle button text', () => {
+ beforeEach(() => {
+ createComponent({ actionItems: mockActionItems });
+ });
+
+ it('defaults to first actionItems title', () => {
+ expect(findDropdown().props().text).toBe(mockActionItems[0].title);
+ });
+
+ it('changes to selected actionItems title', () =>
+ selectItem(1).then(() => {
+ expect(findDropdown().props().text).toBe(mockActionItems[1].title);
+ }));
+ });
+
+ describe('emitted event', () => {
+ let eventHandler;
+
+ beforeEach(() => {
+ createComponent({ actionItems: mockActionItems });
+ });
+
+ const addEventHandler = ({ eventName }) => {
+ eventHandler = jest.fn();
+ wrapper.vm.$once(eventName, () => eventHandler());
+ };
+
+ it('defaults to first actionItems event', () => {
+ addEventHandler(mockActionItems[0]);
+
+ return clickToggleButton().then(() => {
+ expect(eventHandler).toHaveBeenCalled();
+ });
+ });
+
+ it('changes to selected actionItems event', () =>
+ selectItem(1)
+ .then(() => addEventHandler(mockActionItems[1]))
+ .then(clickToggleButton)
+ .then(() => {
+ expect(eventHandler).toHaveBeenCalled();
+ }));
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 258530f32f7..0a9ff36b2fb 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -1,26 +1,37 @@
-import Vue from 'vue';
-import paginationComp from '~/vue_shared/components/pagination/table_pagination.vue';
+import { shallowMount } from '@vue/test-utils';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
describe('Pagination component', () => {
- let component;
- let PaginationComponent;
+ let wrapper;
let spy;
- let mountComponent;
+
+ const mountComponent = props => {
+ wrapper = shallowMount(TablePagination, {
+ sync: false,
+ propsData: props,
+ });
+ };
+
+ const findFirstButtonLink = () => wrapper.find('.js-first-button .page-link');
+ const findPreviousButton = () => wrapper.find('.js-previous-button');
+ const findPreviousButtonLink = () => wrapper.find('.js-previous-button .page-link');
+ const findNextButton = () => wrapper.find('.js-next-button');
+ const findNextButtonLink = () => wrapper.find('.js-next-button .page-link');
+ const findLastButtonLink = () => wrapper.find('.js-last-button .page-link');
+ const findPages = () => wrapper.findAll('.page');
+ const findSeparator = () => wrapper.find('.separator');
beforeEach(() => {
- spy = jasmine.createSpy('spy');
- PaginationComponent = Vue.extend(paginationComp);
+ spy = jest.fn();
+ });
- mountComponent = function(props) {
- return new PaginationComponent({
- propsData: props,
- }).$mount();
- };
+ afterEach(() => {
+ wrapper.destroy();
});
describe('render', () => {
it('should not render anything', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: NaN,
page: 1,
@@ -32,12 +43,12 @@ describe('Pagination component', () => {
change: spy,
});
- expect(component.$el.childNodes.length).toEqual(0);
+ expect(wrapper.isEmpty()).toBe(true);
});
describe('prev button', () => {
it('should be disabled and non clickable', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 2,
page: 1,
@@ -49,17 +60,13 @@ describe('Pagination component', () => {
change: spy,
});
- expect(
- component.$el.querySelector('.js-previous-button').classList.contains('disabled'),
- ).toEqual(true);
-
- component.$el.querySelector('.js-previous-button .page-link').click();
-
+ expect(findPreviousButton().classes()).toContain('disabled');
+ findPreviousButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be disabled and non clickable when total and totalPages are NaN', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 2,
page: 1,
@@ -70,18 +77,13 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(
- component.$el.querySelector('.js-previous-button').classList.contains('disabled'),
- ).toEqual(true);
-
- component.$el.querySelector('.js-previous-button .page-link').click();
-
+ expect(findPreviousButton().classes()).toContain('disabled');
+ findPreviousButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be enabled and clickable', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -92,14 +94,12 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- component.$el.querySelector('.js-previous-button .page-link').click();
-
+ findPreviousButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
it('should be enabled and clickable when total and totalPages are NaN', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -110,16 +110,14 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- component.$el.querySelector('.js-previous-button .page-link').click();
-
+ findPreviousButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
});
describe('first button', () => {
it('should call the change callback with the first page', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -130,18 +128,14 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- const button = component.$el.querySelector('.js-first-button .page-link');
-
- expect(button.textContent.trim()).toEqual('« First');
-
- button.click();
-
+ const button = findFirstButtonLink();
+ expect(button.text().trim()).toEqual('« First');
+ button.trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
it('should call the change callback with the first page when total and totalPages are NaN', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -152,20 +146,16 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- const button = component.$el.querySelector('.js-first-button .page-link');
-
- expect(button.textContent.trim()).toEqual('« First');
-
- button.click();
-
+ const button = findFirstButtonLink();
+ expect(button.text().trim()).toEqual('« First');
+ button.trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
});
describe('last button', () => {
it('should call the change callback with the last page', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -176,18 +166,14 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- const button = component.$el.querySelector('.js-last-button .page-link');
-
- expect(button.textContent.trim()).toEqual('Last »');
-
- button.click();
-
+ const button = findLastButtonLink();
+ expect(button.text().trim()).toEqual('Last »');
+ button.trigger('click');
expect(spy).toHaveBeenCalledWith(5);
});
it('should not render', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
@@ -198,14 +184,13 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelector('.js-last-button .page-link')).toBeNull();
+ expect(findLastButtonLink().exists()).toBe(false);
});
});
describe('next button', () => {
it('should be disabled and non clickable', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: NaN,
page: 5,
@@ -216,16 +201,17 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next ›');
-
- component.$el.querySelector('.js-next-button .page-link').click();
-
+ expect(
+ findNextButton()
+ .text()
+ .trim(),
+ ).toEqual('Next ›');
+ findNextButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be disabled and non clickable when total and totalPages are NaN', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: NaN,
page: 5,
@@ -236,16 +222,17 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next ›');
-
- component.$el.querySelector('.js-next-button .page-link').click();
-
+ expect(
+ findNextButton()
+ .text()
+ .trim(),
+ ).toEqual('Next ›');
+ findNextButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be enabled and clickable', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -256,14 +243,12 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- component.$el.querySelector('.js-next-button .page-link').click();
-
+ findNextButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(4);
});
it('should be enabled and clickable when total and totalPages are NaN', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -274,16 +259,14 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- component.$el.querySelector('.js-next-button .page-link').click();
-
+ findNextButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(4);
});
});
describe('numbered buttons', () => {
it('should render 5 pages', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -294,12 +277,11 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelectorAll('.page').length).toEqual(5);
+ expect(findPages().length).toEqual(5);
});
it('should not render any page', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -310,14 +292,13 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelectorAll('.page').length).toEqual(0);
+ expect(findPages().length).toEqual(0);
});
});
describe('spread operator', () => {
it('should render', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -328,12 +309,15 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelector('.separator').textContent.trim()).toEqual('...');
+ expect(
+ findSeparator()
+ .text()
+ .trim(),
+ ).toEqual('...');
});
it('should not render', () => {
- component = mountComponent({
+ mountComponent({
pageInfo: {
nextPage: 4,
page: 3,
@@ -344,8 +328,7 @@ describe('Pagination component', () => {
},
change: spy,
});
-
- expect(component.$el.querySelector('.separator')).toBeNull();
+ expect(findSeparator().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
new file mode 100644
index 00000000000..2f87359a4a6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -0,0 +1,108 @@
+import { shallowMount } from '@vue/test-utils';
+import { placeholderImage } from '~/lazy_loader';
+import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
+import defaultAvatarUrl from 'images/no_avatar.png';
+
+jest.mock('images/no_avatar.png', () => 'default-avatar-url');
+
+const DEFAULT_PROPS = {
+ size: 99,
+ imgSrc: 'myavatarurl.com',
+ imgAlt: 'mydisplayname',
+ cssClasses: 'myextraavatarclass',
+ tooltipText: 'tooltip text',
+ tooltipPlacement: 'bottom',
+};
+
+describe('User Avatar Image Component', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Initialization', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ },
+ sync: false,
+ });
+ });
+
+ it('should have <img> as a child element', () => {
+ const imageElement = wrapper.find('img');
+
+ expect(imageElement.exists()).toBe(true);
+ expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt);
+ });
+
+ it('should properly render img css', () => {
+ const classes = wrapper.find('img').classes();
+ expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses]));
+ expect(classes).not.toContain('lazy');
+ });
+ });
+
+ describe('Initialization when lazy', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ lazy: true,
+ },
+ sync: false,
+ });
+ });
+
+ it('should add lazy attributes', () => {
+ const imageElement = wrapper.find('img');
+
+ expect(imageElement.classes()).toContain('lazy');
+ expect(imageElement.attributes('src')).toBe(placeholderImage);
+ expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ });
+ });
+
+ describe('Initialization without src', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, { sync: false });
+ });
+
+ it('should have default avatar image', () => {
+ const imageElement = wrapper.find('img');
+
+ expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`);
+ });
+ });
+
+ describe('dynamic tooltip content', () => {
+ const props = DEFAULT_PROPS;
+ const slots = {
+ default: ['Action!'],
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMount(UserAvatarImage, { propsData: { props }, slots, sync: false });
+ });
+
+ it('renders the tooltip slot', () => {
+ expect(wrapper.find('.js-user-avatar-image-toolip').exists()).toBe(true);
+ });
+
+ it('renders the tooltip content', () => {
+ expect(wrapper.find('.js-user-avatar-image-toolip').text()).toContain(slots.default[0]);
+ });
+
+ it('does not render tooltip data attributes for on avatar image', () => {
+ const avatarImg = wrapper.find('img');
+
+ expect(avatarImg.attributes('data-original-title')).toBeFalsy();
+ expect(avatarImg.attributes('data-placement')).not.toBeDefined();
+ expect(avatarImg.attributes('data-container')).not.toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
new file mode 100644
index 00000000000..fc2eb6329b0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -0,0 +1,186 @@
+import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import { mount } from '@vue/test-utils';
+
+const DEFAULT_PROPS = {
+ loaded: true,
+ user: {
+ username: 'root',
+ name: 'Administrator',
+ location: 'Vienna',
+ bio: null,
+ organization: null,
+ status: null,
+ },
+};
+
+describe('User Popover Component', () => {
+ const fixtureTemplate = 'merge_requests/diff_comment.html';
+ preloadFixtures(fixtureTemplate);
+
+ let wrapper;
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Empty', () => {
+ beforeEach(() => {
+ wrapper = mount(UserPopover, {
+ propsData: {
+ target: document.querySelector('.js-user-link'),
+ user: {
+ name: null,
+ username: null,
+ location: null,
+ bio: null,
+ organization: null,
+ status: null,
+ },
+ },
+ sync: false,
+ });
+ });
+
+ it('should return skeleton loaders', () => {
+ expect(wrapper.findAll('.animation-container').length).toBe(4);
+ });
+ });
+
+ describe('basic data', () => {
+ it('should show basic fields', () => {
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
+ expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
+ expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
+ });
+
+ it('shows icon for location', () => {
+ const iconEl = wrapper.find('.js-location svg');
+
+ expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('location');
+ });
+ });
+
+ describe('job data', () => {
+ it('should show only bio if no organization is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.text()).toContain('Engineer');
+ });
+
+ it('should show only organization if no bio is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.organization = 'GitLab';
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.text()).toContain('GitLab');
+ });
+
+ it('should display bio and organization in separate lines', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+ testProps.user.organization = 'GitLab';
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.find('.js-bio').text()).toContain('Engineer');
+ expect(wrapper.find('.js-organization').text()).toContain('GitLab');
+ });
+
+ it('should not encode special characters in bio and organization', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Manager & Team Lead';
+ testProps.user.organization = 'Me & my <funky> Company';
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
+ expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company');
+ });
+
+ it('shows icon for bio', () => {
+ const iconEl = wrapper.find('.js-bio svg');
+
+ expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('profile');
+ });
+
+ it('shows icon for organization', () => {
+ const iconEl = wrapper.find('.js-organization svg');
+
+ expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('work');
+ });
+ });
+
+ describe('status data', () => {
+ it('should show only message', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { message_html: 'Hello World' };
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ },
+ sync: false,
+ });
+
+ expect(wrapper.text()).toContain('Hello World');
+ });
+
+ it('should show message and emoji', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' };
+
+ wrapper = mount(UserPopover, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ status: { emoji: 'basketball_player', message_html: 'Hello World' },
+ },
+ sync: false,
+ });
+
+ expect(wrapper.text()).toContain('Hello World');
+ expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
+ });
+ });
+});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index 9a60ff3b78c..7ad6a622b4b 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -259,7 +259,8 @@ describe 'Gitlab::Graphql::Authorization' do
let(:project_type) do |type|
type_factory do |type|
type.graphql_name 'FakeProjectType'
- type.field :test_issues, issue_type.connection_type, null: false, resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]) }
+ type.field :test_issues, issue_type.connection_type, null: false,
+ resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]).order(id: :asc) }
end
end
let(:query_type) do
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 0a27bbecfef..dcf3c989047 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -36,7 +36,7 @@ describe GitlabSchema do
it 'paginates active record relations using `Gitlab::Graphql::Connections::KeysetConnection`' do
connection = GraphQL::Relay::BaseConnection::CONNECTION_IMPLEMENTATIONS[ActiveRecord::Relation.name]
- expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
+ expect(connection).to eq(Gitlab::Graphql::Connections::Keyset::Connection)
end
describe '.execute' do
diff --git a/spec/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb
new file mode 100644
index 00000000000..e8da0e25b7d
--- /dev/null
+++ b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::MergeRequests::SetAssignees do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:assignee) { create(:user) }
+ let(:assignee2) { create(:user) }
+ let(:assignee_usernames) { [assignee.username] }
+ let(:mutated_merge_request) { subject[:merge_request] }
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames) }
+
+ before do
+ merge_request.project.add_developer(assignee)
+ merge_request.project.add_developer(assignee2)
+ 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
+
+ context 'when the user can update the merge request' do
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'replaces the assignee' do
+ merge_request.assignees = [assignee2]
+ merge_request.save!
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.assignees).to contain_exactly(assignee)
+ expect(subject[:errors]).to be_empty
+ end
+
+ it 'returns errors merge request could not be updated' do
+ # Make the merge request invalid
+ merge_request.allow_broken = true
+ merge_request.update!(source_project: nil)
+
+ expect(subject[:errors]).not_to be_empty
+ end
+
+ context 'when passing an empty assignee list' do
+ let(:assignee_usernames) { [] }
+
+ before do
+ merge_request.assignees = [assignee]
+ merge_request.save!
+ end
+
+ it 'removes all assignees' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.assignees).to eq([])
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when passing "append" as true' do
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames, operation_mode: Types::MutationOperationModeEnum.enum[:append]) }
+
+ before do
+ merge_request.assignees = [assignee2]
+ merge_request.save!
+
+ # In CE, APPEND is a NOOP as you can't have multiple assignees
+ # We test multiple assignment in EE specs
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'is a NO-OP in FOSS' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.assignees).to contain_exactly(assignee2)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when passing "remove" as true' do
+ before do
+ merge_request.assignees = [assignee]
+ merge_request.save!
+ end
+
+ it 'removes named assignee' do
+ mutated_merge_request = mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames, operation_mode: Types::MutationOperationModeEnum.enum[:remove])[:merge_request]
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.assignees).to eq([])
+ expect(subject[:errors]).to be_empty
+ end
+
+ it 'does not remove unnamed assignee' do
+ mutated_merge_request = mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: [assignee2.username], operation_mode: Types::MutationOperationModeEnum.enum[:remove])[:merge_request]
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.assignees).to contain_exactly(assignee)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/graphql/mutations/merge_requests/set_labels_spec.rb
new file mode 100644
index 00000000000..3729251bab7
--- /dev/null
+++ b/spec/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::MergeRequests::SetLabels do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:label) { create(:label, project: merge_request.project) }
+ let(:label2) { create(:label, project: merge_request.project) }
+ let(:label_ids) { [label.to_global_id] }
+ let(:mutated_merge_request) { subject[:merge_request] }
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids) }
+
+ 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 update the merge request' do
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'sets the labels, removing all others' do
+ merge_request.update!(labels: [label2])
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.labels).to contain_exactly(label)
+ expect(subject[:errors]).to be_empty
+ end
+
+ it 'returns errors merge request could not be updated' do
+ # Make the merge request invalid
+ merge_request.allow_broken = true
+ merge_request.update!(source_project: nil)
+
+ expect(subject[:errors]).not_to be_empty
+ end
+
+ context 'when passing an empty array' do
+ let(:label_ids) { [] }
+
+ it 'removes all labels' do
+ merge_request.update!(labels: [label])
+
+ expect(mutated_merge_request.labels).to be_empty
+ end
+ end
+
+ context 'when passing operation_mode as APPEND' do
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:append]) }
+
+ it 'sets the labels, without removing others' do
+ merge_request.update!(labels: [label2])
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.labels).to contain_exactly(label, label2)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when passing operation_mode as REMOVE' do
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, label_ids: label_ids, operation_mode: Types::MutationOperationModeEnum.enum[:remove])}
+
+ it 'removes the labels, without removing others' do
+ merge_request.update!(labels: [label, label2])
+
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.labels).to contain_exactly(label2)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/graphql/mutations/merge_requests/set_locked_spec.rb
new file mode 100644
index 00000000000..51249854378
--- /dev/null
+++ b/spec/graphql/mutations/merge_requests/set_locked_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::MergeRequests::SetLocked do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:locked) { true }
+ let(:mutated_merge_request) { subject[:merge_request] }
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, locked: locked) }
+
+ 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 update the merge request' do
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'returns the merge request as discussion locked' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request).to be_discussion_locked
+ expect(subject[:errors]).to be_empty
+ end
+
+ it 'returns errors merge request could not be updated' do
+ # Make the merge request invalid
+ merge_request.allow_broken = true
+ merge_request.update!(source_project: nil)
+
+ expect(subject[:errors]).not_to be_empty
+ end
+
+ context 'when passing locked as false' do
+ let(:locked) { false }
+
+ it 'unlocks the discussion' do
+ merge_request.update(discussion_locked: true)
+
+ expect(mutated_merge_request).not_to be_discussion_locked
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb
new file mode 100644
index 00000000000..c2792a4bc25
--- /dev/null
+++ b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::MergeRequests::SetMilestone do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:milestone) { create(:milestone, project: merge_request.project) }
+ let(:mutated_merge_request) { subject[:merge_request] }
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, milestone: milestone) }
+
+ 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 update the merge request' do
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'returns the merge request with the milestone' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.milestone).to eq(milestone)
+ expect(subject[:errors]).to be_empty
+ end
+
+ it 'returns errors merge request could not be updated' do
+ # Make the merge request invalid
+ merge_request.allow_broken = true
+ merge_request.update!(source_project: nil)
+
+ expect(subject[:errors]).not_to be_empty
+ end
+
+ context 'when passing milestone_id as nil' do
+ let(:milestone) { nil }
+
+ it 'removes the milestone' do
+ merge_request.update!(milestone: create(:milestone, project: merge_request.project))
+
+ expect(mutated_merge_request.milestone).to eq(nil)
+ end
+
+ it 'does not do anything if the MR already does not have a milestone' do
+ expect(mutated_merge_request.milestone).to eq(nil)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
new file mode 100644
index 00000000000..116a77abcc0
--- /dev/null
+++ b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::MergeRequests::SetSubscription do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:user) { create(:user) }
+ subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
+
+ describe '#resolve' do
+ let(:subscribe) { true }
+ let(:mutated_merge_request) { subject[:merge_request] }
+ subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, subscribed_state: subscribe) }
+
+ 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 update the merge request' do
+ before do
+ merge_request.project.add_developer(user)
+ end
+
+ it 'returns the merge request as discussion locked' do
+ expect(mutated_merge_request).to eq(merge_request)
+ expect(mutated_merge_request.subscribed?(user, project)).to eq(true)
+ expect(subject[:errors]).to be_empty
+ end
+
+ context 'when passing subscribe as false' do
+ let(:subscribe) { false }
+
+ it 'unsubscribes from the discussion' do
+ merge_request.subscribe(user, project)
+
+ expect(mutated_merge_request.subscribed?(user, project)).to eq(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/todos/mark_done_spec.rb b/spec/graphql/mutations/todos/mark_done_spec.rb
new file mode 100644
index 00000000000..761b153d5d1
--- /dev/null
+++ b/spec/graphql/mutations/todos/mark_done_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::Todos::MarkDone do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
+ let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
+
+ let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }) }
+
+ describe '#resolve' do
+ it 'marks a single todo as done' do
+ result = mark_done_mutation(todo1)
+
+ expect(todo1.reload.state).to eq('done')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+
+ todo = result[:todo]
+ expect(todo.id).to eq(todo1.id)
+ expect(todo.state).to eq('done')
+ end
+
+ it 'handles a todo which is already done as expected' do
+ result = mark_done_mutation(todo2)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+
+ todo = result[:todo]
+ expect(todo.id).to eq(todo2.id)
+ expect(todo.state).to eq('done')
+ end
+
+ it 'ignores requests for todos which do not belong to the current user' do
+ expect { mark_done_mutation(other_user_todo) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+ end
+
+ it 'ignores invalid GIDs' do
+ expect { mutation.resolve(id: 'invalid_gid') }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+ end
+ end
+
+ def mark_done_mutation(todo)
+ mutation.resolve(id: global_id_of(todo))
+ end
+
+ def global_id_of(todo)
+ todo.to_global_id.to_s
+ end
+end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index c162fdbbb47..a212bd07f35 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -13,6 +13,14 @@ describe Resolvers::BaseResolver do
end
end
+ let(:last_resolver) do
+ Class.new(described_class) do
+ def resolve(**args)
+ [1, 2]
+ end
+ end
+ end
+
describe '.single' do
it 'returns a subclass from the resolver' do
expect(resolver.single.superclass).to eq(resolver)
@@ -29,6 +37,22 @@ describe Resolvers::BaseResolver do
end
end
+ describe '.last' do
+ it 'returns a subclass from the resolver' do
+ expect(last_resolver.last.superclass).to eq(last_resolver)
+ end
+
+ it 'returns the same subclass every time' do
+ expect(last_resolver.last.object_id).to eq(last_resolver.last.object_id)
+ end
+
+ it 'returns a resolver that gives the last result from the original resolver' do
+ result = resolve(last_resolver.last)
+
+ expect(result).to eq(2)
+ end
+ end
+
context 'when field is a connection' do
it 'increases complexity based on arguments' do
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
diff --git a/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb
new file mode 100644
index 00000000000..93da877d714
--- /dev/null
+++ b/spec/graphql/resolvers/commit_pipelines_resolver_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::CommitPipelinesResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let(:commit) { create(:commit, project: project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let!(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ status: 'success'
+ )
+ end
+ let!(:pipeline2) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'master',
+ status: 'failed'
+ )
+ end
+ let!(:pipeline3) do
+ create(
+ :ci_pipeline,
+ project: project,
+ sha: commit.id,
+ ref: 'my_branch',
+ status: 'failed'
+ )
+ end
+
+ before do
+ commit.project.add_developer(current_user)
+ end
+
+ def resolve_pipelines
+ resolve(described_class, obj: commit, ctx: { current_user: current_user }, args: { ref: 'master' })
+ end
+
+ it 'resolves pipelines for commit and ref' do
+ pipelines = resolve_pipelines
+
+ expect(pipelines).to eq([pipeline2, pipeline])
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 2232c9b7d7b..bf9106643eb 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -72,8 +72,46 @@ describe Resolvers::IssuesResolver do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
end
- it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
+ describe 'sorting' do
+ context 'when sorting by created' do
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: 'created_asc')).to eq [issue1, issue2]
+ end
+
+ it 'sorts issues descending' do
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
+ end
+ 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) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: :due_date_asc)).to eq [due_issue3, due_issue1, due_issue4, due_issue2]
+ end
+
+ it 'sorts issues descending' do
+ expect(resolve_issues(sort: :due_date_desc)).to eq [due_issue1, due_issue3, due_issue4, due_issue2]
+ end
+ 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) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2]
+ end
+ end
end
it 'returns issues user can see' do
diff --git a/spec/graphql/types/base_enum_spec.rb b/spec/graphql/types/base_enum_spec.rb
new file mode 100644
index 00000000000..3eadb492cf5
--- /dev/null
+++ b/spec/graphql/types/base_enum_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::BaseEnum do
+ describe '#enum' do
+ let(:enum) do
+ Class.new(described_class) do
+ value 'TEST', value: 3
+ value 'other'
+ value 'NORMAL'
+ end
+ end
+
+ it 'adds all enum values to #enum' do
+ expect(enum.enum.keys).to contain_exactly('test', 'other', 'normal')
+ expect(enum.enum.values).to contain_exactly(3, 'other', 'NORMAL')
+ end
+
+ it 'is a HashWithIndefferentAccess' do
+ expect(enum.enum).to be_a(HashWithIndifferentAccess)
+ end
+ end
+end
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index 1ff1c97f8db..1c3b46ecfde 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -10,7 +10,7 @@ describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
:id, :sha, :title, :description, :message, :authored_date,
- :author, :web_url, :latest_pipeline, :signature_html
+ :author_name, :author, :web_url, :latest_pipeline, :pipelines, :signature_html
)
end
end
diff --git a/spec/graphql/types/extended_issue_type_spec.rb b/spec/graphql/types/extended_issue_type_spec.rb
deleted file mode 100644
index 72ce53ae1be..00000000000
--- a/spec/graphql/types/extended_issue_type_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe GitlabSchema.types['ExtendedIssue'] do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
-
- it { expect(described_class.graphql_name).to eq('ExtendedIssue') }
-
- it { expect(described_class).to require_graphql_authorizations(:read_issue) }
-
- it { expect(described_class.interfaces).to include(Types::Notes::NoteableType.to_graphql) }
-
- it 'has specific fields' do
- fields = Types::IssueType.fields.keys + [:subscribed]
-
- fields.each do |field_name|
- expect(described_class).to have_graphql_field(field_name)
- end
- end
-end
diff --git a/spec/graphql/types/issue_sort_enum_spec.rb b/spec/graphql/types/issue_sort_enum_spec.rb
new file mode 100644
index 00000000000..1b6aa6d6069
--- /dev/null
+++ b/spec/graphql/types/issue_sort_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['IssueSort'] do
+ it { 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])
+ end
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 8aa2385ddaa..daa2224ef20 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -14,7 +14,7 @@ describe GitlabSchema.types['Issue'] do
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
- 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]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/label_type_spec.rb b/spec/graphql/types/label_type_spec.rb
index 8e7b2c69eff..a023a75eeff 100644
--- a/spec/graphql/types/label_type_spec.rb
+++ b/spec/graphql/types/label_type_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe GitlabSchema.types['Label'] do
it 'has the correct fields' do
- expected_fields = [:description, :description_html, :title, :color, :text_color]
+ expected_fields = [:id, :description, :description_html, :title, :color, :text_color]
is_expected.to have_graphql_fields(*expected_fields)
end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index cfd0f8ec7a7..19a433f090e 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -23,6 +23,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues
issue pipelines
+ removeSourceBranchAfterMerge
]
is_expected.to have_graphql_fields(*expected_fields)
@@ -32,7 +33,7 @@ describe GitlabSchema.types['Project'] do
subject { described_class.fields['issue'] }
it 'returns issue' do
- is_expected.to have_graphql_type(Types::ExtendedIssueType)
+ is_expected.to have_graphql_type(Types::IssueType)
is_expected.to have_graphql_resolver(Resolvers::IssuesResolver.single)
end
end
diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb
index 22c11aff90a..516c862b9c6 100644
--- a/spec/graphql/types/tree/blob_type_spec.rb
+++ b/spec/graphql/types/tree/blob_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Types::Tree::BlobType do
it { expect(described_class.graphql_name).to eq('Blob') }
- it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url, :lfs_oid) }
+ it { 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 768eccba68c..81f7ad825a1 100644
--- a/spec/graphql/types/tree/submodule_type_spec.rb
+++ b/spec/graphql/types/tree/submodule_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Types::Tree::SubmoduleType do
it { expect(described_class.graphql_name).to eq('Submodule') }
- it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url, :tree_url) }
+ it { 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 ea1b6426034..228a4be0949 100644
--- a/spec/graphql/types/tree/tree_entry_type_spec.rb
+++ b/spec/graphql/types/tree/tree_entry_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Types::Tree::TreeEntryType do
it { expect(described_class.graphql_name).to eq('TreeEntry') }
- it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
+ it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url) }
end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index e8c438e459b..d3d25d3cb74 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -210,7 +210,9 @@ describe ApplicationHelper do
let(:user) { create(:user, static_object_token: 'hunter1') }
before do
- allow_any_instance_of(ApplicationSetting).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
+ allow_next_instance_of(ApplicationSetting) do |instance|
+ allow(instance).to receive(:static_objects_external_storage_url).and_return('https://cdn.gitlab.com')
+ end
allow(helper).to receive(:current_user).and_return(user)
end
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 705523f1110..8303c4eafbe 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -36,4 +36,27 @@ describe ApplicationSettingsHelper do
it_behaves_like 'when HTTP protocol is in use', 'https'
it_behaves_like 'when HTTP protocol is in use', 'http'
+
+ context 'with tracking parameters' do
+ it { expect(visible_attributes).to include(*%i(snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id)) }
+ end
+
+ describe '.integration_expanded?' do
+ let(:application_setting) { build(:application_setting) }
+
+ it 'is expanded' do
+ application_setting.plantuml_enabled = true
+ application_setting.valid?
+ helper.instance_variable_set(:@application_setting, application_setting)
+
+ expect(helper.integration_expanded?('plantuml_')).to be_truthy
+ end
+
+ it 'is not expanded' do
+ application_setting.valid?
+ helper.instance_variable_set(:@application_setting, application_setting)
+
+ expect(helper.integration_expanded?('plantuml_')).to be_falsey
+ end
+ end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index aae515def0c..cb7c670198d 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -54,6 +54,23 @@ describe AuthHelper do
end
end
+ describe 'any_form_based_providers_enabled?' do
+ before do
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
+ end
+
+ it 'detects form-based providers' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ expect(helper.any_form_based_providers_enabled?).to be(true)
+ end
+
+ it 'ignores ldap providers when ldap web sign in is disabled' do
+ allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] }
+ allow(helper).to receive(:ldap_sign_in_enabled?).and_return(false)
+ expect(helper.any_form_based_providers_enabled?).to be(false)
+ end
+ end
+
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index 4ea0f76fc28..1ee638ddf04 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -30,4 +30,60 @@ describe ClustersHelper do
end
end
end
+
+ describe '#create_new_cluster_label' do
+ subject { helper.create_new_cluster_label(provider: provider) }
+
+ context 'GCP provider' do
+ let(:provider) { 'gcp' }
+
+ it { is_expected.to eq('Create new Cluster on GKE') }
+ end
+
+ context 'AWS provider' do
+ let(:provider) { 'aws' }
+
+ it { is_expected.to eq('Create new Cluster on EKS') }
+ end
+
+ context 'other provider' do
+ let(:provider) { 'other' }
+
+ it { is_expected.to eq('Create new Cluster') }
+ end
+
+ context 'no provider' do
+ let(:provider) { nil }
+
+ it { is_expected.to eq('Create new Cluster') }
+ end
+ end
+
+ describe '#render_new_provider_form' do
+ subject { helper.new_cluster_partial(provider: provider) }
+
+ context 'GCP provider' do
+ let(:provider) { 'gcp' }
+
+ it { is_expected.to eq('clusters/clusters/gcp/new') }
+ end
+
+ context 'AWS provider' do
+ let(:provider) { 'aws' }
+
+ it { is_expected.to eq('clusters/clusters/aws/new') }
+ end
+
+ context 'other provider' do
+ let(:provider) { 'other' }
+
+ it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
+ end
+
+ context 'no provider' do
+ let(:provider) { nil }
+
+ it { is_expected.to eq('clusters/clusters/cloud_providers/cloud_provider_selector') }
+ end
+ end
end
diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb
index c899c2d9853..8a4ea33ac7c 100644
--- a/spec/helpers/dashboard_helper_spec.rb
+++ b/spec/helpers/dashboard_helper_spec.rb
@@ -25,39 +25,62 @@ describe DashboardHelper do
end
describe '#feature_entry' do
- context 'when implicitly enabled' do
- it 'considers feature enabled by default' do
- entry = feature_entry('Demo', href: 'demo.link')
+ shared_examples "a feature is enabled" do
+ it { is_expected.to include('<p aria-label="Demo: status on">') }
+ end
+
+ shared_examples "a feature is disabled" do
+ it { is_expected.to include('<p aria-label="Demo: status off">') }
+ end
- expect(entry).to include('<p aria-label="Demo: status on">')
- expect(entry).to include('<a href="demo.link">Demo</a>')
+ shared_examples "a feature without link" do
+ it do
+ is_expected.not_to have_link('Demo')
+ is_expected.not_to have_link('Documentation')
end
end
+ shared_examples "a feature with configuration" do
+ it { is_expected.to have_link('Demo', href: 'demo.link') }
+ end
+
+ shared_examples "a feature with documentation" do
+ it { is_expected.to have_link('Documentation', href: 'doc.link') }
+ end
+
+ context 'when implicitly enabled' do
+ subject { feature_entry('Demo') }
+
+ it_behaves_like 'a feature is enabled'
+ end
+
context 'when explicitly enabled' do
- it 'returns a link' do
- entry = feature_entry('Demo', href: 'demo.link', enabled: true)
+ context 'without links' do
+ subject { feature_entry('Demo', enabled: true) }
- expect(entry).to include('<p aria-label="Demo: status on">')
- expect(entry).to include('<a href="demo.link">Demo</a>')
+ it_behaves_like 'a feature is enabled'
+ it_behaves_like 'a feature without link'
end
- it 'returns text if href is not provided' do
- entry = feature_entry('Demo', enabled: true)
+ context 'with configure link' do
+ subject { feature_entry('Demo', href: 'demo.link', enabled: true) }
- expect(entry).to include('<p aria-label="Demo: status on">')
- expect(entry).not_to match(/<a[^>]+>/)
+ it_behaves_like 'a feature with configuration'
+ end
+
+ context 'with configure and documentation links' do
+ subject { feature_entry('Demo', href: 'demo.link', doc_href: 'doc.link', enabled: true) }
+
+ it_behaves_like 'a feature with configuration'
+ it_behaves_like 'a feature with documentation'
end
end
context 'when disabled' do
- it 'returns text without link' do
- entry = feature_entry('Demo', href: 'demo.link', enabled: false)
+ subject { feature_entry('Demo', href: 'demo.link', enabled: false) }
- expect(entry).to include('<p aria-label="Demo: status off">')
- expect(entry).not_to match(/<a[^>]+>/)
- expect(entry).to include('Demo')
- end
+ it_behaves_like 'a feature is disabled'
+ it_behaves_like 'a feature without link'
end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 2b8bf9319fc..a50c8e9bf8e 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -32,6 +32,7 @@ describe EnvironmentsHelper do
'project-path' => project_path(project),
'tags-path' => project_tags_path(project),
'has-metrics' => "#{environment.has_metrics?}",
+ 'prometheus-status' => "#{environment.prometheus_status}",
'external-dashboard-url' => nil
)
end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index bf043f3f013..38699108b06 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -75,6 +75,12 @@ describe GitlabRoutingHelper do
expect(preview_markdown_path(group)).to eq("/groups/#{group.path}/preview_markdown")
end
+ it 'returns group preview markdown path for a group parent with args' do
+ group = create(:group)
+
+ expect(preview_markdown_path(group, { type_id: 5 })).to eq("/groups/#{group.path}/preview_markdown?type_id=5")
+ end
+
it 'returns project preview markdown path for a project parent' do
expect(preview_markdown_path(project)).to eq("/#{project.full_path}/preview_markdown")
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 2f67ea457a0..1af8b7390bb 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -203,42 +203,53 @@ describe IssuablesHelper do
end
describe '#zoomMeetingUrl in issue' do
- let(:issue) { create(:issue, author: user, description: description) }
+ let(:issue) { create(:issue, author: user) }
before do
assign(:project, issue.project)
end
- context 'no zoom links in the issue description' do
- let(:description) { 'issue text' }
-
- it 'does not set zoomMeetingUrl' do
- expect(helper.issuable_initial_data(issue))
- .not_to include(:zoomMeetingUrl)
+ shared_examples 'sets zoomMeetingUrl to nil' do
+ specify do
+ expect(helper.issuable_initial_data(issue)[:zoomMeetingUrl])
+ .to be_nil
end
end
- context 'no zoom links in the issue description if it has link but not a zoom link' do
- let(:description) { 'issue text https://stackoverflow.com/questions/22' }
+ context 'with no "added" zoom mettings' do
+ it_behaves_like 'sets zoomMeetingUrl to nil'
+
+ context 'with multiple removed meetings' do
+ before do
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
+ end
- it 'does not set zoomMeetingUrl' do
- expect(helper.issuable_initial_data(issue))
- .not_to include(:zoomMeetingUrl)
+ it_behaves_like 'sets zoomMeetingUrl to nil'
end
end
- context 'with two zoom links in description' do
- let(:description) do
- <<~TEXT
- issue text and
- zoom call on https://zoom.us/j/123456789 this url
- and new zoom url https://zoom.us/s/lastone and some more text
- TEXT
+ context 'with "added" zoom meeting' do
+ before do
+ create(:zoom_meeting, issue: issue)
end
- it 'sets zoomMeetingUrl value to the last url' do
- expect(helper.issuable_initial_data(issue))
- .to include(zoomMeetingUrl: 'https://zoom.us/s/lastone')
+ shared_examples 'sets zoomMeetingUrl to canonical meeting url' do
+ specify do
+ expect(helper.issuable_initial_data(issue))
+ .to include(zoomMeetingUrl: 'https://zoom.us/j/123456789')
+ end
+ end
+
+ it_behaves_like 'sets zoomMeetingUrl to canonical meeting url'
+
+ context 'with muliple "removed" zoom meetings' do
+ before do
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
+ end
+
+ it_behaves_like 'sets zoomMeetingUrl to canonical meeting url'
end
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 32851249b2e..5ca5f5703cf 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -89,6 +89,35 @@ describe MarkupHelper do
end
end
end
+
+ 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) }
+
+ subject { Nokogiri::HTML.parse(generated_html) }
+
+ context 'when requested_path is provided in the context' do
+ let(:requested_path) { 'files/images/README.md' }
+
+ it 'returns the correct HTML for the image' do
+ expanded_path = "/#{project.full_path}/raw/master/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 requested_path parameter is not provided' do
+ let(:requested_path) { nil }
+
+ it 'returns the link to the image path as a relative path' do
+ expanded_path = "/#{project.full_path}/master/./#{image_file}"
+
+ expect(subject.css('a')[0].attr('href')).to eq(expanded_path)
+ end
+ end
+ end
end
describe '#markdown_field' do
@@ -210,7 +239,7 @@ describe MarkupHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_markdown(':book: Book', '/foo')
expect(actual)
- .to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
+ .to eq '<a href="/foo"><gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji></a><a href="/foo"> Book</a>'
end
end
@@ -232,6 +261,12 @@ describe MarkupHelper do
expect(doc.css('a')[0].attr('href')).to eq link
expect(doc.css('a')[0].text).to eq 'This should finally fix '
end
+
+ it "escapes HTML passed as an emoji" do
+ rendered = '<gl-emoji>&lt;div class="test"&gt;test&lt;/div&gt;</gl-emoji>'
+ expect(helper.link_to_html(rendered, '/foo'))
+ .to eq '<a href="/foo"><gl-emoji>&lt;div class="test"&gt;test&lt;/div&gt;</gl-emoji></a>'
+ end
end
describe '#render_wiki_content' do
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 1fa3c639603..cd1b1f91e9f 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -938,4 +938,22 @@ describe ProjectsHelper do
it { is_expected.to eq(grafana_integration.token) }
end
end
+
+ describe '#grafana_integration_enabled?' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ subject { helper.grafana_integration_enabled? }
+
+ it { is_expected.to eq(nil) }
+
+ context 'grafana integration exists' do
+ let!(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ it { is_expected.to eq(grafana_integration.enabled) }
+ end
+ end
end
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
index 3b4973677ef..3f56c189642 100644
--- a/spec/helpers/releases_helper_spec.rb
+++ b/spec/helpers/releases_helper_spec.rb
@@ -17,9 +17,11 @@ describe ReleasesHelper do
context 'url helpers' do
let(:project) { build(:project, namespace: create(:group)) }
+ let(:release) { create(:release, project: project) }
before do
helper.instance_variable_set(:@project, project)
+ helper.instance_variable_set(:@release, release)
end
describe '#data_for_releases_page' do
@@ -28,5 +30,17 @@ describe ReleasesHelper do
expect(helper.data_for_releases_page.keys).to eq(keys)
end
end
+
+ describe '#data_for_edit_release_page' do
+ it 'has the needed data to display the "edit release" page' do
+ keys = %i(project_id
+ tag_name
+ markdown_preview_path
+ markdown_docs_path
+ releases_page_path
+ update_release_api_docs_path)
+ expect(helper.data_for_edit_release_page.keys).to eq(keys)
+ end
+ end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 9e9f87b3407..bef6fbe3d5f 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -167,6 +167,7 @@ describe SearchHelper do
expect(search_filter_input_options('')[:data]['runner-tags-endpoint']).to eq(tag_list_admin_runners_path)
expect(search_filter_input_options('')[:data]['labels-endpoint']).to eq(project_labels_path(@project))
expect(search_filter_input_options('')[:data]['milestones-endpoint']).to eq(project_milestones_path(@project))
+ expect(search_filter_input_options('')[:data]['releases-endpoint']).to eq(project_releases_path(@project))
end
it 'includes autocomplete=off flag' do
@@ -271,4 +272,50 @@ describe SearchHelper do
expect(link).to have_css('li[data-foo="bar"]')
end
end
+
+ describe '#show_user_search_tab?' do
+ subject { show_user_search_tab? }
+
+ context 'when users_search feature is disabled' do
+ before do
+ stub_feature_flags(users_search: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when project search' do
+ before do
+ @project = :some_project
+
+ expect(self).to receive(:project_search_tabs?)
+ .with(:members)
+ .and_return(:value)
+ end
+
+ it 'delegates to project_search_tabs?' do
+ expect(subject).to eq(:value)
+ end
+ end
+
+ context 'when not project search' do
+ context 'when current_user can read_users_list' do
+ before do
+ allow(self).to receive(:current_user).and_return(:the_current_user)
+ allow(self).to receive(:can?).with(:the_current_user, :read_users_list).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when current_user cannot read_users_list' do
+ before do
+ allow(self).to receive(:current_user).and_return(:the_current_user)
+ allow(self).to receive(:can?).with(:the_current_user, :read_users_list).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index 66c8d576a4c..d88e151a11c 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -3,33 +3,217 @@
require 'spec_helper'
describe SnippetsHelper do
+ include Gitlab::Routing
include IconsHelper
- describe '#embedded_snippet_raw_button' do
- it 'gives view raw button of embedded snippets for project snippets' do
- @snippet = create(:project_snippet, :public)
+ let_it_be(:public_personal_snippet) { create(:personal_snippet, :public) }
+ let_it_be(:public_project_snippet) { create(:project_snippet, :public) }
+
+ describe '#reliable_snippet_path' do
+ subject { reliable_snippet_path(snippet) }
+
+ context 'personal snippets' do
+ let(:snippet) { public_personal_snippet }
+
+ context 'public' do
+ it 'returns a full path' do
+ expect(subject).to eq("/snippets/#{snippet.id}")
+ end
+ end
+ end
+
+ context 'project snippets' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns a full path' do
+ expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}")
+ end
+ end
+ end
+
+ describe '#reliable_snippet_url' do
+ subject { reliable_snippet_url(snippet) }
+
+ context 'personal snippets' do
+ let(:snippet) { public_personal_snippet }
+
+ context 'public' do
+ it 'returns a full url' do
+ expect(subject).to eq("http://test.host/snippets/#{snippet.id}")
+ end
+ end
+ end
+
+ context 'project snippets' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns a full url' do
+ expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}")
+ end
+ end
+ end
+
+ describe '#reliable_raw_snippet_path' do
+ subject { reliable_raw_snippet_path(snippet) }
+
+ context 'personal snippets' do
+ let(:snippet) { public_personal_snippet }
- expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet)}\">#{external_snippet_icon('doc-code')}</a>")
+ context 'public' do
+ it 'returns a full path' do
+ expect(subject).to eq("/snippets/#{snippet.id}/raw")
+ end
+ end
end
- it 'gives view raw button of embedded snippets for personal snippets' do
+ context 'project snippets' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns a full path' do
+ expect(subject).to eq("/#{snippet.project.full_path}/snippets/#{snippet.id}/raw")
+ end
+ end
+ end
+
+ describe '#reliable_raw_snippet_url' do
+ subject { reliable_raw_snippet_url(snippet) }
+
+ context 'personal snippets' do
+ let(:snippet) { public_personal_snippet }
+
+ context 'public' do
+ it 'returns a full url' do
+ expect(subject).to eq("http://test.host/snippets/#{snippet.id}/raw")
+ end
+ end
+ end
+
+ context 'project snippets' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns a full url' do
+ expect(subject).to eq("http://test.host/#{snippet.project.full_path}/snippets/#{snippet.id}/raw")
+ end
+ end
+ end
+
+ describe '#embedded_raw_snippet_button' do
+ subject { embedded_raw_snippet_button.to_s }
+
+ it 'returns view raw button of embedded snippets for personal snippets' do
@snippet = create(:personal_snippet, :public)
- expect(embedded_snippet_raw_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{raw_snippet_url(@snippet)}\">#{external_snippet_icon('doc-code')}</a>")
+ expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw"))
+ end
+
+ it 'returns view raw button of embedded snippets for project snippets' do
+ @snippet = create(:project_snippet, :public)
+
+ expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
+ end
+
+ def download_link(url)
+ "<a class=\"btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Open raw\" href=\"#{url}\">#{external_snippet_icon('doc-code')}</a>"
end
end
describe '#embedded_snippet_download_button' do
- it 'gives download button of embedded snippets for project snippets' do
+ subject { embedded_snippet_download_button }
+
+ it 'returns download button of embedded snippets for personal snippets' do
+ @snippet = create(:personal_snippet, :public)
+
+ expect(subject).to eq(download_link("http://test.host/snippets/#{@snippet.id}/raw"))
+ end
+
+ it 'returns download button of embedded snippets for project snippets' do
@snippet = create(:project_snippet, :public)
- expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_project_snippet_url(@snippet.project, @snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
+ expect(subject).to eq(download_link("http://test.host/#{@snippet.project.path_with_namespace}/snippets/#{@snippet.id}/raw"))
end
- it 'gives download button of embedded snippets for personal snippets' do
- @snippet = create(:personal_snippet, :public)
+ def download_link(url)
+ "<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{url}?inline=false\">#{external_snippet_icon('download')}</a>"
+ end
+ end
+
+ describe '#snippet_embed_tag' do
+ subject { snippet_embed_tag(snippet) }
+
+ context 'personal snippets' do
+ let(:snippet) { public_personal_snippet }
+
+ context 'public' do
+ it 'returns a script tag with the snippet full url' do
+ expect(subject).to eq(script_embed("http://test.host/snippets/#{snippet.id}"))
+ end
+ end
+ end
+
+ context 'project snippets' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns a script tag with the snippet full url' do
+ expect(subject).to eq(script_embed("http://test.host/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}"))
+ end
+ end
+
+ def script_embed(url)
+ "<script src=\"#{url}.js\"></script>"
+ end
+ end
+
+ describe '#download_raw_snippet_button' do
+ subject { download_raw_snippet_button(snippet) }
+
+ context 'with personal snippet' do
+ let(:snippet) { public_personal_snippet }
+
+ it 'returns the download button' do
+ expect(subject).to eq(download_link("/snippets/#{snippet.id}/raw"))
+ end
+ end
+
+ context 'with project snippet' do
+ let(:snippet) { public_project_snippet }
+
+ it 'returns the download button' do
+ expect(subject).to eq(download_link("/#{snippet.project.path_with_namespace}/snippets/#{snippet.id}/raw"))
+ end
+ end
+
+ def download_link(url)
+ "<a target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn btn-sm has-tooltip\" title=\"Download\" data-container=\"body\" href=\"#{url}?inline=false\"><i aria-hidden=\"true\" data-hidden=\"true\" class=\"fa fa-download\"></i></a>"
+ end
+ end
+
+ describe '#snippet_badge' do
+ let(:snippet) { build(:personal_snippet, visibility) }
+
+ subject { snippet_badge(snippet) }
+
+ context 'when snippet is private' do
+ let(:visibility) { :private }
+
+ it 'returns the snippet badge' do
+ expect(subject).to eq "<span class=\"badge badge-gray\"><i class=\"fa fa-lock\"></i> private</span>"
+ end
+ end
+
+ context 'when snippet is public' do
+ let(:visibility) { :public }
+
+ it 'does not return anything' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when snippet is internal' do
+ let(:visibility) { :internal }
- expect(embedded_snippet_download_button.to_s).to eq("<a class=\"btn\" target=\"_blank\" title=\"Download\" rel=\"noopener noreferrer\" href=\"#{raw_snippet_url(@snippet, inline: false)}\">#{external_snippet_icon('download')}</a>")
+ it 'does not return anything' do
+ expect(subject).to be_nil
+ end
end
end
end
diff --git a/spec/helpers/sourcegraph_helper_spec.rb b/spec/helpers/sourcegraph_helper_spec.rb
new file mode 100644
index 00000000000..830bbb3129f
--- /dev/null
+++ b/spec/helpers/sourcegraph_helper_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SourcegraphHelper do
+ describe '#sourcegraph_url_message' do
+ let(:sourcegraph_url) { 'http://sourcegraph.example.com' }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url).and_return(sourcegraph_url)
+ allow(Gitlab::CurrentSettings).to receive(:sourcegraph_url_is_com?).and_return(is_com)
+ end
+
+ subject { helper.sourcegraph_url_message }
+
+ context 'with .com sourcegraph url' do
+ let(:is_com) { true }
+
+ it { is_expected.to have_text('Uses Sourcegraph.com') }
+ it { is_expected.to have_link('Sourcegraph.com', href: sourcegraph_url) }
+ end
+
+ context 'with custom sourcegraph url' do
+ let(:is_com) { false }
+
+ it { is_expected.to have_text('Uses a custom Sourcegraph instance') }
+ it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) }
+
+ context 'with unsafe url' do
+ let(:sourcegraph_url) { '\" onload=\"alert(1);\"' }
+
+ it { is_expected.to have_link('Sourcegraph instance', href: sourcegraph_url) }
+ end
+ end
+ end
+
+ context '#sourcegraph_experimental_message' do
+ let(:feature_conditional) { false }
+ let(:public_only) { false }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:sourcegraph_public_only).and_return(public_only)
+ allow(Gitlab::Sourcegraph).to receive(:feature_conditional?).and_return(feature_conditional)
+ end
+
+ subject { helper.sourcegraph_experimental_message }
+
+ context 'when not limited by feature or public only' do
+ it { is_expected.to eq "This feature is experimental." }
+ end
+
+ context 'when limited by feature' do
+ let(:feature_conditional) { true }
+
+ it { is_expected.to eq "This feature is experimental and currently limited to certain projects." }
+ end
+
+ context 'when limited by public only' do
+ let(:public_only) { true }
+
+ it { is_expected.to eq "This feature is experimental and limited to public projects." }
+ end
+ end
+end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 59abe8c09e1..172ead158fb 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -76,6 +76,10 @@ describe UsersHelper do
allow(helper).to receive(:can?).and_return(false)
end
+ after do
+ expect(items).not_to include(:start_trial)
+ end
+
it 'includes all default items' do
expect(items).to include(:help, :sign_out)
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 73fbd4c7a44..248f967311b 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require_relative '../../config/initializers/6_validations.rb'
diff --git a/spec/initializers/action_mailer_hooks_spec.rb b/spec/initializers/action_mailer_hooks_spec.rb
index 3826ed9b00a..ce6e1ed0fa2 100644
--- a/spec/initializers/action_mailer_hooks_spec.rb
+++ b/spec/initializers/action_mailer_hooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'ActionMailer hooks' do
diff --git a/spec/initializers/asset_proxy_setting_spec.rb b/spec/initializers/asset_proxy_setting_spec.rb
index 42e4d4aa594..7eab5de155b 100644
--- a/spec/initializers/asset_proxy_setting_spec.rb
+++ b/spec/initializers/asset_proxy_setting_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Asset proxy settings initialization' do
diff --git a/spec/initializers/attr_encrypted_no_db_connection_spec.rb b/spec/initializers/attr_encrypted_no_db_connection_spec.rb
index 2da9f1cbd96..14e0e1f2167 100644
--- a/spec/initializers/attr_encrypted_no_db_connection_spec.rb
+++ b/spec/initializers/attr_encrypted_no_db_connection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'GitLab monkey-patches to AttrEncrypted' do
diff --git a/spec/initializers/database_config_spec.rb b/spec/initializers/database_config_spec.rb
new file mode 100644
index 00000000000..a5a074f5884
--- /dev/null
+++ b/spec/initializers/database_config_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Database config initializer' do
+ subject do
+ load Rails.root.join('config/initializers/database_config.rb')
+ end
+
+ before do
+ allow(ActiveRecord::Base).to receive(:establish_connection)
+ end
+
+ context "when using Puma" do
+ let(:puma) { double('puma') }
+ let(:puma_options) { { max_threads: 8 } }
+
+ before do
+ stub_const("Puma", puma)
+ allow(puma).to receive_message_chain(:cli_config, :options).and_return(puma_options)
+ end
+
+ context "and no existing pool size is set" do
+ before do
+ stub_database_config(pool_size: nil)
+ end
+
+ it "sets it to the max number of worker threads" do
+ expect { subject }.to change { Gitlab::Database.config['pool'] }.from(nil).to(8)
+ end
+ end
+
+ context "and the existing pool size is smaller than the max number of worker threads" do
+ before do
+ stub_database_config(pool_size: 7)
+ end
+
+ it "sets it to the max number of worker threads" do
+ expect { subject }.to change { Gitlab::Database.config['pool'] }.from(7).to(8)
+ end
+ end
+
+ context "and the existing pool size is larger than the max number of worker threads" do
+ before do
+ stub_database_config(pool_size: 9)
+ end
+
+ it "keeps the configured pool size" do
+ expect { subject }.not_to change { Gitlab::Database.config['pool'] }
+ end
+ end
+ end
+
+ context "when not using Puma" do
+ before do
+ stub_database_config(pool_size: 7)
+ end
+
+ it "does nothing" do
+ expect { subject }.not_to change { Gitlab::Database.config['pool'] }
+ end
+ end
+
+ def stub_database_config(pool_size:)
+ config = {
+ 'adapter' => 'postgresql',
+ 'host' => 'db.host.com',
+ 'pool' => pool_size
+ }.compact
+
+ allow(Gitlab::Database).to receive(:config).and_return(config)
+ end
+end
diff --git a/spec/initializers/direct_upload_support_spec.rb b/spec/initializers/direct_upload_support_spec.rb
index e51d404e030..4b3fe871cef 100644
--- a/spec/initializers/direct_upload_support_spec.rb
+++ b/spec/initializers/direct_upload_support_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Direct upload support' do
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 1a78196e33d..47c196cb3a3 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require_relative '../../config/initializers/doorkeeper'
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
index 08346b71fee..8a0d7ad8f15 100644
--- a/spec/initializers/fog_google_https_private_urls_spec.rb
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Fog::Storage::GoogleXML::File', :fog_requests do
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index c2c1960eeab..9267231390d 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -68,4 +68,52 @@ describe 'lograge', type: :request do
subject
end
end
+
+ context 'with a log subscriber' do
+ let(:subscriber) { Lograge::RequestLogSubscriber.new }
+
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ 'process_action.action_controller',
+ Time.now,
+ Time.now,
+ 2,
+ status: 200,
+ controller: 'HomeController',
+ action: 'index',
+ format: 'application/json',
+ method: 'GET',
+ path: '/home?foo=bar',
+ params: {},
+ db_runtime: 0.02,
+ view_runtime: 0.01
+ )
+ end
+
+ let(:log_output) { StringIO.new }
+ let(:logger) do
+ Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } }
+ end
+
+ describe 'with an exception' do
+ let(:exception) { RuntimeError.new('bad request') }
+ let(:backtrace) { caller }
+
+ before do
+ allow(exception).to receive(:backtrace).and_return(backtrace)
+ event.payload[:exception_object] = exception
+ Lograge.logger = logger
+ end
+
+ it 'adds exception data to log' do
+ subscriber.process_action(event)
+
+ log_data = JSON.parse(log_output.string)
+
+ expect(log_data['exception']['class']).to eq('RuntimeError')
+ expect(log_data['exception']['message']).to eq('bad request')
+ expect(log_data['exception']['backtrace']).to eq(Gitlab::Profiler.clean_backtrace(backtrace))
+ end
+ end
+ end
end
diff --git a/spec/initializers/rest-client-hostname_override_spec.rb b/spec/initializers/rest-client-hostname_override_spec.rb
index 3707e001d41..90a0305c9a9 100644
--- a/spec/initializers/rest-client-hostname_override_spec.rb
+++ b/spec/initializers/rest-client-hostname_override_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'rest-client dns rebinding protection' do
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 726ce07a2d1..c29f46e7779 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require_relative '../../config/initializers/01_secret_token'
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 57f5adbbc40..6cb45b4c86b 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require_relative '../../config/initializers/1_settings' unless defined?(Settings)
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 02a9446ad7b..a2bd0ff9f1c 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'trusted_proxies' do
diff --git a/spec/initializers/zz_metrics_spec.rb b/spec/initializers/zz_metrics_spec.rb
index 3eaccfe8d8b..b9a1919ceae 100644
--- a/spec/initializers/zz_metrics_spec.rb
+++ b/spec/initializers/zz_metrics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'instrument_classes' do
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 9f441ca319e..51433a58212 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -10,6 +10,7 @@ import eventHub from '~/boards/eventhub';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/list';
+import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
@@ -40,6 +41,7 @@ describe('Board card', () => {
list.issues[0].labels.push(label1);
vm = new BoardCardComp({
+ store,
propsData: {
list,
issue: list.issues[0],
diff --git a/spec/javascripts/boards/board_list_common_spec.js b/spec/javascripts/boards/board_list_common_spec.js
index cb337e4cc83..ada7589b795 100644
--- a/spec/javascripts/boards/board_list_common_spec.js
+++ b/spec/javascripts/boards/board_list_common_spec.js
@@ -10,11 +10,17 @@ import BoardList from '~/boards/components/board_list.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
window.Sortable = Sortable;
-export default function createComponent({ done, listIssueProps = {}, componentProps = {} }) {
+export default function createComponent({
+ done,
+ listIssueProps = {},
+ componentProps = {},
+ listProps = {},
+}) {
const el = document.createElement('div');
document.body.appendChild(el);
@@ -24,7 +30,7 @@ export default function createComponent({ done, listIssueProps = {}, componentPr
boardsStore.create();
const BoardListComp = Vue.extend(BoardList);
- const list = new List(listObj);
+ const list = new List({ ...listObj, ...listProps });
const issue = new ListIssue({
title: 'Testing',
id: 1,
@@ -34,11 +40,14 @@ export default function createComponent({ done, listIssueProps = {}, componentPr
assignees: [],
...listIssueProps,
});
- list.issuesSize = 1;
+ if (!Object.prototype.hasOwnProperty.call(listProps, 'issuesSize')) {
+ list.issuesSize = 1;
+ }
list.issues.push(issue);
const component = new BoardListComp({
el,
+ store,
propsData: {
disabled: false,
list,
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 6774a46ed58..37e96e97279 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -1,156 +1,210 @@
+/* global List */
+
import Vue from 'vue';
import eventHub from '~/boards/eventhub';
import createComponent from './board_list_common_spec';
+import waitForPromises from '../helpers/wait_for_promises';
+
+import '~/boards/models/list';
describe('Board list component', () => {
let mock;
let component;
+ let getIssues;
+ function generateIssues(compWrapper) {
+ for (let i = 1; i < 20; i += 1) {
+ const issue = Object.assign({}, compWrapper.list.issues[0]);
+ issue.id += i;
+ compWrapper.list.issues.push(issue);
+ }
+ }
- beforeEach(done => {
- ({ mock, component } = createComponent({ done }));
- });
+ describe('When Expanded', () => {
+ beforeEach(done => {
+ getIssues = spyOn(List.prototype, 'getIssues').and.returnValue(new Promise(() => {}));
+ ({ mock, component } = createComponent({ done }));
+ });
- afterEach(() => {
- mock.restore();
- });
+ afterEach(() => {
+ mock.restore();
+ component.$destroy();
+ });
- it('renders component', () => {
- expect(component.$el.classList.contains('board-list-component')).toBe(true);
- });
+ it('loads first page of issues', done => {
+ waitForPromises()
+ .then(() => {
+ expect(getIssues).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
- it('renders loading icon', done => {
- component.loading = true;
+ it('renders component', () => {
+ expect(component.$el.classList.contains('board-list-component')).toBe(true);
+ });
+
+ it('renders loading icon', done => {
+ component.loading = true;
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-list-loading')).not.toBeNull();
- done();
+ done();
+ });
});
- });
- it('renders issues', () => {
- expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
- });
+ it('renders issues', () => {
+ expect(component.$el.querySelectorAll('.board-card').length).toBe(1);
+ });
- it('sets data attribute with issue id', () => {
- expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
- });
+ it('sets data attribute with issue id', () => {
+ expect(component.$el.querySelector('.board-card').getAttribute('data-issue-id')).toBe('1');
+ });
- it('shows new issue form', done => {
- component.toggleForm();
+ it('shows new issue form', done => {
+ component.toggleForm();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- done();
+ done();
+ });
});
- });
- it('shows new issue form after eventhub event', done => {
- eventHub.$emit(`hide-issue-form-${component.list.id}`);
+ it('shows new issue form after eventhub event', done => {
+ eventHub.$emit(`hide-issue-form-${component.list.id}`);
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).not.toBeNull();
- expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
+ expect(component.$el.querySelector('.is-smaller')).not.toBeNull();
- done();
+ done();
+ });
});
- });
- it('does not show new issue form for closed list', done => {
- component.list.type = 'closed';
- component.toggleForm();
+ it('does not show new issue form for closed list', done => {
+ component.list.type = 'closed';
+ component.toggleForm();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-new-issue-form')).toBeNull();
- done();
+ done();
+ });
});
- });
- it('shows count list item', done => {
- component.showCount = true;
+ it('shows count list item', done => {
+ component.showCount = true;
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-list-count')).not.toBeNull();
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing all issues',
- );
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing all issues',
+ );
- done();
+ done();
+ });
});
- });
- it('sets data attribute with invalid id', done => {
- component.showCount = true;
+ it('sets data attribute with invalid id', done => {
+ component.showCount = true;
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
- '-1',
- );
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-list-count').getAttribute('data-issue-id')).toBe(
+ '-1',
+ );
- done();
+ done();
+ });
});
- });
- it('shows how many more issues to load', done => {
- component.showCount = true;
- component.list.issuesSize = 20;
+ it('shows how many more issues to load', done => {
+ component.showCount = true;
+ component.list.issuesSize = 20;
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
- 'Showing 1 of 20 issues',
- );
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-list-count').textContent.trim()).toBe(
+ 'Showing 1 of 20 issues',
+ );
- done();
+ done();
+ });
});
- });
-
- it('loads more issues after scrolling', done => {
- spyOn(component.list, 'nextPage');
- component.$refs.list.style.height = '100px';
- component.$refs.list.style.overflow = 'scroll';
- for (let i = 1; i < 20; i += 1) {
- const issue = Object.assign({}, component.list.issues[0]);
- issue.id += i;
- component.list.issues.push(issue);
- }
+ it('loads more issues after scrolling', done => {
+ spyOn(component.list, 'nextPage');
+ component.$refs.list.style.height = '100px';
+ component.$refs.list.style.overflow = 'scroll';
+ generateIssues(component);
+
+ Vue.nextTick(() => {
+ component.$refs.list.scrollTop = 20000;
+
+ waitForPromises()
+ .then(() => {
+ expect(component.list.nextPage).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
- Vue.nextTick(() => {
- component.$refs.list.scrollTop = 20000;
+ it('does not load issues if already loading', done => {
+ component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
+ new Promise(() => {}),
+ );
- setTimeout(() => {
- expect(component.list.nextPage).toHaveBeenCalled();
+ component.onScroll();
+ component.onScroll();
- done();
- });
+ waitForPromises()
+ .then(() => {
+ expect(component.list.nextPage).toHaveBeenCalledTimes(1);
+ })
+ .then(done)
+ .catch(done.fail);
});
- });
- it('does not load issues if already loading', () => {
- component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(
- new Promise(() => {}),
- );
+ it('shows loading more spinner', done => {
+ component.showCount = true;
+ component.list.loadingMore = true;
- component.onScroll();
- component.onScroll();
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
- expect(component.list.nextPage).toHaveBeenCalledTimes(1);
+ done();
+ });
+ });
});
- it('shows loading more spinner', done => {
- component.showCount = true;
- component.list.loadingMore = true;
+ describe('When Collapsed', () => {
+ beforeEach(done => {
+ getIssues = spyOn(List.prototype, 'getIssues').and.returnValue(new Promise(() => {}));
+ ({ mock, component } = createComponent({
+ done,
+ listProps: { type: 'closed', collapsed: true, issuesSize: 50 },
+ }));
+ generateIssues(component);
+ component.scrollHeight = spyOn(component, 'scrollHeight').and.returnValue(0);
+ });
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-count .gl-spinner')).not.toBeNull();
+ afterEach(() => {
+ mock.restore();
+ component.$destroy();
+ });
- done();
+ it('does not load all issues', done => {
+ waitForPromises()
+ .then(() => {
+ // Initial getIssues from list constructor
+ expect(getIssues).toHaveBeenCalledTimes(1);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/boards/components/boards_selector_spec.js b/spec/javascripts/boards/components/boards_selector_spec.js
index 473cc0612ea..d1f36a0a652 100644
--- a/spec/javascripts/boards/components/boards_selector_spec.js
+++ b/spec/javascripts/boards/components/boards_selector_spec.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import BoardService from '~/boards/services/board_service';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
@@ -37,7 +36,6 @@ describe('BoardsSelector', () => {
bulkUpdatePath: '',
boardId: '',
});
- window.gl.boardService = new BoardService();
allBoardsResponse = Promise.resolve({
data: boards,
@@ -46,8 +44,8 @@ describe('BoardsSelector', () => {
data: recentBoards,
});
- spyOn(BoardService.prototype, 'allBoards').and.returnValue(allBoardsResponse);
- spyOn(BoardService.prototype, 'recentBoards').and.returnValue(recentBoardsResponse);
+ spyOn(boardsStore, 'allBoards').and.returnValue(allBoardsResponse);
+ spyOn(boardsStore, 'recentBoards').and.returnValue(recentBoardsResponse);
const Component = Vue.extend(BoardsSelector);
vm = mountComponent(
@@ -94,7 +92,6 @@ describe('BoardsSelector', () => {
afterEach(() => {
vm.$destroy();
- window.gl.boardService = undefined;
});
describe('filtering', () => {
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
deleted file mode 100644
index de48e3f6091..00000000000
--- a/spec/javascripts/boards/components/issue_time_estimate_spec.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import Vue from 'vue';
-import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
-import boardsStore from '~/boards/stores/boards_store';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('Issue Time Estimate component', () => {
- let vm;
-
- beforeEach(() => {
- boardsStore.create();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when limitToHours is false', () => {
- beforeEach(() => {
- boardsStore.timeTracking.limitToHours = false;
-
- const Component = Vue.extend(IssueTimeEstimate);
- vm = mountComponent(Component, {
- estimate: 374460,
- });
- });
-
- it('renders the correct time estimate', () => {
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
- });
-
- it('renders expanded time estimate in tooltip', () => {
- expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
- '2 weeks 3 days 1 minute',
- );
- });
-
- it('prevents tooltip xss', done => {
- const alertSpy = spyOn(window, 'alert');
- vm.estimate = 'Foo <script>alert("XSS")</script>';
-
- vm.$nextTick(() => {
- expect(alertSpy).not.toHaveBeenCalled();
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
- expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
- done();
- });
- });
- });
-
- describe('when limitToHours is true', () => {
- beforeEach(() => {
- boardsStore.timeTracking.limitToHours = true;
-
- const Component = Vue.extend(IssueTimeEstimate);
- vm = mountComponent(Component, {
- estimate: 374460,
- });
- });
-
- it('renders the correct time estimate', () => {
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual('104h 1m');
- });
-
- it('renders expanded time estimate in tooltip', () => {
- expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
- '104 hours 1 minute',
- );
- });
- });
-});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
deleted file mode 100644
index 314e051665e..00000000000
--- a/spec/javascripts/boards/issue_card_spec.js
+++ /dev/null
@@ -1,292 +0,0 @@
-/* global ListAssignee */
-/* global ListLabel */
-/* global ListIssue */
-
-import Vue from 'vue';
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import IssueCardInner from '~/boards/components/issue_card_inner.vue';
-import { listObj } from './mock_data';
-
-describe('Issue card component', () => {
- const user = new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- avatar: 'test_image',
- });
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: 'blue',
- text_color: 'white',
- description: 'test',
- });
- let component;
- let issue;
- let list;
-
- beforeEach(() => {
- setFixtures('<div class="test-container"></div>');
-
- list = {
- ...listObj,
- type: 'label',
- };
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [list.label],
- assignees: [],
- reference_path: '#1',
- real_path: '/test/1',
- weight: 1,
- });
-
- component = new Vue({
- el: document.querySelector('.test-container'),
- components: {
- 'issue-card': IssueCardInner,
- },
- data() {
- return {
- list,
- issue,
- issueLinkBase: '/test',
- rootPath: '/',
- };
- },
- template: `
- <issue-card
- :issue="issue"
- :list="list"
- :issue-link-base="issueLinkBase"
- :root-path="rootPath"></issue-card>
- `,
- });
- });
-
- it('renders issue title', () => {
- expect(component.$el.querySelector('.board-card-title').textContent).toContain(issue.title);
- });
-
- it('includes issue base in link', () => {
- expect(component.$el.querySelector('.board-card-title a').getAttribute('href')).toContain(
- '/test',
- );
- });
-
- it('includes issue title on link', () => {
- expect(component.$el.querySelector('.board-card-title a').getAttribute('title')).toBe(
- issue.title,
- );
- });
-
- it('does not render confidential icon', () => {
- expect(component.$el.querySelector('.fa-eye-flash')).toBeNull();
- });
-
- it('renders confidential icon', done => {
- component.issue.confidential = true;
-
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.confidential-icon')).not.toBeNull();
- done();
- });
- });
-
- it('renders issue ID with #', () => {
- expect(component.$el.querySelector('.board-card-number').textContent).toContain(`#${issue.id}`);
- });
-
- describe('assignee', () => {
- it('does not render assignee', () => {
- expect(component.$el.querySelector('.board-card-assignee .avatar')).toBeNull();
- });
-
- describe('exists', () => {
- beforeEach(done => {
- component.issue.assignees = [user];
-
- Vue.nextTick(() => done());
- });
-
- it('renders assignee', () => {
- expect(component.$el.querySelector('.board-card-assignee .avatar')).not.toBeNull();
- });
-
- it('sets title', () => {
- expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain(
- `${user.name}`,
- );
- });
-
- it('sets users path', () => {
- expect(component.$el.querySelector('.board-card-assignee a').getAttribute('href')).toBe(
- '/test',
- );
- });
-
- it('renders avatar', () => {
- expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
- });
- });
-
- describe('assignee default avatar', () => {
- beforeEach(done => {
- component.issue.assignees = [
- new ListAssignee(
- {
- id: 1,
- name: 'testing 123',
- username: 'test',
- },
- 'default_avatar',
- ),
- ];
-
- Vue.nextTick(done);
- });
-
- it('displays defaults avatar if users avatar is null', () => {
- expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
- expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe(
- 'default_avatar?width=24',
- );
- });
- });
- });
-
- describe('multiple assignees', () => {
- beforeEach(done => {
- component.issue.assignees = [
- new ListAssignee({
- id: 2,
- name: 'user2',
- username: 'user2',
- avatar: 'test_image',
- }),
- new ListAssignee({
- id: 3,
- name: 'user3',
- username: 'user3',
- avatar: 'test_image',
- }),
- new ListAssignee({
- id: 4,
- name: 'user4',
- username: 'user4',
- avatar: 'test_image',
- }),
- ];
-
- Vue.nextTick(() => done());
- });
-
- it('renders all three assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
- });
-
- describe('more than three assignees', () => {
- beforeEach(done => {
- component.issue.assignees.push(
- new ListAssignee({
- id: 5,
- name: 'user5',
- username: 'user5',
- avatar: 'test_image',
- }),
- );
-
- Vue.nextTick(() => done());
- });
-
- it('renders more avatar counter', () => {
- expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
- ).toEqual('+2');
- });
-
- it('renders two assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2);
- });
-
- it('renders 99+ avatar counter', done => {
- for (let i = 5; i < 104; i += 1) {
- const u = new ListAssignee({
- id: i,
- name: 'name',
- username: 'username',
- avatar: 'test_image',
- });
- component.issue.assignees.push(u);
- }
-
- Vue.nextTick(() => {
- expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
- ).toEqual('99+');
- done();
- });
- });
- });
- });
-
- describe('labels', () => {
- beforeEach(done => {
- component.issue.addLabel(label1);
-
- Vue.nextTick(() => done());
- });
-
- it('does not render list label but renders all other labels', () => {
- expect(component.$el.querySelectorAll('.badge').length).toBe(1);
- });
-
- it('renders label', () => {
- const nodes = [];
- component.$el.querySelectorAll('.badge').forEach(label => {
- nodes.push(label.getAttribute('data-original-title'));
- });
-
- expect(nodes.includes(label1.description)).toBe(true);
- });
-
- it('sets label description as title', () => {
- expect(component.$el.querySelector('.badge').getAttribute('data-original-title')).toContain(
- label1.description,
- );
- });
-
- it('sets background color of button', () => {
- const nodes = [];
- component.$el.querySelectorAll('.badge').forEach(label => {
- nodes.push(label.style.backgroundColor);
- });
-
- expect(nodes.includes(label1.color)).toBe(true);
- });
-
- it('does not render label if label does not have an ID', done => {
- component.issue.addLabel(
- new ListLabel({
- title: 'closed',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(component.$el.querySelectorAll('.badge').length).toBe(1);
- expect(component.$el.textContent).not.toContain('closed');
-
- done();
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js
index 35340a3bc42..6957cf40301 100644
--- a/spec/javascripts/bootstrap_jquery_spec.js
+++ b/spec/javascripts/bootstrap_jquery_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-var */
-
import $ from 'jquery';
import '~/commons/bootstrap';
@@ -10,15 +8,13 @@ describe('Bootstrap jQuery extensions', function() {
});
it('adds the disabled attribute', function() {
- var $input;
- $input = $('input').first();
+ const $input = $('input').first();
$input.disable();
expect($input).toHaveAttr('disabled', 'disabled');
});
return it('adds the disabled class', function() {
- var $input;
- $input = $('input').first();
+ const $input = $('input').first();
$input.disable();
expect($input).toHaveClass('disabled');
@@ -30,15 +26,13 @@ describe('Bootstrap jQuery extensions', function() {
});
it('removes the disabled attribute', function() {
- var $input;
- $input = $('input').first();
+ const $input = $('input').first();
$input.enable();
expect($input).not.toHaveAttr('disabled');
});
return it('removes the disabled class', function() {
- var $input;
- $input = $('input').first();
+ const $input = $('input').first();
$input.enable();
expect($input).not.toHaveClass('disabled');
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index b2fe315f6c6..b53e30b6896 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -3,7 +3,7 @@ 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 VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => {
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 3ca2d1dc934..6ffdb6ba85d 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -3,14 +3,15 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import { createStore } from 'ee_else_ce/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import diffFileMockData from '../mock_data/diff_file';
+import diffFileMockDataReadable from '../mock_data/diff_file';
+import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
describe('DiffFile', () => {
let vm;
beforeEach(() => {
vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
- file: JSON.parse(JSON.stringify(diffFileMockData)),
+ file: JSON.parse(JSON.stringify(diffFileMockDataReadable)),
canCurrentUserFork: false,
}).$mount();
});
@@ -81,6 +82,24 @@ describe('DiffFile', () => {
});
});
+ it('should be collapsable for unreadable files', done => {
+ vm.$destroy();
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
+ file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
+ canCurrentUserFork: false,
+ }).$mount();
+
+ vm.renderIt = false;
+ vm.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).toContain('This diff is collapsed');
+ expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
+
+ done();
+ });
+ });
+
it('should be collapsed for renamed files', done => {
vm.renderIt = true;
vm.isCollapsed = false;
@@ -184,5 +203,31 @@ describe('DiffFile', () => {
.then(done)
.catch(done.fail);
});
+
+ it('does not call handleLoadCollapsedDiff if collapsed changed & file is unreadable', done => {
+ vm.$destroy();
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
+ file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
+ canCurrentUserFork: false,
+ }).$mount();
+
+ spyOn(vm, 'handleLoadCollapsedDiff');
+
+ vm.file.highlighted_diff_lines = undefined;
+ vm.file.parallel_diff_lines = [];
+ vm.isCollapsed = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.isCollapsed = false;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.handleLoadCollapsedDiff).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/diffs/mock_data/diff_file_unreadable.js b/spec/javascripts/diffs/mock_data/diff_file_unreadable.js
new file mode 100644
index 00000000000..8c2df45988e
--- /dev/null
+++ b/spec/javascripts/diffs/mock_data/diff_file_unreadable.js
@@ -0,0 +1,244 @@
+export default {
+ submodule: false,
+ submodule_link: null,
+ blob: {
+ id: '9e10516ca50788acf18c518a231914a21e5f16f7',
+ path: 'CHANGELOG',
+ name: 'CHANGELOG',
+ mode: '100644',
+ readable_text: false,
+ icon: 'file-text-o',
+ },
+ blob_path: 'CHANGELOG',
+ blob_name: 'CHANGELOG',
+ blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
+ file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
+ file_path: 'CHANGELOG',
+ new_file: false,
+ deleted_file: false,
+ renamed_file: false,
+ old_path: 'CHANGELOG',
+ new_path: 'CHANGELOG',
+ mode_changed: false,
+ a_mode: '100644',
+ b_mode: '100644',
+ text: true,
+ viewer: {
+ name: 'text',
+ error: null,
+ collapsed: false,
+ },
+ added_lines: 0,
+ removed_lines: 0,
+ diff_refs: {
+ base_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a',
+ start_sha: 'd9eaefe5a676b820c57ff18cf5b68316025f7962',
+ head_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ },
+ content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
+ stored_externally: null,
+ external_storage: null,
+ old_path_html: 'CHANGELOG',
+ new_path_html: 'CHANGELOG',
+ edit_path: '/gitlab-org/gitlab-test/edit/spooky-stuff/CHANGELOG',
+ view_path: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
+ replaced_view_path: null,
+ collapsed: false,
+ renderIt: false,
+ too_large: false,
+ context_lines_path:
+ '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
+ highlighted_diff_lines: [
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ discussions: [],
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ discussions: [],
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ discussions: [],
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ discussions: [],
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ discussions: [],
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ discussions: [],
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ ],
+ parallel_diff_lines: [
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1',
+ type: 'new',
+ old_line: null,
+ new_line: 1,
+ discussions: [],
+ text: '+<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ rich_text: '<span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ type: 'empty-cell',
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
+ type: 'new',
+ old_line: null,
+ new_line: 2,
+ discussions: [],
+ text: '+<span id="LC2" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC2" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_Code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ discussions: [],
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3',
+ type: null,
+ old_line: 1,
+ new_line: 3,
+ discussions: [],
+ text: ' <span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ rich_text: '<span id="LC3" class="line" lang="plaintext">v6.8.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ discussions: [],
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4',
+ type: null,
+ old_line: 2,
+ new_line: 4,
+ discussions: [],
+ text: ' <span id="LC4" class="line" lang="plaintext"></span>\n',
+ rich_text: '<span id="LC4" class="line" lang="plaintext"></span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ discussions: [],
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ right: {
+ line_code: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_3_5',
+ type: null,
+ old_line: 3,
+ new_line: 5,
+ discussions: [],
+ text: ' <span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ rich_text: '<span id="LC5" class="line" lang="plaintext">v6.7.0</span>\n',
+ meta_data: null,
+ },
+ },
+ {
+ left: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ discussions: [],
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ right: {
+ line_code: null,
+ type: 'match',
+ old_line: null,
+ new_line: null,
+ discussions: [],
+ text: '',
+ rich_text: '',
+ meta_data: {
+ old_pos: 3,
+ new_pos: 5,
+ },
+ },
+ },
+ ],
+ discussions: [],
+ renderingLines: false,
+};
diff --git a/spec/javascripts/dropzone_input_spec.js b/spec/javascripts/dropzone_input_spec.js
index ef899612b08..125dcdb3763 100644
--- a/spec/javascripts/dropzone_input_spec.js
+++ b/spec/javascripts/dropzone_input_spec.js
@@ -13,54 +13,68 @@ const TEMPLATE = `<form class="gfm-form" data-uploads-path="${TEST_UPLOAD_PATH}"
</form>`;
describe('dropzone_input', () => {
- let form;
- let dropzone;
- let xhr;
- let oldXMLHttpRequest;
+ it('returns null when failed to initialize', () => {
+ const dropzone = dropzoneInput($('<form class="gfm-form"></form>'));
- beforeEach(() => {
- form = $(TEMPLATE);
+ expect(dropzone).toBeNull();
+ });
- dropzone = dropzoneInput(form);
+ it('returns valid dropzone when successfully initialize', () => {
+ const dropzone = dropzoneInput($(TEMPLATE));
- xhr = jasmine.createSpyObj(Object.keys(XMLHttpRequest.prototype));
- oldXMLHttpRequest = window.XMLHttpRequest;
- window.XMLHttpRequest = () => xhr;
+ expect(dropzone.version).toBeTruthy();
});
- afterEach(() => {
- window.XMLHttpRequest = oldXMLHttpRequest;
- });
+ describe('shows error message', () => {
+ let form;
+ let dropzone;
+ let xhr;
+ let oldXMLHttpRequest;
- it('shows error message, when AJAX fails with json', () => {
- xhr = {
- ...xhr,
- statusCode: 400,
- readyState: 4,
- responseText: JSON.stringify({ message: TEST_ERROR_MESSAGE }),
- getResponseHeader: () => 'application/json',
- };
+ beforeEach(() => {
+ form = $(TEMPLATE);
- dropzone.processFile(TEST_FILE);
+ dropzone = dropzoneInput(form);
- xhr.onload();
+ xhr = jasmine.createSpyObj(Object.keys(XMLHttpRequest.prototype));
+ oldXMLHttpRequest = window.XMLHttpRequest;
+ window.XMLHttpRequest = () => xhr;
+ });
- expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE);
- });
+ afterEach(() => {
+ window.XMLHttpRequest = oldXMLHttpRequest;
+ });
+
+ it('when AJAX fails with json', () => {
+ xhr = {
+ ...xhr,
+ statusCode: 400,
+ readyState: 4,
+ responseText: JSON.stringify({ message: TEST_ERROR_MESSAGE }),
+ getResponseHeader: () => 'application/json',
+ };
+
+ dropzone.processFile(TEST_FILE);
+
+ xhr.onload();
+
+ expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE);
+ });
- it('shows error message, when AJAX fails with text', () => {
- xhr = {
- ...xhr,
- statusCode: 400,
- readyState: 4,
- responseText: TEST_ERROR_MESSAGE,
- getResponseHeader: () => 'text/plain',
- };
+ it('when AJAX fails with text', () => {
+ xhr = {
+ ...xhr,
+ statusCode: 400,
+ readyState: 4,
+ responseText: TEST_ERROR_MESSAGE,
+ getResponseHeader: () => 'text/plain',
+ };
- dropzone.processFile(TEST_FILE);
+ dropzone.processFile(TEST_FILE);
- xhr.onload();
+ xhr.onload();
- expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE);
+ expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE);
+ });
});
});
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
index 36dd8604d08..da0427d650a 100644
--- a/spec/javascripts/frequent_items/components/app_spec.js
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -247,7 +247,7 @@ describe('Frequent Items App Component', () => {
.then(() => {
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
- mockSearchedProjects.length,
+ mockSearchedProjects.data.length,
);
})
.then(done)
diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js
index 3ca5b4c7446..7f7d7b1cdbf 100644
--- a/spec/javascripts/frequent_items/mock_data.js
+++ b/spec/javascripts/frequent_items/mock_data.js
@@ -68,7 +68,7 @@ export const mockFrequentGroups = [
},
];
-export const mockSearchedGroups = [mockRawGroup];
+export const mockSearchedGroups = { data: [mockRawGroup] };
export const mockProcessedSearchedGroups = [mockGroup];
export const mockProject = {
@@ -135,7 +135,7 @@ export const mockFrequentProjects = [
},
];
-export const mockSearchedProjects = [mockRawProject];
+export const mockSearchedProjects = { data: [mockRawProject] };
export const mockProcessedSearchedProjects = [mockProject];
export const unsortedFrequentItems = [
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
index 0a8525e77d6..7b065b69cce 100644
--- a/spec/javascripts/frequent_items/store/actions_spec.js
+++ b/spec/javascripts/frequent_items/store/actions_spec.js
@@ -169,7 +169,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
it('should dispatch `receiveSearchedItemsSuccess`', done => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
testAction(
actions.fetchSearchedItems,
@@ -178,7 +178,10 @@ describe('Frequent Items Dropdown Store Actions', () => {
[],
[
{ type: 'requestSearchedItems' },
- { type: 'receiveSearchedItemsSuccess', payload: mockSearchedProjects },
+ {
+ type: 'receiveSearchedItemsSuccess',
+ payload: { data: mockSearchedProjects, headers: {} },
+ },
],
done,
);
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
deleted file mode 100644
index 563d134ca81..00000000000
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ /dev/null
@@ -1,152 +0,0 @@
-/* eslint-disable jasmine/no-suite-dupes, vars-on-top, no-var */
-
-import { scaleLinear, scaleTime } from 'd3-scale';
-import { timeParse } from 'd3-time-format';
-import {
- ContributorsGraph,
- ContributorsMasterGraph,
-} from '~/pages/projects/graphs/show/stat_graph_contributors_graph';
-
-const d3 = { scaleLinear, scaleTime, timeParse };
-
-describe('ContributorsGraph', function() {
- describe('#set_x_domain', function() {
- it('set the x_domain', function() {
- ContributorsGraph.set_x_domain(20);
-
- expect(ContributorsGraph.prototype.x_domain).toEqual(20);
- });
- });
-
- describe('#set_y_domain', function() {
- it('sets the y_domain', function() {
- ContributorsGraph.set_y_domain([{ commits: 30 }]);
-
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
- });
- });
-
- describe('#init_x_domain', function() {
- it('sets the initial x_domain', function() {
- ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]);
-
- expect(ContributorsGraph.prototype.x_domain).toEqual(['2012-01-31', '2013-01-31']);
- });
- });
-
- describe('#init_y_domain', function() {
- it('sets the initial y_domain', function() {
- ContributorsGraph.init_y_domain([{ commits: 30 }]);
-
- expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
- });
- });
-
- describe('#init_domain', function() {
- it('calls init_x_domain and init_y_domain', function() {
- spyOn(ContributorsGraph, 'init_x_domain');
- spyOn(ContributorsGraph, 'init_y_domain');
- ContributorsGraph.init_domain();
-
- expect(ContributorsGraph.init_x_domain).toHaveBeenCalled();
- expect(ContributorsGraph.init_y_domain).toHaveBeenCalled();
- });
- });
-
- describe('#set_dates', function() {
- it('sets the dates', function() {
- ContributorsGraph.set_dates('2013-12-01');
-
- expect(ContributorsGraph.prototype.dates).toEqual('2013-12-01');
- });
- });
-
- describe('#set_x_domain', function() {
- it("sets the instance's x domain using the prototype's x_domain", function() {
- ContributorsGraph.prototype.x_domain = 20;
- var instance = new ContributorsGraph();
- instance.x = d3
- .scaleTime()
- .range([0, 100])
- .clamp(true);
- spyOn(instance.x, 'domain');
- instance.set_x_domain();
-
- expect(instance.x.domain).toHaveBeenCalledWith(20);
- });
- });
-
- describe('#set_y_domain', function() {
- it("sets the instance's y domain using the prototype's y_domain", function() {
- ContributorsGraph.prototype.y_domain = 30;
- var instance = new ContributorsGraph();
- instance.y = d3
- .scaleLinear()
- .range([100, 0])
- .nice();
- spyOn(instance.y, 'domain');
- instance.set_y_domain();
-
- expect(instance.y.domain).toHaveBeenCalledWith(30);
- });
- });
-
- describe('#set_domain', function() {
- it('calls set_x_domain and set_y_domain', function() {
- var instance = new ContributorsGraph();
- spyOn(instance, 'set_x_domain');
- spyOn(instance, 'set_y_domain');
- instance.set_domain();
-
- expect(instance.set_x_domain).toHaveBeenCalled();
- expect(instance.set_y_domain).toHaveBeenCalled();
- });
- });
-
- describe('#set_data', function() {
- it('sets the data', function() {
- var instance = new ContributorsGraph();
- instance.set_data('20');
-
- expect(instance.data).toEqual('20');
- });
- });
-});
-
-describe('ContributorsMasterGraph', function() {
- // TODO: fix or remove
- // describe("#process_dates", function () {
- // it("gets and parses dates", function () {
- // var graph = new ContributorsMasterGraph();
- // var data = 'random data here';
- // spyOn(graph, 'parse_dates');
- // spyOn(graph, 'get_dates').andReturn("get");
- // spyOn(ContributorsGraph,'set_dates').andCallThrough();
- // graph.process_dates(data);
- // expect(graph.parse_dates).toHaveBeenCalledWith(data);
- // expect(graph.get_dates).toHaveBeenCalledWith(data);
- // expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get");
- // });
- // });
-
- describe('#get_dates', function() {
- it('plucks the date field from data collection', function() {
- var graph = new ContributorsMasterGraph();
- var data = [{ date: '2013-01-01' }, { date: '2012-12-15' }];
-
- expect(graph.get_dates(data)).toEqual(['2013-01-01', '2012-12-15']);
- });
- });
-
- describe('#parse_dates', function() {
- it('parses the dates', function() {
- var graph = new ContributorsMasterGraph();
- var parseDate = d3.timeParse('%Y-%m-%d');
- var data = [{ date: '2013-01-01' }, { date: '2012-12-15' }];
- var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
- graph.parse_dates(data);
-
- expect(data).toEqual(correct);
- });
- });
-});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js
deleted file mode 100644
index 2ebb6845a8b..00000000000
--- a/spec/javascripts/graphs/stat_graph_contributors_spec.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import ContributorsStatGraph from '~/pages/projects/graphs/show/stat_graph_contributors';
-import { ContributorsGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph';
-
-import { setLanguage } from '../helpers/locale_helper';
-
-describe('ContributorsStatGraph', () => {
- describe('change_date_header', () => {
- beforeAll(() => {
- setLanguage('de');
- });
-
- afterAll(() => {
- setLanguage(null);
- });
-
- it('uses the locale to display date ranges', () => {
- ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]);
- setFixtures('<div id="date_header"></div>');
- const graph = new ContributorsStatGraph();
-
- graph.change_date_header();
-
- expect(document.getElementById('date_header').innerText).toBe(
- '31. Januar 2012 – 31. Januar 2013',
- );
- });
- });
-});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
deleted file mode 100644
index 511b660c671..00000000000
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ /dev/null
@@ -1,298 +0,0 @@
-/* eslint-disable no-var, camelcase, vars-on-top */
-
-import ContributorsStatGraphUtil from '~/pages/projects/graphs/show/stat_graph_contributors_util';
-
-describe('ContributorsStatGraphUtil', function() {
- describe('#parse_log', function() {
- it('returns a correctly parsed log', function() {
- var fake_log = [
- {
- author_email: 'karlo@email.com',
- author_name: 'Karlo Soriano',
- date: '2013-05-09',
- additions: 471,
- },
- {
- author_email: 'dzaporozhets@email.com',
- author_name: 'Dmitriy Zaporozhets',
- date: '2013-05-08',
- additions: 6,
- deletions: 1,
- },
- {
- author_email: 'dzaporozhets@email.com',
- author_name: 'Dmitriy Zaporozhets',
- date: '2013-05-08',
- additions: 19,
- deletions: 3,
- },
- {
- author_email: 'dzaporozhets@email.com',
- author_name: 'Dmitriy Zaporozhets',
- date: '2013-05-08',
- additions: 29,
- deletions: 3,
- },
- ];
-
- var correct_parsed_log = {
- total: [
- { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- ],
- by_author: [
- {
- author_name: 'Karlo Soriano',
- author_email: 'karlo@email.com',
- '2013-05-09': { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- },
- {
- author_name: 'Dmitriy Zaporozhets',
- author_email: 'dzaporozhets@email.com',
- '2013-05-08': { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- },
- ],
- };
-
- expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log);
- });
- });
-
- describe('#store_data', function() {
- var fake_entry = { author: 'Karlo Soriano', date: '2013-05-09', additions: 471 };
- var fake_total = {};
- var fake_by_author = {};
-
- it('calls #store_commits', function() {
- spyOn(ContributorsStatGraphUtil, 'store_commits');
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
-
- expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled();
- });
-
- it('calls #store_additions', function() {
- spyOn(ContributorsStatGraphUtil, 'store_additions');
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
-
- expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled();
- });
-
- it('calls #store_deletions', function() {
- spyOn(ContributorsStatGraphUtil, 'store_deletions');
- ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author);
-
- expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled();
- });
- });
-
- // TODO: fix or remove
- // describe("#store_commits", function () {
- // var fake_total = "fake_total";
- // var fake_by_author = "fake_by_author";
- //
- // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- // spyOn(ContributorsStatGraphUtil, 'add');
- // ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author);
- // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]);
- // });
- // });
-
- describe('#add', function() {
- it('adds 1 to current test_field in collection', function() {
- var fake_collection = { test_field: 10 };
- ContributorsStatGraphUtil.add(fake_collection, 'test_field', 1);
-
- expect(fake_collection.test_field).toEqual(11);
- });
-
- it('inits and adds 1 if test_field in collection is not defined', function() {
- var fake_collection = {};
- ContributorsStatGraphUtil.add(fake_collection, 'test_field', 1);
-
- expect(fake_collection.test_field).toEqual(1);
- });
- });
-
- // TODO: fix or remove
- // describe("#store_additions", function () {
- // var fake_entry = {additions: 10};
- // var fake_total= "fake_total";
- // var fake_by_author = "fake_by_author";
- // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- // spyOn(ContributorsStatGraphUtil, 'add');
- // ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author);
- // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]);
- // });
- // });
-
- // TODO: fix or remove
- // describe("#store_deletions", function () {
- // var fake_entry = {deletions: 10};
- // var fake_total= "fake_total";
- // var fake_by_author = "fake_by_author";
- // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () {
- // spyOn(ContributorsStatGraphUtil, 'add');
- // ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author);
- // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]);
- // });
- // });
-
- describe('#add_date', function() {
- it('adds a date field to the collection', function() {
- var fake_date = '2013-10-02';
- var fake_collection = {};
- ContributorsStatGraphUtil.add_date(fake_date, fake_collection);
-
- expect(fake_collection[fake_date].date).toEqual('2013-10-02');
- });
- });
-
- describe('#add_author', function() {
- it('adds an author field to the collection', function() {
- var fake_author = { author_name: 'Author', author_email: 'fake@email.com' };
- var fake_author_collection = {};
- var fake_email_collection = {};
- ContributorsStatGraphUtil.add_author(
- fake_author,
- fake_author_collection,
- fake_email_collection,
- );
-
- expect(fake_author_collection[fake_author.author_name].author_name).toEqual('Author');
- expect(fake_email_collection[fake_author.author_email].author_name).toEqual('Author');
- });
- });
-
- describe('#get_total_data', function() {
- it('returns the collection sorted via specified field', function() {
- var fake_parsed_log = {
- total: [
- { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- ],
- by_author: [
- {
- author: 'Karlo Soriano',
- '2013-05-09': { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- },
- {
- author: 'Dmitriy Zaporozhets',
- '2013-05-08': { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- },
- ],
- };
- var correct_total_data = [
- { date: '2013-05-08', commits: 3 },
- { date: '2013-05-09', commits: 1 },
- ];
-
- expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, 'commits')).toEqual(
- correct_total_data,
- );
- });
- });
-
- describe('#pick_field', function() {
- it('returns the collection with only the specified field and date', function() {
- var fake_parsed_log_total = [
- { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- ];
- ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, 'commits');
- var correct_pick_field_data = [
- { date: '2013-05-09', commits: 1 },
- { date: '2013-05-08', commits: 3 },
- ];
-
- expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, 'commits')).toEqual(
- correct_pick_field_data,
- );
- });
- });
-
- describe('#get_author_data', function() {
- it('returns the log by author sorted by specified field', function() {
- var fake_parsed_log = {
- total: [
- { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- ],
- by_author: [
- {
- author_name: 'Karlo Soriano',
- author_email: 'karlo@email.com',
- '2013-05-09': { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- },
- {
- author_name: 'Dmitriy Zaporozhets',
- author_email: 'dzaporozhets@email.com',
- '2013-05-08': { date: '2013-05-08', additions: 54, deletions: 7, commits: 3 },
- },
- ],
- };
- var correct_author_data = [
- {
- author_name: 'Dmitriy Zaporozhets',
- author_email: 'dzaporozhets@email.com',
- dates: { '2013-05-08': 3 },
- deletions: 7,
- additions: 54,
- commits: 3,
- },
- {
- author_name: 'Karlo Soriano',
- author_email: 'karlo@email.com',
- dates: { '2013-05-09': 1 },
- deletions: 0,
- additions: 471,
- commits: 1,
- },
- ];
-
- expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, 'commits')).toEqual(
- correct_author_data,
- );
- });
- });
-
- describe('#parse_log_entry', function() {
- it('adds the corresponding info from the log entry to the author', function() {
- var fake_log_entry = {
- author_name: 'Karlo Soriano',
- author_email: 'karlo@email.com',
- '2013-05-09': { date: '2013-05-09', additions: 471, deletions: 0, commits: 1 },
- };
- var correct_parsed_log = {
- author_name: 'Karlo Soriano',
- author_email: 'karlo@email.com',
- dates: { '2013-05-09': 1 },
- deletions: 0,
- additions: 471,
- commits: 1,
- };
-
- expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(
- correct_parsed_log,
- );
- });
- });
-
- describe('#in_range', function() {
- var date = '2013-05-09';
- it('returns true if date_range is null', function() {
- expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true);
- });
-
- it('returns true if date is in range', function() {
- var date_range = [new Date('2013-01-01'), new Date('2013-12-12')];
-
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true);
- });
-
- it('returns false if date is not in range', function() {
- var date_range = [new Date('1999-12-01'), new Date('2000-12-01')];
-
- expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/jobs/stage_spec.js b/spec/javascripts/ide/components/jobs/stage_spec.js
deleted file mode 100644
index fc3831f2d05..00000000000
--- a/spec/javascripts/ide/components/jobs/stage_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import Vue from 'vue';
-import Stage from '~/ide/components/jobs/stage.vue';
-import { stages, jobs } from '../../mock_data';
-
-describe('IDE pipeline stage', () => {
- const Component = Vue.extend(Stage);
- let vm;
- let stage;
-
- beforeEach(() => {
- stage = {
- ...stages[0],
- id: 0,
- dropdownPath: stages[0].dropdown_path,
- jobs: [...jobs],
- isLoading: false,
- isCollapsed: false,
- };
-
- vm = new Component({
- propsData: { stage },
- });
-
- spyOn(vm, '$emit');
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('emits fetch event when mounted', () => {
- expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage);
- });
-
- it('renders stages details', () => {
- expect(vm.$el.textContent).toContain(vm.stage.name);
- });
-
- it('renders CI icon', () => {
- expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null);
- });
-
- describe('collapsed', () => {
- it('emits event when clicking header', done => {
- vm.$el.querySelector('.card-header').click();
-
- vm.$nextTick(() => {
- expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id);
-
- done();
- });
- });
-
- it('toggles collapse status when collapsed', done => {
- vm.stage.isCollapsed = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.card-body').style.display).toBe('none');
-
- done();
- });
- });
-
- it('sets border bottom class when collapsed', done => {
- vm.stage.isCollapsed = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0');
-
- done();
- });
- });
- });
-
- it('renders jobs count', () => {
- expect(vm.$el.querySelector('.badge').textContent).toContain('4');
- });
-
- it('renders loading icon when no jobs and isLoading is true', done => {
- vm.stage.isLoading = true;
- vm.stage.jobs = [];
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
-
- done();
- });
- });
-
- it('renders list of jobs', () => {
- expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4);
- });
-});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index d1b43df74b9..21fb5449858 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -261,10 +261,10 @@ describe('RepoEditor', () => {
});
it('updates state when model content changed', done => {
- vm.model.setValue('testing 123');
+ vm.model.setValue('testing 123\n');
setTimeout(() => {
- expect(vm.file.content).toBe('testing 123');
+ expect(vm.file.content).toBe('testing 123\n');
done();
});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 021c3076094..03d1125c23a 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -182,13 +182,25 @@ describe('IDE store file actions', () => {
spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = `project/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
+
+ store.state.currentProjectId = 'test/test';
+ store.state.currentBranchId = 'master';
+
+ store.state.projects['test/test'] = {
+ branches: {
+ master: {
+ commit: {
+ id: '7297abc',
+ },
+ },
+ },
+ };
});
describe('success', () => {
beforeEach(() => {
- mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
+ mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).replyOnce(
200,
{
blame_path: 'blame_path',
@@ -210,7 +222,7 @@ describe('IDE store file actions', () => {
.dispatch('getFileData', { path: localFile.path })
.then(() => {
expect(service.getFileData).toHaveBeenCalledWith(
- `${RELATIVE_URL_ROOT}/project/getFileDataURL`,
+ `${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`,
);
done();
@@ -229,12 +241,11 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
- it('sets document title', done => {
+ it('sets document title with the branchId', done => {
store
.dispatch('getFileData', { path: localFile.path })
.then(() => {
- expect(document.title).toBe('testing getFileData');
-
+ expect(document.title).toBe(`${localFile.path} · master · test/test · GitLab`);
done();
})
.catch(done.fail);
@@ -283,7 +294,7 @@ describe('IDE store file actions', () => {
localFile.path = 'new-shiny-file';
store.state.entries[localFile.path] = localFile;
- mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
+ mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/old-dull-file`).replyOnce(
200,
{
blame_path: 'blame_path',
@@ -304,7 +315,7 @@ describe('IDE store file actions', () => {
store
.dispatch('getFileData', { path: localFile.path })
.then(() => {
- expect(document.title).toBe('testing new-shiny-file');
+ expect(document.title).toBe(`new-shiny-file · master · test/test · GitLab`);
done();
})
@@ -314,14 +325,17 @@ describe('IDE store file actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`project/getFileDataURL`).networkError();
+ mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).networkError();
});
it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatch');
actions
- .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path })
+ .getFileData(
+ { state: store.state, commit() {}, dispatch, getters: store.getters },
+ { path: localFile.path },
+ )
.then(() => {
expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
text: 'An error occurred whilst loading the file.',
@@ -455,6 +469,8 @@ describe('IDE store file actions', () => {
beforeEach(() => {
tmpFile = file('tmpFile');
+ tmpFile.content = '\n';
+ tmpFile.raw = '\n';
store.state.entries[tmpFile.path] = tmpFile;
});
@@ -462,10 +478,24 @@ describe('IDE store file actions', () => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
+ content: 'content\n',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content\n');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds a newline to the end of the file if it doesnt already exist', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
content: 'content',
})
.then(() => {
- expect(tmpFile.content).toBe('content');
+ expect(tmpFile.content).toBe('content\n');
done();
})
@@ -510,12 +540,12 @@ describe('IDE store file actions', () => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
- content: 'content',
+ content: 'content\n',
})
.then(() =>
store.dispatch('changeFileContent', {
path: tmpFile.path,
- content: '',
+ content: '\n',
}),
)
.then(() => {
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index 4dd0c1150eb..a8894c644be 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -356,8 +356,30 @@ describe('IDE store merge request actions', () => {
changes: [],
};
store.state.entries = {
- foo: {},
- bar: {},
+ foo: {
+ type: 'blob',
+ },
+ bar: {
+ type: 'blob',
+ },
+ };
+
+ store.state.currentProjectId = 'test/test';
+ store.state.currentBranchId = 'master';
+
+ store.state.projects['test/test'] = {
+ branches: {
+ master: {
+ commit: {
+ id: '7297abc',
+ },
+ },
+ abcbranch: {
+ commit: {
+ id: '29020fc',
+ },
+ },
+ },
};
const originalDispatch = store.dispatch;
@@ -415,9 +437,11 @@ describe('IDE store merge request actions', () => {
it('updates activity bar view and gets file data, if changes are found', done => {
store.state.entries.foo = {
url: 'test',
+ type: 'blob',
};
store.state.entries.bar = {
url: 'test',
+ type: 'blob',
};
testMergeRequestChanges.changes = [
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 0c3c4147501..e2d8cc195ae 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -31,7 +31,10 @@ describe('Multi-file store tree actions', () => {
web_url: '',
branches: {
master: {
- workingReference: '1',
+ workingReference: '12345678',
+ commit: {
+ id: '12345678',
+ },
},
},
};
@@ -61,7 +64,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getFiles', basicCallParameters)
.then(() => {
- expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+ expect(service.getFiles).toHaveBeenCalledWith('', '12345678');
done();
})
@@ -99,8 +102,18 @@ describe('Multi-file store tree actions', () => {
store.state.projects = {
'abc/def': {
web_url: `${gl.TEST_HOST}/files`,
+ branches: {
+ 'master-testing': {
+ commit: {
+ id: '12345',
+ },
+ },
+ },
},
};
+ const getters = {
+ findBranch: () => store.state.projects['abc/def'].branches['master-testing'],
+ };
mock.onGet(/(.*)/).replyOnce(500);
@@ -109,6 +122,7 @@ describe('Multi-file store tree actions', () => {
commit() {},
dispatch,
state: store.state,
+ getters,
},
{
projectId: 'abc/def',
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 73a8d993a13..558674cc845 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -163,20 +163,57 @@ describe('IDE store getters', () => {
describe('currentBranch', () => {
it('returns current projects branch', () => {
- const localGetters = {
- currentProject: {
- branches: {
- master: {
- name: 'master',
- },
+ localState.currentProjectId = 'abcproject';
+ localState.currentBranchId = 'master';
+ localState.projects.abcproject = {
+ name: 'abcproject',
+ branches: {
+ master: {
+ name: 'master',
},
},
};
+ const localGetters = {
+ findBranch: jasmine.createSpy('findBranchSpy'),
+ };
+ getters.currentBranch(localState, localGetters);
+
+ expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'master');
+ });
+ });
+
+ describe('findProject', () => {
+ it('returns the project matching the id', () => {
+ localState.currentProjectId = 'abcproject';
+ localState.projects.abcproject = {
+ name: 'abcproject',
+ };
+
+ expect(getters.findProject(localState)('abcproject').name).toBe('abcproject');
+ });
+ });
+
+ describe('findBranch', () => {
+ let result;
+
+ it('returns the selected branch from a project', () => {
+ localState.currentProjectId = 'abcproject';
localState.currentBranchId = 'master';
+ localState.projects.abcproject = {
+ name: 'abcproject',
+ branches: {
+ master: {
+ name: 'master',
+ },
+ },
+ };
+ const localGetters = {
+ findProject: () => localState.projects.abcproject,
+ };
- expect(getters.currentBranch(localState, localGetters)).toEqual({
- name: 'master',
- });
+ result = getters.findBranch(localState, localGetters)('abcproject', 'master');
+
+ expect(result.name).toBe('master');
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 95d927065f0..d464f30b947 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -292,6 +292,8 @@ describe('IDE commit module actions', () => {
type: 'blob',
active: true,
lastCommitSha: TEST_COMMIT_SHA,
+ content: '\n',
+ raw: '\n',
};
Object.assign(store.state, {
@@ -359,7 +361,7 @@ describe('IDE commit module actions', () => {
{
action: commitActionTypes.update,
file_path: jasmine.anything(),
- content: undefined,
+ content: '\n',
encoding: jasmine.anything(),
last_commit_id: undefined,
previous_path: undefined,
@@ -386,7 +388,7 @@ describe('IDE commit module actions', () => {
{
action: commitActionTypes.update,
file_path: jasmine.anything(),
- content: undefined,
+ content: '\n',
encoding: jasmine.anything(),
last_commit_id: TEST_COMMIT_SHA,
previous_path: undefined,
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index a477d4fc200..37290864e3d 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -11,6 +11,23 @@ describe('Multi-file store utils', () => {
});
});
+ describe('setPageTitleForFile', () => {
+ it('sets the document page title for the file passed', () => {
+ const f = {
+ path: 'README.md',
+ };
+
+ const state = {
+ currentBranchId: 'master',
+ currentProjectId: 'test/test',
+ };
+
+ utils.setPageTitleForFile(state, f);
+
+ expect(document.title).toBe('README.md · master · test/test · GitLab');
+ });
+ });
+
describe('findIndexOfFile', () => {
let localState;
@@ -597,4 +614,17 @@ describe('Multi-file store utils', () => {
});
});
});
+
+ describe('addFinalNewlineIfNeeded', () => {
+ it('adds a newline if it doesnt already exist', () => {
+ [
+ { input: 'some text', output: 'some text\n' },
+ { input: 'some text\n', output: 'some text\n' },
+ { input: 'some text\n\n', output: 'some text\n\n' },
+ { input: 'some\n text', output: 'some\n text\n' },
+ ].forEach(({ input, output }) => {
+ expect(utils.addFinalNewlineIfNeeded(input)).toEqual(output);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js
index 5d2ced98ae4..951acfd4e10 100644
--- a/spec/javascripts/issue_show/helpers.js
+++ b/spec/javascripts/issue_show/helpers.js
@@ -1,10 +1 @@
-// eslint-disable-next-line import/prefer-default-export
-export const keyboardDownEvent = (code, metaKey = false, ctrlKey = false) => {
- const e = new CustomEvent('keydown');
-
- e.keyCode = code;
- e.metaKey = metaKey;
- e.ctrlKey = ctrlKey;
-
- return e;
-};
+export * from '../../frontend/issue_show/helpers.js';
diff --git a/spec/javascripts/lib/utils/tick_formats_spec.js b/spec/javascripts/lib/utils/tick_formats_spec.js
deleted file mode 100644
index 283989b4fc8..00000000000
--- a/spec/javascripts/lib/utils/tick_formats_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { dateTickFormat, initDateFormats } from '~/lib/utils/tick_formats';
-
-import { setLanguage } from '../../helpers/locale_helper';
-
-describe('tick formats', () => {
- describe('dateTickFormat', () => {
- beforeAll(() => {
- setLanguage('de');
- initDateFormats();
- });
-
- afterAll(() => {
- setLanguage(null);
- });
-
- it('returns year for first of January', () => {
- const tick = dateTickFormat(new Date('2001-01-01'));
-
- expect(tick).toBe('2001');
- });
-
- it('returns month for first of February', () => {
- const tick = dateTickFormat(new Date('2001-02-01'));
-
- expect(tick).toBe('Februar');
- });
-
- it('returns day and month for second of February', () => {
- const tick = dateTickFormat(new Date('2001-02-02'));
-
- expect(tick).toBe('2. Feb.');
- });
-
- it('ignores time', () => {
- const tick = dateTickFormat(new Date('2001-02-02 12:34:56'));
-
- expect(tick).toBe('2. Feb.');
- });
- });
-});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 72d6e832aca..54071ccc5c2 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-return-assign */
-
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -22,7 +20,8 @@ describe('MergeRequest', function() {
.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
.reply(200, {});
- return (this.merge = new MergeRequest());
+ this.merge = new MergeRequest();
+ return this.merge;
});
afterEach(() => {
@@ -34,10 +33,30 @@ describe('MergeRequest', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]')
+ .first()
+ .attr('checked', true)[0]
+ .dispatchEvent(changeEvent);
+ setTimeout(() => {
+ expect($('.js-task-list-field').val()).toBe(
+ '- [x] Task List Item\n- [ ] \n- [ ] Task List Item 2\n',
+ );
+ done();
+ });
+ });
+
+ it('ensure that task with only spaces does not get checked incorrectly', done => {
+ // fixed in 'deckar01-task_list', '2.2.1' gem
+ spyOn($, 'ajax').and.stub();
+ const changeEvent = document.createEvent('HTMLEvents');
+ changeEvent.initEvent('change', true, true);
+ $('input[type=checkbox]')
+ .last()
.attr('checked', true)[0]
.dispatchEvent(changeEvent);
setTimeout(() => {
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ expect($('.js-task-list-field').val()).toBe(
+ '- [ ] Task List Item\n- [ ] \n- [x] Task List Item 2\n',
+ );
done();
});
});
@@ -59,7 +78,7 @@ describe('MergeRequest', function() {
`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
{
merge_request: {
- description: '- [ ] Task List Item',
+ description: '- [ ] Task List Item\n- [ ] \n- [ ] Task List Item 2\n',
lock_version: 0,
update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
},
@@ -70,7 +89,8 @@ describe('MergeRequest', function() {
});
});
- it('shows an error notification when tasklist update failed', done => {
+ // eslint-disable-next-line jasmine/no-disabled-tests
+ xit('shows an error notification when tasklist update failed', done => {
mock
.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
.reply(409, {});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index b424cbc866d..73b1ea4d36f 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-var */
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -11,9 +10,9 @@ import initMrPage from './helpers/init_vue_mr_page_helper';
describe('MergeRequestTabs', function() {
let mrPageMock;
- var stubLocation = {};
- var setLocation = function(stubs) {
- var defaults = {
+ const stubLocation = {};
+ const setLocation = function(stubs) {
+ const defaults = {
pathname: '',
search: '',
hash: '',
@@ -44,9 +43,9 @@ describe('MergeRequestTabs', function() {
});
describe('opensInNewTab', function() {
- var tabUrl;
- var windowTarget = '_blank';
+ const windowTarget = '_blank';
let clickTabParams;
+ let tabUrl;
beforeEach(function() {
loadFixtures('merge_requests/merge_request_with_task_list.html');
@@ -193,11 +192,10 @@ describe('MergeRequestTabs', function() {
});
it('replaces the current history state', function() {
- var newState;
setLocation({
pathname: '/foo/bar/merge_requests/1',
});
- newState = this.subject('commits');
+ const newState = this.subject('commits');
expect(this.spies.history).toHaveBeenCalledWith(
{
diff --git a/spec/javascripts/monitoring/charts/heatmap_spec.js b/spec/javascripts/monitoring/charts/heatmap_spec.js
new file mode 100644
index 00000000000..9a98fc6fb05
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/heatmap_spec.js
@@ -0,0 +1,69 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlHeatmap } from '@gitlab/ui/dist/charts';
+import Heatmap from '~/monitoring/components/charts/heatmap.vue';
+import { graphDataPrometheusQueryRangeMultiTrack } from '../mock_data';
+
+describe('Heatmap component', () => {
+ let heatmapChart;
+ let store;
+
+ beforeEach(() => {
+ heatmapChart = shallowMount(Heatmap, {
+ propsData: {
+ graphData: graphDataPrometheusQueryRangeMultiTrack,
+ containerWidth: 100,
+ },
+ store,
+ });
+ });
+
+ afterEach(() => {
+ heatmapChart.destroy();
+ });
+
+ describe('wrapped components', () => {
+ describe('GitLab UI heatmap chart', () => {
+ let glHeatmapChart;
+
+ beforeEach(() => {
+ glHeatmapChart = heatmapChart.find(GlHeatmap);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glHeatmapChart.isVueInstance()).toBe(true);
+ });
+
+ it('should display a label on the x axis', () => {
+ expect(heatmapChart.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label);
+ });
+
+ it('should display a label on the y axis', () => {
+ expect(heatmapChart.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label);
+ });
+
+ // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data
+ // each row of the heatmap chart is represented by an array inside another parent array
+ // e.g. [[0, 0, 10]], the format represents the column, the row and finally the value
+ // corresponding to the cell
+
+ it('should return chartData with a length of x by y, with a length of 3 per array', () => {
+ const row = heatmapChart.vm.chartData[0];
+
+ expect(row.length).toBe(3);
+ expect(heatmapChart.vm.chartData.length).toBe(30);
+ });
+
+ it('returns a series of labels for the x axis', () => {
+ const { xAxisLabels } = heatmapChart.vm;
+
+ expect(xAxisLabels.length).toBe(5);
+ });
+
+ it('returns a series of labels for the y axis', () => {
+ const { yAxisLabels } = heatmapChart.vm;
+
+ expect(yAxisLabels.length).toBe(6);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 75df2ce3103..0f20171726c 100644
--- a/spec/javascripts/monitoring/components/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -7,11 +7,12 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
-import MonitoringMock, {
+import {
metricsGroupsAPIResponse,
+ mockedQueryResultPayload,
+ mockedQueryResultPayloadCoresTotal,
mockApiEndpoint,
environmentData,
- singleGroupResponse,
dashboardGitResponse,
} from '../mock_data';
@@ -44,12 +45,33 @@ const resetSpy = spy => {
export default propsData;
+function setupComponentStore(component) {
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ metricsGroupsAPIResponse,
+ );
+
+ // Load 2 panels to the dashboard
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_QUERY_RESULT}`,
+ mockedQueryResultPayload,
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_QUERY_RESULT}`,
+ mockedQueryResultPayloadCoresTotal,
+ );
+
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+}
+
describe('Dashboard', () => {
let DashboardComponent;
let mock;
let store;
let component;
- let mockGraphData;
beforeEach(() => {
setFixtures(`
@@ -100,6 +122,32 @@ describe('Dashboard', () => {
});
});
+ describe('cluster health', () => {
+ let wrapper;
+
+ beforeEach(done => {
+ wrapper = shallowMount(DashboardComponent, {
+ localVue,
+ sync: false,
+ propsData: { ...propsData, hasMetrics: true },
+ store,
+ });
+
+ // all_dashboards is not defined in health dashboards
+ wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
+ wrapper.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correctly', () => {
+ expect(wrapper.isVueInstance()).toBe(true);
+ expect(wrapper.exists()).toBe(true);
+ });
+ });
+
describe('requests information to the server', () => {
let spy;
beforeEach(() => {
@@ -123,25 +171,6 @@ describe('Dashboard', () => {
});
});
- it('hides the legend when showLegend is false', done => {
- component = new DashboardComponent({
- el: document.querySelector('.prometheus-graphs'),
- propsData: {
- ...propsData,
- hasMetrics: true,
- showLegend: false,
- },
- store,
- });
-
- setTimeout(() => {
- expect(component.showEmptyState).toEqual(false);
- expect(component.$el.querySelector('.legend-group')).toEqual(null);
- expect(component.$el.querySelector('.prometheus-graph-group')).toBeTruthy();
- done();
- });
- });
-
it('hides the group panels when showPanels is false', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
@@ -153,52 +182,66 @@ describe('Dashboard', () => {
store,
});
- setTimeout(() => {
- expect(component.showEmptyState).toEqual(false);
- expect(component.$el.querySelector('.prometheus-panel')).toEqual(null);
- expect(component.$el.querySelector('.prometheus-graph-group')).toBeTruthy();
- done();
- });
+ setupComponentStore(component);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(component.showEmptyState).toEqual(false);
+ expect(component.$el.querySelector('.prometheus-panel')).toEqual(null);
+ expect(component.$el.querySelector('.prometheus-graph-group')).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
});
- it('renders the environments dropdown with a number of environments', done => {
- component = new DashboardComponent({
- el: document.querySelector('.prometheus-graphs'),
- propsData: {
- ...propsData,
- hasMetrics: true,
- showPanels: false,
- },
- store,
+ describe('when all the requests have been commited by the store', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ },
+ store,
+ });
+
+ setupComponentStore(component);
});
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- singleGroupResponse,
- );
+ it('renders the environments dropdown with a number of environments', done => {
+ Vue.nextTick()
+ .then(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item',
+ );
- Vue.nextTick()
- .then(() => {
- const dropdownMenuEnvironments = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item',
- );
+ expect(component.environments.length).toEqual(environmentData.length);
+ expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
- expect(component.environments.length).toEqual(environmentData.length);
- expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
+ Array.from(dropdownMenuEnvironments).forEach((value, index) => {
+ if (environmentData[index].metrics_path) {
+ expect(value).toHaveAttr('href', environmentData[index].metrics_path);
+ }
+ });
- Array.from(dropdownMenuEnvironments).forEach((value, index) => {
- if (environmentData[index].metrics_path) {
- expect(value).toHaveAttr('href', environmentData[index].metrics_path);
- }
- });
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
+ it('renders the environments dropdown with a single active element', done => {
+ Vue.nextTick()
+ .then(() => {
+ const dropdownItems = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item.active',
+ );
+
+ expect(dropdownItems.length).toEqual(1);
+ done();
+ })
+ .catch(done.fail);
+ });
});
it('hides the environments dropdown list when there is no environments', done => {
@@ -207,15 +250,17 @@ describe('Dashboard', () => {
propsData: {
...propsData,
hasMetrics: true,
- showPanels: false,
},
store,
});
- component.$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, []);
component.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- singleGroupResponse,
+ metricsGroupsAPIResponse,
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_QUERY_RESULT}`,
+ mockedQueryResultPayload,
);
Vue.nextTick()
@@ -230,7 +275,7 @@ describe('Dashboard', () => {
.catch(done.fail);
});
- it('renders the environments dropdown with a single active element', done => {
+ it('renders the datetimepicker dropdown', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
@@ -241,64 +286,16 @@ describe('Dashboard', () => {
store,
});
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- singleGroupResponse,
- );
+ setupComponentStore(component);
Vue.nextTick()
.then(() => {
- const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item.active',
- );
-
- expect(dropdownItems.length).toEqual(1);
+ expect(component.$el.querySelector('.js-time-window-dropdown')).not.toBeNull();
done();
})
.catch(done.fail);
});
- it('hides the dropdown', done => {
- component = new DashboardComponent({
- el: document.querySelector('.prometheus-graphs'),
- propsData: {
- ...propsData,
- hasMetrics: true,
- showPanels: false,
- environmentsEndpoint: '',
- },
- store,
- });
-
- Vue.nextTick(() => {
- const dropdownIsActiveElement = component.$el.querySelectorAll('.environments');
-
- expect(dropdownIsActiveElement.length).toEqual(0);
- done();
- });
- });
-
- it('renders the datetimepicker dropdown', done => {
- component = new DashboardComponent({
- el: document.querySelector('.prometheus-graphs'),
- propsData: {
- ...propsData,
- hasMetrics: true,
- showPanels: false,
- },
- store,
- });
-
- setTimeout(() => {
- expect(component.$el.querySelector('.js-time-window-dropdown')).not.toBeNull();
- done();
- });
- });
-
it('fetches the metrics data with proper time window', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
@@ -347,14 +344,21 @@ describe('Dashboard', () => {
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true },
store,
+ sync: false,
});
- setTimeout(() => {
- const selectedTimeWindow = component.$el.querySelector('.js-time-window-dropdown .active');
+ setupComponentStore(component);
- expect(selectedTimeWindow.textContent.trim()).toEqual('30 minutes');
- done();
- });
+ Vue.nextTick()
+ .then(() => {
+ const selectedTimeWindow = component.$el.querySelector(
+ '.js-time-window-dropdown .active',
+ );
+
+ expect(selectedTimeWindow.textContent.trim()).toEqual('30 minutes');
+ done();
+ })
+ .catch(done.fail);
});
it('shows an error message if invalid url parameters are passed', done => {
@@ -381,29 +385,36 @@ describe('Dashboard', () => {
describe('drag and drop function', () => {
let wrapper;
let expectedPanelCount; // also called metrics, naming to be improved: https://gitlab.com/gitlab-org/gitlab/issues/31565
+
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
- beforeEach(done => {
+ beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
- expectedPanelCount = metricsGroupsAPIResponse.data.reduce(
- (acc, d) => d.metrics.length + acc,
+ expectedPanelCount = metricsGroupsAPIResponse.reduce(
+ (acc, group) => group.panels.length + acc,
0,
);
- store.dispatch('monitoringDashboard/setFeatureFlags', { additionalPanelTypesEnabled: true });
+ });
+ beforeEach(done => {
wrapper = shallowMount(DashboardComponent, {
localVue,
sync: false,
propsData: { ...propsData, hasMetrics: true },
store,
+ attachToDocument: true,
});
- // not using $nextTicket becuase we must wait for the dashboard
- // to be populated with the mock data results.
- setTimeout(done);
+ setupComponentStore(wrapper.vm);
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
it('wraps vuedraggable', () => {
@@ -442,6 +453,28 @@ describe('Dashboard', () => {
expect(findEnabledDraggables()).toEqual(findDraggables());
});
+ it('metrics can be swapped', done => {
+ const firstDraggable = findDraggables().at(0);
+ const mockMetrics = [...metricsGroupsAPIResponse[0].panels];
+ const value = () => firstDraggable.props('value');
+
+ expect(value().length).toBe(mockMetrics.length);
+ value().forEach((metric, i) => {
+ expect(metric.title).toBe(mockMetrics[i].title);
+ });
+
+ // swap two elements and `input` them
+ [mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]];
+ firstDraggable.vm.$emit('input', mockMetrics);
+
+ firstDraggable.vm.$nextTick(() => {
+ value().forEach((metric, i) => {
+ expect(metric.title).toBe(mockMetrics[i].title);
+ });
+ done();
+ });
+ });
+
it('shows a remove button, which removes a panel', done => {
expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false);
@@ -449,8 +482,6 @@ describe('Dashboard', () => {
findFirstDraggableRemoveButton().trigger('click');
wrapper.vm.$nextTick(() => {
- // At present graphs will not be removed in backend
- // See https://gitlab.com/gitlab-org/gitlab/issues/27835
expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1);
done();
});
@@ -466,10 +497,6 @@ describe('Dashboard', () => {
});
});
});
-
- afterEach(() => {
- wrapper.destroy();
- });
});
// https://gitlab.com/gitlab-org/gitlab-ce/issues/66922
@@ -539,42 +566,93 @@ describe('Dashboard', () => {
});
});
- describe('when the window resizes', () => {
+ describe('responds to window resizes', () => {
+ let promPanel;
+ let promGroup;
+ let panelToggle;
+ let chart;
beforeEach(() => {
mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
- jasmine.clock().install();
- });
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- it('sets elWidth to page width when the sidebar is resized', done => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
...propsData,
hasMetrics: true,
- showPanels: false,
+ showPanels: true,
},
store,
});
- expect(component.elWidth).toEqual(0);
+ setupComponentStore(component);
- const pageLayoutEl = document.querySelector('.layout-page');
- pageLayoutEl.classList.add('page-with-icon-sidebar');
+ return Vue.nextTick().then(() => {
+ promPanel = component.$el.querySelector('.prometheus-panel');
+ promGroup = promPanel.querySelector('.prometheus-graph-group');
+ panelToggle = promPanel.querySelector('.js-graph-group-toggle');
+ chart = promGroup.querySelector('.position-relative svg');
+ });
+ });
- Vue.nextTick()
- .then(() => {
- jasmine.clock().tick(1000);
- return Vue.nextTick();
- })
- .then(() => {
- expect(component.elWidth).toEqual(pageLayoutEl.clientWidth);
- done();
- })
- .catch(done.fail);
+ 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);
+ });
+ });
+
+ describe('dashboard edit link', () => {
+ let wrapper;
+ const findEditLink = () => wrapper.find('.js-edit-link');
+
+ beforeEach(done => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+
+ wrapper = shallowMount(DashboardComponent, {
+ localVue,
+ sync: false,
+ attachToDocument: true,
+ propsData: { ...propsData, hasMetrics: true },
+ store,
+ });
+
+ wrapper.vm.$store.commit(
+ `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
+ dashboardGitResponse,
+ );
+ wrapper.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is not present for the default dashboard', () => {
+ expect(findEditLink().exists()).toBe(false);
+ });
+
+ it('is present for a custom dashboard, and links to its edit_path', done => {
+ const dashboard = dashboardGitResponse[1]; // non-default dashboard
+ const currentDashboard = dashboard.path;
+
+ wrapper.setProps({ currentDashboard });
+ wrapper.vm.$nextTick(() => {
+ expect(findEditLink().exists()).toBe(true);
+ expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
+ done();
+ });
});
});
@@ -619,20 +697,6 @@ describe('Dashboard', () => {
store,
});
- component.$store.dispatch('monitoringDashboard/setFeatureFlags', {
- prometheusEndpoint: false,
- });
-
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
-
- component.$store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- singleGroupResponse,
- );
-
component.$store.commit(
`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
dashboardGitResponse,
@@ -648,36 +712,4 @@ describe('Dashboard', () => {
});
});
});
-
- describe('when downloading metrics data as CSV', () => {
- beforeEach(() => {
- component = new DashboardComponent({
- propsData: {
- ...propsData,
- },
- store,
- });
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
- MonitoringMock.data,
- );
- [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics;
- });
-
- describe('csvText', () => {
- it('converts metrics data from json to csv', () => {
- const header = `timestamp,${mockGraphData.y_label}`;
- const data = mockGraphData.queries[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
-
- expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`);
- });
- });
-
- describe('downloadCsv', () => {
- it('produces a link with a Blob', () => {
- expect(component.downloadCsv(mockGraphData)).toContain(`blob:`);
- });
- });
- });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 17e7314e214..f9cc839bde6 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,943 +1,103 @@
-export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+import {
+ anomalyMockGraphData as importedAnomalyMockGraphData,
+ metricsGroupsAPIResponse as importedMetricsGroupsAPIResponse,
+ environmentData as importedEnvironmentData,
+ dashboardGitResponse as importedDashboardGitResponse,
+} from '../../frontend/monitoring/mock_data';
-export const mockProjectPath = '/frontend-fixtures/environments-project';
+export const anomalyMockGraphData = importedAnomalyMockGraphData;
+export const metricsGroupsAPIResponse = importedMetricsGroupsAPIResponse;
+export const environmentData = importedEnvironmentData;
+export const dashboardGitResponse = importedDashboardGitResponse;
-export const metricsGroupsAPIResponse = {
- success: true,
- data: [
- {
- group: 'Kubernetes',
- priority: 1,
- metrics: [
- {
- id: 5,
- title: 'Memory usage',
- weight: 1,
- queries: [
- {
- query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
- label: 'Memory',
- unit: 'MiB',
- result: [
- {
- metric: {},
- values: [
- [1495700554.925, '8.0390625'],
- [1495700614.925, '8.0390625'],
- [1495700674.925, '8.0390625'],
- [1495700734.925, '8.0390625'],
- [1495700794.925, '8.0390625'],
- [1495700854.925, '8.0390625'],
- [1495700914.925, '8.0390625'],
- [1495700974.925, '8.0390625'],
- [1495701034.925, '8.0390625'],
- [1495701094.925, '8.0390625'],
- [1495701154.925, '8.0390625'],
- [1495701214.925, '8.0390625'],
- [1495701274.925, '8.0390625'],
- [1495701334.925, '8.0390625'],
- [1495701394.925, '8.0390625'],
- [1495701454.925, '8.0390625'],
- [1495701514.925, '8.0390625'],
- [1495701574.925, '8.0390625'],
- [1495701634.925, '8.0390625'],
- [1495701694.925, '8.0390625'],
- [1495701754.925, '8.0390625'],
- [1495701814.925, '8.0390625'],
- [1495701874.925, '8.0390625'],
- [1495701934.925, '8.0390625'],
- [1495701994.925, '8.0390625'],
- [1495702054.925, '8.0390625'],
- [1495702114.925, '8.0390625'],
- [1495702174.925, '8.0390625'],
- [1495702234.925, '8.0390625'],
- [1495702294.925, '8.0390625'],
- [1495702354.925, '8.0390625'],
- [1495702414.925, '8.0390625'],
- [1495702474.925, '8.0390625'],
- [1495702534.925, '8.0390625'],
- [1495702594.925, '8.0390625'],
- [1495702654.925, '8.0390625'],
- [1495702714.925, '8.0390625'],
- [1495702774.925, '8.0390625'],
- [1495702834.925, '8.0390625'],
- [1495702894.925, '8.0390625'],
- [1495702954.925, '8.0390625'],
- [1495703014.925, '8.0390625'],
- [1495703074.925, '8.0390625'],
- [1495703134.925, '8.0390625'],
- [1495703194.925, '8.0390625'],
- [1495703254.925, '8.03515625'],
- [1495703314.925, '8.03515625'],
- [1495703374.925, '8.03515625'],
- [1495703434.925, '8.03515625'],
- [1495703494.925, '8.03515625'],
- [1495703554.925, '8.03515625'],
- [1495703614.925, '8.03515625'],
- [1495703674.925, '8.03515625'],
- [1495703734.925, '8.03515625'],
- [1495703794.925, '8.03515625'],
- [1495703854.925, '8.03515625'],
- [1495703914.925, '8.03515625'],
- [1495703974.925, '8.03515625'],
- [1495704034.925, '8.03515625'],
- [1495704094.925, '8.03515625'],
- [1495704154.925, '8.03515625'],
- [1495704214.925, '7.9296875'],
- [1495704274.925, '7.9296875'],
- [1495704334.925, '7.9296875'],
- [1495704394.925, '7.9296875'],
- [1495704454.925, '7.9296875'],
- [1495704514.925, '7.9296875'],
- [1495704574.925, '7.9296875'],
- [1495704634.925, '7.9296875'],
- [1495704694.925, '7.9296875'],
- [1495704754.925, '7.9296875'],
- [1495704814.925, '7.9296875'],
- [1495704874.925, '7.9296875'],
- [1495704934.925, '7.9296875'],
- [1495704994.925, '7.9296875'],
- [1495705054.925, '7.9296875'],
- [1495705114.925, '7.9296875'],
- [1495705174.925, '7.9296875'],
- [1495705234.925, '7.9296875'],
- [1495705294.925, '7.9296875'],
- [1495705354.925, '7.9296875'],
- [1495705414.925, '7.9296875'],
- [1495705474.925, '7.9296875'],
- [1495705534.925, '7.9296875'],
- [1495705594.925, '7.9296875'],
- [1495705654.925, '7.9296875'],
- [1495705714.925, '7.9296875'],
- [1495705774.925, '7.9296875'],
- [1495705834.925, '7.9296875'],
- [1495705894.925, '7.9296875'],
- [1495705954.925, '7.9296875'],
- [1495706014.925, '7.9296875'],
- [1495706074.925, '7.9296875'],
- [1495706134.925, '7.9296875'],
- [1495706194.925, '7.9296875'],
- [1495706254.925, '7.9296875'],
- [1495706314.925, '7.9296875'],
- [1495706374.925, '7.9296875'],
- [1495706434.925, '7.9296875'],
- [1495706494.925, '7.9296875'],
- [1495706554.925, '7.9296875'],
- [1495706614.925, '7.9296875'],
- [1495706674.925, '7.9296875'],
- [1495706734.925, '7.9296875'],
- [1495706794.925, '7.9296875'],
- [1495706854.925, '7.9296875'],
- [1495706914.925, '7.9296875'],
- [1495706974.925, '7.9296875'],
- [1495707034.925, '7.9296875'],
- [1495707094.925, '7.9296875'],
- [1495707154.925, '7.9296875'],
- [1495707214.925, '7.9296875'],
- [1495707274.925, '7.9296875'],
- [1495707334.925, '7.9296875'],
- [1495707394.925, '7.9296875'],
- [1495707454.925, '7.9296875'],
- [1495707514.925, '7.9296875'],
- [1495707574.925, '7.9296875'],
- [1495707634.925, '7.9296875'],
- [1495707694.925, '7.9296875'],
- [1495707754.925, '7.9296875'],
- [1495707814.925, '7.9296875'],
- [1495707874.925, '7.9296875'],
- [1495707934.925, '7.9296875'],
- [1495707994.925, '7.9296875'],
- [1495708054.925, '7.9296875'],
- [1495708114.925, '7.9296875'],
- [1495708174.925, '7.9296875'],
- [1495708234.925, '7.9296875'],
- [1495708294.925, '7.9296875'],
- [1495708354.925, '7.9296875'],
- [1495708414.925, '7.9296875'],
- [1495708474.925, '7.9296875'],
- [1495708534.925, '7.9296875'],
- [1495708594.925, '7.9296875'],
- [1495708654.925, '7.9296875'],
- [1495708714.925, '7.9296875'],
- [1495708774.925, '7.9296875'],
- [1495708834.925, '7.9296875'],
- [1495708894.925, '7.9296875'],
- [1495708954.925, '7.8984375'],
- [1495709014.925, '7.8984375'],
- [1495709074.925, '7.8984375'],
- [1495709134.925, '7.8984375'],
- [1495709194.925, '7.8984375'],
- [1495709254.925, '7.89453125'],
- [1495709314.925, '7.89453125'],
- [1495709374.925, '7.89453125'],
- [1495709434.925, '7.89453125'],
- [1495709494.925, '7.89453125'],
- [1495709554.925, '7.89453125'],
- [1495709614.925, '7.89453125'],
- [1495709674.925, '7.89453125'],
- [1495709734.925, '7.89453125'],
- [1495709794.925, '7.89453125'],
- [1495709854.925, '7.89453125'],
- [1495709914.925, '7.89453125'],
- [1495709974.925, '7.89453125'],
- [1495710034.925, '7.89453125'],
- [1495710094.925, '7.89453125'],
- [1495710154.925, '7.89453125'],
- [1495710214.925, '7.89453125'],
- [1495710274.925, '7.89453125'],
- [1495710334.925, '7.89453125'],
- [1495710394.925, '7.89453125'],
- [1495710454.925, '7.89453125'],
- [1495710514.925, '7.89453125'],
- [1495710574.925, '7.89453125'],
- [1495710634.925, '7.89453125'],
- [1495710694.925, '7.89453125'],
- [1495710754.925, '7.89453125'],
- [1495710814.925, '7.89453125'],
- [1495710874.925, '7.89453125'],
- [1495710934.925, '7.89453125'],
- [1495710994.925, '7.89453125'],
- [1495711054.925, '7.89453125'],
- [1495711114.925, '7.89453125'],
- [1495711174.925, '7.8515625'],
- [1495711234.925, '7.8515625'],
- [1495711294.925, '7.8515625'],
- [1495711354.925, '7.8515625'],
- [1495711414.925, '7.8515625'],
- [1495711474.925, '7.8515625'],
- [1495711534.925, '7.8515625'],
- [1495711594.925, '7.8515625'],
- [1495711654.925, '7.8515625'],
- [1495711714.925, '7.8515625'],
- [1495711774.925, '7.8515625'],
- [1495711834.925, '7.8515625'],
- [1495711894.925, '7.8515625'],
- [1495711954.925, '7.8515625'],
- [1495712014.925, '7.8515625'],
- [1495712074.925, '7.8515625'],
- [1495712134.925, '7.8515625'],
- [1495712194.925, '7.8515625'],
- [1495712254.925, '7.8515625'],
- [1495712314.925, '7.8515625'],
- [1495712374.925, '7.8515625'],
- [1495712434.925, '7.83203125'],
- [1495712494.925, '7.83203125'],
- [1495712554.925, '7.83203125'],
- [1495712614.925, '7.83203125'],
- [1495712674.925, '7.83203125'],
- [1495712734.925, '7.83203125'],
- [1495712794.925, '7.83203125'],
- [1495712854.925, '7.83203125'],
- [1495712914.925, '7.83203125'],
- [1495712974.925, '7.83203125'],
- [1495713034.925, '7.83203125'],
- [1495713094.925, '7.83203125'],
- [1495713154.925, '7.83203125'],
- [1495713214.925, '7.83203125'],
- [1495713274.925, '7.83203125'],
- [1495713334.925, '7.83203125'],
- [1495713394.925, '7.8125'],
- [1495713454.925, '7.8125'],
- [1495713514.925, '7.8125'],
- [1495713574.925, '7.8125'],
- [1495713634.925, '7.8125'],
- [1495713694.925, '7.8125'],
- [1495713754.925, '7.8125'],
- [1495713814.925, '7.8125'],
- [1495713874.925, '7.8125'],
- [1495713934.925, '7.8125'],
- [1495713994.925, '7.8125'],
- [1495714054.925, '7.8125'],
- [1495714114.925, '7.8125'],
- [1495714174.925, '7.8125'],
- [1495714234.925, '7.8125'],
- [1495714294.925, '7.8125'],
- [1495714354.925, '7.80859375'],
- [1495714414.925, '7.80859375'],
- [1495714474.925, '7.80859375'],
- [1495714534.925, '7.80859375'],
- [1495714594.925, '7.80859375'],
- [1495714654.925, '7.80859375'],
- [1495714714.925, '7.80859375'],
- [1495714774.925, '7.80859375'],
- [1495714834.925, '7.80859375'],
- [1495714894.925, '7.80859375'],
- [1495714954.925, '7.80859375'],
- [1495715014.925, '7.80859375'],
- [1495715074.925, '7.80859375'],
- [1495715134.925, '7.80859375'],
- [1495715194.925, '7.80859375'],
- [1495715254.925, '7.80859375'],
- [1495715314.925, '7.80859375'],
- [1495715374.925, '7.80859375'],
- [1495715434.925, '7.80859375'],
- [1495715494.925, '7.80859375'],
- [1495715554.925, '7.80859375'],
- [1495715614.925, '7.80859375'],
- [1495715674.925, '7.80859375'],
- [1495715734.925, '7.80859375'],
- [1495715794.925, '7.80859375'],
- [1495715854.925, '7.80859375'],
- [1495715914.925, '7.80078125'],
- [1495715974.925, '7.80078125'],
- [1495716034.925, '7.80078125'],
- [1495716094.925, '7.80078125'],
- [1495716154.925, '7.80078125'],
- [1495716214.925, '7.796875'],
- [1495716274.925, '7.796875'],
- [1495716334.925, '7.796875'],
- [1495716394.925, '7.796875'],
- [1495716454.925, '7.796875'],
- [1495716514.925, '7.796875'],
- [1495716574.925, '7.796875'],
- [1495716634.925, '7.796875'],
- [1495716694.925, '7.796875'],
- [1495716754.925, '7.796875'],
- [1495716814.925, '7.796875'],
- [1495716874.925, '7.79296875'],
- [1495716934.925, '7.79296875'],
- [1495716994.925, '7.79296875'],
- [1495717054.925, '7.79296875'],
- [1495717114.925, '7.79296875'],
- [1495717174.925, '7.7890625'],
- [1495717234.925, '7.7890625'],
- [1495717294.925, '7.7890625'],
- [1495717354.925, '7.7890625'],
- [1495717414.925, '7.7890625'],
- [1495717474.925, '7.7890625'],
- [1495717534.925, '7.7890625'],
- [1495717594.925, '7.7890625'],
- [1495717654.925, '7.7890625'],
- [1495717714.925, '7.7890625'],
- [1495717774.925, '7.7890625'],
- [1495717834.925, '7.77734375'],
- [1495717894.925, '7.77734375'],
- [1495717954.925, '7.77734375'],
- [1495718014.925, '7.77734375'],
- [1495718074.925, '7.77734375'],
- [1495718134.925, '7.7421875'],
- [1495718194.925, '7.7421875'],
- [1495718254.925, '7.7421875'],
- [1495718314.925, '7.7421875'],
- ],
- },
- ],
- },
- ],
- },
- {
- id: 6,
- title: 'CPU usage',
- y_label: 'CPU',
- weight: 1,
- queries: [
- {
- appearance: {
- line: {
- width: 2,
- },
- },
- query_range:
- 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
- label: 'Core Usage',
- unit: 'Cores',
- result: [
- {
- metric: {},
- values: [
- [1495700554.925, '0.0010794445585559514'],
- [1495700614.925, '0.003927214935433527'],
- [1495700674.925, '0.0053045219047619975'],
- [1495700734.925, '0.0048892095238097155'],
- [1495700794.925, '0.005827140952381137'],
- [1495700854.925, '0.00569846906219937'],
- [1495700914.925, '0.004972616802849382'],
- [1495700974.925, '0.005117509523809902'],
- [1495701034.925, '0.00512389061919564'],
- [1495701094.925, '0.005199100501890691'],
- [1495701154.925, '0.005415746394885837'],
- [1495701214.925, '0.005607682788146286'],
- [1495701274.925, '0.005641300000000118'],
- [1495701334.925, '0.0071166279368766495'],
- [1495701394.925, '0.0063242138095234044'],
- [1495701454.925, '0.005793314698235304'],
- [1495701514.925, '0.00703934942237556'],
- [1495701574.925, '0.006357007076123191'],
- [1495701634.925, '0.003753167300126738'],
- [1495701694.925, '0.005018469678430698'],
- [1495701754.925, '0.0045217153371887'],
- [1495701814.925, '0.006140104285714119'],
- [1495701874.925, '0.004818684285714102'],
- [1495701934.925, '0.005079509718955242'],
- [1495701994.925, '0.005059981142498263'],
- [1495702054.925, '0.005269098389538773'],
- [1495702114.925, '0.005269954285714175'],
- [1495702174.925, '0.014199241435795856'],
- [1495702234.925, '0.01511936843111017'],
- [1495702294.925, '0.0060933692920682875'],
- [1495702354.925, '0.004945682380952493'],
- [1495702414.925, '0.005641266666666565'],
- [1495702474.925, '0.005223752857142996'],
- [1495702534.925, '0.005743098505699831'],
- [1495702594.925, '0.00538493380952391'],
- [1495702654.925, '0.005507793883751339'],
- [1495702714.925, '0.005666705714285466'],
- [1495702774.925, '0.006231530000000112'],
- [1495702834.925, '0.006570768635394899'],
- [1495702894.925, '0.005551146666666895'],
- [1495702954.925, '0.005602604737098058'],
- [1495703014.925, '0.00613993580402159'],
- [1495703074.925, '0.004770258764368832'],
- [1495703134.925, '0.005512376671364914'],
- [1495703194.925, '0.005254436666666674'],
- [1495703254.925, '0.0050109839141320505'],
- [1495703314.925, '0.0049478019256960016'],
- [1495703374.925, '0.0037666860965123463'],
- [1495703434.925, '0.004813526061656314'],
- [1495703494.925, '0.005047748095238278'],
- [1495703554.925, '0.00386494081008772'],
- [1495703614.925, '0.004304037408111405'],
- [1495703674.925, '0.004999466661587168'],
- [1495703734.925, '0.004689140476190834'],
- [1495703794.925, '0.004746126153582475'],
- [1495703854.925, '0.004482706382572302'],
- [1495703914.925, '0.004032808931864524'],
- [1495703974.925, '0.005728319047618988'],
- [1495704034.925, '0.004436139179627006'],
- [1495704094.925, '0.004553455714285617'],
- [1495704154.925, '0.003455244285714341'],
- [1495704214.925, '0.004742244761904621'],
- [1495704274.925, '0.005366978571428422'],
- [1495704334.925, '0.004257954837665058'],
- [1495704394.925, '0.005431603259831257'],
- [1495704454.925, '0.0052009214498621986'],
- [1495704514.925, '0.004317201904761618'],
- [1495704574.925, '0.004307384285714157'],
- [1495704634.925, '0.004789801146644822'],
- [1495704694.925, '0.0051429795906706485'],
- [1495704754.925, '0.005322495714285479'],
- [1495704814.925, '0.004512809333244233'],
- [1495704874.925, '0.004953843582568726'],
- [1495704934.925, '0.005812690120858119'],
- [1495704994.925, '0.004997024285714838'],
- [1495705054.925, '0.005246216154439592'],
- [1495705114.925, '0.0063494966618726795'],
- [1495705174.925, '0.005306004342898225'],
- [1495705234.925, '0.005081412857142978'],
- [1495705294.925, '0.00511409523809522'],
- [1495705354.925, '0.0047861001481192'],
- [1495705414.925, '0.005107688228042962'],
- [1495705474.925, '0.005271929582294012'],
- [1495705534.925, '0.004453254502681249'],
- [1495705594.925, '0.005799134293959226'],
- [1495705654.925, '0.005340865929502478'],
- [1495705714.925, '0.004911654761904942'],
- [1495705774.925, '0.005888234873953261'],
- [1495705834.925, '0.005565283333332954'],
- [1495705894.925, '0.005522869047618869'],
- [1495705954.925, '0.005177549737621646'],
- [1495706014.925, '0.0053145810232096465'],
- [1495706074.925, '0.004751095238095275'],
- [1495706134.925, '0.006242077142856976'],
- [1495706194.925, '0.00621034406957871'],
- [1495706254.925, '0.006887592738978596'],
- [1495706314.925, '0.006328128779726213'],
- [1495706374.925, '0.007488363809523927'],
- [1495706434.925, '0.006193758571428157'],
- [1495706494.925, '0.0068798371839706935'],
- [1495706554.925, '0.005757034340423128'],
- [1495706614.925, '0.004571388497294698'],
- [1495706674.925, '0.00620283044923395'],
- [1495706734.925, '0.005607562380952455'],
- [1495706794.925, '0.005506969933620308'],
- [1495706854.925, '0.005621118095238131'],
- [1495706914.925, '0.004876606098698849'],
- [1495706974.925, '0.0047871205988517206'],
- [1495707034.925, '0.00526405939458784'],
- [1495707094.925, '0.005716323800605852'],
- [1495707154.925, '0.005301459523809575'],
- [1495707214.925, '0.0051613042857144905'],
- [1495707274.925, '0.005384792857142714'],
- [1495707334.925, '0.005259719047619222'],
- [1495707394.925, '0.00584101142857182'],
- [1495707454.925, '0.0060066121920326326'],
- [1495707514.925, '0.006359978571428453'],
- [1495707574.925, '0.006315876322151109'],
- [1495707634.925, '0.005590012517198831'],
- [1495707694.925, '0.005517419877137072'],
- [1495707754.925, '0.006089813430348506'],
- [1495707814.925, '0.00466754476190479'],
- [1495707874.925, '0.006059954380517721'],
- [1495707934.925, '0.005085657142856972'],
- [1495707994.925, '0.005897665238095296'],
- [1495708054.925, '0.0062282023199555885'],
- [1495708114.925, '0.00526214553236979'],
- [1495708174.925, '0.0044803300000000644'],
- [1495708234.925, '0.005421443333333592'],
- [1495708294.925, '0.005694326244512144'],
- [1495708354.925, '0.005527721904761457'],
- [1495708414.925, '0.005988819523809819'],
- [1495708474.925, '0.005484704285714448'],
- [1495708534.925, '0.005041123649230085'],
- [1495708594.925, '0.005717767639612059'],
- [1495708654.925, '0.005412954417342863'],
- [1495708714.925, '0.005833343333333254'],
- [1495708774.925, '0.005448135238094969'],
- [1495708834.925, '0.005117341428571432'],
- [1495708894.925, '0.005888345825277833'],
- [1495708954.925, '0.005398543809524135'],
- [1495709014.925, '0.005325611428571416'],
- [1495709074.925, '0.005848668571428527'],
- [1495709134.925, '0.005135003105145044'],
- [1495709194.925, '0.0054551400000003'],
- [1495709254.925, '0.005319472937322171'],
- [1495709314.925, '0.00585677857142792'],
- [1495709374.925, '0.0062146261904759215'],
- [1495709434.925, '0.0067105060904182265'],
- [1495709494.925, '0.005829691904762108'],
- [1495709554.925, '0.005719280952381261'],
- [1495709614.925, '0.005682603793416407'],
- [1495709674.925, '0.0055272846277326934'],
- [1495709734.925, '0.0057123680952386735'],
- [1495709794.925, '0.00520597958075818'],
- [1495709854.925, '0.005584358957263837'],
- [1495709914.925, '0.005601104275197466'],
- [1495709974.925, '0.005991657142857066'],
- [1495710034.925, '0.00553722238095218'],
- [1495710094.925, '0.005127883122696293'],
- [1495710154.925, '0.005498111927534584'],
- [1495710214.925, '0.005609934069084202'],
- [1495710274.925, '0.00459206285714307'],
- [1495710334.925, '0.0047910828571428084'],
- [1495710394.925, '0.0056014671288845685'],
- [1495710454.925, '0.005686936791078528'],
- [1495710514.925, '0.00444480476190448'],
- [1495710574.925, '0.005780394696738921'],
- [1495710634.925, '0.0053107227550210365'],
- [1495710694.925, '0.005096031495761817'],
- [1495710754.925, '0.005451377979091524'],
- [1495710814.925, '0.005328136666667083'],
- [1495710874.925, '0.006020612857143043'],
- [1495710934.925, '0.0061063585714285365'],
- [1495710994.925, '0.006018346015752312'],
- [1495711054.925, '0.005069130952381193'],
- [1495711114.925, '0.005458406190476052'],
- [1495711174.925, '0.00577219190476179'],
- [1495711234.925, '0.005760814645658314'],
- [1495711294.925, '0.005371875716579101'],
- [1495711354.925, '0.0064232666666665834'],
- [1495711414.925, '0.009369806836906667'],
- [1495711474.925, '0.008956864761904692'],
- [1495711534.925, '0.005266849368559271'],
- [1495711594.925, '0.005335111364934262'],
- [1495711654.925, '0.006461778319586945'],
- [1495711714.925, '0.004687939890762393'],
- [1495711774.925, '0.004438831245760684'],
- [1495711834.925, '0.005142786666666613'],
- [1495711894.925, '0.007257734212054963'],
- [1495711954.925, '0.005621991904761494'],
- [1495712014.925, '0.007868689999999862'],
- [1495712074.925, '0.00910970215275738'],
- [1495712134.925, '0.006151004285714278'],
- [1495712194.925, '0.005447120924961522'],
- [1495712254.925, '0.005150705153929503'],
- [1495712314.925, '0.006358108714969314'],
- [1495712374.925, '0.0057725354795696475'],
- [1495712434.925, '0.005232139047619015'],
- [1495712494.925, '0.004932809617949037'],
- [1495712554.925, '0.004511607508499662'],
- [1495712614.925, '0.00440487701522666'],
- [1495712674.925, '0.005479113333333174'],
- [1495712734.925, '0.004726317619047547'],
- [1495712794.925, '0.005582041102958029'],
- [1495712854.925, '0.006381481216082099'],
- [1495712914.925, '0.005474260014095208'],
- [1495712974.925, '0.00567597142857188'],
- [1495713034.925, '0.0064741233333332985'],
- [1495713094.925, '0.005467475714285271'],
- [1495713154.925, '0.004868648393824457'],
- [1495713214.925, '0.005254923286444893'],
- [1495713274.925, '0.005599217150312865'],
- [1495713334.925, '0.005105413720618919'],
- [1495713394.925, '0.007246073333333279'],
- [1495713454.925, '0.005990312380952272'],
- [1495713514.925, '0.005594601853351101'],
- [1495713574.925, '0.004739258673727054'],
- [1495713634.925, '0.003932121428571783'],
- [1495713694.925, '0.005018188268459395'],
- [1495713754.925, '0.004538238095237985'],
- [1495713814.925, '0.00561816643265435'],
- [1495713874.925, '0.0063132584495033586'],
- [1495713934.925, '0.00442385238095213'],
- [1495713994.925, '0.004181795887658453'],
- [1495714054.925, '0.004437759047619037'],
- [1495714114.925, '0.006421748157178241'],
- [1495714174.925, '0.006525143809523842'],
- [1495714234.925, '0.004715904935144247'],
- [1495714294.925, '0.005966040152763461'],
- [1495714354.925, '0.005614535466921674'],
- [1495714414.925, '0.004934375119415906'],
- [1495714474.925, '0.0054122933333327385'],
- [1495714534.925, '0.004926540699612279'],
- [1495714594.925, '0.006124649517134237'],
- [1495714654.925, '0.004629427092013995'],
- [1495714714.925, '0.005117951257607005'],
- [1495714774.925, '0.004868774512685422'],
- [1495714834.925, '0.005310093333333399'],
- [1495714894.925, '0.0054907752286127345'],
- [1495714954.925, '0.004597678117351089'],
- [1495715014.925, '0.0059622552380952'],
- [1495715074.925, '0.005352457072655368'],
- [1495715134.925, '0.005491630952381143'],
- [1495715194.925, '0.006391770078379791'],
- [1495715254.925, '0.005933472857142518'],
- [1495715314.925, '0.005301314285714163'],
- [1495715374.925, '0.0058352959724814165'],
- [1495715434.925, '0.006154755147867044'],
- [1495715494.925, '0.009391935637482038'],
- [1495715554.925, '0.007846462857142592'],
- [1495715614.925, '0.00477608215316353'],
- [1495715674.925, '0.006132865238094998'],
- [1495715734.925, '0.006159762457649516'],
- [1495715794.925, '0.005957307073265968'],
- [1495715854.925, '0.006652319091792501'],
- [1495715914.925, '0.005493557402895287'],
- [1495715974.925, '0.0058652434829145166'],
- [1495716034.925, '0.005627400430468021'],
- [1495716094.925, '0.006240656190475609'],
- [1495716154.925, '0.006305997676168624'],
- [1495716214.925, '0.005388057732783248'],
- [1495716274.925, '0.0052814916048421244'],
- [1495716334.925, '0.00699498614272497'],
- [1495716394.925, '0.00627768693035141'],
- [1495716454.925, '0.0042411487048161145'],
- [1495716514.925, '0.005348647473627653'],
- [1495716574.925, '0.0047176657142853975'],
- [1495716634.925, '0.004437898571428686'],
- [1495716694.925, '0.004923527366927261'],
- [1495716754.925, '0.005131935066048421'],
- [1495716814.925, '0.005046949523809611'],
- [1495716874.925, '0.00547184095238092'],
- [1495716934.925, '0.005224140016380444'],
- [1495716994.925, '0.005297991171665292'],
- [1495717054.925, '0.005492965995623498'],
- [1495717114.925, '0.005754660000000403'],
- [1495717174.925, '0.005949557138639285'],
- [1495717234.925, '0.006091816112534666'],
- [1495717294.925, '0.005554210080192063'],
- [1495717354.925, '0.006411504395279871'],
- [1495717414.925, '0.006319643996609606'],
- [1495717474.925, '0.005539174405717675'],
- [1495717534.925, '0.0053157078842772255'],
- [1495717594.925, '0.005247480952381066'],
- [1495717654.925, '0.004820141620396252'],
- [1495717714.925, '0.005906173868322844'],
- [1495717774.925, '0.006173117219570961'],
- [1495717834.925, '0.005963340952380661'],
- [1495717894.925, '0.005698976627681527'],
- [1495717954.925, '0.004751279096346378'],
- [1495718014.925, '0.005733142379359711'],
- [1495718074.925, '0.004831689010348035'],
- [1495718134.925, '0.005188370476191092'],
- [1495718194.925, '0.004793227554547938'],
- [1495718254.925, '0.003997442857142731'],
- [1495718314.925, '0.004386040132951264'],
- ],
- },
- ],
- },
- ],
- },
- ],
- },
+export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+
+export const mockedQueryResultPayload = {
+ metricId: '17_system_metrics_kubernetes_container_memory_average',
+ result: [
{
- group: 'NGINX',
- priority: 2,
- metrics: [
- {
- id: 100,
- title: 'Http Error Rate',
- weight: 100,
- queries: [
- {
- query_range:
- 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
- label: '5xx errors',
- unit: '%',
- result: [
- {
- metric: {},
- values: [
- [1495700554.925, NaN],
- [1495700614.925, NaN],
- [1495700674.925, NaN],
- [1495700734.925, NaN],
- [1495700794.925, NaN],
- [1495700854.925, NaN],
- [1495700914.925, NaN],
- ],
- },
- ],
- },
- ],
- },
+ metric: {},
+ values: [
+ [1563272065.589, '10.396484375'],
+ [1563272125.589, '10.333984375'],
+ [1563272185.589, '10.333984375'],
+ [1563272245.589, '10.333984375'],
+ [1563272305.589, '10.333984375'],
+ [1563272365.589, '10.333984375'],
+ [1563272425.589, '10.38671875'],
+ [1563272485.589, '10.333984375'],
+ [1563272545.589, '10.333984375'],
+ [1563272605.589, '10.333984375'],
+ [1563272665.589, '10.333984375'],
+ [1563272725.589, '10.333984375'],
+ [1563272785.589, '10.396484375'],
+ [1563272845.589, '10.333984375'],
+ [1563272905.589, '10.333984375'],
+ [1563272965.589, '10.3984375'],
+ [1563273025.589, '10.337890625'],
+ [1563273085.589, '10.34765625'],
+ [1563273145.589, '10.337890625'],
+ [1563273205.589, '10.337890625'],
+ [1563273265.589, '10.337890625'],
+ [1563273325.589, '10.337890625'],
+ [1563273385.589, '10.337890625'],
+ [1563273445.589, '10.337890625'],
+ [1563273505.589, '10.337890625'],
+ [1563273565.589, '10.337890625'],
+ [1563273625.589, '10.337890625'],
+ [1563273685.589, '10.337890625'],
+ [1563273745.589, '10.337890625'],
+ [1563273805.589, '10.337890625'],
+ [1563273865.589, '10.390625'],
+ [1563273925.589, '10.390625'],
],
},
],
- last_update: '2017-05-25T13:18:34.949Z',
};
-export const singleGroupResponse = [
- {
- group: 'System metrics (Kubernetes)',
- priority: 5,
- metrics: [
- {
- title: 'Memory Usage (Total)',
- weight: 0,
- y_label: 'Total Memory Used',
- queries: [
- {
- query_range:
- 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^production-(.*)",namespace="autodevops-deploy-33"}) by (job)) without (job) /1024/1024/1024',
- unit: 'GB',
- label: 'Total',
- result: [
- {
- metric: {},
- values: [
- [1558453960.079, '0.0357666015625'],
- [1558454020.079, '0.035675048828125'],
- [1558454080.079, '0.035152435302734375'],
- [1558454140.079, '0.035221099853515625'],
- [1558454200.079, '0.0352325439453125'],
- [1558454260.079, '0.03479766845703125'],
- [1558454320.079, '0.034793853759765625'],
- [1558454380.079, '0.034931182861328125'],
- [1558454440.079, '0.034816741943359375'],
- [1558454500.079, '0.034816741943359375'],
- [1558454560.079, '0.034816741943359375'],
- ],
- },
- ],
- },
- ],
- id: 15,
- },
- ],
- },
-];
-
-export default metricsGroupsAPIResponse;
-
-export const deploymentData = [
- {
- id: 111,
- iid: 3,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'master',
- },
- created_at: '2017-05-31T21:23:37.881Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': true,
- },
- {
- id: 110,
- iid: 2,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'master',
- },
- created_at: '2017-05-30T20:08:04.629Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
- {
- id: 109,
- iid: 1,
- sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- ref: {
- name: 'update2-readme',
- },
- created_at: '2017-05-30T17:42:38.409Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
-];
-
-export const statePaths = {
- settingsPath: '/root/hello-prometheus/services/prometheus/edit',
- clustersPath: '/root/hello-prometheus/clusters',
- documentationPath: '/help/administration/monitoring/prometheus/index.md',
-};
-
-export const queryWithoutData = {
- title: 'HTTP Error rate',
- weight: 10,
- y_label: 'Http Error Rate',
- queries: [
+export const mockedQueryResultPayloadCoresTotal = {
+ metricId: '13_system_metrics_kubernetes_container_cores_total',
+ result: [
{
- query_range:
- 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
- label: '5xx errors',
- unit: '%',
- result: [],
+ metric: {},
+ values: [
+ [1563272065.589, '9.396484375'],
+ [1563272125.589, '9.333984375'],
+ [1563272185.589, '9.333984375'],
+ [1563272245.589, '9.333984375'],
+ [1563272305.589, '9.333984375'],
+ [1563272365.589, '9.333984375'],
+ [1563272425.589, '9.38671875'],
+ [1563272485.589, '9.333984375'],
+ [1563272545.589, '9.333984375'],
+ [1563272605.589, '9.333984375'],
+ [1563272665.589, '9.333984375'],
+ [1563272725.589, '9.333984375'],
+ [1563272785.589, '9.396484375'],
+ [1563272845.589, '9.333984375'],
+ [1563272905.589, '9.333984375'],
+ [1563272965.589, '9.3984375'],
+ [1563273025.589, '9.337890625'],
+ [1563273085.589, '9.34765625'],
+ [1563273145.589, '9.337890625'],
+ [1563273205.589, '9.337890625'],
+ [1563273265.589, '9.337890625'],
+ [1563273325.589, '9.337890625'],
+ [1563273385.589, '9.337890625'],
+ [1563273445.589, '9.337890625'],
+ [1563273505.589, '9.337890625'],
+ [1563273565.589, '9.337890625'],
+ [1563273625.589, '9.337890625'],
+ [1563273685.589, '9.337890625'],
+ [1563273745.589, '9.337890625'],
+ [1563273805.589, '9.337890625'],
+ [1563273865.589, '9.390625'],
+ [1563273925.589, '9.390625'],
+ ],
},
],
};
-export function convertDatesMultipleSeries(multipleSeries) {
- const convertedMultiple = multipleSeries;
- multipleSeries.forEach((column, index) => {
- let convertedResult = [];
- convertedResult = column.queries[0].result.map(resultObj => {
- const convertedMetrics = {};
- convertedMetrics.values = resultObj.values.map(val => ({
- time: new Date(val.time),
- value: val.value,
- }));
- convertedMetrics.metric = resultObj.metric;
- return convertedMetrics;
- });
- convertedMultiple[index].queries[0].result = convertedResult;
- });
- return convertedMultiple;
-}
-
-export const environmentData = [
- {
- id: 34,
- name: 'production',
- state: 'available',
- external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
- environment_type: null,
- stop_action: false,
- metrics_path: '/root/hello-prometheus/environments/34/metrics',
- environment_path: '/root/hello-prometheus/environments/34',
- stop_path: '/root/hello-prometheus/environments/34/stop',
- terminal_path: '/root/hello-prometheus/environments/34/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/production',
- created_at: '2018-06-29T16:53:38.301Z',
- updated_at: '2018-06-29T16:57:09.825Z',
- last_deployment: {
- id: 127,
- },
- },
- {
- id: 35,
- name: 'review/noop-branch',
- state: 'available',
- external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
- environment_type: 'review',
- stop_action: true,
- metrics_path: '/root/hello-prometheus/environments/35/metrics',
- environment_path: '/root/hello-prometheus/environments/35',
- stop_path: '/root/hello-prometheus/environments/35/stop',
- terminal_path: '/root/hello-prometheus/environments/35/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/review',
- created_at: '2018-07-03T18:39:41.702Z',
- updated_at: '2018-07-03T18:44:54.010Z',
- last_deployment: {
- id: 128,
- },
- },
- {
- id: 36,
- name: 'no-deployment/noop-branch',
- state: 'available',
- created_at: '2018-07-04T18:39:41.702Z',
- updated_at: '2018-07-04T18:44:54.010Z',
- },
-];
-
-export const metricsDashboardResponse = {
- dashboard: {
- dashboard: 'Environment metrics',
- priority: 1,
- panel_groups: [
- {
- group: 'System metrics (Kubernetes)',
- priority: 5,
- panels: [
- {
- title: 'Memory Usage (Total)',
- type: 'area-chart',
- y_label: 'Total Memory Used',
- 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',
- label: 'Total',
- unit: 'GB',
- metric_id: 12,
- prometheus_endpoint_path: 'http://test',
- },
- ],
- },
- {
- title: 'Core Usage (Total)',
- type: 'area-chart',
- y_label: 'Total Cores',
- 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)',
- label: 'Total',
- unit: 'cores',
- metric_id: 13,
- },
- ],
- },
- {
- title: 'Memory Usage (Pod average)',
- type: 'line-chart',
- y_label: 'Memory Used per Pod',
- 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}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
- label: 'Pod average',
- unit: 'MB',
- metric_id: 14,
- },
- ],
- },
- ],
- },
- ],
- },
- status: 'success',
-};
-
-export const dashboardGitResponse = [
- {
- path: 'config/prometheus/common_metrics.yml',
- display_name: 'Common Metrics',
- default: true,
- },
- {
- path: '.gitlab/dashboards/super.yml',
- display_name: 'Custom Dashboard 1',
- default: false,
- },
-];
-
export const graphDataPrometheusQuery = {
title: 'Super Chart A2',
type: 'single-stat',
@@ -975,7 +135,7 @@ export const graphDataPrometheusQuery = {
export const graphDataPrometheusQueryRange = {
title: 'Super Chart A1',
- type: 'area',
+ type: 'area-chart',
weight: 2,
metrics: [
{
@@ -991,7 +151,7 @@ export const graphDataPrometheusQueryRange = {
],
queries: [
{
- metricId: null,
+ metricId: '10',
id: 'metric_a1',
metric_id: 2,
query_range:
@@ -1009,3 +169,82 @@ export const graphDataPrometheusQueryRange = {
},
],
};
+
+export const graphDataPrometheusQueryRangeMultiTrack = {
+ title: 'Super Chart A3',
+ type: 'heatmap',
+ weight: 3,
+ x_label: 'Status Code',
+ y_label: 'Time',
+ metrics: [],
+ queries: [
+ {
+ metricId: '1',
+ id: 'response_metrics_nginx_ingress_throughput_status_code',
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
+ unit: 'req / sec',
+ label: 'Status Code',
+ metric_id: 1,
+ prometheus_endpoint_path:
+ '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
+ result: [
+ {
+ metric: { status_code: '1xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 0],
+ ['2019-08-30T16:00:00.000Z', 2],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 0],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 3],
+ ],
+ },
+ {
+ metric: { status_code: '2xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 1],
+ ['2019-08-30T16:00:00.000Z', 3],
+ ['2019-08-30T17:00:00.000Z', 6],
+ ['2019-08-30T18:00:00.000Z', 10],
+ ['2019-08-30T19:00:00.000Z', 8],
+ ['2019-08-30T20:00:00.000Z', 6],
+ ],
+ },
+ {
+ metric: { status_code: '3xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 1],
+ ['2019-08-30T16:00:00.000Z', 2],
+ ['2019-08-30T17:00:00.000Z', 3],
+ ['2019-08-30T18:00:00.000Z', 3],
+ ['2019-08-30T19:00:00.000Z', 2],
+ ['2019-08-30T20:00:00.000Z', 1],
+ ],
+ },
+ {
+ metric: { status_code: '4xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 2],
+ ['2019-08-30T16:00:00.000Z', 0],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 2],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 2],
+ ],
+ },
+ {
+ metric: { status_code: '5xx' },
+ values: [
+ ['2019-08-30T15:00:00.000Z', 0],
+ ['2019-08-30T16:00:00.000Z', 1],
+ ['2019-08-30T17:00:00.000Z', 0],
+ ['2019-08-30T18:00:00.000Z', 0],
+ ['2019-08-30T19:00:00.000Z', 0],
+ ['2019-08-30T20:00:00.000Z', 2],
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js
deleted file mode 100644
index a2366e74d43..00000000000
--- a/spec/javascripts/monitoring/panel_type_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { shallowMount } from '@vue/test-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 { graphDataPrometheusQueryRange } from './mock_data';
-import { createStore } from '~/monitoring/stores';
-
-describe('Panel Type component', () => {
- let store;
- let panelType;
- const dashboardWidth = 100;
-
- describe('When no graphData is available', () => {
- let glEmptyChart;
- // Deep clone object before modifying
- const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange));
- graphDataNoResult.queries[0].result = [];
-
- beforeEach(() => {
- panelType = shallowMount(PanelType, {
- propsData: {
- clipboardText: 'dashboard_link',
- dashboardWidth,
- graphData: graphDataNoResult,
- },
- });
- });
-
- afterEach(() => {
- panelType.destroy();
- });
-
- describe('Empty Chart component', () => {
- beforeEach(() => {
- glEmptyChart = panelType.find(EmptyChart);
- });
-
- it('is a Vue instance', () => {
- expect(glEmptyChart.isVueInstance()).toBe(true);
- });
-
- it('it receives a graph title', () => {
- const props = glEmptyChart.props();
-
- expect(props.graphTitle).toBe(panelType.vm.graphData.title);
- });
- });
- });
-
- describe('when Graph data is available', () => {
- const exampleText = 'example_text';
-
- beforeEach(() => {
- store = createStore();
- panelType = shallowMount(PanelType, {
- propsData: {
- clipboardText: exampleText,
- dashboardWidth,
- graphData: graphDataPrometheusQueryRange,
- },
- store,
- });
- });
-
- describe('Time Series Chart panel type', () => {
- it('is rendered', () => {
- expect(panelType.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(panelType.find(TimeSeriesChart).exists()).toBe(true);
- });
-
- it('sets clipboard text on the dropdown', () => {
- const link = () => panelType.find('.js-chart-link');
- const clipboardText = () => link().element.dataset.clipboardText;
-
- expect(clipboardText()).toBe(exampleText);
- });
- });
- });
-});
diff --git a/spec/javascripts/monitoring/shared/prometheus_header_spec.js b/spec/javascripts/monitoring/shared/prometheus_header_spec.js
new file mode 100644
index 00000000000..9f916a4dfbb
--- /dev/null
+++ b/spec/javascripts/monitoring/shared/prometheus_header_spec.js
@@ -0,0 +1,26 @@
+import { shallowMount } from '@vue/test-utils';
+import PrometheusHeader from '~/monitoring/components/shared/prometheus_header.vue';
+
+describe('Prometheus Header component', () => {
+ let prometheusHeader;
+
+ beforeEach(() => {
+ prometheusHeader = shallowMount(PrometheusHeader, {
+ propsData: {
+ graphTitle: 'graph header',
+ },
+ });
+ });
+
+ afterEach(() => {
+ prometheusHeader.destroy();
+ });
+
+ describe('Prometheus header component', () => {
+ it('should show a title', () => {
+ const title = prometheusHeader.vm.$el.querySelector('.js-graph-title').textContent;
+
+ expect(title).toBe('graph header');
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
index 512dd2a0eb3..202b4ec8f2e 100644
--- a/spec/javascripts/monitoring/utils_spec.js
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -7,9 +7,14 @@ import {
stringToISODate,
ISODateToString,
isValidDate,
+ graphDataValidatorForAnomalyValues,
} from '~/monitoring/utils';
import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
-import { graphDataPrometheusQuery, graphDataPrometheusQueryRange } from './mock_data';
+import {
+ graphDataPrometheusQuery,
+ graphDataPrometheusQueryRange,
+ anomalyMockGraphData,
+} from './mock_data';
describe('getTimeDiff', () => {
function secondsBetween({ start, end }) {
@@ -307,3 +312,34 @@ describe('isDateTimePickerInputValid', () => {
});
});
});
+
+describe('graphDataValidatorForAnomalyValues', () => {
+ let oneQuery;
+ let threeQueries;
+ let fourQueries;
+ beforeEach(() => {
+ oneQuery = graphDataPrometheusQuery;
+ threeQueries = anomalyMockGraphData;
+
+ const queries = [...threeQueries.queries];
+ queries.push(threeQueries.queries[0]);
+ fourQueries = {
+ ...anomalyMockGraphData,
+ queries,
+ };
+ });
+ /*
+ * Anomaly charts can accept results for exactly 3 queries,
+ */
+ it('validates passes with the right query format', () => {
+ expect(graphDataValidatorForAnomalyValues(threeQueries)).toBe(true);
+ });
+
+ it('validation fails for wrong format, 1 metric', () => {
+ expect(graphDataValidatorForAnomalyValues(oneQuery)).toBe(false);
+ });
+
+ it('validation fails for wrong format, more than 3 metrics', () => {
+ expect(graphDataValidatorForAnomalyValues(fourQueries)).toBe(false);
+ });
+});
diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
deleted file mode 100644
index 88c86746992..00000000000
--- a/spec/javascripts/notes/components/comment_form_spec.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import Autosize from 'autosize';
-import createStore from '~/notes/stores';
-import CommentForm from '~/notes/components/comment_form.vue';
-import * as constants from '~/notes/constants';
-import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
-import { keyboardDownEvent } from '../../issue_show/helpers';
-
-describe('issue_comment_form component', () => {
- let store;
- let vm;
- const Component = Vue.extend(CommentForm);
- let mountComponent;
-
- beforeEach(() => {
- store = createStore();
- mountComponent = (noteableType = 'issue') =>
- new Component({
- propsData: {
- noteableType,
- },
- store,
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('user is logged in', () => {
- beforeEach(() => {
- store.dispatch('setUserData', userDataMock);
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
-
- vm = mountComponent();
- });
-
- it('should render user avatar with link', () => {
- expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(
- userDataMock.path,
- );
- });
-
- describe('handleSave', () => {
- it('should request to save note when note is entered', () => {
- vm.note = 'hello world';
- spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
- spyOn(vm, 'resizeTextarea');
- spyOn(vm, 'stopPolling');
-
- vm.handleSave();
-
- expect(vm.isSubmitting).toEqual(true);
- expect(vm.note).toEqual('');
- expect(vm.saveNote).toHaveBeenCalled();
- expect(vm.stopPolling).toHaveBeenCalled();
- expect(vm.resizeTextarea).toHaveBeenCalled();
- });
-
- it('should toggle issue state when no note', () => {
- spyOn(vm, 'toggleIssueState');
-
- vm.handleSave();
-
- expect(vm.toggleIssueState).toHaveBeenCalled();
- });
-
- it('should disable action button whilst submitting', done => {
- const saveNotePromise = Promise.resolve();
- vm.note = 'hello world';
- spyOn(vm, 'saveNote').and.returnValue(saveNotePromise);
- spyOn(vm, 'stopPolling');
-
- const actionButton = vm.$el.querySelector('.js-action-button');
-
- vm.handleSave();
-
- Vue.nextTick()
- .then(() => {
- expect(actionButton.disabled).toBeTruthy();
- })
- .then(saveNotePromise)
- .then(Vue.nextTick)
- .then(() => {
- expect(actionButton.disabled).toBeFalsy();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('textarea', () => {
- it('should render textarea with placeholder', () => {
- expect(
- vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
- ).toEqual('Write a comment or drag your files here…');
- });
-
- it('should make textarea disabled while requesting', done => {
- const $submitButton = $(vm.$el.querySelector('.js-comment-submit-button'));
- vm.note = 'hello world';
- spyOn(vm, 'stopPolling');
- spyOn(vm, 'saveNote').and.returnValue(new Promise(() => {}));
-
- vm.$nextTick(() => {
- // Wait for vm.note change triggered. It should enable $submitButton.
- $submitButton.trigger('click');
-
- vm.$nextTick(() => {
- // Wait for vm.isSubmitting triggered. It should disable textarea.
- expect(vm.$el.querySelector('.js-main-target-form textarea').disabled).toBeTruthy();
- done();
- });
- });
- });
-
- it('should support quick actions', () => {
- expect(
- vm.$el
- .querySelector('.js-main-target-form textarea')
- .getAttribute('data-supports-quick-actions'),
- ).toEqual('true');
- });
-
- it('should link to markdown docs', () => {
- const { markdownDocsPath } = notesDataMock;
-
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
- 'Markdown',
- );
- });
-
- it('should link to quick actions docs', () => {
- const { quickActionsDocsPath } = notesDataMock;
-
- expect(
- vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim(),
- ).toEqual('quick actions');
- });
-
- it('should resize textarea after note discarded', done => {
- spyOn(Autosize, 'update');
- spyOn(vm, 'discard').and.callThrough();
-
- vm.note = 'foo';
- vm.discard();
-
- Vue.nextTick(() => {
- expect(Autosize.update).toHaveBeenCalled();
- done();
- });
- });
-
- describe('edit mode', () => {
- it('should enter edit mode when arrow up is pressed', () => {
- spyOn(vm, 'editCurrentUserLastNote').and.callThrough();
- vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el
- .querySelector('.js-main-target-form textarea')
- .dispatchEvent(keyboardDownEvent(38, true));
-
- expect(vm.editCurrentUserLastNote).toHaveBeenCalled();
- });
-
- it('inits autosave', () => {
- expect(vm.autosave).toBeDefined();
- expect(vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`);
- });
- });
-
- describe('event enter', () => {
- it('should save note when cmd+enter is pressed', () => {
- spyOn(vm, 'handleSave').and.callThrough();
- vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el
- .querySelector('.js-main-target-form textarea')
- .dispatchEvent(keyboardDownEvent(13, true));
-
- expect(vm.handleSave).toHaveBeenCalled();
- });
-
- it('should save note when ctrl+enter is pressed', () => {
- spyOn(vm, 'handleSave').and.callThrough();
- vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
- vm.$el
- .querySelector('.js-main-target-form textarea')
- .dispatchEvent(keyboardDownEvent(13, false, true));
-
- expect(vm.handleSave).toHaveBeenCalled();
- });
- });
- });
-
- describe('actions', () => {
- it('should be possible to close the issue', () => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
- 'Close issue',
- );
- });
-
- it('should render comment button as disabled', () => {
- expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(
- 'disabled',
- );
- });
-
- it('should enable comment button if it has note', done => {
- vm.note = 'Foo';
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled'),
- ).toEqual(null);
- done();
- });
- });
-
- it('should update buttons texts when it has note', done => {
- vm.note = 'Foo';
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
- 'Comment & close issue',
- );
-
- done();
- });
- });
-
- it('updates button text with noteable type', done => {
- vm.noteableType = constants.MERGE_REQUEST_NOTEABLE_TYPE;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual(
- 'Close merge request',
- );
- done();
- });
- });
-
- describe('when clicking close/reopen button', () => {
- it('should disable button and show a loading spinner', done => {
- const toggleStateButton = vm.$el.querySelector('.js-action-button');
-
- toggleStateButton.click();
- Vue.nextTick(() => {
- expect(toggleStateButton.disabled).toEqual(true);
- expect(toggleStateButton.querySelector('.js-loading-button-icon')).not.toBeNull();
-
- done();
- });
- });
- });
-
- describe('when toggling state', () => {
- it('should update MR count', done => {
- spyOn(vm, 'closeIssue').and.returnValue(Promise.resolve());
-
- const updateMrCountSpy = spyOnDependency(CommentForm, 'refreshUserMergeRequestCounts');
- vm.toggleIssueState();
-
- Vue.nextTick(() => {
- expect(updateMrCountSpy).toHaveBeenCalled();
-
- done();
- });
- });
- });
- });
-
- describe('issue is confidential', () => {
- it('shows information warning', done => {
- store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
- done();
- });
- });
- });
- });
-
- describe('user is not logged in', () => {
- beforeEach(() => {
- store.dispatch('setUserData', null);
- store.dispatch('setNoteableData', loggedOutnoteableData);
- store.dispatch('setNotesData', notesDataMock);
-
- vm = mountComponent();
- });
-
- it('should render signed out widget', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual(
- 'Please register or sign in to reply',
- );
- });
-
- it('should not render submission form', () => {
- expect(vm.$el.querySelector('textarea')).toEqual(null);
- });
- });
-});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index ea5c57b8a7c..ea1ed3da112 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -23,7 +23,7 @@ describe('noteable_discussion component', () => {
store.dispatch('setNotesData', notesDataMock);
const localVue = createLocalVue();
- wrapper = shallowMount(noteableDiscussion, {
+ wrapper = mount(noteableDiscussion, {
store,
propsData: { discussion: discussionMock },
localVue,
@@ -35,16 +35,6 @@ describe('noteable_discussion component', () => {
wrapper.destroy();
});
- it('should render user avatar', () => {
- const discussion = { ...discussionMock };
- discussion.diff_file = mockDiffFile;
- discussion.diff_discussion = true;
-
- wrapper.setProps({ discussion, renderDiffFile: true });
-
- expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
- });
-
it('should not render thread header for non diff threads', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
@@ -134,105 +124,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('action text', () => {
- const commitId = 'razupaltuff';
- const truncatedCommitId = commitId.substr(0, 8);
- let commitElement;
-
- beforeEach(done => {
- store.state.diffs = {
- projectPath: 'something',
- };
-
- wrapper.setProps({
- discussion: {
- ...discussionMock,
- for_commit: true,
- commit_id: commitId,
- diff_discussion: true,
- diff_file: {
- ...mockDiffFile,
- },
- },
- renderDiffFile: true,
- });
-
- wrapper.vm
- .$nextTick()
- .then(() => {
- commitElement = wrapper.find('.commit-sha');
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('for commit threads', () => {
- it('should display a monospace started a thread on commit', () => {
- expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
- expect(commitElement.exists()).toBe(true);
- expect(commitElement.text()).toContain(truncatedCommitId);
- });
- });
-
- describe('for diff thread with a commit id', () => {
- it('should display started thread on commit header', done => {
- wrapper.vm.discussion.for_commit = false;
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
-
- expect(commitElement).not.toBe(null);
-
- done();
- });
- });
-
- it('should display outdated change on commit header', done => {
- wrapper.vm.discussion.for_commit = false;
- wrapper.vm.discussion.active = false;
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain(
- `started a thread on an outdated change in commit ${truncatedCommitId}`,
- );
-
- expect(commitElement).not.toBe(null);
-
- done();
- });
- });
- });
-
- describe('for diff threads without a commit id', () => {
- it('should show started a thread on the diff text', done => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
- });
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a thread on the diff');
-
- done();
- });
- });
-
- it('should show thread on older version text', done => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
- active: false,
- });
-
- wrapper.vm.$nextTick(() => {
- expect(wrapper.text()).toContain('started a thread on an old version of the diff');
-
- done();
- });
- });
- });
- });
-
describe('for resolved thread', () => {
beforeEach(() => {
const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
@@ -262,6 +153,7 @@ describe('noteable_discussion component', () => {
}));
wrapper.setProps({ discussion });
+
wrapper.vm
.$nextTick()
.then(done)
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index dc914ce8355..89e4553092a 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1,1255 +1 @@
-// Copied to ee/spec/frontend/notes/mock_data.js
-
-export const notesDataMock = {
- discussionsPath: '/gitlab-org/gitlab-foss/issues/26/discussions.json',
- lastFetchedAt: 1501862675,
- markdownDocsPath: '/help/user/markdown',
- newSessionPath: '/users/sign_in?redirect_to_referer=yes',
- notesPath: '/gitlab-org/gitlab-foss/noteable/issue/98/notes',
- quickActionsDocsPath: '/help/user/project/quick_actions',
- registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
- prerenderedNotesCount: 1,
- closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
- reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
- canAwardEmoji: true,
-};
-
-export const userDataMock = {
- avatar_url: 'mock_path',
- id: 1,
- name: 'Root',
- path: '/root',
- state: 'active',
- username: 'root',
-};
-
-export const noteableDataMock = {
- assignees: [],
- author_id: 1,
- branch_name: null,
- confidential: false,
- create_note_path: '/gitlab-org/gitlab-foss/notes?target_id=98&target_type=issue',
- created_at: '2017-02-07T10:11:18.395Z',
- current_user: {
- can_create_note: true,
- can_update: true,
- can_award_emoji: true,
- },
- description: '',
- due_date: null,
- human_time_estimate: null,
- human_total_time_spent: null,
- id: 98,
- iid: 26,
- labels: [],
- lock_version: null,
- milestone: null,
- milestone_id: null,
- moved_to_id: null,
- preview_note_path: '/gitlab-org/gitlab-foss/preview_markdown?target_id=98&target_type=Issue',
- project_id: 2,
- state: 'opened',
- time_estimate: 0,
- title: '14',
- total_time_spent: 0,
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- updated_at: '2017-08-04T09:53:01.226Z',
- updated_by_id: 1,
- web_url: '/gitlab-org/gitlab-foss/issues/26',
- noteableType: 'issue',
-};
-
-export const lastFetchedAt = '1501862675';
-
-export const individualNote = {
- expanded: true,
- id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- individual_note: true,
- notes: [
- {
- id: '1390',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: 'test',
- path: '/root',
- },
- created_at: '2017-08-01T17: 09: 33.762Z',
- updated_at: '2017-08-01T17: 09: 33.762Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'sdfdsaf',
- note_html: "<p dir='auto'>sdfdsaf</p>",
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- emoji_awardable: true,
- award_emoji: [
- { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
- { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
- ],
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji',
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- note_url: '/group/project/merge_requests/1#note_1',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1390',
- },
- ],
- reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
-};
-
-export const note = {
- id: '546',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2017-08-10T15:24:03.087Z',
- updated_at: '2017-08-10T15:24:03.087Z',
- system: false,
- noteable_id: 67,
- noteable_type: 'Issue',
- noteable_iid: 7,
- type: null,
- human_access: 'Owner',
- note: 'Vel id placeat reprehenderit sit numquam.',
- note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>',
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0',
- emoji_awardable: true,
- award_emoji: [
- {
- name: 'baseball',
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- },
- },
- {
- name: 'bath_tone3',
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- },
- },
- ],
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/546/toggle_award_emoji',
- note_url: '/group/project/merge_requests/1#note_1',
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/546',
-};
-
-export const discussionMock = {
- id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- expanded: true,
- notes: [
- {
- id: '1395',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-02T10:51:58.559Z',
- updated_at: '2017-08-02T10:51:58.559Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'THIS IS A DICUSSSION!',
- note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>",
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- can_resolve: true,
- },
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1395/toggle_award_emoji',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1395',
- },
- {
- id: '1396',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-02T10:56:50.980Z',
- updated_at: '2017-08-03T14:19:35.691Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'sadfasdsdgdsf',
- note_html: "<p dir='auto'>sadfasdsdgdsf</p>",
- last_edited_at: '2017-08-03T14:19:35.691Z',
- last_edited_by: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- can_resolve: true,
- },
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1396/toggle_award_emoji',
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1396',
- },
- {
- id: '1437',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-03T18:11:18.780Z',
- updated_at: '2017-08-04T09:52:31.062Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'adsfasf Should disappear',
- note_html: "<p dir='auto'>adsfasf Should disappear</p>",
- last_edited_at: '2017-08-04T09:52:31.062Z',
- last_edited_by: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- can_resolve: true,
- },
- discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
- emoji_awardable: true,
- award_emoji: [],
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1437/toggle_award_emoji',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1437',
- },
- ],
- individual_note: false,
- resolvable: true,
- active: true,
-};
-
-export const loggedOutnoteableData = {
- id: '98',
- iid: 26,
- author_id: 1,
- description: '',
- lock_version: 1,
- milestone_id: null,
- state: 'opened',
- title: 'asdsa',
- updated_by_id: 1,
- created_at: '2017-02-07T10:11:18.395Z',
- updated_at: '2017-08-08T10:22:51.564Z',
- time_estimate: 0,
- total_time_spent: 0,
- human_time_estimate: null,
- human_total_time_spent: null,
- milestone: null,
- labels: [],
- branch_name: null,
- confidential: false,
- assignees: [
- {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- web_url: 'http://localhost:3000/root',
- },
- ],
- due_date: null,
- moved_to_id: null,
- project_id: 2,
- web_url: '/gitlab-org/gitlab-foss/issues/26',
- current_user: {
- can_create_note: false,
- can_update: false,
- },
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- create_note_path: '/gitlab-org/gitlab-foss/notes?target_id=98&target_type=issue',
- preview_note_path: '/gitlab-org/gitlab-foss/preview_markdown?target_id=98&target_type=Issue',
-};
-
-export const collapseNotesMock = [
- {
- expanded: true,
- id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- individual_note: true,
- notes: [
- {
- id: '1390',
- attachment: null,
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: 'test',
- path: '/root',
- },
- created_at: '2018-02-26T18:07:41.071Z',
- updated_at: '2018-02-26T18:07:41.071Z',
- system: true,
- system_note_icon_name: 'pencil',
- noteable_id: 98,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false },
- discussion_id: 'b97fb7bda470a65b3e009377a9032edec0a4dd05',
- emoji_awardable: false,
- path: '/h5bp/html5-boilerplate/notes/1057',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
- },
- ],
- },
- {
- expanded: true,
- id: 'ffde43f25984ad7f2b4275135e0e2846875336c0',
- individual_note: true,
- notes: [
- {
- id: '1391',
- attachment: null,
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: 'test',
- path: '/root',
- },
- created_at: '2018-02-26T18:13:24.071Z',
- updated_at: '2018-02-26T18:13:24.071Z',
- system: true,
- system_note_icon_name: 'pencil',
- noteable_id: 99,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false },
- discussion_id: '3eb958b4d81dec207ec3537a2f3bd8b9f271bb34',
- emoji_awardable: false,
- path: '/h5bp/html5-boilerplate/notes/1057',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1',
- },
- ],
- },
-];
-
-export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
- GET: {
- '/gitlab-org/gitlab-foss/issues/26/discussions.json': [
- {
- id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- expanded: true,
- notes: [
- {
- id: '1390',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-01T17:09:33.762Z',
- updated_at: '2017-08-01T17:09:33.762Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'sdfdsaf',
- note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e',
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
- emoji_awardable: true,
- award_emoji: [
- {
- name: 'baseball',
- user: {
- id: 1,
- name: 'Root',
- username: 'root',
- },
- },
- {
- name: 'art',
- user: {
- id: 1,
- name: 'Root',
- username: 'root',
- },
- },
- ],
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1390/toggle_award_emoji',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1390',
- },
- ],
- individual_note: true,
- },
- {
- id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
- reply_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
- expanded: true,
- notes: [
- {
- id: '1391',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-02T10:51:38.685Z',
- updated_at: '2017-08-02T10:51:38.685Z',
- system: false,
- noteable_id: 98,
- noteable_type: 'Issue',
- type: null,
- human_access: 'Owner',
- note: 'New note!',
- note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e',
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
- emoji_awardable: true,
- award_emoji: [],
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1391/toggle_award_emoji',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1391',
- },
- ],
- individual_note: true,
- },
- ],
- '/gitlab-org/gitlab-foss/noteable/issue/98/notes': {
- last_fetched_at: 1512900838,
- notes: [],
- },
- },
- PUT: {
- '/gitlab-org/gitlab-foss/notes/1471': {
- commands_changes: null,
- valid: true,
- id: '1471',
- attachment: null,
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-08T16:53:00.666Z',
- updated_at: '2017-12-10T11:03:21.876Z',
- system: false,
- noteable_id: 124,
- noteable_type: 'Issue',
- noteable_iid: 29,
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'Adding a comment',
- note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
- last_edited_at: '2017-12-10T11:03:21.876Z',
- last_edited_by: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
- emoji_awardable: true,
- award_emoji: [],
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1471',
- },
- },
-};
-
-export const DISCUSSION_NOTE_RESPONSE_MAP = {
- ...INDIVIDUAL_NOTE_RESPONSE_MAP,
- GET: {
- ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
- '/gitlab-org/gitlab-foss/issues/26/discussions.json': [
- {
- id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
- reply_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
- expanded: true,
- notes: [
- {
- id: '1471',
- attachment: {
- url: null,
- filename: null,
- image: false,
- },
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: null,
- path: '/root',
- },
- created_at: '2017-08-08T16:53:00.666Z',
- updated_at: '2017-08-08T16:53:00.666Z',
- system: false,
- noteable_id: 124,
- noteable_type: 'Issue',
- noteable_iid: 29,
- type: 'DiscussionNote',
- human_access: 'Owner',
- note: 'Adding a comment',
- note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
- current_user: {
- can_edit: true,
- can_award_emoji: true,
- },
- discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
- emoji_awardable: true,
- award_emoji: [],
- toggle_award_path: '/gitlab-org/gitlab-foss/notes/1471/toggle_award_emoji',
- noteable_note_url: '/group/project/merge_requests/1#note_1',
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
- path: '/gitlab-org/gitlab-foss/notes/1471',
- },
- ],
- individual_note: false,
- },
- ],
- },
-};
-
-export function getIndividualNoteResponse(config) {
- return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
-}
-
-export function getDiscussionNoteResponse(config) {
- return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
-}
-
-export const notesWithDescriptionChanges = [
- {
- id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- expanded: true,
- notes: [
- {
- id: '901',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:05:36.117Z',
- updated_at: '2018-05-29T12:05:36.117Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- note_html:
- '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/901',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- expanded: true,
- notes: [
- {
- id: '902',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:05:58.694Z',
- updated_at: '2018-05-29T12:05:58.694Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note:
- 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
- note_html:
- '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/902',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '7f1feda384083eb31763366e6392399fde6f3f31',
- reply_id: '7f1feda384083eb31763366e6392399fde6f3f31',
- expanded: true,
- notes: [
- {
- id: '903',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:06:05.772Z',
- updated_at: '2018-05-29T12:06:05.772Z',
- system: true,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- system_note_icon_name: 'pencil-square',
- discussion_id: '7f1feda384083eb31763366e6392399fde6f3f31',
- emoji_awardable: false,
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_903&user_id=1',
- human_access: 'Owner',
- path: '/gitlab-org/gitlab-shell/notes/903',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- expanded: true,
- notes: [
- {
- id: '904',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:06:16.112Z',
- updated_at: '2018-05-29T12:06:16.112Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'Ullamcorper eget nulla facilisi etiam',
- note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/904',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- expanded: true,
- notes: [
- {
- id: '905',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:06:28.851Z',
- updated_at: '2018-05-29T12:06:28.851Z',
- system: true,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- system_note_icon_name: 'pencil-square',
- discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- emoji_awardable: false,
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
- human_access: 'Owner',
- path: '/gitlab-org/gitlab-shell/notes/905',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- expanded: true,
- notes: [
- {
- id: '906',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:20:02.925Z',
- updated_at: '2018-05-29T12:20:02.925Z',
- system: true,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- system_note_icon_name: 'pencil-square',
- discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- emoji_awardable: false,
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
- human_access: 'Owner',
- path: '/gitlab-org/gitlab-shell/notes/906',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
-];
-
-export const collapsedSystemNotes = [
- {
- id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- expanded: true,
- notes: [
- {
- id: '901',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:05:36.117Z',
- updated_at: '2018-05-29T12:05:36.117Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note:
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
- note_html:
- '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/901',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- expanded: true,
- notes: [
- {
- id: '902',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:05:58.694Z',
- updated_at: '2018-05-29T12:05:58.694Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note:
- 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.',
- note_html:
- '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/902',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- expanded: true,
- notes: [
- {
- id: '904',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:06:16.112Z',
- updated_at: '2018-05-29T12:06:16.112Z',
- system: false,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'Ullamcorper eget nulla facilisi etiam',
- note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>',
- current_user: { can_edit: true, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5',
- emoji_awardable: true,
- award_emoji: [],
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1',
- human_access: 'Owner',
- toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji',
- path: '/gitlab-org/gitlab-shell/notes/904',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- expanded: true,
- notes: [
- {
- id: '905',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:06:28.851Z',
- updated_at: '2018-05-29T12:06:28.851Z',
- system: true,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'changed the description',
- note_html: ' <p dir="auto">changed the description 2 times within 1 minute </p>',
- current_user: { can_edit: false, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- system_note_icon_name: 'pencil-square',
- discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044',
- emoji_awardable: false,
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1',
- human_access: 'Owner',
- path: '/gitlab-org/gitlab-shell/notes/905',
- times_updated: 2,
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
- {
- id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- expanded: true,
- notes: [
- {
- id: '906',
- type: null,
- attachment: null,
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- path: '/root',
- },
- created_at: '2018-05-29T12:20:02.925Z',
- updated_at: '2018-05-29T12:20:02.925Z',
- system: true,
- noteable_id: 182,
- noteable_type: 'Issue',
- resolvable: false,
- noteable_iid: 12,
- note: 'changed the description',
- note_html: '<p dir="auto">changed the description</p>',
- current_user: { can_edit: false, can_award_emoji: true },
- resolved: false,
- resolved_by: null,
- system_note_icon_name: 'pencil-square',
- discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620',
- emoji_awardable: false,
- report_abuse_path:
- '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1',
- human_access: 'Owner',
- path: '/gitlab-org/gitlab-shell/notes/906',
- },
- ],
- individual_note: true,
- resolvable: false,
- resolved: false,
- diff_discussion: false,
- },
-];
-
-export const discussion1 = {
- id: 'abc1',
- resolvable: true,
- resolved: false,
- active: true,
- diff_file: {
- file_path: 'about.md',
- },
- position: {
- new_line: 50,
- old_line: null,
- },
- notes: [
- {
- created_at: '2018-07-04T16:25:41.749Z',
- },
- ],
-};
-
-export const resolvedDiscussion1 = {
- id: 'abc1',
- resolvable: true,
- resolved: true,
- diff_file: {
- file_path: 'about.md',
- },
- position: {
- new_line: 50,
- old_line: null,
- },
- notes: [
- {
- created_at: '2018-07-04T16:25:41.749Z',
- },
- ],
-};
-
-export const discussion2 = {
- id: 'abc2',
- resolvable: true,
- resolved: false,
- active: true,
- diff_file: {
- file_path: 'README.md',
- },
- position: {
- new_line: null,
- old_line: 20,
- },
- notes: [
- {
- created_at: '2018-07-04T12:05:41.749Z',
- },
- ],
-};
-
-export const discussion3 = {
- id: 'abc3',
- resolvable: true,
- active: true,
- resolved: false,
- diff_file: {
- file_path: 'README.md',
- },
- position: {
- new_line: 21,
- old_line: null,
- },
- notes: [
- {
- created_at: '2018-07-05T17:25:41.749Z',
- },
- ],
-};
-
-export const unresolvableDiscussion = {
- resolvable: false,
-};
-
-export const discussionFiltersMock = [
- {
- title: 'Show all activity',
- value: 0,
- },
- {
- title: 'Show comments only',
- value: 1,
- },
- {
- title: 'Show system notes only',
- value: 2,
- },
-];
+export * from '../../frontend/notes/mock_data.js';
diff --git a/spec/javascripts/notes/stores/collapse_utils_spec.js b/spec/javascripts/notes/stores/collapse_utils_spec.js
index 8ede9319088..d3019f4b9a4 100644
--- a/spec/javascripts/notes/stores/collapse_utils_spec.js
+++ b/spec/javascripts/notes/stores/collapse_utils_spec.js
@@ -1,6 +1,5 @@
import {
isDescriptionSystemNote,
- changeDescriptionNote,
getTimeDifferenceMinutes,
collapseSystemNotes,
} from '~/notes/stores/collapse_utils';
@@ -24,15 +23,6 @@ describe('Collapse utils', () => {
);
});
- it('changes the description to contain the number of changed times', () => {
- const changedNote = changeDescriptionNote(mockSystemNote, 3, 5);
-
- expect(changedNote.times_updated).toEqual(3);
- expect(changedNote.note_html.trim()).toContain(
- '<p dir="auto">changed the description 3 times within 5 minutes </p>',
- );
- });
-
it('gets the time difference between two notes', () => {
const anotherSystemNote = {
created_at: '2018-05-14T21:33:00.000Z',
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
deleted file mode 100644
index 321497b35b5..00000000000
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import actionComponent from '~/pipelines/components/graph/action_component.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('pipeline graph action component', () => {
- let component;
- let mock;
-
- beforeEach(done => {
- const ActionComponent = Vue.extend(actionComponent);
- mock = new MockAdapter(axios);
-
- mock.onPost('foo.json').reply(200);
-
- component = mountComponent(ActionComponent, {
- tooltipText: 'bar',
- link: 'foo',
- actionIcon: 'cancel',
- });
-
- Vue.nextTick(done);
- });
-
- afterEach(() => {
- mock.restore();
- component.$destroy();
- });
-
- it('should render the provided title as a bootstrap tooltip', () => {
- expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
- });
-
- it('should update bootstrap tooltip when title changes', done => {
- component.tooltipText = 'changed';
-
- component
- .$nextTick()
- .then(() => {
- expect(component.$el.getAttribute('data-original-title')).toBe('changed');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should render an svg', () => {
- expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
- expect(component.$el.querySelector('svg')).toBeDefined();
- });
-
- describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successful request', done => {
- spyOn(component, '$emit');
-
- component.$el.click();
-
- setTimeout(() => {
- component
- .$nextTick()
- .then(() => {
- expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
- })
- .catch(done.fail);
-
- done();
- }, 0);
- });
-
- it('renders a loading icon while waiting for request', done => {
- component.$el.click();
-
- component.$nextTick(() => {
- expect(component.$el.querySelector('.js-action-icon-loading')).not.toBeNull();
- setTimeout(() => {
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
deleted file mode 100644
index af634a0c196..00000000000
--- a/spec/javascripts/raven/raven_config_spec.js
+++ /dev/null
@@ -1,254 +0,0 @@
-import Raven from 'raven-js';
-import RavenConfig from '~/raven/raven_config';
-
-describe('RavenConfig', () => {
- describe('IGNORE_ERRORS', () => {
- it('should be an array of strings', () => {
- const areStrings = RavenConfig.IGNORE_ERRORS.every(error => typeof error === 'string');
-
- expect(areStrings).toBe(true);
- });
- });
-
- describe('IGNORE_URLS', () => {
- it('should be an array of regexps', () => {
- const areRegExps = RavenConfig.IGNORE_URLS.every(url => url instanceof RegExp);
-
- expect(areRegExps).toBe(true);
- });
- });
-
- describe('SAMPLE_RATE', () => {
- it('should be a finite number', () => {
- expect(typeof RavenConfig.SAMPLE_RATE).toEqual('number');
- });
- });
-
- describe('init', () => {
- const options = {
- currentUserId: 1,
- };
-
- beforeEach(() => {
- spyOn(RavenConfig, 'configure');
- spyOn(RavenConfig, 'bindRavenErrors');
- spyOn(RavenConfig, 'setUser');
-
- RavenConfig.init(options);
- });
-
- it('should set the options property', () => {
- expect(RavenConfig.options).toEqual(options);
- });
-
- it('should call the configure method', () => {
- expect(RavenConfig.configure).toHaveBeenCalled();
- });
-
- it('should call the error bindings method', () => {
- expect(RavenConfig.bindRavenErrors).toHaveBeenCalled();
- });
-
- it('should call setUser', () => {
- expect(RavenConfig.setUser).toHaveBeenCalled();
- });
-
- it('should not call setUser if there is no current user ID', () => {
- RavenConfig.setUser.calls.reset();
-
- options.currentUserId = undefined;
-
- RavenConfig.init(options);
-
- expect(RavenConfig.setUser).not.toHaveBeenCalled();
- });
- });
-
- describe('configure', () => {
- let raven;
- let ravenConfig;
- const options = {
- sentryDsn: '//sentryDsn',
- whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
- environment: 'test',
- release: 'revision',
- tags: {
- revision: 'revision',
- },
- };
-
- beforeEach(() => {
- ravenConfig = jasmine.createSpyObj('ravenConfig', ['shouldSendSample']);
- raven = jasmine.createSpyObj('raven', ['install']);
-
- spyOn(Raven, 'config').and.returnValue(raven);
-
- ravenConfig.options = options;
- ravenConfig.IGNORE_ERRORS = 'ignore_errors';
- ravenConfig.IGNORE_URLS = 'ignore_urls';
-
- RavenConfig.configure.call(ravenConfig);
- });
-
- it('should call Raven.config', () => {
- expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, {
- release: options.release,
- tags: options.tags,
- whitelistUrls: options.whitelistUrls,
- environment: 'test',
- ignoreErrors: ravenConfig.IGNORE_ERRORS,
- ignoreUrls: ravenConfig.IGNORE_URLS,
- shouldSendCallback: jasmine.any(Function),
- });
- });
-
- it('should call Raven.install', () => {
- expect(raven.install).toHaveBeenCalled();
- });
-
- it('should set environment from options', () => {
- ravenConfig.options.environment = 'development';
-
- RavenConfig.configure.call(ravenConfig);
-
- expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, {
- release: options.release,
- tags: options.tags,
- whitelistUrls: options.whitelistUrls,
- environment: 'development',
- ignoreErrors: ravenConfig.IGNORE_ERRORS,
- ignoreUrls: ravenConfig.IGNORE_URLS,
- shouldSendCallback: jasmine.any(Function),
- });
- });
- });
-
- describe('setUser', () => {
- let ravenConfig;
-
- beforeEach(() => {
- ravenConfig = { options: { currentUserId: 1 } };
- spyOn(Raven, 'setUserContext');
-
- RavenConfig.setUser.call(ravenConfig);
- });
-
- it('should call .setUserContext', function() {
- expect(Raven.setUserContext).toHaveBeenCalledWith({
- id: ravenConfig.options.currentUserId,
- });
- });
- });
-
- describe('handleRavenErrors', () => {
- let event;
- let req;
- let config;
- let err;
-
- beforeEach(() => {
- event = {};
- req = { status: 'status', responseText: 'responseText', statusText: 'statusText' };
- config = { type: 'type', url: 'url', data: 'data' };
- err = {};
-
- spyOn(Raven, 'captureMessage');
-
- RavenConfig.handleRavenErrors(event, req, config, err);
- });
-
- it('should call Raven.captureMessage', () => {
- expect(Raven.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: err,
- event,
- },
- });
- });
-
- describe('if no err is provided', () => {
- beforeEach(() => {
- Raven.captureMessage.calls.reset();
-
- RavenConfig.handleRavenErrors(event, req, config);
- });
-
- it('should use req.statusText as the error value', () => {
- expect(Raven.captureMessage).toHaveBeenCalledWith(req.statusText, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: req.responseText,
- error: req.statusText,
- event,
- },
- });
- });
- });
-
- describe('if no req.responseText is provided', () => {
- beforeEach(() => {
- req.responseText = undefined;
-
- Raven.captureMessage.calls.reset();
-
- RavenConfig.handleRavenErrors(event, req, config, err);
- });
-
- it('should use `Unknown response text` as the response', () => {
- expect(Raven.captureMessage).toHaveBeenCalledWith(err, {
- extra: {
- type: config.type,
- url: config.url,
- data: config.data,
- status: req.status,
- response: 'Unknown response text',
- error: err,
- event,
- },
- });
- });
- });
- });
-
- describe('shouldSendSample', () => {
- let randomNumber;
-
- beforeEach(() => {
- RavenConfig.SAMPLE_RATE = 50;
-
- spyOn(Math, 'random').and.callFake(() => randomNumber);
- });
-
- it('should call Math.random', () => {
- RavenConfig.shouldSendSample();
-
- expect(Math.random).toHaveBeenCalled();
- });
-
- it('should return true if the sample rate is greater than the random number * 100', () => {
- randomNumber = 0.1;
-
- expect(RavenConfig.shouldSendSample()).toBe(true);
- });
-
- it('should return false if the sample rate is less than the random number * 100', () => {
- randomNumber = 0.9;
-
- expect(RavenConfig.shouldSendSample()).toBe(false);
- });
-
- it('should return true if the sample rate is equal to the random number * 100', () => {
- randomNumber = 0.5;
-
- expect(RavenConfig.shouldSendSample()).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 9702cb56d99..1798f9962e2 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-var, one-var, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, vars-on-top */
+/* eslint-disable no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign */
import $ from 'jquery';
import '~/gl_dropdown';
@@ -6,41 +6,27 @@ import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
describe('Search autocomplete dropdown', () => {
- var assertLinks,
- dashboardIssuesPath,
- dashboardMRsPath,
- groupIssuesPath,
- groupMRsPath,
- groupName,
- mockDashboardOptions,
- mockGroupOptions,
- mockProjectOptions,
- projectIssuesPath,
- projectMRsPath,
- projectName,
- userId,
- widget;
- var userName = 'root';
+ let widget = null;
- widget = null;
+ const userName = 'root';
- userId = 1;
+ const userId = 1;
- dashboardIssuesPath = '/dashboard/issues';
+ const dashboardIssuesPath = '/dashboard/issues';
- dashboardMRsPath = '/dashboard/merge_requests';
+ const dashboardMRsPath = '/dashboard/merge_requests';
- projectIssuesPath = '/gitlab-org/gitlab-foss/issues';
+ const projectIssuesPath = '/gitlab-org/gitlab-foss/issues';
- projectMRsPath = '/gitlab-org/gitlab-foss/merge_requests';
+ const projectMRsPath = '/gitlab-org/gitlab-foss/merge_requests';
- groupIssuesPath = '/groups/gitlab-org/issues';
+ const groupIssuesPath = '/groups/gitlab-org/issues';
- groupMRsPath = '/groups/gitlab-org/merge_requests';
+ const groupMRsPath = '/groups/gitlab-org/merge_requests';
- projectName = 'GitLab Community Edition';
+ const projectName = 'GitLab Community Edition';
- groupName = 'Gitlab Org';
+ const groupName = 'Gitlab Org';
const removeBodyAttributes = function() {
const $body = $('body');
@@ -76,7 +62,7 @@ describe('Search autocomplete dropdown', () => {
};
// Mock `gl` object in window for dashboard specific page. App code will need it.
- mockDashboardOptions = function() {
+ const mockDashboardOptions = function() {
window.gl || (window.gl = {});
return (window.gl.dashboardOptions = {
issuesPath: dashboardIssuesPath,
@@ -85,7 +71,7 @@ describe('Search autocomplete dropdown', () => {
};
// Mock `gl` object in window for project specific page. App code will need it.
- mockProjectOptions = function() {
+ const mockProjectOptions = function() {
window.gl || (window.gl = {});
return (window.gl.projectOptions = {
'gitlab-ce': {
@@ -96,7 +82,7 @@ describe('Search autocomplete dropdown', () => {
});
};
- mockGroupOptions = function() {
+ const mockGroupOptions = function() {
window.gl || (window.gl = {});
return (window.gl.groupOptions = {
'gitlab-org': {
@@ -107,7 +93,7 @@ describe('Search autocomplete dropdown', () => {
});
};
- assertLinks = function(list, issuesPath, mrsPath) {
+ const assertLinks = function(list, issuesPath, mrsPath) {
if (issuesPath) {
const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_username=${userName}"]`;
const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_username=${userName}"]`;
@@ -144,29 +130,26 @@ describe('Search autocomplete dropdown', () => {
});
it('should show Dashboard specific dropdown menu', function() {
- var list;
addBodyAttributes();
mockDashboardOptions();
widget.searchInput.triggerHandler('focus');
- list = widget.wrap.find('.dropdown-menu').find('ul');
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
it('should show Group specific dropdown menu', function() {
- var list;
addBodyAttributes('group');
mockGroupOptions();
widget.searchInput.triggerHandler('focus');
- list = widget.wrap.find('.dropdown-menu').find('ul');
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
it('should show Project specific dropdown menu', function() {
- var list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.triggerHandler('focus');
- list = widget.wrap.find('.dropdown-menu').find('ul');
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
@@ -180,26 +163,25 @@ describe('Search autocomplete dropdown', () => {
});
it('should not show category related menu if there is text in the input', function() {
- var link, list;
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
widget.searchInput.triggerHandler('focus');
- list = widget.wrap.find('.dropdown-menu').find('ul');
- link = `a[href='${projectIssuesPath}/?assignee_username=${userName}']`;
+ const list = widget.wrap.find('.dropdown-menu').find('ul');
+ const link = `a[href='${projectIssuesPath}/?assignee_username=${userName}']`;
expect(list.find(link).length).toBe(0);
});
it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
- var ENTER = 13;
- var DOWN = 40;
+ const ENTER = 13;
+ const DOWN = 40;
addBodyAttributes();
mockDashboardOptions(true);
- var submitSpy = spyOnEvent('form', 'submit');
+ const submitSpy = spyOnEvent('form', 'submit');
widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
- var enterKeyEvent = $.Event('keydown', { which: ENTER });
+ const enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
// This does not currently catch failing behavior. For security reasons,
// browsers will not trigger default behavior (form submit, in this
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index a97608d6b8a..1256852c472 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -76,4 +76,25 @@ describe('Subscriptions', function() {
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/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index ef5c774736b..966ae55ce14 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -1,5 +1,7 @@
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
+import trackData from '~/pages/sessions/new/index';
+import Tracking from '~/tracking';
describe('SigninTabsMemoizer', () => {
const fixtureTemplate = 'static/signin_tabs.html';
@@ -93,6 +95,50 @@ describe('SigninTabsMemoizer', () => {
});
});
+ describe('trackData', () => {
+ beforeEach(() => {
+ spyOn(Tracking, 'event');
+ });
+
+ describe('with tracking data', () => {
+ beforeEach(() => {
+ gon.tracking_data = {
+ category: 'Growth::Acquisition::Experiment::SignUpFlow',
+ action: 'start',
+ label: 'uuid',
+ property: 'control_group',
+ };
+ trackData();
+ });
+
+ it('should track data when the "click" event of the register tab is triggered', () => {
+ document.querySelector('a[href="#register-pane"]').click();
+
+ expect(Tracking.event).toHaveBeenCalledWith(
+ 'Growth::Acquisition::Experiment::SignUpFlow',
+ 'start',
+ {
+ label: 'uuid',
+ property: 'control_group',
+ },
+ );
+ });
+ });
+
+ describe('without tracking data', () => {
+ beforeEach(() => {
+ gon.tracking_data = undefined;
+ trackData();
+ });
+
+ it('should not track data when the "click" event of the register tab is triggered', () => {
+ document.querySelector('a[href="#register-pane"]').click();
+
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('saveData', () => {
beforeEach(() => {
memo = {
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 5438368ccbe..99c47fa31d4 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,11 +1,10 @@
-/* eslint-disable no-var, no-return-assign */
+/* eslint-disable no-return-assign */
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
describe('Syntax Highlighter', function() {
- var stubUserColorScheme;
- stubUserColorScheme = function(value) {
+ const stubUserColorScheme = function(value) {
if (window.gon == null) {
window.gon = {};
}
@@ -40,9 +39,8 @@ describe('Syntax Highlighter', function() {
});
it('prevents an infinite loop when no matches exist', function() {
- var highlight;
setFixtures('<div></div>');
- highlight = function() {
+ const highlight = function() {
return syntaxHighlight($('div'));
};
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index cb6b158f01c..859745ee9fc 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -171,38 +171,7 @@ describe('test errors', () => {
// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
- const troubleMakers = [
- './blob_edit/blob_bundle.js',
- './boards/components/modal/empty_state.vue',
- './boards/components/modal/footer.js',
- './boards/components/modal/header.js',
- './cycle_analytics/cycle_analytics_bundle.js',
- './cycle_analytics/components/stage_plan_component.js',
- './cycle_analytics/components/stage_staging_component.js',
- './cycle_analytics/components/stage_test_component.js',
- './commit/pipelines/pipelines_bundle.js',
- './diff_notes/diff_notes_bundle.js',
- './diff_notes/components/jump_to_discussion.js',
- './diff_notes/components/resolve_count.js',
- './dispatcher.js',
- './environments/environments_bundle.js',
- './graphs/graphs_bundle.js',
- './issuable/time_tracking/time_tracking_bundle.js',
- './main.js',
- './merge_conflicts/merge_conflicts_bundle.js',
- './merge_conflicts/components/inline_conflict_lines.js',
- './merge_conflicts/components/parallel_conflict_lines.js',
- './monitoring/monitoring_bundle.js',
- './network/network_bundle.js',
- './network/branch_graph.js',
- './profile/profile_bundle.js',
- './protected_branches/protected_branches_bundle.js',
- './snippet/snippet_bundle.js',
- './terminal/terminal_bundle.js',
- './users/users_bundle.js',
- './issue_show/index.js',
- './pages/admin/application_settings/general/index.js',
- ];
+ const troubleMakers = ['./pages/admin/application_settings/general/index.js'];
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index 26ddd8ade61..ec8425a4e3e 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,20 +1,16 @@
-/* eslint-disable no-unused-expressions, no-return-assign, no-param-reassign */
+/* eslint-disable no-unused-expressions */
export default class MockU2FDevice {
constructor() {
this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
window.u2f || (window.u2f = {});
- window.u2f.register = (function(_this) {
- return function(appId, registerRequests, signRequests, callback) {
- return (_this.registerCallback = callback);
- };
- })(this);
- window.u2f.sign = (function(_this) {
- return function(appId, challenges, signRequests, callback) {
- return (_this.authenticateCallback = callback);
- };
- })(this);
+ window.u2f.register = (appId, registerRequests, signRequests, callback) => {
+ this.registerCallback = callback;
+ };
+ window.u2f.sign = (appId, challenges, signRequests, callback) => {
+ this.authenticateCallback = callback;
+ };
}
respondToRegisterRequest(params) {
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
index bdf802052b9..16997e9dc67 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -70,4 +70,30 @@ describe('ContentViewer', () => {
done();
});
});
+
+ 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',
+ });
+
+ setTimeout(() => {
+ expect(axios.post).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/testproject/preview_markdown`,
+ { path: 'foo/test.md', text: '* Test' },
+ jasmine.any(Object),
+ );
+
+ done();
+ });
+ });
});
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
index 660eaddf01f..1acd6b3ebe7 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -1,13 +1,23 @@
import Vue from 'vue';
+
import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
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);
}
@@ -20,15 +30,11 @@ describe('DiffViewer', () => {
relative_url_root: '',
};
- createComponent({
- diffMode: 'replaced',
- diffViewerMode: 'image',
- newPath: GREEN_BOX_IMAGE_URL,
- newSha: 'ABC',
- oldPath: RED_BOX_IMAGE_URL,
- oldSha: 'DEF',
- projectPath: '',
- });
+ createComponent(
+ Object.assign({}, requiredProps, {
+ projectPath: '',
+ }),
+ );
setTimeout(() => {
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
@@ -44,14 +50,13 @@ describe('DiffViewer', () => {
});
it('renders fallback download diff display', done => {
- createComponent({
- diffMode: 'replaced',
- diffViewerMode: 'added',
- newPath: 'test.abc',
- newSha: 'ABC',
- oldPath: 'testold.abc',
- oldSha: 'DEF',
- });
+ createComponent(
+ Object.assign({}, requiredProps, {
+ diffViewerMode: 'added',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ }),
+ );
setTimeout(() => {
expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
@@ -72,29 +77,28 @@ describe('DiffViewer', () => {
});
it('renders renamed component', () => {
- createComponent({
- diffMode: 'renamed',
- diffViewerMode: 'renamed',
- newPath: 'test.abc',
- newSha: 'ABC',
- oldPath: 'testold.abc',
- oldSha: 'DEF',
- });
+ 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({
- diffMode: 'mode_changed',
- diffViewerMode: 'image',
- newPath: 'test.abc',
- newSha: 'ABC',
- oldPath: 'testold.abc',
- oldSha: 'DEF',
- aMode: '123',
- bMode: '321',
- });
+ 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/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 97c870f27d9..0cb26d5000b 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -4,6 +4,11 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
describe('ImageDiffViewer', () => {
+ const requiredProps = {
+ diffMode: 'replaced',
+ newPath: GREEN_BOX_IMAGE_URL,
+ oldPath: RED_BOX_IMAGE_URL,
+ };
let vm;
function createComponent(props) {
@@ -45,11 +50,7 @@ describe('ImageDiffViewer', () => {
});
it('renders image diff for replaced', done => {
- createComponent({
- diffMode: 'replaced',
- newPath: GREEN_BOX_IMAGE_URL,
- oldPath: RED_BOX_IMAGE_URL,
- });
+ createComponent(requiredProps);
setTimeout(() => {
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
@@ -70,11 +71,12 @@ describe('ImageDiffViewer', () => {
});
it('renders image diff for new', done => {
- createComponent({
- diffMode: 'new',
- newPath: GREEN_BOX_IMAGE_URL,
- oldPath: '',
- });
+ createComponent(
+ Object.assign({}, requiredProps, {
+ diffMode: 'new',
+ oldPath: '',
+ }),
+ );
setTimeout(() => {
expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
@@ -84,11 +86,12 @@ describe('ImageDiffViewer', () => {
});
it('renders image diff for deleted', done => {
- createComponent({
- diffMode: 'deleted',
- newPath: '',
- oldPath: RED_BOX_IMAGE_URL,
- });
+ createComponent(
+ Object.assign({}, requiredProps, {
+ diffMode: 'deleted',
+ newPath: '',
+ }),
+ );
setTimeout(() => {
expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(RED_BOX_IMAGE_URL);
@@ -119,11 +122,7 @@ describe('ImageDiffViewer', () => {
describe('swipeMode', () => {
beforeEach(done => {
- createComponent({
- diffMode: 'replaced',
- newPath: GREEN_BOX_IMAGE_URL,
- oldPath: RED_BOX_IMAGE_URL,
- });
+ createComponent(requiredProps);
setTimeout(() => {
done();
@@ -142,11 +141,7 @@ describe('ImageDiffViewer', () => {
describe('onionSkin', () => {
beforeEach(done => {
- createComponent({
- diffMode: 'replaced',
- newPath: GREEN_BOX_IMAGE_URL,
- oldPath: RED_BOX_IMAGE_URL,
- });
+ createComponent(requiredProps);
setTimeout(() => {
done();
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index 7390798afa8..ecaef414464 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Icon from '~/vue_shared/components/icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
describe('Sprite Icon Component', function() {
describe('Initialization', function() {
@@ -57,4 +58,16 @@ describe('Sprite Icon Component', function() {
expect(Icon.props.name.validator('commit')).toBe(true);
});
});
+
+ it('should call registered listeners when they are triggered', () => {
+ const clickHandler = jasmine.createSpy('clickHandler');
+ const wrapper = mount(Icon, {
+ propsData: { name: 'commit' },
+ listeners: { click: clickHandler },
+ });
+
+ wrapper.find('svg').trigger('click');
+
+ expect(clickHandler).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
index 9c2deca585b..323a0f03017 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -3,7 +3,7 @@ import _ from 'underscore';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { trimText } from 'spec/helpers/text_helper';
@@ -91,6 +91,13 @@ describe('ProjectSelector component', () => {
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', _.first(searchResults));
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
deleted file mode 100644
index c5045afc5b0..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import Vue from 'vue';
-import { placeholderImage } from '~/lazy_loader';
-import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
-import defaultAvatarUrl from '~/../images/no_avatar.png';
-
-const DEFAULT_PROPS = {
- size: 99,
- imgSrc: 'myavatarurl.com',
- imgAlt: 'mydisplayname',
- cssClasses: 'myextraavatarclass',
- tooltipText: 'tooltip text',
- tooltipPlacement: 'bottom',
-};
-
-describe('User Avatar Image Component', function() {
- let vm;
- let UserAvatarImage;
-
- beforeEach(() => {
- UserAvatarImage = Vue.extend(userAvatarImage);
- });
-
- describe('Initialization', function() {
- beforeEach(function() {
- vm = mountComponent(UserAvatarImage, {
- ...DEFAULT_PROPS,
- }).$mount();
- });
-
- it('should return a defined Vue component', function() {
- expect(vm).toBeDefined();
- });
-
- it('should have <img> as a child element', function() {
- const imageElement = vm.$el.querySelector('img');
-
- expect(imageElement).not.toBe(null);
- expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
- });
-
- it('should properly compute avatarSizeClass', function() {
- expect(vm.avatarSizeClass).toBe('s99');
- });
-
- it('should properly render img css', function() {
- const { classList } = vm.$el.querySelector('img');
- const containsAvatar = classList.contains('avatar');
- const containsSizeClass = classList.contains('s99');
- const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
- const lazyClass = classList.contains('lazy');
-
- expect(containsAvatar).toBe(true);
- expect(containsSizeClass).toBe(true);
- expect(containsCustomClass).toBe(true);
- expect(lazyClass).toBe(false);
- });
- });
-
- describe('Initialization when lazy', function() {
- beforeEach(function() {
- vm = mountComponent(UserAvatarImage, {
- ...DEFAULT_PROPS,
- lazy: true,
- }).$mount();
- });
-
- it('should add lazy attributes', function() {
- const imageElement = vm.$el.querySelector('img');
- const lazyClass = imageElement.classList.contains('lazy');
-
- expect(lazyClass).toBe(true);
- expect(imageElement.getAttribute('src')).toBe(placeholderImage);
- expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- });
- });
-
- describe('Initialization without src', function() {
- beforeEach(function() {
- vm = mountComponent(UserAvatarImage);
- });
-
- it('should have default avatar image', function() {
- const imageElement = vm.$el.querySelector('img');
-
- expect(imageElement.getAttribute('src')).toBe(defaultAvatarUrl);
- });
- });
-
- describe('dynamic tooltip content', () => {
- const props = DEFAULT_PROPS;
- const slots = {
- default: ['Action!'],
- };
-
- beforeEach(() => {
- vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount();
- });
-
- it('renders the tooltip slot', () => {
- expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null);
- });
-
- it('renders the tooltip content', () => {
- expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain(
- slots.default[0],
- );
- });
-
- it('does not render tooltip data attributes for on avatar image', () => {
- const avatarImg = vm.$el.querySelector('img');
-
- expect(avatarImg.dataset.originalTitle).not.toBeDefined();
- expect(avatarImg.dataset.placement).not.toBeDefined();
- expect(avatarImg.dataset.container).not.toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
deleted file mode 100644
index c7e0d806d80..00000000000
--- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
+++ /dev/null
@@ -1,167 +0,0 @@
-import Vue from 'vue';
-import userPopover from '~/vue_shared/components/user_popover/user_popover.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const DEFAULT_PROPS = {
- loaded: true,
- user: {
- username: 'root',
- name: 'Administrator',
- location: 'Vienna',
- bio: null,
- organization: null,
- status: null,
- },
-};
-
-const UserPopover = Vue.extend(userPopover);
-
-describe('User Popover Component', () => {
- const fixtureTemplate = 'merge_requests/diff_comment.html';
- preloadFixtures(fixtureTemplate);
-
- let vm;
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('Empty', () => {
- beforeEach(() => {
- vm = mountComponent(UserPopover, {
- target: document.querySelector('.js-user-link'),
- user: {
- name: null,
- username: null,
- location: null,
- bio: null,
- organization: null,
- status: null,
- },
- });
- });
-
- it('should return skeleton loaders', () => {
- expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4);
- });
- });
-
- describe('basic data', () => {
- it('should show basic fields', () => {
- vm = mountComponent(UserPopover, {
- ...DEFAULT_PROPS,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name);
- expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username);
- expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location);
- });
-
- it('shows icon for location', () => {
- const iconEl = vm.$el.querySelector('.js-location svg');
-
- expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('location');
- });
- });
-
- describe('job data', () => {
- it('should show only bio if no organization is available', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.bio = 'Engineer';
-
- vm = mountComponent(UserPopover, {
- ...testProps,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.textContent).toContain('Engineer');
- });
-
- it('should show only organization if no bio is available', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.organization = 'GitLab';
-
- vm = mountComponent(UserPopover, {
- ...testProps,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.textContent).toContain('GitLab');
- });
-
- it('should display bio and organization in separate lines', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.bio = 'Engineer';
- testProps.user.organization = 'GitLab';
-
- vm = mountComponent(UserPopover, {
- ...DEFAULT_PROPS,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.querySelector('.js-bio').textContent).toContain('Engineer');
- expect(vm.$el.querySelector('.js-organization').textContent).toContain('GitLab');
- });
-
- it('should not encode special characters in bio and organization', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.bio = 'Manager & Team Lead';
- testProps.user.organization = 'Me & my <funky> Company';
-
- vm = mountComponent(UserPopover, {
- ...DEFAULT_PROPS,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.querySelector('.js-bio').textContent).toContain('Manager & Team Lead');
- expect(vm.$el.querySelector('.js-organization').textContent).toContain(
- 'Me & my <funky> Company',
- );
- });
-
- it('shows icon for bio', () => {
- const iconEl = vm.$el.querySelector('.js-bio svg');
-
- expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('profile');
- });
-
- it('shows icon for organization', () => {
- const iconEl = vm.$el.querySelector('.js-organization svg');
-
- expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('work');
- });
- });
-
- describe('status data', () => {
- it('should show only message', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.status = { message_html: 'Hello World' };
-
- vm = mountComponent(UserPopover, {
- ...DEFAULT_PROPS,
- target: document.querySelector('.js-user-link'),
- });
-
- expect(vm.$el.textContent).toContain('Hello World');
- });
-
- it('should show message and emoji', () => {
- const testProps = Object.assign({}, DEFAULT_PROPS);
- testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' };
-
- vm = mountComponent(UserPopover, {
- ...DEFAULT_PROPS,
- target: document.querySelector('.js-user-link'),
- status: { emoji: 'basketball_player', message_html: 'Hello World' },
- });
-
- expect(vm.$el.textContent).toContain('Hello World');
- expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"');
- });
- });
-});
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index b57adb46385..040ff1a8ebe 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -3,399 +3,20 @@
require 'spec_helper'
describe API::Helpers::Pagination do
- let(:resource) { Project.all }
- let(:custom_port) { 8080 }
- let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" }
+ subject { Class.new.include(described_class).new }
- before do
- stub_config_setting(port: custom_port)
- end
-
- subject do
- Class.new.include(described_class).new
- end
-
- describe '#paginate (keyset pagination)' do
- let(:value) { spy('return value') }
- let(:base_query) do
- {
- pagination: 'keyset',
- foo: 'bar',
- bar: 'baz'
- }
- end
- let(:query) { base_query }
-
- before do
- allow(subject).to receive(:header).and_return(value)
- allow(subject).to receive(:params).and_return(query)
- allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}"))
- end
-
- context 'when resource can be paginated' do
- let!(:projects) do
- [
- create(:project, name: 'One'),
- create(:project, name: 'Two'),
- create(:project, name: 'Three')
- ].sort_by { |e| -e.id } # sort by id desc (this is the default sort order for the API)
- end
-
- describe 'first page' do
- let(:query) { base_query.merge(per_page: 2) }
-
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 2
- end
-
- it 'returns the first two records (by id desc)' do
- expect(subject.paginate(resource)).to eq(projects[0..1])
- end
-
- it 'adds appropriate headers' do
- expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
-
- expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="next"')
- end
-
- subject.paginate(resource)
- end
- end
-
- describe 'second page' do
- let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[1].id) }
-
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 1
- end
-
- it 'returns the third record' do
- expect(subject.paginate(resource)).to eq(projects[2..2])
- end
-
- it 'adds appropriate headers' do
- expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
-
- expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="next"')
- end
-
- subject.paginate(resource)
- end
- end
-
- describe 'third page' do
- let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[2].id) }
-
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 0
- end
-
- it 'adds appropriate headers' do
- expect_header('X-Per-Page', '2')
- expect_no_header('X-Next-Page')
- expect(subject).not_to receive(:header).with('Link')
-
- subject.paginate(resource)
- end
- end
-
- context 'if order' do
- context 'is not present' do
- let(:query) { base_query.merge(per_page: 2) }
-
- it 'is not present it adds default order(:id) desc' do
- resource.order_values = []
-
- paginated_relation = subject.paginate(resource)
-
- expect(resource.order_values).to be_empty
- expect(paginated_relation.order_values).to be_present
- expect(paginated_relation.order_values.size).to eq(1)
- expect(paginated_relation.order_values.first).to be_descending
- expect(paginated_relation.order_values.first.expr.name).to eq 'id'
- end
- end
-
- context 'is present' do
- let(:resource) { Project.all.order(name: :desc) }
- let!(:projects) do
- [
- create(:project, name: 'One'),
- create(:project, name: 'Two'),
- create(:project, name: 'Three'),
- create(:project, name: 'Three'), # Note the duplicate name
- create(:project, name: 'Four'),
- create(:project, name: 'Five'),
- create(:project, name: 'Six')
- ]
-
- # if we sort this by name descending, id descending, this yields:
- # {
- # 2 => "Two",
- # 4 => "Three",
- # 3 => "Three",
- # 7 => "Six",
- # 1 => "One",
- # 5 => "Four",
- # 6 => "Five"
- # }
- #
- # (key is the id)
- end
-
- it 'also orders by primary key' do
- paginated_relation = subject.paginate(resource)
-
- expect(paginated_relation.order_values).to be_present
- expect(paginated_relation.order_values.size).to eq(2)
- expect(paginated_relation.order_values.first).to be_descending
- expect(paginated_relation.order_values.first.expr.name).to eq 'name'
- expect(paginated_relation.order_values.second).to be_descending
- expect(paginated_relation.order_values.second.expr.name).to eq 'id'
- end
-
- it 'returns the right records (first page)' do
- result = subject.paginate(resource)
-
- expect(result.first).to eq(projects[1])
- expect(result.second).to eq(projects[3])
- end
-
- describe 'second page' do
- let(:query) { base_query.merge(ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2) }
-
- it 'returns the right records (second page)' do
- result = subject.paginate(resource)
-
- expect(result.first).to eq(projects[2])
- expect(result.second).to eq(projects[6])
- end
-
- it 'returns the right link to the next page' do
- expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
- expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="next"')
- end
-
- subject.paginate(resource)
- end
- end
-
- describe 'third page' do
- let(:query) { base_query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5) }
-
- it 'returns the right records (third page), note increased per_page' do
- result = subject.paginate(resource)
-
- expect(result.size).to eq(3)
- expect(result.first).to eq(projects[0])
- expect(result.second).to eq(projects[4])
- expect(result.last).to eq(projects[5])
- end
- end
- end
- end
- end
- end
-
- describe '#paginate (default offset-based pagination)' do
- let(:value) { spy('return value') }
- let(:base_query) { { foo: 'bar', bar: 'baz' } }
- let(:query) { base_query }
-
- before do
- allow(subject).to receive(:header).and_return(value)
- allow(subject).to receive(:params).and_return(query)
- allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}"))
- end
-
- context 'when resource can be paginated' do
- before do
- create_list(:project, 3)
- end
-
- describe 'first page' do
- shared_examples 'response with pagination headers' do
- it 'adds appropriate headers' do
- expect_header('X-Total', '3')
- expect_header('X-Total-Pages', '2')
- expect_header('X-Per-Page', '2')
- expect_header('X-Page', '1')
- expect_header('X-Next-Page', '2')
- expect_header('X-Prev-Page', '')
-
- expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
- expect(val).not_to include('rel="prev"')
- end
-
- subject.paginate(resource)
- end
- end
-
- shared_examples 'paginated response' do
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 2
- end
-
- it 'executes only one SELECT COUNT query' do
- expect { subject.paginate(resource) }.to make_queries_matching(/SELECT COUNT/, 1)
- end
- end
-
- let(:query) { base_query.merge(page: 1, per_page: 2) }
-
- context 'when the api_kaminari_count_with_limit feature flag is unset' do
- it_behaves_like 'paginated response'
- it_behaves_like 'response with pagination headers'
- end
-
- context 'when the api_kaminari_count_with_limit feature flag is disabled' do
- before do
- stub_feature_flags(api_kaminari_count_with_limit: false)
- end
-
- it_behaves_like 'paginated response'
- it_behaves_like 'response with pagination headers'
- end
-
- context 'when the api_kaminari_count_with_limit feature flag is enabled' do
- before do
- stub_feature_flags(api_kaminari_count_with_limit: true)
- end
-
- context 'when resources count is less than MAX_COUNT_LIMIT' do
- before do
- stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 4)
- end
-
- it_behaves_like 'paginated response'
- it_behaves_like 'response with pagination headers'
- end
-
- context 'when resources count is more than MAX_COUNT_LIMIT' do
- before do
- stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 2)
- end
-
- it_behaves_like 'paginated response'
-
- it 'does not return the X-Total and X-Total-Pages headers' do
- expect_no_header('X-Total')
- expect_no_header('X-Total-Pages')
- expect_header('X-Per-Page', '2')
- expect_header('X-Page', '1')
- expect_header('X-Next-Page', '2')
- expect_header('X-Prev-Page', '')
+ describe '#paginate' do
+ let(:relation) { double("relation") }
+ let(:offset_pagination) { double("offset pagination") }
+ let(:expected_result) { double("result") }
- expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
- expect(val).not_to include('rel="last"')
- expect(val).not_to include('rel="prev"')
- end
+ it 'delegates to OffsetPagination' do
+ expect(::Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(offset_pagination)
+ expect(offset_pagination).to receive(:paginate).with(relation).and_return(expected_result)
- subject.paginate(resource)
- end
- end
- end
- end
+ result = subject.paginate(relation)
- describe 'second page' do
- let(:query) { base_query.merge(page: 2, per_page: 2) }
-
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 1
- end
-
- it 'adds appropriate headers' do
- expect_header('X-Total', '3')
- expect_header('X-Total-Pages', '2')
- expect_header('X-Per-Page', '2')
- expect_header('X-Page', '2')
- expect_header('X-Next-Page', '')
- expect_header('X-Prev-Page', '1')
-
- expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
- expect(val).not_to include('rel="next"')
- end
-
- subject.paginate(resource)
- end
- end
-
- context 'if order' do
- it 'is not present it adds default order(:id) if no order is present' do
- resource.order_values = []
-
- paginated_relation = subject.paginate(resource)
-
- expect(resource.order_values).to be_empty
- expect(paginated_relation.order_values).to be_present
- expect(paginated_relation.order_values.first).to be_ascending
- expect(paginated_relation.order_values.first.expr.name).to eq 'id'
- end
-
- it 'is present it does not add anything' do
- paginated_relation = subject.paginate(resource.order(created_at: :desc))
-
- expect(paginated_relation.order_values).to be_present
- expect(paginated_relation.order_values.first).to be_descending
- expect(paginated_relation.order_values.first.expr.name).to eq 'created_at'
- end
- end
+ expect(result).to eq(expected_result)
end
-
- context 'when resource empty' do
- describe 'first page' do
- let(:query) { base_query.merge(page: 1, per_page: 2) }
-
- it 'returns appropriate amount of resources' do
- expect(subject.paginate(resource).count).to eq 0
- end
-
- it 'adds appropriate headers' do
- expect_header('X-Total', '0')
- expect_header('X-Total-Pages', '1')
- expect_header('X-Per-Page', '2')
- expect_header('X-Page', '1')
- expect_header('X-Next-Page', '')
- expect_header('X-Prev-Page', '')
-
- expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
- expect(val).not_to include('rel="prev"')
- expect(val).not_to include('rel="next"')
- expect(val).not_to include('page=0')
- end
-
- subject.paginate(resource)
- end
- end
- end
- end
-
- def expect_header(*args, &block)
- expect(subject).to receive(:header).with(*args, &block)
- end
-
- def expect_no_header(*args, &block)
- expect(subject).not_to receive(:header).with(*args)
- end
-
- def expect_message(method)
- expect(subject).to receive(method)
- .at_least(:once).and_return(value)
end
end
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 0624c25e734..81c4563feb6 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -174,4 +174,18 @@ describe API::Helpers do
end
end
end
+
+ describe '#track_event' do
+ it "creates a gitlab tracking event" do
+ expect(Gitlab::Tracking).to receive(:event).with('foo', 'my_event', {})
+
+ subject.track_event('my_event', category: 'foo')
+ end
+
+ it "logs an exception" do
+ expect(Rails.logger).to receive(:warn).with(/Tracking event failed/)
+
+ subject.track_event('my_event', category: nil)
+ end
+ end
end
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index bf827fb3914..5f120f258cd 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -70,7 +70,7 @@ describe Backup::Repository do
end
context 'restoring object pools' do
- it 'schedules restoring of the pool' do
+ it 'schedules restoring of the pool', :sidekiq_might_not_need_inline do
pool_repository = create(:pool_repository, :failed)
pool_repository.delete_object_pool
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index 0c4ccbf28f4..ff2346fe1ba 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Banzai::Filter::AssetProxyFilter do
diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
new file mode 100644
index 00000000000..fd6f8816b63
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::InlineGrafanaMetricsFilter do
+ include FilterSpecHelper
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ let(:url) { grafana_integration.grafana_url + dashboard_path }
+ let(:dashboard_path) do
+ '/d/XDaNK6amz/gitlab-omnibus-redis' \
+ '?from=1570397739557&to=1570484139557' \
+ '&var-instance=All&panelId=14'
+ end
+
+ it 'appends a metrics charts placeholder with dashboard url after metrics links' do
+ node = doc.at_css('.js-render-metrics')
+ expect(node).to be_present
+
+ dashboard_url = urls.project_grafana_api_metrics_dashboard_url(
+ project,
+ embedded: true,
+ grafana_url: url,
+ start: "2019-10-06T21:35:39Z",
+ end: "2019-10-07T21:35:39Z"
+ )
+
+ expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
+ end
+
+ context 'when the dashboard link is part of a paragraph' do
+ let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
+ let(:input) { %(<p>#{paragraph}</p>) }
+
+ it 'appends the charts placeholder after the enclosing paragraph' do
+ expect(unescape(doc.at_css('p').to_s)).to include(paragraph)
+ expect(doc.at_css('.js-render-metrics')).to be_present
+ end
+ end
+
+ context 'when grafana is not configured' do
+ before do
+ allow(project).to receive(:grafana_integration).and_return(nil)
+ end
+
+ it 'leaves the markdown unchanged' do
+ expect(unescape(doc.to_s)).to eq(input)
+ end
+ end
+
+ context 'when parameters are missing' do
+ let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' }
+
+ it 'leaves the markdown unchanged' do
+ expect(unescape(doc.to_s)).to eq(input)
+ end
+ end
+
+ private
+
+ # Nokogiri escapes the URLs, but we don't care about that
+ # distinction for the purposes of this filter
+ def unescape(html)
+ CGI.unescapeHTML(html)
+ end
+end
diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
index a99cd7d6076..745b9133529 100644
--- a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -18,30 +18,48 @@ describe Banzai::Filter::InlineMetricsRedactorFilter do
end
context 'with a metrics charts placeholder' do
- let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
+ shared_examples_for 'a supported metrics dashboard url' do
+ context 'no user is logged in' do
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
- context 'no user is logged in' do
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
+ context 'the user does not have permission do see charts' do
+ let(:doc) { filter(input, current_user: build(:user)) }
+
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
end
- end
- context 'the user does not have permission do see charts' do
- let(:doc) { filter(input, current_user: build(:user)) }
+ context 'the user has requisite permissions' do
+ let(:user) { create(:user) }
+ let(:doc) { filter(input, current_user: user) }
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
+ it 'leaves the placeholder' do
+ project.add_maintainer(user)
+
+ expect(doc.to_s).to eq input
+ end
end
end
- context 'the user has requisite permissions' do
- let(:user) { create(:user) }
- let(:doc) { filter(input, current_user: user) }
+ let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
- it 'leaves the placeholder' do
- project.add_maintainer(user)
+ it_behaves_like 'a supported metrics dashboard url'
+
+ context 'for a grafana dashboard' do
+ let(:url) { urls.project_grafana_api_metrics_dashboard_url(project, embedded: true) }
+
+ it_behaves_like 'a supported metrics dashboard url'
+ end
- expect(doc.to_s).to eq input
+ context 'for an internal non-dashboard url' do
+ let(:url) { urls.project_url(project) }
+
+ it 'leaves the placeholder' do
+ expect(doc.to_s).to be_empty
end
end
end
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
index a395b021f32..c324c36fe4d 100644
--- a/spec/lib/banzai/filter/video_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -32,7 +32,7 @@ describe Banzai::Filter::VideoLinkFilter do
expect(video.name).to eq 'video'
expect(video['src']).to eq src
- expect(video['width']).to eq "100%"
+ expect(video['width']).to eq "400"
expect(paragraph.name).to eq 'p'
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
index 70b51b8efec..6a9df0e5099 100644
--- a/spec/lib/bitbucket/representation/pull_request_spec.rb
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -20,6 +20,7 @@ describe Bitbucket::Representation::PullRequest do
describe '#state' do
it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') }
it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') }
+ it { expect(described_class.new({ 'state' => 'SUPERSEDED' }).state).to eq('closed') }
it { expect(described_class.new({}).state).to eq('opened') }
end
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 3782c30e88a..a493b96b1e4 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -99,8 +99,8 @@ describe ContainerRegistry::Client do
stub_upload('path', 'content', 'sha256:123', 400)
end
- it 'returns nil' do
- expect(subject).to be nil
+ it 'returns a failure' do
+ expect(subject).not_to be_success
end
end
end
@@ -125,6 +125,14 @@ describe ContainerRegistry::Client do
expect(subject).to eq(result_manifest)
end
+
+ context 'when upload fails' do
+ before do
+ stub_upload('path', "{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', 500)
+ end
+
+ it { is_expected.to be nil }
+ end
end
describe '#put_tag' do
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 7c65525b8dc..415a6e62374 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -58,7 +58,7 @@ module Gitlab
},
'image with onerror' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt='Alt text\" onerror=\"alert(7)'></span></p>\n</div>"
+ output: "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt='Alt text\" onerror=\"alert(7)' class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
},
'fenced code with inline script' => {
input: '```mypre"><script>alert(3)</script>',
@@ -73,6 +73,20 @@ module Gitlab
end
end
+ context "images" do
+ it "does lazy load and link image" do
+ input = 'image:https://localhost.com/image.png[]'
+ output = "<div>\n<p><span><a class=\"no-attachment-icon\" href=\"https://localhost.com/image.png\" target=\"_blank\" rel=\"noopener noreferrer\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
+ end
+
+ it "does not automatically link image if link is explicitly defined" do
+ input = 'image:https://localhost.com/image.png[link=https://gitlab.com]'
+ output = "<div>\n<p><span><a href=\"https://gitlab.com\" rel=\"nofollow noreferrer noopener\" target=\"_blank\"><img src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"image\" class=\"lazy\" data-src=\"https://localhost.com/image.png\"></a></span></p>\n</div>"
+ expect(render(input, context)).to include(output)
+ end
+ end
+
context 'with admonition' do
it 'preserves classes' do
input = <<~ADOC
@@ -107,7 +121,7 @@ module Gitlab
ADOC
output = <<~HTML
- <h2>Title</h2>
+ <h2>Title</h2>
HTML
expect(render(input, context)).to include(output.strip)
@@ -149,15 +163,15 @@ module Gitlab
ADOC
output = <<~HTML
- <div>
- <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
- </div>
- <div>
- <hr>
- <div id="_footnotedef_1">
- <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
- </div>
- </div>
+ <div>
+ <p>This paragraph has a footnote.<sup>[<a id="_footnoteref_1" href="#_footnotedef_1" title="View footnote.">1</a>]</sup></p>
+ </div>
+ <div>
+ <hr>
+ <div id="_footnotedef_1">
+ <a href="#_footnoteref_1">1</a>. This is the text of the footnote.
+ </div>
+ </div>
HTML
expect(render(input, context)).to include(output.strip)
@@ -183,34 +197,34 @@ module Gitlab
ADOC
output = <<~HTML
- <h1>Title</h1>
- <div>
- <h2 id="user-content-first-section">
- <a class="anchor" href="#user-content-first-section"></a>First section</h2>
- <div>
- <div>
- <p>This is the first section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-second-section">
- <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
- <div>
- <div>
- <p>This is the second section.</p>
- </div>
- </div>
- </div>
- <div>
- <h2 id="user-content-thunder">
- <a class="anchor" href="#user-content-thunder"></a>Thunder âš¡ !</h2>
- <div>
- <div>
- <p>This is the third section.</p>
- </div>
- </div>
- </div>
+ <h1>Title</h1>
+ <div>
+ <h2 id="user-content-first-section">
+ <a class="anchor" href="#user-content-first-section"></a>First section</h2>
+ <div>
+ <div>
+ <p>This is the first section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-second-section">
+ <a class="anchor" href="#user-content-second-section"></a>Second section</h2>
+ <div>
+ <div>
+ <p>This is the second section.</p>
+ </div>
+ </div>
+ </div>
+ <div>
+ <h2 id="user-content-thunder">
+ <a class="anchor" href="#user-content-thunder"></a>Thunder âš¡ !</h2>
+ <div>
+ <div>
+ <p>This is the third section.</p>
+ </div>
+ </div>
+ </div>
HTML
expect(render(input, context)).to include(output.strip)
diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
index 05541972f87..adb8e138ca7 100644
--- a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Auth::LDAP::AuthHash do
@@ -91,7 +93,7 @@ describe Gitlab::Auth::LDAP::AuthHash do
let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
before do
- raw_info[:uid] = ['JOHN']
+ raw_info[:uid] = [+'JOHN']
end
it 'enabled the username attribute is lower cased' do
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index 577dfe51949..e4a90d4018d 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -535,4 +535,23 @@ AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
end
end
end
+
+ describe 'sign_in_enabled?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:enabled, :prevent_ldap_sign_in, :result) do
+ true | false | true
+ 'true' | false | true
+ true | true | false
+ false | nil | false
+ end
+
+ with_them do
+ it do
+ stub_ldap_setting(enabled: enabled, prevent_ldap_sign_in: prevent_ldap_sign_in)
+
+ expect(described_class.sign_in_enabled?).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
index 1527fe60fb9..985732e69f9 100644
--- a/spec/lib/gitlab/auth/ldap/person_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Auth::LDAP::Person do
@@ -135,7 +137,7 @@ describe Gitlab::Auth::LDAP::Person do
let(:username_attribute) { 'uid' }
before do
- entry[username_attribute] = 'JOHN'
+ entry[username_attribute] = +'JOHN'
@person = described_class.new(entry, 'ldapmain')
end
diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
index c1eaf1d3433..f2de73d5aea 100644
--- a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb
@@ -91,15 +91,26 @@ describe Gitlab::BackgroundMigration::LegacyUploadMover do
end
end
- context 'when no model found for the upload' do
+ context 'when no note found for the upload' do
before do
- legacy_upload.model = nil
+ legacy_upload.model_id = nil
+ legacy_upload.model_type = 'Note'
expect_error_log
end
it_behaves_like 'legacy upload deletion'
end
+ context 'when upload does not belong to a note' do
+ before do
+ legacy_upload.model = create(:appearance)
+ end
+
+ it 'does not remove the upload' do
+ expect { described_class.new(legacy_upload).execute }.not_to change { Upload.count }
+ end
+ end
+
context 'when the upload move fails' do
before do
expect(FileUploader).to receive(:copy_to).and_raise('failed')
diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
index cabca3dbef9..85187d039c1 100644
--- a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
+++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb
@@ -35,6 +35,8 @@ describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do
let!(:legacy_upload_no_file) { create_upload(note2, false) }
let!(:legacy_upload_legacy_project) { create_upload(note_legacy) }
+ let!(:appearance) { create(:appearance, :with_logo) }
+
let(:start_id) { 1 }
let(:end_id) { 10000 }
@@ -52,12 +54,18 @@ describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do
expect(File.exist?(legacy_upload_legacy_project.absolute_path)).to be_falsey
end
- it 'removes all AttachmentUploader records' do
- expect { subject }.to change { Upload.where(uploader: 'AttachmentUploader').count }.from(3).to(0)
+ it 'removes all Note AttachmentUploader records' do
+ expect { subject }.to change { Upload.where(uploader: 'AttachmentUploader').count }.from(4).to(1)
end
it 'creates new uploads for successfully migrated records' do
expect { subject }.to change { Upload.where(uploader: 'FileUploader').count }.from(0).to(2)
end
+
+ it 'does not remove appearance uploads' do
+ subject
+
+ expect(appearance.logo.file).to exist
+ end
end
# rubocop: enable RSpec/FactoriesInMigrationSpecs
diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
index f877e8cc1b8..399db4ac259 100644
--- a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
+++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
@@ -33,7 +33,7 @@ describe ScheduleCalculateWikiSizes, :migration, :sidekiq do
end
end
- it 'calculates missing wiki sizes' do
+ it 'calculates missing wiki sizes', :sidekiq_might_not_need_inline do
expect(project_statistics.find_by(id: 2).wiki_size).to be_nil
expect(project_statistics.find_by(id: 3).wiki_size).to be_nil
diff --git a/spec/lib/gitlab/badge/pipeline/status_spec.rb b/spec/lib/gitlab/badge/pipeline/status_spec.rb
index 684c6829879..ab8d1f0ec5b 100644
--- a/spec/lib/gitlab/badge/pipeline/status_spec.rb
+++ b/spec/lib/gitlab/badge/pipeline/status_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::Badge::Pipeline::Status do
end
end
- context 'pipeline exists' do
+ context 'pipeline exists', :sidekiq_might_not_need_inline do
let!(:pipeline) { create_pipeline(project, sha, branch) }
context 'pipeline success' do
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 2fb9f1a0a08..ddb1d3cea21 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -90,7 +90,7 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
hook_path = File.join(repo_path, 'hooks')
expect(gitlab_shell.repository_exists?(project.repository_storage, repo_path)).to be(true)
- expect(gitlab_shell.exists?(project.repository_storage, hook_path)).to be(true)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, hook_path)).to be(true)
end
context 'hashed storage enabled' do
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index 7f7a285c453..b0d07c6e0b0 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -158,6 +158,7 @@ describe Gitlab::BitbucketImport::Importer do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
+ expect(merge_request.state).to eq('merged')
expect(merge_request.notes.count).to eq(2)
expect(merge_request.notes.map(&:discussion_id).uniq.count).to eq(1)
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 88e8f5d74d1..505f117034e 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -58,7 +58,7 @@ describe Gitlab::Checks::LfsIntegrity do
end
end
- context 'for forked project' do
+ context 'for forked project', :sidekiq_might_not_need_inline do
let(:parent_project) { create(:project, :repository) }
let(:project) { fork_project(parent_project, nil, repository: true) }
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
index 88a0ca35859..5110c215415 100644
--- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -143,6 +143,7 @@ describe Gitlab::Ci::Ansi2json::Style do
[[], %w[106], 'term-bg-l-cyan', 'sets bg color light cyan'],
[[], %w[107], 'term-bg-l-white', 'sets bg color light white'],
# reset
+ [%w[1], %w[], '', 'resets style from format bold'],
[%w[1], %w[0], '', 'resets style from format bold'],
[%w[1 3], %w[0], '', 'resets style from format bold and italic'],
[%w[1 3 term-fg-l-red term-bg-yellow], %w[0], '', 'resets all formats and colors'],
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb
index 3c6bc46436b..124379fa321 100644
--- a/spec/lib/gitlab/ci/ansi2json_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json_spec.rb
@@ -12,11 +12,26 @@ describe Gitlab::Ci::Ansi2json do
])
end
- it 'adds new line in a separate element' do
- expect(convert_json("Hello\nworld")).to eq([
- { offset: 0, content: [{ text: 'Hello' }] },
- { offset: 6, content: [{ text: 'world' }] }
- ])
+ context 'new lines' do
+ it 'adds new line when encountering \n' do
+ expect(convert_json("Hello\nworld")).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] },
+ { offset: 6, content: [{ text: 'world' }] }
+ ])
+ end
+
+ it 'adds new line when encountering \r\n' do
+ expect(convert_json("Hello\r\nworld")).to eq([
+ { offset: 0, content: [{ text: 'Hello' }] },
+ { offset: 7, content: [{ text: 'world' }] }
+ ])
+ end
+
+ it 'replace the current line when encountering \r' do
+ expect(convert_json("Hello\rworld")).to eq([
+ { offset: 0, content: [{ text: 'world' }] }
+ ])
+ end
end
it 'recognizes color changing ANSI sequences' do
@@ -113,10 +128,6 @@ describe Gitlab::Ci::Ansi2json do
content: [],
section_duration: '01:03',
section: 'prepare-script'
- },
- {
- offset: 63,
- content: []
}
])
end
@@ -134,10 +145,6 @@ describe Gitlab::Ci::Ansi2json do
content: [],
section: 'prepare-script',
section_duration: '01:03'
- },
- {
- offset: 56,
- content: []
}
])
end
@@ -157,7 +164,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '01:03'
},
{
- offset: 49,
+ offset: 91,
content: [{ text: 'world' }]
}
])
@@ -198,7 +205,7 @@ describe Gitlab::Ci::Ansi2json do
expect(convert_json("#{section_start}hello")).to eq([
{
offset: 0,
- content: [{ text: "#{section_start.gsub("\033[0K", '')}hello" }]
+ content: [{ text: 'hello' }]
}
])
end
@@ -211,30 +218,26 @@ describe Gitlab::Ci::Ansi2json do
expect(convert_json("#{section_start}hello")).to eq([
{
offset: 0,
- content: [{ text: "#{section_start.gsub("\033[0K", '').gsub('<', '&lt;')}hello" }]
+ content: [{ text: 'hello' }]
}
])
end
end
- it 'prevents XSS injection' do
- trace = "#{section_start}section_end:1:2<script>alert('XSS Hack!');</script>#{section_end}"
+ it 'prints HTML tags as is' do
+ trace = "#{section_start}section_end:1:2<div>hello</div>#{section_end}"
expect(convert_json(trace)).to eq([
{
offset: 0,
- content: [{ text: "section_end:1:2&lt;script>alert('XSS Hack!');&lt;/script>" }],
+ content: [{ text: "section_end:1:2<div>hello</div>" }],
section: 'prepare-script',
section_header: true
},
{
- offset: 95,
+ offset: 75,
content: [],
section: 'prepare-script',
section_duration: '01:03'
- },
- {
- offset: 95,
- content: []
}
])
end
@@ -274,7 +277,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '00:02'
},
{
- offset: 106,
+ offset: 155,
content: [{ text: 'baz' }],
section: 'prepare-script'
},
@@ -285,7 +288,7 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '01:03'
},
{
- offset: 158,
+ offset: 200,
content: [{ text: 'world' }]
}
])
@@ -318,14 +321,10 @@ describe Gitlab::Ci::Ansi2json do
section_duration: '00:02'
},
{
- offset: 115,
+ offset: 164,
content: [],
section: 'prepare-script',
section_duration: '01:03'
- },
- {
- offset: 164,
- content: []
}
])
end
@@ -380,7 +379,7 @@ describe Gitlab::Ci::Ansi2json do
]
end
- it 'returns the full line' do
+ it 'returns the line since last partially processed line' do
expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_truthy
end
@@ -399,7 +398,7 @@ describe Gitlab::Ci::Ansi2json do
]
end
- it 'returns the full line' do
+ it 'returns the line since last partially processed line' do
expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_falsey
end
@@ -416,7 +415,7 @@ describe Gitlab::Ci::Ansi2json do
]
end
- it 'returns the full line' do
+ it 'returns a blank line and the next line' do
expect(pass2.lines).to eq(lines)
expect(pass2.append).to be_falsey
end
@@ -502,10 +501,6 @@ describe Gitlab::Ci::Ansi2json do
content: [],
section: 'prepare-script',
section_duration: '01:03'
- },
- {
- offset: 77,
- content: []
}
]
end
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
new file mode 100644
index 00000000000..3adde213f59
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Context::Build do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:seed_attributes) { { 'name' => 'some-job' } }
+
+ let(:context) { described_class.new(pipeline, seed_attributes) }
+
+ describe '#variables' do
+ subject { context.variables }
+
+ it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') }
+ it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) }
+ it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
+ it { is_expected.to include('CI_JOB_NAME' => 'some-job') }
+ it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') }
+
+ context 'without passed build-specific attributes' do
+ let(:context) { described_class.new(pipeline) }
+
+ it { is_expected.to include('CI_JOB_NAME' => nil) }
+ it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') }
+ it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb
new file mode 100644
index 00000000000..6bc8f862779
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/context/global_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Context::Global do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:yaml_variables) { {} }
+
+ let(:context) { described_class.new(pipeline, yaml_variables: yaml_variables) }
+
+ describe '#variables' do
+ subject { context.variables }
+
+ it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') }
+ it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) }
+ it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
+
+ it { is_expected.not_to have_key('CI_JOB_NAME') }
+ it { is_expected.not_to have_key('CI_BUILD_REF_NAME') }
+
+ context 'with passed yaml variables' do
+ let(:yaml_variables) { [{ key: 'SUPPORTED', value: 'parsed', public: true }] }
+
+ it { is_expected.to include('SUPPORTED' => 'parsed') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 7140c14facb..66f2cb640b9 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do
double('build seed',
to_resource: ci_build,
- scoped_variables_hash: ci_build.scoped_variables_hash
+ variables: ci_build.scoped_variables_hash
)
end
@@ -91,7 +91,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do
double('bridge seed',
to_resource: bridge,
- scoped_variables_hash: ci_build.scoped_variables_hash
+ variables: ci_build.scoped_variables_hash
)
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
index 99852bd4228..04cdaa9d0ae 100644
--- a/spec/lib/gitlab/ci/build/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Ci::Build::Rules::Rule do
let(:seed) do
double('build seed',
to_resource: ci_build,
- scoped_variables_hash: ci_build.scoped_variables_hash
+ variables: ci_build.scoped_variables_hash
)
end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index d7793ebc806..1ebcc4f9414 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Ci::Build::Rules do
@@ -7,11 +9,11 @@ describe Gitlab::Ci::Build::Rules do
let(:seed) do
double('build seed',
to_resource: ci_build,
- scoped_variables_hash: ci_build.scoped_variables_hash
+ variables: ci_build.scoped_variables_hash
)
end
- let(:rules) { described_class.new(rule_list) }
+ let(:rules) { described_class.new(rule_list, default_when: 'on_success') }
describe '.new' do
let(:rules_ivar) { rules.instance_variable_get :@rule_list }
@@ -60,7 +62,7 @@ describe Gitlab::Ci::Build::Rules do
context 'with a specified default when:' do
let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
- let(:rules) { described_class.new(rule_list, 'manual') }
+ let(:rules) { described_class.new(rule_list, default_when: 'manual') }
it 'sets @rule_list to an array of a single rule' do
expect(rules_ivar).to be_an(Array)
@@ -81,7 +83,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('on_success')) }
context 'and when:manual set as the default' do
- let(:rules) { described_class.new(rule_list, 'manual') }
+ let(:rules) { described_class.new(rule_list, default_when: 'manual') }
it { is_expected.to eq(described_class::Result.new('manual')) }
end
@@ -93,7 +95,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) }
context 'and when:manual set as the default' do
- let(:rules) { described_class.new(rule_list, 'manual') }
+ let(:rules) { described_class.new(rule_list, default_when: 'manual') }
it { is_expected.to eq(described_class::Result.new('never')) }
end
@@ -157,7 +159,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) }
context 'and when:manual set as the default' do
- let(:rules) { described_class.new(rule_list, 'manual') }
+ let(:rules) { described_class.new(rule_list, default_when: 'manual') }
it 'does not return the default when:' do
expect(subject).to eq(described_class::Result.new('never'))
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index a7f457e0f5e..513a9b8f2b4 100644
--- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -28,6 +28,14 @@ describe Gitlab::Ci::Config::Entry::Artifacts do
expect(entry.value).to eq config
end
end
+
+ context "when value includes 'expose_as' keyword" do
+ let(:config) { { paths: %w[results.txt], expose_as: "Test results" } }
+
+ it 'returns general artifact and report-type artifacts configuration' do
+ expect(entry.value).to eq config
+ end
+ end
end
context 'when entry value is not correct' do
@@ -58,6 +66,84 @@ describe Gitlab::Ci::Config::Entry::Artifacts do
.to include 'artifacts reports should be a hash'
end
end
+
+ context "when 'expose_as' is not a string" do
+ let(:config) { { paths: %w[results.txt], expose_as: 1 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts expose as should be a string'
+ end
+ end
+
+ context "when 'expose_as' is too long" do
+ let(:config) { { paths: %w[results.txt], expose_as: 'A' * 101 } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts expose as is too long (maximum is 100 characters)'
+ end
+ end
+
+ context "when 'expose_as' is an empty string" do
+ let(:config) { { paths: %w[results.txt], expose_as: '' } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts expose as ' + Gitlab::Ci::Config::Entry::Artifacts::EXPOSE_AS_ERROR_MESSAGE
+ end
+ end
+
+ context "when 'expose_as' contains invalid characters" do
+ let(:config) do
+ { paths: %w[results.txt], expose_as: '<script>alert("xss");</script>' }
+ end
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include 'artifacts expose as ' + Gitlab::Ci::Config::Entry::Artifacts::EXPOSE_AS_ERROR_MESSAGE
+ end
+ end
+
+ context "when 'expose_as' is used without 'paths'" do
+ let(:config) { { expose_as: 'Test results' } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include "artifacts paths can't be blank"
+ end
+ end
+
+ context "when 'paths' includes '*' and 'expose_as' is defined" do
+ let(:config) { { expose_as: 'Test results', paths: ['test.txt', 'test*.txt'] } }
+
+ it 'reports error' do
+ expect(entry.errors)
+ .to include "artifacts paths can't contain '*' when used with 'expose_as'"
+ end
+ end
+ end
+
+ context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do
+ before do
+ stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false)
+ end
+
+ context 'when syntax is correct' do
+ let(:config) { { expose_as: 'Test results', paths: ['test.txt'] } }
+
+ it 'is valid' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ context 'when syntax for :expose_as is incorrect' do
+ let(:config) { { paths: %w[results.txt], expose_as: '' } }
+
+ it 'is valid' do
+ expect(entry.errors).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 9aab3664e1c..4fa0a57dc82 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -12,22 +12,53 @@ describe Gitlab::Ci::Config::Entry::Cache do
context 'when entry config value is correct' do
let(:policy) { nil }
+ let(:key) { 'some key' }
let(:config) do
- { key: 'some key',
+ { key: key,
untracked: true,
paths: ['some/path/'],
policy: policy }
end
describe '#value' do
- it 'returns hash value' do
- expect(entry.value).to eq(key: 'some key', untracked: true, paths: ['some/path/'], policy: 'pull-push')
+ shared_examples 'hash key value' do
+ it 'returns hash value' do
+ expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push')
+ end
+ end
+
+ it_behaves_like 'hash key value'
+
+ context 'with files' do
+ let(:key) { { files: ['a-file', 'other-file'] } }
+
+ it_behaves_like 'hash key value'
+ end
+
+ context 'with files and prefix' do
+ let(:key) { { files: ['a-file', 'other-file'], prefix: 'prefix-value' } }
+
+ it_behaves_like 'hash key value'
+ end
+
+ context 'with prefix' do
+ let(:key) { { prefix: 'prefix-value' } }
+
+ it 'key is nil' do
+ expect(entry.value).to match(a_hash_including(key: nil))
+ end
end
end
describe '#valid?' do
it { is_expected.to be_valid }
+
+ context 'with files' do
+ let(:key) { { files: ['a-file', 'other-file'] } }
+
+ it { is_expected.to be_valid }
+ end
end
context 'policy is pull-push' do
@@ -87,10 +118,44 @@ describe Gitlab::Ci::Config::Entry::Cache do
end
context 'when descendants are invalid' do
- let(:config) { { key: 1 } }
+ context 'with invalid keys' do
+ let(:config) { { key: 1 } }
- it 'reports error with descendants' do
- is_expected.to include 'key config should be a string or symbol'
+ it 'reports error with descendants' do
+ is_expected.to include 'key should be a hash, a string or a symbol'
+ end
+ end
+
+ context 'with empty key' do
+ let(:config) { { key: {} } }
+
+ it 'reports error with descendants' do
+ is_expected.to include 'key config missing required keys: files'
+ end
+ end
+
+ context 'with invalid files' do
+ let(:config) { { key: { files: 'a-file' } } }
+
+ it 'reports error with descendants' do
+ is_expected.to include 'key:files config should be an array of strings'
+ end
+ end
+
+ context 'with prefix without files' do
+ let(:config) { { key: { prefix: 'a-prefix' } } }
+
+ it 'reports error with descendants' do
+ is_expected.to include 'key config missing required keys: files'
+ end
+ end
+
+ context 'when there is an unknown key present' do
+ let(:config) { { key: { unknown: 'a-file' } } }
+
+ it 'reports error with descendants' do
+ is_expected.to include 'key config contains unknown keys: unknown'
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index 269a3406913..8e7f9ab9706 100644
--- a/spec/lib/gitlab/ci/config/entry/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
- context 'when entry config value is an array' do
+ context 'when entry config value is an array of strings' do
let(:config) { %w(ls pwd) }
describe '#value' do
@@ -37,13 +37,74 @@ describe Gitlab::Ci::Config::Entry::Commands do
end
end
- context 'when entry value is not valid' do
+ context 'when entry config value is array of arrays of strings' do
+ let(:config) { [['ls'], ['pwd', 'echo 1']] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry config value is array of strings and arrays of strings' do
+ let(:config) { ['ls', ['pwd', 'echo 1']] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is integer' do
let(:config) { 1 }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'commands config should be an array of strings or a string'
+ .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ end
+ end
+ end
+
+ context 'when entry value is multi-level nested array' do
+ let(:config) { [['ls', ['echo 1']], 'pwd'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'commands config should be a string or an array containing strings and arrays of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb
index 27d63dbd407..dad4f408e50 100644
--- a/spec/lib/gitlab/ci/config/entry/default_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb
@@ -5,6 +5,18 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Default do
let(:entry) { described_class.new(config) }
+ it_behaves_like 'with inheritable CI config' do
+ let(:inheritable_key) { nil }
+ let(:inheritable_class) { Gitlab::Ci::Config::Entry::Root }
+
+ # These are entries defined in Root
+ # that we know that we don't want to inherit
+ # as they do not have sense in context of Default
+ let(:ignored_inheritable_columns) do
+ %i[default include variables stages types workflow]
+ end
+ end
+
describe '.nodes' do
it 'returns a hash' do
expect(described_class.nodes).to be_a(Hash)
@@ -14,7 +26,7 @@ describe Gitlab::Ci::Config::Entry::Default do
it 'contains the expected node names' do
expect(described_class.nodes.keys)
.to match_array(%i[before_script image services
- after_script cache])
+ after_script cache interruptible])
end
end
end
@@ -87,7 +99,7 @@ describe Gitlab::Ci::Config::Entry::Default do
it 'raises error' do
expect { entry.compose!(deps) }.to raise_error(
- Gitlab::Ci::Config::Entry::Default::DuplicateError)
+ Gitlab::Ci::Config::Entry::Default::InheritError)
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/files_spec.rb b/spec/lib/gitlab/ci/config/entry/files_spec.rb
new file mode 100644
index 00000000000..2bebbd7b198
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/files_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Files do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ let(:config) { ['some/file', 'some/path/'] }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ describe '#errors' do
+ context 'when entry value is not an array' do
+ let(:config) { 'string' }
+
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'files config should be an array of strings'
+ end
+ end
+
+ context 'when entry value is not an array of strings' do
+ let(:config) { [1] }
+
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'files config should be an array of strings'
+ end
+ end
+
+ context 'when entry value contains more than two values' do
+ let(:config) { %w[file1 file2 file3] }
+
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'files config has too many items (maximum is 2)'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 1c4887e87c4..fe83171c57a 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -5,14 +5,26 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
+ it_behaves_like 'with inheritable CI config' do
+ let(:inheritable_key) { 'default' }
+ let(:inheritable_class) { Gitlab::Ci::Config::Entry::Default }
+
+ # These are entries defined in Default
+ # that we know that we don't want to inherit
+ # as they do not have sense in context of Job
+ let(:ignored_inheritable_columns) do
+ %i[]
+ end
+ end
+
describe '.nodes' do
context 'when filtering all the entry/node names' do
subject { described_class.nodes.keys }
let(:result) do
%i[before_script script stage type after_script cache
- image services only except rules variables artifacts
- environment coverage retry]
+ image services only except rules needs variables artifacts
+ environment coverage retry interruptible]
end
it { is_expected.to match_array result }
@@ -372,21 +384,6 @@ describe Gitlab::Ci::Config::Entry::Job do
end
context 'when has needs' do
- context 'that are not a array of strings' do
- let(:config) do
- {
- stage: 'test',
- script: 'echo',
- needs: 'build-job'
- }
- end
-
- it 'returns error about invalid type' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'job needs should be an array of strings'
- end
- end
-
context 'when have dependencies that are not subset of needs' do
let(:config) do
{
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index a7874447725..327607e2266 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -6,38 +6,38 @@ describe Gitlab::Ci::Config::Entry::Key do
let(:entry) { described_class.new(config) }
describe 'validations' do
- shared_examples 'key with slash' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
+ it_behaves_like 'key entry validations', 'simple key'
- it 'reports errors with config value' do
- expect(entry.errors).to include 'key config cannot contain the "/" character'
- end
- end
+ context 'when entry config value is correct' do
+ context 'when key is a hash' do
+ let(:config) { { files: ['test'], prefix: 'something' } }
- shared_examples 'key with only dots' do
- it 'is invalid' do
- expect(entry).not_to be_valid
- end
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to match(config)
+ end
+ end
- it 'reports errors with config value' do
- expect(entry.errors).to include 'key config cannot be "." or ".."'
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
end
- end
- context 'when entry config value is correct' do
- let(:config) { 'test' }
+ context 'when key is a symbol' do
+ let(:config) { :key }
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq 'test'
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(config.to_s)
+ end
end
- end
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
end
end
end
@@ -47,53 +47,11 @@ describe Gitlab::Ci::Config::Entry::Key do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'key config should be a string or symbol'
+ expect(entry.errors.first)
+ .to match /should be a hash, a string or a symbol/
end
end
end
-
- context 'when entry value contains slash' do
- let(:config) { 'key/with/some/slashes' }
-
- it_behaves_like 'key with slash'
- end
-
- context 'when entry value contains URI encoded slash (%2F)' do
- let(:config) { 'key%2Fwith%2Fsome%2Fslashes' }
-
- it_behaves_like 'key with slash'
- end
-
- context 'when entry value is a dot' do
- let(:config) { '.' }
-
- it_behaves_like 'key with only dots'
- end
-
- context 'when entry value is two dots' do
- let(:config) { '..' }
-
- it_behaves_like 'key with only dots'
- end
-
- context 'when entry value is a URI encoded dot (%2E)' do
- let(:config) { '%2e' }
-
- it_behaves_like 'key with only dots'
- end
-
- context 'when entry value is two URI encoded dots (%2E)' do
- let(:config) { '%2E%2e' }
-
- it_behaves_like 'key with only dots'
- end
-
- context 'when entry value is one dot and one URI encoded dot' do
- let(:config) { '.%2e' }
-
- it_behaves_like 'key with only dots'
- end
end
describe '.default' do
diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb
new file mode 100644
index 00000000000..d119e604900
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::Ci::Config::Entry::Need do
+ subject(:need) { described_class.new(config) }
+
+ context 'when job is specified' do
+ let(:config) { 'job_name' }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ it 'returns job needs configuration' do
+ expect(need.value).to eq(name: 'job_name')
+ end
+ end
+ end
+
+ context 'when need is empty' do
+ let(:config) { '' }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'is returns an error about an empty config' do
+ expect(need.errors)
+ .to contain_exactly("job config can't be blank")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
new file mode 100644
index 00000000000..f4a76b52d30
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::Ci::Config::Entry::Needs do
+ subject(:needs) { described_class.new(config) }
+
+ before do
+ needs.metadata[:allowed_needs] = %i[job]
+ end
+
+ describe 'validations' do
+ before do
+ needs.compose!
+ end
+
+ context 'when entry config value is correct' do
+ let(:config) { ['job_name'] }
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when config value has wrong type' do
+ let(:config) { 123 }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(needs.errors)
+ .to include('needs config can only be a hash or an array')
+ end
+ end
+ end
+
+ context 'when wrong needs type is used' do
+ let(:config) { [123] }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(needs.errors).to contain_exactly(
+ 'need has an unsupported type')
+ end
+ end
+ end
+ end
+
+ describe '.compose!' do
+ context 'when valid job entries composed' do
+ let(:config) { %w[first_job_name second_job_name] }
+
+ before do
+ needs.compose!
+ end
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(needs.value).to eq(
+ job: [
+ { name: 'first_job_name' },
+ { name: 'second_job_name' }
+ ]
+ )
+ end
+ end
+
+ describe '#descendants' do
+ it 'creates valid descendant nodes' do
+ expect(needs.descendants.count).to eq 2
+ expect(needs.descendants)
+ .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/prefix_spec.rb b/spec/lib/gitlab/ci/config/entry/prefix_spec.rb
new file mode 100644
index 00000000000..8132a674488
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/prefix_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Prefix do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ it_behaves_like 'key entry validations', :prefix
+
+ context 'when entry value is not correct' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'prefix config should be a string or symbol'
+ end
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'returns default key' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 7e1a80414d4..43bd53b780f 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -12,10 +12,14 @@ describe Gitlab::Ci::Config::Entry::Root do
context 'when filtering all the entry/node names' do
it 'contains the expected node names' do
+ # No inheritable fields should be added to the `Root`
+ #
+ # Inheritable configuration can only be added to `default:`
+ #
+ # The purpose of `Root` is have only globally defined configuration.
expect(described_class.nodes.keys)
- .to match_array(%i[before_script image services
- after_script variables cache
- stages types include default])
+ .to match_array(%i[before_script image services after_script
+ variables cache stages types include default workflow])
end
end
end
@@ -45,7 +49,7 @@ describe Gitlab::Ci::Config::Entry::Root do
end
it 'creates node object for each entry' do
- expect(root.descendants.count).to eq 10
+ expect(root.descendants.count).to eq 11
end
it 'creates node object using valid class' do
@@ -198,7 +202,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#nodes' do
it 'instantizes all nodes' do
- expect(root.descendants.count).to eq 10
+ expect(root.descendants.count).to eq 11
end
it 'contains unspecified nodes' do
@@ -293,7 +297,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#errors' do
it 'reports errors from child nodes' do
expect(root.errors)
- .to include 'before_script config should be an array of strings'
+ .to include 'before_script config should be an array containing strings and arrays of strings'
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index 9d4f7153cd0..216f5d0c77d 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -1,10 +1,22 @@
+# frozen_string_literal: true
+
require 'fast_spec_helper'
require 'gitlab_chronic_duration'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Rules::Rule do
- let(:entry) { described_class.new(config) }
+ let(:factory) do
+ Gitlab::Config::Entry::Factory.new(described_class)
+ .metadata(metadata)
+ .value(config)
+ end
+
+ let(:metadata) do
+ { allowed_when: %w[on_success on_failure always never manual delayed] }
+ end
+
+ let(:entry) { factory.create! }
describe '.new' do
subject { entry }
@@ -210,6 +222,112 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
.to include(/should be a hash/)
end
end
+
+ context 'when: validation' do
+ context 'with an invalid boolean when:' do
+ let(:config) do
+ { if: '$THIS == "that"', when: false }
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: false/)
+ end
+
+ context 'when composed' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: false/)
+ end
+ end
+ end
+
+ context 'with an invalid string when:' do
+ let(:config) do
+ { if: '$THIS == "that"', when: 'explode' }
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+
+ context 'when composed' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+ end
+ end
+
+ context 'with a string passed in metadata but not allowed in the class' do
+ let(:metadata) { { allowed_when: %w[explode] } }
+
+ let(:config) do
+ { if: '$THIS == "that"', when: 'explode' }
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+
+ context 'when composed' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+ end
+ end
+
+ context 'with a string allowed in the class but not passed in metadata' do
+ let(:metadata) { { allowed_when: %w[always never] } }
+
+ let(:config) do
+ { if: '$THIS == "that"', when: 'on_success' }
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: on_success/)
+ end
+
+ context 'when composed' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: on_success/)
+ end
+ end
+ end
+ end
end
describe '#value' do
diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
index 291e7373daf..3c050801023 100644
--- a/spec/lib/gitlab/ci/config/entry/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
@@ -1,9 +1,18 @@
+# frozen_string_literal: true
+
require 'fast_spec_helper'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Rules do
- let(:entry) { described_class.new(config) }
+ let(:factory) do
+ Gitlab::Config::Entry::Factory.new(described_class)
+ .metadata(metadata)
+ .value(config)
+ end
+
+ let(:metadata) { { allowed_when: %w[always never] } }
+ let(:entry) { factory.create! }
describe '.new' do
subject { entry }
@@ -16,7 +25,7 @@ describe Gitlab::Ci::Config::Entry::Rules do
it { is_expected.to be_a(described_class) }
it { is_expected.to be_valid }
- context 'after #compose!' do
+ context 'when composed' do
before do
subject.compose!
end
@@ -36,7 +45,7 @@ describe Gitlab::Ci::Config::Entry::Rules do
it { is_expected.to be_a(described_class) }
it { is_expected.to be_valid }
- context 'after #compose!' do
+ context 'when composed' do
before do
subject.compose!
end
@@ -52,48 +61,6 @@ describe Gitlab::Ci::Config::Entry::Rules do
it { is_expected.not_to be_valid }
end
-
- context 'with an invalid boolean when:' do
- let(:config) do
- [{ if: '$THIS == "that"', when: false }]
- end
-
- it { is_expected.to be_a(described_class) }
- it { is_expected.to be_valid }
-
- context 'after #compose!' do
- before do
- subject.compose!
- end
-
- it { is_expected.not_to be_valid }
-
- it 'returns an error about invalid when:' do
- expect(subject.errors).to include(/when unknown value: false/)
- end
- end
- end
-
- context 'with an invalid string when:' do
- let(:config) do
- [{ if: '$THIS == "that"', when: 'explode' }]
- end
-
- it { is_expected.to be_a(described_class) }
- it { is_expected.to be_valid }
-
- context 'after #compose!' do
- before do
- subject.compose!
- end
-
- it { is_expected.not_to be_valid }
-
- it 'returns an error about invalid when:' do
- expect(subject.errors).to include(/when unknown value: explode/)
- end
- end
- end
end
describe '#value' do
diff --git a/spec/lib/gitlab/ci/config/entry/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
index d523243d3b6..57dc20ea628 100644
--- a/spec/lib/gitlab/ci/config/entry/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Config::Entry::Script do
let(:entry) { described_class.new(config) }
describe 'validations' do
- context 'when entry config value is correct' do
+ context 'when entry config value is array of strings' do
let(:config) { %w(ls pwd) }
describe '#value' do
@@ -28,13 +28,74 @@ describe Gitlab::Ci::Config::Entry::Script do
end
end
- context 'when entry value is not correct' do
+ context 'when entry config value is array of arrays of strings' do
+ let(:config) { [['ls'], ['pwd', 'echo 1']] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry config value is array containing strings and arrays of strings' do
+ let(:config) { ['ls', ['pwd', 'echo 1']] }
+
+ describe '#value' do
+ it 'returns array of strings' do
+ expect(entry.value).to eq ['ls', 'pwd', 'echo 1']
+ end
+ end
+
+ describe '#errors' do
+ it 'does not append errors' do
+ expect(entry.errors).to be_empty
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is string' do
let(:config) { 'ls' }
describe '#errors' do
it 'saves errors' do
expect(entry.errors)
- .to include 'script config should be an array of strings'
+ .to include 'script config should be an array containing strings and arrays of strings'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when entry value is multi-level nested array' do
+ let(:config) { [['ls', ['echo 1']], 'pwd'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'script config should be an array containing strings and arrays of strings'
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
new file mode 100644
index 00000000000..f2832b94bf0
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Workflow do
+ let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(rules_hash) }
+ let(:config) { factory.create! }
+
+ describe 'validations' do
+ context 'when work config value is a string' do
+ let(:rules_hash) { 'build' }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'attaches an error specifying that workflow should point to a hash' do
+ expect(config.errors).to include('workflow config should be a hash')
+ end
+ end
+
+ describe '#value' do
+ it 'returns the invalid configuration' do
+ expect(config.value).to eq(rules_hash)
+ end
+ end
+ end
+
+ context 'when work config value is a hash' do
+ let(:rules_hash) { { rules: [{ if: '$VAR' }] } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(config).to be_valid
+ end
+
+ it 'attaches no errors' do
+ expect(config.errors).to be_empty
+ end
+ end
+
+ describe '#value' do
+ it 'returns the config' do
+ expect(config.value).to eq(rules_hash)
+ end
+ end
+
+ context 'with an invalid key' do
+ let(:rules_hash) { { trash: [{ if: '$VAR' }] } }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(config).not_to be_valid
+ end
+
+ it 'attaches an error specifying the unknown key' do
+ expect(config.errors).to include('workflow config contains unknown keys: trash')
+ end
+ end
+
+ describe '#value' do
+ it 'returns the invalid configuration' do
+ expect(config.value).to eq(rules_hash)
+ end
+ end
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'is nil' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb
index 6b766cc37bf..bf880478387 100644
--- a/spec/lib/gitlab/ci/config/normalizer_spec.rb
+++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb
@@ -7,6 +7,16 @@ describe Gitlab::Ci::Config::Normalizer do
let(:job_config) { { script: 'rspec', parallel: 5, name: 'rspec' } }
let(:config) { { job_name => job_config } }
+ let(:expanded_job_names) do
+ [
+ "rspec 1/5",
+ "rspec 2/5",
+ "rspec 3/5",
+ "rspec 4/5",
+ "rspec 5/5"
+ ]
+ end
+
describe '.normalize_jobs' do
subject { described_class.new(config).normalize_jobs }
@@ -15,9 +25,7 @@ describe Gitlab::Ci::Config::Normalizer do
end
it 'has parallelized jobs' do
- job_names = [:"rspec 1/5", :"rspec 2/5", :"rspec 3/5", :"rspec 4/5", :"rspec 5/5"]
-
- is_expected.to include(*job_names)
+ is_expected.to include(*expanded_job_names.map(&:to_sym))
end
it 'sets job instance in options' do
@@ -43,49 +51,109 @@ describe Gitlab::Ci::Config::Normalizer do
let(:job_name) { :"rspec 35/2" }
it 'properly parallelizes job names' do
- job_names = [:"rspec 35/2 1/5", :"rspec 35/2 2/5", :"rspec 35/2 3/5", :"rspec 35/2 4/5", :"rspec 35/2 5/5"]
+ job_names = [
+ :"rspec 35/2 1/5",
+ :"rspec 35/2 2/5",
+ :"rspec 35/2 3/5",
+ :"rspec 35/2 4/5",
+ :"rspec 35/2 5/5"
+ ]
is_expected.to include(*job_names)
end
end
- %i[dependencies needs].each do |context|
- context "when job has #{context} on parallelized jobs" do
+ context 'for dependencies' do
+ context "when job has dependencies on parallelized jobs" do
let(:config) do
{
job_name => job_config,
- other_job: { script: 'echo 1', context => [job_name.to_s] }
+ other_job: { script: 'echo 1', dependencies: [job_name.to_s] }
}
end
- it "parallelizes #{context}" do
- job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"]
-
- expect(subject[:other_job][context]).to include(*job_names)
+ it "parallelizes dependencies" do
+ expect(subject[:other_job][:dependencies]).to eq(expanded_job_names)
end
it "does not include original job name in #{context}" do
- expect(subject[:other_job][context]).not_to include(job_name)
+ expect(subject[:other_job][:dependencies]).not_to include(job_name)
end
end
- context "when there are #{context} which are both parallelized and not" do
+ context "when there are dependencies which are both parallelized and not" do
let(:config) do
{
job_name => job_config,
other_job: { script: 'echo 1' },
- final_job: { script: 'echo 1', context => [job_name.to_s, "other_job"] }
+ final_job: { script: 'echo 1', dependencies: [job_name.to_s, "other_job"] }
}
end
- it "parallelizes #{context}" do
+ it "parallelizes dependencies" do
job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"]
- expect(subject[:final_job][context]).to include(*job_names)
+ expect(subject[:final_job][:dependencies]).to include(*job_names)
+ end
+
+ it "includes the regular job in dependencies" do
+ expect(subject[:final_job][:dependencies]).to include('other_job')
+ end
+ end
+ end
+
+ context 'for needs' do
+ let(:expanded_job_attributes) do
+ expanded_job_names.map do |job_name|
+ { name: job_name }
+ end
+ end
+
+ context "when job has needs on parallelized jobs" do
+ let(:config) do
+ {
+ job_name => job_config,
+ other_job: {
+ script: 'echo 1',
+ needs: {
+ job: [
+ { name: job_name.to_s }
+ ]
+ }
+ }
+ }
+ end
+
+ it "parallelizes needs" do
+ expect(subject.dig(:other_job, :needs, :job)).to eq(expanded_job_attributes)
+ end
+ end
+
+ context "when there are dependencies which are both parallelized and not" do
+ let(:config) do
+ {
+ job_name => job_config,
+ other_job: {
+ script: 'echo 1'
+ },
+ final_job: {
+ script: 'echo 1',
+ needs: {
+ job: [
+ { name: job_name.to_s },
+ { name: "other_job" }
+ ]
+ }
+ }
+ }
+ end
+
+ it "parallelizes dependencies" do
+ expect(subject.dig(:final_job, :needs, :job)).to include(*expanded_job_attributes)
end
- it "includes the regular job in #{context}" do
- expect(subject[:final_job][context]).to include('other_job')
+ it "includes the regular job in dependencies" do
+ expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job')
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index ba4f841cf43..a631cd2777b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -11,6 +11,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
[{ key: 'first', secret_value: 'world' },
{ key: 'second', secret_value: 'second_world' }]
end
+
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
source: :push,
@@ -51,12 +52,6 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
.to eq variables_attributes.map(&:with_indifferent_access)
end
- it 'sets a valid config source' do
- step.perform!
-
- expect(pipeline.repository_source?).to be true
- end
-
it 'returns a valid pipeline' do
step.perform!
diff --git a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb
new file mode 100644
index 00000000000..7b76adaf683
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ describe '#perform!' do
+ context 'when pipeline has been skipped by workflow configuration' do
+ before do
+ allow(step).to receive(:workflow_passed?)
+ .and_return(false)
+
+ step.perform!
+ end
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'attaches an error to the pipeline' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+ end
+
+ context 'when pipeline has not been skipped by workflow configuration' do
+ before do
+ allow(step).to receive(:workflow_passed?)
+ .and_return(true)
+
+ step.perform!
+ end
+
+ it 'continues the pipeline processing chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'does not skip the pipeline' do
+ expect(pipeline).not_to be_persisted
+ expect(pipeline).not_to be_skipped
+ end
+
+ it 'attaches no errors' do
+ expect(pipeline.errors).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 9bccd5be4fe..52e9432dc92 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -7,9 +7,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
set(:user) { create(:user) }
let(:pipeline) do
- build(:ci_pipeline_with_one_job, project: project,
- ref: 'master',
- user: user)
+ build(:ci_pipeline, project: project, ref: 'master', user: user)
end
let(:command) do
@@ -20,11 +18,32 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
seeds_block: nil)
end
+ let(:dependencies) do
+ [
+ Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Seed.new(pipeline, command)
+ ]
+ end
+
let(:step) { described_class.new(pipeline, command) }
+ let(:config) do
+ { rspec: { script: 'rspec' } }
+ end
+
+ def run_chain
+ dependencies.map(&:perform!)
+ step.perform!
+ end
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
context 'when pipeline doesn not have seeds block' do
before do
- step.perform!
+ run_chain
end
it 'does not persist the pipeline' do
@@ -59,12 +78,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
} }
end
- let(:pipeline) do
- build(:ci_pipeline, project: project, config: config)
- end
-
before do
- step.perform!
+ run_chain
end
it 'breaks the chain' do
@@ -82,16 +97,16 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
describe 'pipeline protect' do
- subject { step.perform! }
-
context 'when ref is protected' do
before do
allow(project).to receive(:protected_for?).with('master').and_return(true)
allow(project).to receive(:protected_for?).with('refs/heads/master').and_return(true)
+
+ dependencies.map(&:perform!)
end
it 'does not protect the pipeline' do
- subject
+ run_chain
expect(pipeline.protected).to eq(true)
end
@@ -99,7 +114,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
context 'when ref is not protected' do
it 'does not protect the pipeline' do
- subject
+ run_chain
expect(pipeline.protected).to eq(false)
end
@@ -112,7 +127,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
before do
- step.perform!
+ run_chain
end
it 'breaks the chain' do
@@ -144,7 +159,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
it 'populates pipeline with resources described in the seeds block' do
- step.perform!
+ run_chain
expect(pipeline).not_to be_persisted
expect(pipeline.variables).not_to be_empty
@@ -154,7 +169,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
it 'has pipeline iid' do
- step.perform!
+ run_chain
expect(pipeline.iid).to be > 0
end
@@ -166,7 +181,7 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
end
it 'wastes pipeline iid' do
- expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
+ expect { run_chain }.to raise_error(ActiveRecord::RecordNotSaved)
last_iid = InternalId.ci_pipelines
.where(project_id: project.id)
@@ -181,14 +196,14 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'raises error' do
- expect { step.perform! }.to raise_error(described_class::PopulateError)
+ expect { run_chain }.to raise_error(described_class::PopulateError)
end
end
context 'when variables policy is specified' do
shared_examples_for 'a correct pipeline' do
it 'populates pipeline according to used policies' do
- step.perform!
+ run_chain
expect(pipeline.stages.size).to eq 1
expect(pipeline.stages.first.statuses.size).to eq 1
@@ -202,10 +217,6 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
end
- let(:pipeline) do
- build(:ci_pipeline, ref: 'master', project: project, config: config)
- end
-
it_behaves_like 'a correct pipeline'
context 'when variables expression is specified' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb
index 7c1c016b4bb..92eadf5548c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb
@@ -2,32 +2,38 @@
require 'spec_helper'
-describe Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do
- let(:project) { create(:project, :repository) }
+describe ::Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do
+ let(:project) { create(:project) }
let(:pipeline) do
- build(:ci_pipeline_with_one_job, project: project, ref: 'master')
+ build(:ci_pipeline, project: project)
end
let(:command) do
- double(:command, project: project, chat_data: { command: 'echo' })
+ double(:command,
+ config_processor: double(:processor,
+ jobs: { echo: double(:job_echo), rspec: double(:job_rspec) }),
+ project: project,
+ chat_data: { command: 'echo' })
end
describe '#perform!' do
- it 'removes unwanted jobs for chat pipelines' do
- allow(pipeline).to receive(:chat?).and_return(true)
+ subject { described_class.new(pipeline, command).perform! }
- pipeline.config_processor.jobs[:echo] = double(:job)
+ it 'removes unwanted jobs for chat pipelines' do
+ expect(pipeline).to receive(:chat?).and_return(true)
- described_class.new(pipeline, command).perform!
+ subject
- expect(pipeline.config_processor.jobs.keys).to eq([:echo])
+ expect(command.config_processor.jobs.keys).to eq([:echo])
end
- end
- it 'does not remove any jobs for non-chat pipelines' do
- described_class.new(pipeline, command).perform!
+ it 'does not remove any jobs for non chat-pipelines' do
+ expect(pipeline).to receive(:chat?).and_return(false)
- expect(pipeline.config_processor.jobs.keys).to eq([:rspec])
+ subject
+
+ expect(command.config_processor.jobs.keys).to eq([:echo, :rspec])
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
new file mode 100644
index 00000000000..aa54f19b26c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Seed do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user, developer_projects: [project]) }
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ origin_ref: 'master',
+ seeds_block: nil)
+ end
+
+ def run_chain(pipeline, command)
+ [
+ Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
+ Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command)
+ ].map(&:perform!)
+
+ described_class.new(pipeline, command).perform!
+ end
+
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+
+ describe '#perform!' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ run_chain(pipeline, command)
+ end
+
+ let(:config) do
+ { rspec: { script: 'rake' } }
+ end
+
+ it 'allocates next IID' do
+ expect(pipeline.iid).to be_present
+ end
+
+ it 'sets the seeds in the command object' do
+ expect(command.stage_seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
+ expect(command.stage_seeds.count).to eq 1
+ end
+
+ context 'when no ref policy is specified' do
+ let(:config) do
+ {
+ production: { stage: 'deploy', script: 'cap prod' },
+ rspec: { stage: 'test', script: 'rspec' },
+ spinach: { stage: 'test', script: 'spinach' }
+ }
+ end
+
+ it 'correctly fabricates a stage seeds object' do
+ seeds = command.stage_seeds
+ expect(seeds.size).to eq 2
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.second.attributes[:name]).to eq 'deploy'
+ expect(seeds.dig(0, 0, :name)).to eq 'rspec'
+ expect(seeds.dig(0, 1, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
+ end
+
+ context 'when refs policy is specified' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: 'feature', tag: true)
+ end
+
+ let(:config) do
+ {
+ production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] }
+ }
+ end
+
+ it 'returns stage seeds only assigned to master' do
+ seeds = command.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when source policy is specified' do
+ let(:pipeline) { create(:ci_pipeline, source: :schedule) }
+
+ let(:config) do
+ {
+ production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }
+ }
+ end
+
+ it 'returns stage seeds only assigned to schedules' do
+ seeds = command.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ end
+ end
+
+ context 'when kubernetes policy is specified' do
+ let(:config) do
+ {
+ spinach: { stage: 'test', script: 'spinach' },
+ production: {
+ stage: 'deploy',
+ script: 'cap',
+ only: { kubernetes: 'active' }
+ }
+ }
+ end
+
+ context 'when kubernetes is active' do
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { build(:ci_pipeline, project: project) }
+
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = command.stage_seeds
+
+ expect(seeds.size).to eq 2
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
+ end
+ end
+
+ context 'when kubernetes is not active' do
+ it 'does not return seeds for kubernetes dependent job' do
+ seeds = command.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ end
+ end
+ end
+
+ context 'when variables policy is specified' do
+ let(:config) do
+ {
+ unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
+ feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } }
+ }
+ end
+
+ it 'returns stage seeds only when variables expression is truthy' do
+ seeds = command.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.dig(0, 0, :name)).to eq 'unit'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
deleted file mode 100644
index 79acd3e4f54..00000000000
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
- set(:project) { create(:project, :repository) }
- set(:user) { create(:user) }
-
- let(:command) do
- Gitlab::Ci::Pipeline::Chain::Command.new(
- project: project,
- current_user: user,
- save_incompleted: true)
- end
-
- let!(:step) { described_class.new(pipeline, command) }
-
- before do
- step.perform!
- end
-
- context 'when pipeline has no YAML configuration' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, project: project)
- end
-
- it 'appends errors about missing configuration' do
- expect(pipeline.errors.to_a)
- .to include 'Missing .gitlab-ci.yml file'
- end
-
- it 'breaks the chain' do
- expect(step.break?).to be true
- end
- end
-
- context 'when YAML configuration contains errors' do
- let(:pipeline) do
- build(:ci_pipeline, project: project, config: 'invalid YAML')
- end
-
- it 'appends errors about YAML errors' do
- expect(pipeline.errors.to_a)
- .to include 'Invalid configuration format'
- end
-
- it 'breaks the chain' do
- expect(step.break?).to be true
- end
-
- context 'when saving incomplete pipeline is allowed' do
- let(:command) do
- double('command', project: project,
- current_user: user,
- save_incompleted: true)
- end
-
- it 'fails the pipeline' do
- expect(pipeline.reload).to be_failed
- end
-
- it 'sets a config error failure reason' do
- expect(pipeline.reload.config_error?).to eq true
- end
- end
-
- context 'when saving incomplete pipeline is not allowed' do
- let(:command) do
- double('command', project: project,
- current_user: user,
- save_incompleted: false)
- end
-
- it 'does not drop pipeline' do
- expect(pipeline).not_to be_failed
- expect(pipeline).not_to be_persisted
- end
- end
- end
-
- context 'when pipeline contains configuration validation errors' do
- let(:config) do
- {
- rspec: {
- before_script: 10,
- script: 'ls -al'
- }
- }
- end
-
- let(:pipeline) do
- build(:ci_pipeline, project: project, config: config)
- end
-
- it 'appends configuration validation errors to pipeline errors' do
- expect(pipeline.errors.to_a)
- .to include "jobs:rspec:before_script config should be an array of strings"
- end
-
- it 'breaks the chain' do
- expect(step.break?).to be true
- end
- end
-
- context 'when pipeline is correct and complete' do
- let(:pipeline) do
- build(:ci_pipeline_with_one_job, project: project)
- end
-
- it 'does not invalidate the pipeline' do
- expect(pipeline).to be_valid
- end
-
- it 'does not break the chain' do
- expect(step.break?).to be false
- end
- end
-
- context 'when pipeline source is merge request' do
- before do
- stub_ci_pipeline_yaml_file(YAML.dump(config))
- end
-
- let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
-
- let(:merge_request_pipeline) do
- build(:ci_pipeline, source: :merge_request_event, project: project)
- end
-
- let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) }
-
- context "when config contains 'merge_requests' keyword" do
- let(:config) { { rspec: { script: 'echo', only: ['merge_requests'] } } }
-
- it 'does not break the chain' do
- expect(chain).not_to be_break
- end
- end
-
- context "when config contains 'merge_request' keyword" do
- let(:config) { { rspec: { script: 'echo', only: ['merge_request'] } } }
-
- it 'does not break the chain' do
- expect(chain).not_to be_break
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
new file mode 100644
index 00000000000..6a8b804597c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Build::Cache do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:head_sha) { project.repository.head_commit.id }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) }
+
+ let(:processor) { described_class.new(pipeline, config) }
+
+ describe '#build_attributes' do
+ subject { processor.build_attributes }
+
+ context 'with cache:key' do
+ let(:config) do
+ {
+ key: 'a-key',
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it { is_expected.to include(options: { cache: config }) }
+ end
+
+ context 'with cache:key as a symbol' do
+ let(:config) do
+ {
+ key: :a_key,
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it { is_expected.to include(options: { cache: config.merge(key: "a_key") }) }
+ end
+
+ context 'with cache:key:files' do
+ shared_examples 'default key' do
+ let(:config) do
+ { key: { files: files } }
+ end
+
+ it 'uses default key' do
+ expected = { options: { cache: { key: 'default' } } }
+
+ is_expected.to include(expected)
+ end
+ end
+
+ shared_examples 'version and gemfile files' do
+ let(:config) do
+ {
+ key: {
+ files: files
+ },
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it 'builds a string key' do
+ expected = {
+ options: {
+ cache: {
+ key: '703ecc8fef1635427a1f86a8a1a308831c122392',
+ paths: ['vendor/ruby']
+ }
+ }
+ }
+
+ is_expected.to include(expected)
+ end
+ end
+
+ context 'with existing files' do
+ let(:files) { ['VERSION', 'Gemfile.zip'] }
+
+ it_behaves_like 'version and gemfile files'
+ end
+
+ context 'with files starting with ./' do
+ let(:files) { ['Gemfile.zip', './VERSION'] }
+
+ it_behaves_like 'version and gemfile files'
+ end
+
+ context 'with feature flag disabled' do
+ let(:files) { ['VERSION', 'Gemfile.zip'] }
+
+ before do
+ stub_feature_flags(ci_file_based_cache: false)
+ end
+
+ it_behaves_like 'default key'
+ end
+
+ context 'with files ending with /' do
+ let(:files) { ['Gemfile.zip/'] }
+
+ it_behaves_like 'default key'
+ end
+
+ context 'with new line in filenames' do
+ let(:files) { ["Gemfile.zip\nVERSION"] }
+
+ it_behaves_like 'default key'
+ end
+
+ context 'with missing files' do
+ let(:files) { ['project-gemfile.lock', ''] }
+
+ it_behaves_like 'default key'
+ end
+
+ context 'with directories' do
+ shared_examples 'foo/bar directory key' do
+ let(:config) do
+ {
+ key: {
+ files: files
+ }
+ }
+ end
+
+ it 'builds a string key' do
+ expected = {
+ options: {
+ cache: { key: '74bf43fb1090f161bdd4e265802775dbda2f03d1' }
+ }
+ }
+
+ is_expected.to include(expected)
+ end
+ end
+
+ context 'with directory' do
+ let(:files) { ['foo/bar'] }
+
+ it_behaves_like 'foo/bar directory key'
+ end
+
+ context 'with directory ending in slash' do
+ let(:files) { ['foo/bar/'] }
+
+ it_behaves_like 'foo/bar directory key'
+ end
+
+ context 'with directories ending in slash star' do
+ let(:files) { ['foo/bar/*'] }
+
+ it_behaves_like 'foo/bar directory key'
+ end
+ end
+ end
+
+ context 'with cache:key:prefix' do
+ context 'without files' do
+ let(:config) do
+ {
+ key: {
+ prefix: 'a-prefix'
+ },
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it 'adds prefix to default key' do
+ expected = {
+ options: {
+ cache: {
+ key: 'a-prefix-default',
+ paths: ['vendor/ruby']
+ }
+ }
+ }
+
+ is_expected.to include(expected)
+ end
+ end
+
+ context 'with existing files' do
+ let(:config) do
+ {
+ key: {
+ files: ['VERSION', 'Gemfile.zip'],
+ prefix: 'a-prefix'
+ },
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it 'adds prefix key' do
+ expected = {
+ options: {
+ cache: {
+ key: 'a-prefix-703ecc8fef1635427a1f86a8a1a308831c122392',
+ paths: ['vendor/ruby']
+ }
+ }
+ }
+
+ is_expected.to include(expected)
+ end
+ end
+
+ context 'with missing files' do
+ let(:config) do
+ {
+ key: {
+ files: ['project-gemfile.lock', ''],
+ prefix: 'a-prefix'
+ },
+ paths: ['vendor/ruby']
+ }
+ end
+
+ it 'adds prefix to default key' do
+ expected = {
+ options: {
+ cache: {
+ key: 'a-prefix-default',
+ paths: ['vendor/ruby']
+ }
+ }
+ }
+
+ is_expected.to include(expected)
+ end
+ end
+ end
+
+ context 'with all cache option keys' do
+ let(:config) do
+ {
+ key: 'a-key',
+ paths: ['vendor/ruby'],
+ untracked: true,
+ policy: 'push'
+ }
+ end
+
+ it { is_expected.to include(options: { cache: config }) }
+ end
+
+ context 'with unknown cache option keys' do
+ let(:config) do
+ {
+ key: 'a-key',
+ unknown_key: true
+ }
+ end
+
+ it { expect { subject }.to raise_error(ArgumentError, /unknown_key/) }
+ end
+
+ context 'with empty config' do
+ let(:config) { {} }
+
+ it { is_expected.to include(options: {}) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 945baf47b7b..53dcb6359fe 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -4,7 +4,8 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build do
let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:head_sha) { project.repository.head_commit.id }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: head_sha) }
let(:attributes) { { name: 'rspec', ref: 'master' } }
let(:previous_stages) { [] }
@@ -69,6 +70,101 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to include(when: 'never') }
end
end
+
+ context 'with cache:key' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {
+ key: 'a-value'
+ }
+ }
+ end
+
+ it { is_expected.to include(options: { cache: { key: 'a-value' } }) }
+ end
+
+ context 'with cache:key:files' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {
+ key: {
+ files: ['VERSION']
+ }
+ }
+ }
+ end
+
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: {
+ key: 'f155568ad0933d8358f66b846133614f76dd0ca4'
+ }
+ }
+ }
+
+ is_expected.to include(cache_options)
+ end
+ end
+
+ context 'with cache:key:prefix' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {
+ key: {
+ prefix: 'something'
+ }
+ }
+ }
+ end
+
+ it { is_expected.to include(options: { cache: { key: 'something-default' } }) }
+ end
+
+ context 'with cache:key:files and prefix' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {
+ key: {
+ files: ['VERSION'],
+ prefix: 'something'
+ }
+ }
+ }
+ end
+
+ it 'includes cache options' do
+ cache_options = {
+ options: {
+ cache: {
+ key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4'
+ }
+ }
+ }
+
+ is_expected.to include(cache_options)
+ end
+ end
+
+ context 'with empty cache' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ ref: 'master',
+ cache: {}
+ }
+ end
+
+ it { is_expected.to include(options: {}) }
+ end
end
describe '#bridge?' do
@@ -773,10 +869,4 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
end
-
- describe '#scoped_variables_hash' do
- subject { seed_build.scoped_variables_hash }
-
- it { is_expected.to eq(seed_build.to_resource.scoped_variables_hash) }
- end
end
diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb
index 1725d954b92..857483a9e0a 100644
--- a/spec/lib/gitlab/ci/status/composite_spec.rb
+++ b/spec/lib/gitlab/ci/status/composite_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Ci::Status::Composite do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index 1baea13299b..45b59541ce6 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
@@ -100,7 +102,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
describe '#append' do
shared_examples_for 'appends' do
it "truncates and append content" do
- stream.append("89", 4)
+ stream.append(+"89", 4)
stream.seek(0)
expect(stream.size).to eq(6)
@@ -108,7 +110,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
end
it 'appends in binary mode' do
- '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
+ (+'😺').force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
stream.append(byte, offset)
end
@@ -154,7 +156,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
describe '#set' do
shared_examples_for 'sets' do
before do
- stream.set("8901")
+ stream.set(+"8901")
end
it "overwrite content" do
@@ -168,7 +170,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
context 'when stream is StringIO' do
let(:stream) do
described_class.new do
- StringIO.new("12345678")
+ StringIO.new(+"12345678")
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index cb5ebde16d7..4b1c7483b11 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -108,6 +108,25 @@ module Gitlab
it { expect(subject[:interruptible]).to be_falsy }
end
+
+ it "returns interruptible when overridden for job" do
+ config = YAML.dump({ default: { interruptible: true },
+ rspec: { script: "rspec" } })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
+ stage: "test",
+ stage_idx: 2,
+ name: "rspec",
+ options: { script: ["rspec"] },
+ interruptible: true,
+ allow_failure: false,
+ when: "on_success",
+ yaml_variables: []
+ })
+ end
end
describe 'retry entry' do
@@ -249,6 +268,108 @@ module Gitlab
end
end
+ describe '#workflow_attributes' do
+ context 'with disallowed workflow:variables' do
+ let(:config) do
+ <<-EOYML
+ workflow:
+ rules:
+ - if: $VAR == "value"
+ variables:
+ UNSUPPORTED: "unparsed"
+ EOYML
+ end
+
+ it 'parses the workflow:rules configuration' do
+ expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'workflow config contains unknown keys: variables')
+ end
+ end
+
+ context 'with rules and variables' do
+ let(:config) do
+ <<-EOYML
+ variables:
+ SUPPORTED: "parsed"
+
+ workflow:
+ rules:
+ - if: $VAR == "value"
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:rules configuration' do
+ expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' })
+ end
+
+ it 'parses the root:variables as yaml_variables:' do
+ expect(subject.workflow_attributes[:yaml_variables])
+ .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
+ end
+ end
+
+ context 'with rules and no variables' do
+ let(:config) do
+ <<-EOYML
+ workflow:
+ rules:
+ - if: $VAR == "value"
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:rules configuration' do
+ expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' })
+ end
+
+ it 'parses the root:variables as yaml_variables:' do
+ expect(subject.workflow_attributes[:yaml_variables]).to eq([])
+ end
+ end
+
+ context 'with variables and no rules' do
+ let(:config) do
+ <<-EOYML
+ variables:
+ SUPPORTED: "parsed"
+
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:rules configuration' do
+ expect(subject.workflow_attributes[:rules]).to be_nil
+ end
+
+ it 'parses the root:variables as yaml_variables:' do
+ expect(subject.workflow_attributes[:yaml_variables])
+ .to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
+ end
+ end
+
+ context 'with no rules and no variables' do
+ let(:config) do
+ <<-EOYML
+ hello:
+ script: echo world
+ EOYML
+ end
+
+ it 'parses the workflow:rules configuration' do
+ expect(subject.workflow_attributes[:rules]).to be_nil
+ end
+
+ it 'parses the root:variables as yaml_variables:' do
+ expect(subject.workflow_attributes[:yaml_variables]).to eq([])
+ end
+ end
+ end
+
describe 'only / except policies validations' do
context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
@@ -330,7 +451,7 @@ module Gitlab
}
end
- it "return commands with scripts concencaced" do
+ it "return commands with scripts concatenated" do
expect(subject[:options][:before_script]).to eq(["global script"])
end
end
@@ -343,7 +464,7 @@ module Gitlab
}
end
- it "return commands with scripts concencaced" do
+ it "return commands with scripts concatenated" do
expect(subject[:options][:before_script]).to eq(["global script"])
end
end
@@ -356,21 +477,48 @@ module Gitlab
}
end
- it "return commands with scripts concencaced" do
+ it "return commands with scripts concatenated" do
expect(subject[:options][:before_script]).to eq(["local script"])
end
end
+
+ context 'when script is array of arrays of strings' do
+ let(:config) do
+ {
+ before_script: [["global script", "echo 1"], ["ls"], "pwd"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concatenated" do
+ expect(subject[:options][:before_script]).to eq(["global script", "echo 1", "ls", "pwd"])
+ end
+ end
end
describe "script" do
- let(:config) do
- {
- test: { script: ["script"] }
- }
+ context 'when script is array of strings' do
+ let(:config) do
+ {
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concatenated" do
+ expect(subject[:options][:script]).to eq(["script"])
+ end
end
- it "return commands with scripts concencaced" do
- expect(subject[:options][:script]).to eq(["script"])
+ context 'when script is array of arrays of strings' do
+ let(:config) do
+ {
+ test: { script: [["script"], ["echo 1"], "ls"] }
+ }
+ end
+
+ it "return commands with scripts concatenated" do
+ expect(subject[:options][:script]).to eq(["script", "echo 1", "ls"])
+ end
end
end
@@ -413,6 +561,19 @@ module Gitlab
expect(subject[:options][:after_script]).to eq(["local after_script"])
end
end
+
+ context 'when script is array of arrays of strings' do
+ let(:config) do
+ {
+ after_script: [["global script", "echo 1"], ["ls"], "pwd"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["global script", "echo 1", "ls", "pwd"])
+ end
+ end
end
end
@@ -891,7 +1052,7 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
- expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -903,7 +1064,7 @@ module Gitlab
config = YAML.dump(
{
default: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }
+ cache: { paths: ["logs/", "binaries/"], untracked: true, key: { files: ['file'] } }
},
rspec: {
script: "rspec"
@@ -913,33 +1074,79 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
- expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
- key: 'key',
+ key: { files: ['file'] },
policy: 'pull-push'
)
end
- it "returns cache when defined in a job" do
+ it 'returns cache key when defined in a job' do
config = YAML.dump({
rspec: {
- cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
- script: "rspec"
+ cache: { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' },
+ script: 'rspec'
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.stage_builds_attributes("test").size).to eq(1)
- expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
- paths: ["logs/", "binaries/"],
+ expect(config_processor.stage_builds_attributes('test').size).to eq(1)
+ expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
+ paths: ['logs/', 'binaries/'],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
+ it 'returns cache files' do
+ config = YAML.dump(
+ rspec: {
+ cache: {
+ paths: ['logs/', 'binaries/'],
+ untracked: true,
+ key: { files: ['file'] }
+ },
+ script: 'rspec'
+ }
+ )
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.stage_builds_attributes('test').size).to eq(1)
+ expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
+ paths: ['logs/', 'binaries/'],
+ untracked: true,
+ key: { files: ['file'] },
+ policy: 'pull-push'
+ )
+ end
+
+ it 'returns cache files with prefix' do
+ config = YAML.dump(
+ rspec: {
+ cache: {
+ paths: ['logs/', 'binaries/'],
+ untracked: true,
+ key: { files: ['file'], prefix: 'prefix' }
+ },
+ script: 'rspec'
+ }
+ )
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+
+ expect(config_processor.stage_builds_attributes('test').size).to eq(1)
+ expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq(
+ paths: ['logs/', 'binaries/'],
+ untracked: true,
+ key: { files: ['file'], prefix: 'prefix' },
+ policy: 'pull-push'
+ )
+ end
+
it "overwrite cache when defined for a job and globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
@@ -952,7 +1159,7 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.stage_builds_attributes("test").size).to eq(1)
- expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
@@ -970,6 +1177,7 @@ module Gitlab
rspec: {
artifacts: {
paths: ["logs/", "binaries/"],
+ expose_as: "Exposed artifacts",
untracked: true,
name: "custom_name",
expire_in: "7d"
@@ -993,6 +1201,7 @@ module Gitlab
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
+ expose_as: "Exposed artifacts",
untracked: true,
expire_in: "7d"
}
@@ -1251,7 +1460,7 @@ module Gitlab
end
end
- describe "Needs" do
+ describe "Job Needs" do
let(:needs) { }
let(:dependencies) { }
@@ -1259,6 +1468,7 @@ module Gitlab
{
build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' },
+ parallel: { stage: 'build', script: 'test', parallel: 2 },
test1: { stage: 'test', script: 'test', needs: needs, dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
@@ -1275,7 +1485,7 @@ module Gitlab
let(:needs) { %w(build1 build2) }
it "does create jobs with valid specification" do
- expect(subject.builds.size).to eq(5)
+ expect(subject.builds.size).to eq(7)
expect(subject.builds[0]).to eq(
stage: "build",
stage_idx: 1,
@@ -1287,16 +1497,11 @@ module Gitlab
allow_failure: false,
yaml_variables: []
)
- expect(subject.builds[2]).to eq(
+ expect(subject.builds[4]).to eq(
stage: "test",
stage_idx: 2,
name: "test1",
- options: {
- script: ["test"],
- # This does not make sense, there is a follow-up:
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/65569
- bridge_needs: %w[build1 build2]
- },
+ options: { script: ["test"] },
needs_attributes: [
{ name: "build1" },
{ name: "build2" }
@@ -1308,10 +1513,25 @@ module Gitlab
end
end
- context 'needs two builds defined as symbols' do
- let(:needs) { [:build1, :build2] }
+ context 'needs parallel job' do
+ let(:needs) { %w(parallel) }
- it { expect { subject }.not_to raise_error }
+ it "does create jobs with valid specification" do
+ expect(subject.builds.size).to eq(7)
+ expect(subject.builds[4]).to eq(
+ stage: "test",
+ stage_idx: 2,
+ name: "test1",
+ options: { script: ["test"] },
+ needs_attributes: [
+ { name: "parallel 1/2" },
+ { name: "parallel 2/2" }
+ ],
+ when: "on_success",
+ allow_failure: false,
+ yaml_variables: []
+ )
+ end
end
context 'undefined need' do
@@ -1545,28 +1765,42 @@ module Gitlab
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array containing strings and arrays of strings")
end
it "returns errors if job before_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array containing strings and arrays of strings")
+ end
+
+ it "returns errors if job before_script parameter is multi-level nested array of strings" do
+ config = YAML.dump({ rspec: { script: "test", before_script: [["ls", ["pwd"]], "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array containing strings and arrays of strings")
end
it "returns errors if after_script parameter is invalid" do
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array containing strings and arrays of strings")
end
it "returns errors if job after_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array containing strings and arrays of strings")
+ end
+
+ it "returns errors if job after_script parameter is multi-level nested array of strings" do
+ config = YAML.dump({ rspec: { script: "test", after_script: [["ls", ["pwd"]], "test"] } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array containing strings and arrays of strings")
end
it "returns errors if image parameter is invalid" do
@@ -1776,14 +2010,42 @@ module Gitlab
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key should be a hash, a string or a symbol")
end
it "returns errors if job cache:key is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key should be a hash, a string or a symbol")
+ end
+
+ it 'returns errors if job cache:key:files is not an array of strings' do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [1] } } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config should be an array of strings')
+ end
+
+ it 'returns errors if job cache:key:files is an empty array' do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { files: [] } } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:files config requires at least 1 item')
+ end
+
+ it 'returns errors if job defines only cache:key:prefix' do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 'prefix-key' } } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key config missing required keys: files')
+ end
+
+ it 'returns errors if job cache:key:prefix is not an a string' do
+ config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: { prefix: 1, files: ['file'] } } } })
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:cache:key:prefix config should be a string or symbol')
end
it "returns errors if job cache:untracked is not an array of strings" do
diff --git a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
index 974cc2c4660..fc9792e16d7 100644
--- a/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_job_artifact_files_spec.rb
@@ -21,11 +21,10 @@ describe Gitlab::Cleanup::OrphanJobArtifactFiles do
end
it 'errors when invalid niceness is given' do
+ allow(Gitlab::Utils).to receive(:which).with('ionice').and_return('/fake/ionice')
cleanup = described_class.new(logger: null_logger, niceness: 'FooBar')
- expect(null_logger).to receive(:error).with(/FooBar/)
-
- cleanup.run!
+ expect { cleanup.run! }.to raise_error('Invalid niceness')
end
it 'finds artifacts on disk' do
@@ -63,6 +62,8 @@ describe Gitlab::Cleanup::OrphanJobArtifactFiles do
def mock_artifacts_found(cleanup, *files)
mock = allow(cleanup).to receive(:find_artifacts)
- files.each { |file| mock.and_yield(file) }
+ # Because we shell out to run `find -L ...`, each file actually
+ # contains a trailing newline
+ files.each { |file| mock.and_yield("#{file}\n") }
end
end
diff --git a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
index 1eddf488c5d..b8ac8c5b95c 100644
--- a/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
+++ b/spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb
@@ -8,15 +8,28 @@ describe Gitlab::Cluster::Mixins::PumaCluster do
PUMA_STARTUP_TIMEOUT = 30
context 'when running Puma in Cluster-mode' do
- %i[USR1 USR2 INT HUP].each do |signal|
- it "for #{signal} does execute phased restart block" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:signal, :exitstatus, :termsig) do
+ # executes phased restart block
+ :USR1 | 140 | nil
+ :USR2 | 140 | nil
+ :INT | 140 | nil
+ :HUP | 140 | nil
+
+ # does not execute phased restart block
+ :TERM | nil | 15
+ end
+
+ with_them do
+ it 'properly handles process lifecycle' do
with_puma(workers: 1) do |pid|
Process.kill(signal, pid)
child_pid, child_status = Process.wait2(pid)
expect(child_pid).to eq(pid)
- expect(child_status).to be_exited
- expect(child_status.exitstatus).to eq(140)
+ expect(child_status.exitstatus).to eq(exitstatus)
+ expect(child_status.termsig).to eq(termsig)
end
end
end
@@ -62,8 +75,12 @@ describe Gitlab::Cluster::Mixins::PumaCluster do
Puma::Cluster.prepend(#{described_class})
- Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
- exit(140)
+ mutex = Mutex.new
+
+ Gitlab::Cluster::LifecycleEvents.on_before_blackout_period do
+ mutex.synchronize do
+ exit(140)
+ end
end
# redirect stderr to stdout
diff --git a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
index 2b3a267991c..ebe019924d5 100644
--- a/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
+++ b/spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb
@@ -5,31 +5,30 @@ require 'spec_helper'
# For easier debugging set `UNICORN_DEBUG=1`
describe Gitlab::Cluster::Mixins::UnicornHttpServer do
- UNICORN_STARTUP_TIMEOUT = 10
+ UNICORN_STARTUP_TIMEOUT = 30
context 'when running Unicorn' do
- %i[USR2].each do |signal|
- it "for #{signal} does execute phased restart block" do
- with_unicorn(workers: 1) do |pid|
- Process.kill(signal, pid)
+ using RSpec::Parameterized::TableSyntax
- child_pid, child_status = Process.wait2(pid)
- expect(child_pid).to eq(pid)
- expect(child_status).to be_exited
- expect(child_status.exitstatus).to eq(140)
- end
- end
+ where(:signal, :exitstatus, :termsig) do
+ # executes phased restart block
+ :USR2 | 140 | nil
+ :QUIT | 140 | nil
+
+ # does not execute phased restart block
+ :INT | 0 | nil
+ :TERM | 0 | nil
end
- %i[QUIT TERM INT].each do |signal|
- it "for #{signal} does not execute phased restart block" do
+ with_them do
+ it 'properly handles process lifecycle' do
with_unicorn(workers: 1) do |pid|
Process.kill(signal, pid)
child_pid, child_status = Process.wait2(pid)
expect(child_pid).to eq(pid)
- expect(child_status).to be_exited
- expect(child_status.exitstatus).to eq(0)
+ expect(child_status.exitstatus).to eq(exitstatus)
+ expect(child_status.termsig).to eq(termsig)
end
end
end
@@ -74,8 +73,12 @@ describe Gitlab::Cluster::Mixins::UnicornHttpServer do
Unicorn::HttpServer.prepend(#{described_class})
- Gitlab::Cluster::LifecycleEvents.on_before_phased_restart do
- exit(140)
+ mutex = Mutex.new
+
+ Gitlab::Cluster::LifecycleEvents.on_before_blackout_period do
+ mutex.synchronize do
+ exit(140)
+ end
end
# redirect stderr to stdout
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index a163de07967..9eee7e89062 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -129,7 +129,7 @@ describe 'cycle analytics events' do
end
end
- describe '#test_events' do
+ describe '#test_events', :sidekiq_might_not_need_inline do
let(:stage) { :test }
let(:merge_request) { MergeRequest.first }
@@ -234,7 +234,7 @@ describe 'cycle analytics events' do
end
end
- describe '#staging_events' do
+ describe '#staging_events', :sidekiq_might_not_need_inline do
let(:stage) { :staging }
let(:merge_request) { MergeRequest.first }
@@ -306,7 +306,7 @@ describe 'cycle analytics events' do
end
end
- describe '#production_events' do
+ describe '#production_events', :sidekiq_might_not_need_inline do
let(:stage) { :production }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
diff --git a/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb
index d5c2f7cc579..664009f140f 100644
--- a/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb
@@ -44,6 +44,14 @@ describe Gitlab::CycleAnalytics::GroupStageSummary 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
@@ -97,6 +105,14 @@ describe Gitlab::CycleAnalytics::GroupStageSummary 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
diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
index e568ea633db..d4ab9bc225b 100644
--- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
@@ -71,7 +71,7 @@ describe Gitlab::CycleAnalytics::UsageData do
}
end
- it 'returns the aggregated usage data of every selected project' do
+ it 'returns the aggregated usage data of every selected project', :sidekiq_might_not_need_inline do
result = subject.to_json
expect(result).to have_key(:avg_cycle_analytics)
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 1696d3566ad..8056418e697 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -178,6 +178,7 @@ describe Gitlab::Danger::Helper do
'app/assets/foo' | :frontend
'app/views/foo' | :frontend
'public/foo' | :frontend
+ 'scripts/frontend/foo' | :frontend
'spec/javascripts/foo' | :frontend
'spec/frontend/bar' | :frontend
'vendor/assets/foo' | :frontend
@@ -193,10 +194,8 @@ describe Gitlab::Danger::Helper do
'app/models/foo' | :backend
'bin/foo' | :backend
'config/foo' | :backend
- 'danger/foo' | :backend
'lib/foo' | :backend
'rubocop/foo' | :backend
- 'scripts/foo' | :backend
'spec/foo' | :backend
'spec/foo/bar' | :backend
@@ -209,16 +208,24 @@ describe Gitlab::Danger::Helper do
'vendor/languages.yml' | :backend
'vendor/licenses.csv' | :backend
- 'Dangerfile' | :backend
'Gemfile' | :backend
'Gemfile.lock' | :backend
'Procfile' | :backend
'Rakefile' | :backend
'FOO_VERSION' | :backend
+ 'Dangerfile' | :engineering_productivity
+ 'danger/commit_messages/Dangerfile' | :engineering_productivity
+ 'ee/danger/commit_messages/Dangerfile' | :engineering_productivity
+ 'danger/commit_messages/' | :engineering_productivity
+ 'ee/danger/commit_messages/' | :engineering_productivity
'.gitlab-ci.yml' | :engineering_productivity
'.gitlab/ci/cng.gitlab-ci.yml' | :engineering_productivity
'.gitlab/ci/ee-specific-checks.gitlab-ci.yml' | :engineering_productivity
+ 'scripts/foo' | :engineering_productivity
+ 'lib/gitlab/danger/foo' | :engineering_productivity
+ 'ee/lib/gitlab/danger/foo' | :engineering_productivity
+
'lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' | :backend
'ee/FOO_VERSION' | :unknown
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index bd1c2b10dc8..35edfa08a63 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Danger::Teammate do
expect(subject.maintainer?(project, :frontend, labels)).to be_truthy
end
- context 'when labels contain Create and the category is test' do
+ context 'when labels contain devops::create and the category is test' do
let(:labels) { ['devops::create'] }
context 'when role is Test Automation Engineer, Create' do
@@ -79,6 +79,22 @@ describe Gitlab::Danger::Teammate do
it '#maintainer? returns false' do
expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_falsey
end
+
+ context 'when capabilities include maintainer backend' do
+ let(:capabilities) { ['maintainer backend'] }
+
+ it '#maintainer? returns true' do
+ expect(subject.maintainer?(project, :engineering_productivity, labels)).to be_truthy
+ end
+ end
+
+ context 'when capabilities include trainee_maintainer backend' do
+ let(:capabilities) { ['trainee_maintainer backend'] }
+
+ it '#traintainer? returns true' do
+ expect(subject.traintainer?(project, :engineering_productivity, labels)).to be_truthy
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
index 0a6e2302b09..42d7329494d 100644
--- a/spec/lib/gitlab/data_builder/deployment_spec.rb
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -35,5 +35,12 @@ describe Gitlab::DataBuilder::Deployment do
expect(data[:commit_url]).to eq(expected_commit_url)
expect(data[:commit_title]).to eq(commit.title)
end
+
+ it 'does not include the deployable URL when there is no deployable' do
+ deployment = create(:deployment, status: :failed, deployable: nil)
+ data = described_class.build(deployment)
+
+ expect(data[:deployable_url]).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 58509b69463..cbc03fc38eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -57,6 +57,32 @@ describe Gitlab::DataBuilder::Push do
include_examples 'deprecated repository hook data'
end
+ describe '.sample_data' do
+ let(:data) { described_class.sample_data }
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:before]).to eq('95790bf891e76fee5e1747ab589903a6a1f80f22') }
+ it { expect(data[:after]).to eq('da1560886d4f094c3e6c9ef40349f7d38b5d27d7') }
+ it { expect(data[:ref]).to eq('refs/heads/master') }
+ it { expect(data[:project_id]).to eq(15) }
+ it { expect(data[:commits].size).to eq(1) }
+ it { expect(data[:total_commits_count]).to eq(1) }
+ it 'contains project data' do
+ expect(data[:project]).to be_a(Hash)
+ expect(data[:project][:id]).to eq(15)
+ expect(data[:project][:name]).to eq('gitlab')
+ expect(data[:project][:description]).to eq('')
+ expect(data[:project][:web_url]).to eq('http://test.example.com/gitlab/gitlab')
+ expect(data[:project][:avatar_url]).to eq('https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80')
+ expect(data[:project][:git_http_url]).to eq('http://test.example.com/gitlab/gitlab.git')
+ expect(data[:project][:git_ssh_url]).to eq('git@test.example.com:gitlab/gitlab.git')
+ expect(data[:project][:namespace]).to eq('gitlab')
+ expect(data[:project][:visibility_level]).to eq(0)
+ expect(data[:project][:path_with_namespace]).to eq('gitlab/gitlab')
+ expect(data[:project][:default_branch]).to eq('master')
+ end
+ end
+
describe '.build' do
let(:data) do
described_class.build(
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 49f92f14559..449eee7a371 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -142,7 +142,6 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:transaction_open?).and_return(false)
allow(model).to receive(:index_exists?).and_return(true)
allow(model).to receive(:disable_statement_timeout).and_call_original
- allow(model).to receive(:supports_drop_index_concurrently?).and_return(true)
end
describe 'by column name' do
diff --git a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
index aab6fbcbbd1..5b1a17e734d 100644
--- a/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
+++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb
@@ -164,15 +164,6 @@ describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do
end
it_behaves_like 'has prometheus service', 'http://localhost:9090'
-
- it 'does not overwrite the existing whitelist' do
- application_setting.outbound_local_requests_whitelist = ['example.com']
-
- expect(result[:status]).to eq(:success)
- expect(application_setting.outbound_local_requests_whitelist).to contain_exactly(
- 'example.com', 'localhost'
- )
- end
end
context 'with non default prometheus address' do
diff --git a/spec/lib/gitlab/devise_failure_spec.rb b/spec/lib/gitlab/devise_failure_spec.rb
new file mode 100644
index 00000000000..eee05c7befd
--- /dev/null
+++ b/spec/lib/gitlab/devise_failure_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::DeviseFailure do
+ let(:env) do
+ {
+ 'REQUEST_URI' => 'http://test.host/',
+ 'HTTP_HOST' => 'test.host',
+ 'REQUEST_METHOD' => 'GET',
+ 'warden.options' => { scope: :user },
+ 'rack.session' => {},
+ 'rack.session.options' => {},
+ 'rack.input' => "",
+ 'warden' => OpenStruct.new(message: nil)
+ }
+ end
+
+ let(:response) { described_class.call(env).to_a }
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ context 'When redirecting' do
+ it 'sets the expire_after key' do
+ response
+
+ expect(env['rack.session.options']).to have_key(:expire_after)
+ end
+
+ it 'returns to the default redirect location' do
+ expect(response.first).to eq(302)
+ expect(request.flash[:alert]).to eq('You need to sign in or sign up before continuing.')
+ expect(response.second['Location']).to eq('http://test.host/users/sign_in')
+ end
+ end
+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 35aa663b0a5..a65214fab61 100644
--- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
index c3b706fc538..747fe369c78 100644
--- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index aed7d8d81ce..0739f622af5 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index 2e5fd16d370..9be6ace3be5 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -2,81 +2,194 @@
require 'spec_helper'
-describe Gitlab::Experimentation::ControllerConcern, type: :controller do
- controller(ApplicationController) do
- include Gitlab::Experimentation::ControllerConcern
+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'
+ }
+ })
- def index
- head :ok
- end
+ stub_feature_flags(feature_toggle => true)
end
- describe '#set_experimentation_subject_id_cookie' do
- before do
- get :index
+ let(:feature_toggle) { :test_experiment_toggle }
+ let(:environment) { Rails.env.test? }
+ let(:enabled_ratio) { 0.1 }
+
+ describe Gitlab::Experimentation::ControllerConcern, type: :controller do
+ controller(ApplicationController) do
+ include Gitlab::Experimentation::ControllerConcern
+
+ def index
+ head :ok
+ end
end
- context 'cookie is present' do
+ describe '#set_experimentation_subject_id_cookie' do
before do
- cookies[:experimentation_subject_id] = 'test'
+ get :index
end
- it 'does not change the cookie' do
- expect(cookies[:experimentation_subject_id]).to eq 'test'
+ context 'cookie is present' do
+ before do
+ cookies[:experimentation_subject_id] = 'test'
+ end
+
+ it 'does not change the cookie' do
+ expect(cookies[:experimentation_subject_id]).to eq 'test'
+ end
end
- end
- context 'cookie is not present' do
- it 'sets a permanent signed cookie' do
- expect(cookies.permanent.signed[:experimentation_subject_id]).to be_present
+ context 'cookie is not present' do
+ it 'sets a permanent signed cookie' do
+ expect(cookies.permanent.signed[:experimentation_subject_id]).to be_present
+ end
end
end
- end
- describe '#experiment_enabled?' do
- context 'cookie is not present' do
- it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of nil' do
- expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, nil)
- controller.experiment_enabled?(:test_experiment)
+ describe '#experiment_enabled?' do
+ context 'cookie is not present' do
+ it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of nil' do
+ expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, nil) # rubocop:disable RSpec/DescribedClass
+ controller.experiment_enabled?(:test_experiment)
+ end
+ end
+
+ context 'cookie is present' do
+ before do
+ cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
+ get :index
+ end
+
+ it 'calls Gitlab::Experimentation.enabled_for_user? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
+ # 'abcd1234'.hex % 100 = 76
+ expect(Gitlab::Experimentation).to receive(:enabled_for_user?).with(:test_experiment, 76) # rubocop:disable RSpec/DescribedClass
+ controller.experiment_enabled?(:test_experiment)
+ end
+ end
+
+ describe 'URL parameter to force enable experiment' do
+ it 'returns true' do
+ get :index, params: { force_experiment: :test_experiment }
+
+ expect(controller.experiment_enabled?(:test_experiment)).to be_truthy
+ end
end
end
- context 'cookie is present' do
- before do
- cookies.permanent.signed[:experimentation_subject_id] = 'abcd-1234'
- get :index
+ describe '#track_experiment_event' do
+ context 'when the experiment is enabled' do
+ before do
+ stub_experiment(test_experiment: true)
+ end
+
+ context 'the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(test_experiment: true)
+ end
+
+ it 'tracks the event with the right parameters' do
+ expect(Gitlab::Tracking).to receive(:event).with(
+ 'Team',
+ 'start',
+ label: nil,
+ property: 'experimental_group'
+ )
+ controller.track_experiment_event(:test_experiment, 'start')
+ end
+ end
+
+ context 'the user is part of the control group' do
+ before do
+ stub_experiment_for_user(test_experiment: false)
+ end
+
+ it 'tracks the event with the right parameters' do
+ expect(Gitlab::Tracking).to receive(:event).with(
+ 'Team',
+ 'start',
+ label: nil,
+ property: 'control_group'
+ )
+ controller.track_experiment_event(:test_experiment, 'start')
+ end
+ end
end
- it 'calls Gitlab::Experimentation.enabled? with the name of the experiment and an experimentation_subject_index of the modulo 100 of the hex value of the uuid' do
- # 'abcd1234'.hex % 100 = 76
- expect(Gitlab::Experimentation).to receive(:enabled?).with(:test_experiment, 76)
- controller.experiment_enabled?(:test_experiment)
+ context 'when the experiment is disabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ end
+
+ it 'does not track the event' do
+ expect(Gitlab::Tracking).not_to receive(:event)
+ controller.track_experiment_event(:test_experiment, 'start')
+ end
end
end
- end
-end
-describe Gitlab::Experimentation do
- before do
- stub_const('Gitlab::Experimentation::EXPERIMENTS', {
- test_experiment: {
- feature_toggle: feature_toggle,
- environment: environment,
- enabled_ratio: enabled_ratio
- }
- })
+ describe '#frontend_experimentation_tracking_data' do
+ context 'when the experiment is enabled' do
+ before do
+ stub_experiment(test_experiment: true)
+ end
- stub_feature_flags(feature_toggle => true)
- end
+ context 'the user is part of the experimental group' do
+ before do
+ stub_experiment_for_user(test_experiment: true)
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ label: nil,
+ property: 'experimental_group'
+ }
+ )
+ end
+ end
- let(:feature_toggle) { :test_experiment_toggle }
- let(:environment) { Rails.env.test? }
- let(:enabled_ratio) { 0.1 }
+ context 'the user is part of the control group' do
+ before do
+ allow_any_instance_of(described_class).to receive(:experiment_enabled?).with(:test_experiment).and_return(false)
+ end
+
+ it 'pushes the right parameters to gon' do
+ controller.frontend_experimentation_tracking_data(:test_experiment, 'start')
+ expect(Gon.tracking_data).to eq(
+ {
+ category: 'Team',
+ action: 'start',
+ label: nil,
+ property: 'control_group'
+ }
+ )
+ end
+ end
+ end
- describe '.enabled?' do
- subject { described_class.enabled?(:test_experiment, experimentation_subject_index) }
+ context 'when the experiment is disabled' do
+ before do
+ stub_experiment(test_experiment: false)
+ end
- let(:experimentation_subject_index) { 9 }
+ it 'does not push data to gon' do
+ expect(Gon.method_defined?(:tracking_data)).to be_falsey
+ controller.track_experiment_event(:test_experiment, 'start')
+ end
+ end
+ end
+ end
+
+ describe '.enabled?' do
+ subject { described_class.enabled?(:test_experiment) }
context 'feature toggle is enabled, we are on the right environment and we are selected' do
it { is_expected.to be_truthy }
@@ -84,7 +197,7 @@ describe Gitlab::Experimentation do
describe 'experiment is not defined' do
it 'returns false' do
- expect(described_class.enabled?(:missing_experiment, experimentation_subject_index)).to be_falsey
+ expect(described_class.enabled?(:missing_experiment)).to be_falsey
end
end
@@ -127,30 +240,52 @@ describe Gitlab::Experimentation do
it { is_expected.to be_falsey }
end
end
+ end
- describe 'enabled ratio' do
- context 'enabled ratio is not set' do
- let(:enabled_ratio) { nil }
+ describe '.enabled_for_user?' do
+ subject { described_class.enabled_for_user?(:test_experiment, experimentation_subject_index) }
- it { is_expected.to be_falsey }
+ let(:experimentation_subject_index) { 9 }
+
+ context 'experiment is disabled' do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(false)
end
- context 'experimentation_subject_index is not set' do
- let(:experimentation_subject_index) { nil }
+ it { is_expected.to be_falsey }
+ end
- it { is_expected.to be_falsey }
+ context 'experiment is enabled' do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
end
- context 'experimentation_subject_index is an empty string' do
- let(:experimentation_subject_index) { '' }
+ it { is_expected.to be_truthy }
+
+ context 'enabled ratio is not set' do
+ let(:enabled_ratio) { nil }
it { is_expected.to be_falsey }
end
- context 'experimentation_subject_index outside enabled ratio' do
- let(:experimentation_subject_index) { 11 }
+ describe 'experimentation_subject_index' do
+ context 'experimentation_subject_index is not set' do
+ let(:experimentation_subject_index) { nil }
- it { is_expected.to be_falsey }
+ it { is_expected.to be_falsey }
+ end
+
+ context 'experimentation_subject_index is an empty string' do
+ let(:experimentation_subject_index) { '' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'experimentation_subject_index outside enabled ratio' do
+ let(:experimentation_subject_index) { 11 }
+
+ it { is_expected.to be_falsey }
+ end
end
end
end
diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb
index 5dc2521b310..8a08b2a6275 100644
--- a/spec/lib/gitlab/external_authorization/access_spec.rb
+++ b/spec/lib/gitlab/external_authorization/access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb
index 58e7d626707..1f217249f97 100644
--- a/spec/lib/gitlab/external_authorization/cache_spec.rb
+++ b/spec/lib/gitlab/external_authorization/cache_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
index a87f50b4586..a17d933e3bb 100644
--- a/spec/lib/gitlab/external_authorization/client_spec.rb
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Client do
diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb
index 81f1b2390e6..380e765309c 100644
--- a/spec/lib/gitlab/external_authorization/logger_spec.rb
+++ b/spec/lib/gitlab/external_authorization/logger_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Logger do
diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb
index 43211043eca..e1f6e9ac1fa 100644
--- a/spec/lib/gitlab/external_authorization/response_spec.rb
+++ b/spec/lib/gitlab/external_authorization/response_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization::Response do
diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb
index c45fcca3f06..97055e7b3f9 100644
--- a/spec/lib/gitlab/external_authorization_spec.rb
+++ b/spec/lib/gitlab/external_authorization_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ExternalAuthorization, :request_store do
diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb
index c81cb83d9f4..6a872185713 100644
--- a/spec/lib/gitlab/fake_application_settings_spec.rb
+++ b/spec/lib/gitlab/fake_application_settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::FakeApplicationSettings do
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 617c0f88a89..884425dab3b 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe Gitlab::Favicon, :request_store do
diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb
index 4ba9094b24e..f3a9f706e86 100644
--- a/spec/lib/gitlab/file_detector_spec.rb
+++ b/spec/lib/gitlab/file_detector_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::FileDetector do
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index b49c5817131..7ea9d43c9f7 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::FileFinder do
@@ -6,11 +8,11 @@ describe Gitlab::FileFinder do
subject { described_class.new(project, project.default_branch) }
it_behaves_like 'file finder' do
- let(:expected_file_by_name) { 'files/images/wm.svg' }
+ let(:expected_file_by_path) { 'files/images/wm.svg' }
let(:expected_file_by_content) { 'CHANGELOG' }
end
- it 'filters by name' do
+ it 'filters by filename' do
results = subject.find('files filename:wm.svg')
expect(results.count).to eq(1)
diff --git a/spec/lib/gitlab/fogbugz_import/client_spec.rb b/spec/lib/gitlab/fogbugz_import/client_spec.rb
index dcd1a2d9813..676511211c8 100644
--- a/spec/lib/gitlab/fogbugz_import/client_spec.rb
+++ b/spec/lib/gitlab/fogbugz_import/client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::FogbugzImport::Client do
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 790b0428d19..026fd1fedde 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index eef3b9de476..5a930d44dcb 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Gfm::UploadsRewriter do
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 23651e3d7f2..cdab7127748 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -428,7 +428,9 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- shared_examples 'extracting commit signature' do
+ describe '.extract_signature_lazily' do
+ subject { described_class.extract_signature_lazily(repository, commit_id).itself }
+
context 'when the commit is signed' do
let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
@@ -492,10 +494,8 @@ describe Gitlab::Git::Commit, :seed_helper do
expect { subject }.to raise_error(ArgumentError)
end
end
- end
- describe '.extract_signature_lazily' do
- describe 'loading signatures in batch once' do
+ context 'when loading signatures in batch once' do
it 'fetches signatures in batch once' do
commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6]
signatures = commit_ids.map do |commit_id|
@@ -516,16 +516,6 @@ describe Gitlab::Git::Commit, :seed_helper do
2.times { signatures.each(&:itself) }
end
end
-
- subject { described_class.extract_signature_lazily(repository, commit_id).itself }
-
- it_behaves_like 'extracting commit signature'
- end
-
- describe '.extract_signature' do
- subject { described_class.extract_signature(repository, commit_id) }
-
- it_behaves_like 'extracting commit signature'
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 81dc96b538a..f74cc5623c9 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitAccess do
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 6ba65b56618..99c9369a2b9 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitAccessWiki do
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index b63389af29f..1531317c514 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitRefValidator do
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 505bc470644..fbc49e05c37 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Git do
diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
index a2770ef2fe4..887a6baf659 100644
--- a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::BlobService do
diff --git a/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb
index 742b2872c40..e88b86c71f2 100644
--- a/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/blobs_stitcher_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::BlobsStitcher do
diff --git a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
index c42332dc27b..c6c7fa1c38a 100644
--- a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::CleanupService do
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 71489adb373..1abdabe17bb 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::CommitService do
diff --git a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
index a3602463756..db734b1c129 100644
--- a/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflict_files_stitcher_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::ConflictFilesStitcher do
diff --git a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
index 52630ba0223..f19bcae2470 100644
--- a/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/conflicts_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::ConflictsService do
diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb
index ec7ab2fdedb..d86497da7f5 100644
--- a/spec/lib/gitlab/gitaly_client/diff_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::Diff do
diff --git a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
index cd3242b9326..c9d42ad32cf 100644
--- a/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/diff_stitcher_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::DiffStitcher do
diff --git a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb
index 2c7e5eb5787..615bc80fff2 100644
--- a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::HealthCheckService do
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index f38b8d31237..d4337c51279 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::OperationService do
@@ -209,10 +211,12 @@ describe Gitlab::GitalyClient::OperationService do
end
context 'when a create_tree_error is present' do
- let(:response) { response_class.new(create_tree_error: "something failed") }
+ let(:response) { response_class.new(create_tree_error: "something failed", create_tree_error_code: 'EMPTY') }
it 'raises a CreateTreeError' do
- expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError, "something failed")
+ expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError) do |error|
+ expect(error.error_code).to eq(:empty)
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 0bb6e582159..2b4fe2ea5c0 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::RefService do
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index d5508dbff5d..929ff5dee5d 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::RemoteService do
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index f4b73931f21..503ac57ade6 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::RepositoryService do
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
index 2f83e5a5221..a6b29489df3 100644
--- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::StorageSettings do
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index 78a5e195ad1..f31b7c349ff 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::Util do
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
index 4fa8e97aca0..cb04f9a1637 100644
--- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GitalyClient::WikiService do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index b8df9ad642a..b6c0c0ad523 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want
@@ -399,6 +401,8 @@ describe Gitlab::GitalyClient do
context 'when the request store is active', :request_store do
it 'records call details if a RPC is called' do
+ expect(described_class).to receive(:measure_timings).and_call_original
+
gitaly_server.server_version
expect(described_class.list_call_details).not_to be_empty
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 91229d9c7d4..3266ec4ab50 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::BulkImporting do
diff --git a/spec/lib/gitlab/github_import/caching_spec.rb b/spec/lib/gitlab/github_import/caching_spec.rb
index 70ecdc16da1..18c3e382532 100644
--- a/spec/lib/gitlab/github_import/caching_spec.rb
+++ b/spec/lib/gitlab/github_import/caching_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Caching, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 5b2642d9473..3b269d64b07 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Client do
diff --git a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
index 1568c657a1e..484458289af 100644
--- a/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_note_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::DiffNoteImporter do
diff --git a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
index 4713c6795bb..23ed21294e3 100644
--- a/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/diff_notes_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::DiffNotesImporter do
diff --git a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
index 665b31ef244..399e2d9a563 100644
--- a/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_and_label_links_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::IssueAndLabelLinksImporter do
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index dab5767ece1..a003ad7e091 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
index e237e79e94b..8920ef9fedb 100644
--- a/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issues_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::IssuesImporter do
diff --git a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
index e2a71e78574..19d40b2f380 100644
--- a/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/label_links_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::LabelLinksImporter do
diff --git a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
index 156ef96a0fa..2dcf1433154 100644
--- a/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/labels_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::LabelsImporter, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
index 8fd328d9c1e..a02b620f131 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::LfsObjectImporter do
diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
index 50442552eee..bec039a48eb 100644
--- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::LfsObjectsImporter do
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index 120a07ff2b3..eaf63e0e11b 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
index 9bdcc42be19..d2b8ba186c8 100644
--- a/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/note_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::NoteImporter do
diff --git a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
index f046d13f879..128f8f95fa0 100644
--- a/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/notes_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::NotesImporter do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 8331f0b6bc7..50c27e7f4b7 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index c51985f00a2..e2d810d5ddc 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::PullRequestsImporter do
diff --git a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
index 6a31c57a73d..f8d53208619 100644
--- a/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/releases_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::ReleasesImporter do
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 705df1f4fe7..c65b28fafbf 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Importer::RepositoryImporter do
diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
index da69911812a..b8a6feb6c73 100644
--- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/label_finder_spec.rb b/spec/lib/gitlab/github_import/label_finder_spec.rb
index 8ba766944d6..039ae27ad57 100644
--- a/spec/lib/gitlab/github_import/label_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/label_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::LabelFinder, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb
index 1ff5b9d66b3..a1216db7aac 100644
--- a/spec/lib/gitlab/github_import/markdown_text_spec.rb
+++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::MarkdownText do
diff --git a/spec/lib/gitlab/github_import/milestone_finder_spec.rb b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
index dff931a2fe8..407e2e67ec9 100644
--- a/spec/lib/gitlab/github_import/milestone_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/milestone_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::MilestoneFinder, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/page_counter_spec.rb b/spec/lib/gitlab/github_import/page_counter_spec.rb
index c2613a9a415..87f3ce45fd3 100644
--- a/spec/lib/gitlab/github_import/page_counter_spec.rb
+++ b/spec/lib/gitlab/github_import/page_counter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::PageCounter, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
index ecab64a372a..a9b7d3d388c 100644
--- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ParallelImporter do
diff --git a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
index 98205d3ee25..f4d107e3dce 100644
--- a/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_scheduling_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ParallelScheduling do
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
index 7b0a1ea4948..e743a87cdd1 100644
--- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::DiffNote do
diff --git a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
index 15de0fe49ff..e3b48df4ae9 100644
--- a/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/expose_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::ExposeAttribute do
diff --git a/spec/lib/gitlab/github_import/representation/issue_spec.rb b/spec/lib/gitlab/github_import/representation/issue_spec.rb
index 99330ce42cb..741a912e53b 100644
--- a/spec/lib/gitlab/github_import/representation/issue_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/issue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::Issue do
diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb
index f2c1c66b357..a171a38bc9e 100644
--- a/spec/lib/gitlab/github_import/representation/note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::Note do
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
index d478e5ae899..b6dcd098c9c 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::PullRequest do
diff --git a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
index c296aa0a45b..9c47349b376 100644
--- a/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/to_hash_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::ToHash do
diff --git a/spec/lib/gitlab/github_import/representation/user_spec.rb b/spec/lib/gitlab/github_import/representation/user_spec.rb
index 4e63e8ea568..a7ad6bda3ad 100644
--- a/spec/lib/gitlab/github_import/representation/user_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/user_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation::User do
diff --git a/spec/lib/gitlab/github_import/representation_spec.rb b/spec/lib/gitlab/github_import/representation_spec.rb
index 0b0610817b0..76753a0ff21 100644
--- a/spec/lib/gitlab/github_import/representation_spec.rb
+++ b/spec/lib/gitlab/github_import/representation_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Representation do
diff --git a/spec/lib/gitlab/github_import/sequential_importer_spec.rb b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
index 05d3243f806..8b1e8fbf3b7 100644
--- a/spec/lib/gitlab/github_import/sequential_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/sequential_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::SequentialImporter do
diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb
index 29f4c00d9c7..74b5c1c52cd 100644
--- a/spec/lib/gitlab/github_import/user_finder_spec.rb
+++ b/spec/lib/gitlab/github_import/user_finder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do
diff --git a/spec/lib/gitlab/github_import_spec.rb b/spec/lib/gitlab/github_import_spec.rb
index 496244c91bf..c3ddac01c87 100644
--- a/spec/lib/gitlab/github_import_spec.rb
+++ b/spec/lib/gitlab/github_import_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport do
diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb
index d4b6c629659..3290bef8aa5 100644
--- a/spec/lib/gitlab/gl_repository_spec.rb
+++ b/spec/lib/gitlab/gl_repository_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ::Gitlab::GlRepository do
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index 1dfca0b056c..da307754243 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
verification_status: 'verified'
end
- it 'assigns the gpg key to the signature when the missing gpg key is added' do
+ it 'assigns the gpg key to the signature when the missing gpg key is added', :sidekiq_might_not_need_inline do
# InvalidGpgSignatureUpdater is called by the after_create hook
gpg_key = create :gpg_key,
key: GpgHelpers::User1.public_key,
@@ -86,7 +86,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
verification_status: 'unknown_key'
end
- it 'updates the signature to being valid when the missing gpg key is added' do
+ it 'updates the signature to being valid when the missing gpg key is added', :sidekiq_might_not_need_inline do
# InvalidGpgSignatureUpdater is called by the after_create hook
gpg_key = create :gpg_key,
key: GpgHelpers::User1.public_key,
@@ -133,7 +133,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
verification_status: 'unknown_key'
end
- it 'updates the signature to being valid when the user updates the email address' do
+ it 'updates the signature to being valid when the user updates the email address', :sidekiq_might_not_need_inline do
gpg_key = create :gpg_key,
key: GpgHelpers::User1.public_key,
user: user
@@ -152,7 +152,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
)
end
- it 'keeps the signature at being invalid when the changed email address is still unrelated' do
+ it 'keeps the signature at being invalid when the changed email address is still unrelated', :sidekiq_might_not_need_inline do
gpg_key = create :gpg_key,
key: GpgHelpers::User1.public_key,
user: user
@@ -192,7 +192,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
verification_status: 'unknown_key'
end
- it 'updates the signature to being valid when the missing gpg key is added' do
+ it 'updates the signature to being valid when the missing gpg key is added', :sidekiq_might_not_need_inline do
# InvalidGpgSignatureUpdater is called by the after_create hook
gpg_key = create(:gpg_key, key: GpgHelpers::User3.public_key, user: user)
subkey = gpg_key.subkeys.last
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 77d318c9b23..52d6a86f7d0 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Gpg do
@@ -63,7 +65,7 @@ describe Gitlab::Gpg do
it 'downcases the email' do
public_key = double(:key)
fingerprints = double(:fingerprints)
- uid = double(:uid, name: 'Nannie Bernhard', email: 'NANNIE.BERNHARD@EXAMPLE.COM')
+ uid = double(:uid, name: +'Nannie Bernhard', email: +'NANNIE.BERNHARD@EXAMPLE.COM')
raw_key = double(:raw_key, uids: [uid])
allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
@@ -78,8 +80,8 @@ describe Gitlab::Gpg do
it 'rejects non UTF-8 names and addresses' do
public_key = double(:key)
fingerprints = double(:fingerprints)
- email = "\xEEch@test.com".force_encoding('ASCII-8BIT')
- uid = double(:uid, name: 'Test User', email: email)
+ email = (+"\xEEch@test.com").force_encoding('ASCII-8BIT')
+ uid = double(:uid, name: +'Test User', email: email)
raw_key = double(:raw_key, uids: [uid])
allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
@@ -139,6 +141,96 @@ describe Gitlab::Gpg do
end
end.not_to raise_error
end
+
+ it 'keeps track of created and removed keychains in counters' do
+ created = Gitlab::Metrics.counter(:gpg_tmp_keychains_created_total, 'The number of temporary GPG keychains')
+ removed = Gitlab::Metrics.counter(:gpg_tmp_keychains_removed_total, 'The number of temporary GPG keychains')
+
+ initial_created = created.get
+ initial_removed = removed.get
+
+ described_class.using_tmp_keychain do
+ expect(created.get).to eq(initial_created + 1)
+ expect(removed.get).to eq(initial_removed)
+ end
+
+ expect(removed.get).to eq(initial_removed + 1)
+ end
+
+ it 'cleans up the tmp directory after finishing' do
+ tmp_directory = nil
+
+ described_class.using_tmp_keychain do
+ tmp_directory = described_class.current_home_dir
+ expect(File.exist?(tmp_directory)).to be true
+ end
+
+ expect(tmp_directory).not_to be_nil
+ expect(File.exist?(tmp_directory)).to be false
+ end
+
+ it 'does not fail if the homedir was deleted while running' do
+ expect do
+ described_class.using_tmp_keychain do
+ FileUtils.remove_entry(described_class.current_home_dir)
+ end
+ end.not_to raise_error
+ end
+
+ shared_examples 'multiple deletion attempts of the tmp-dir' do |seconds|
+ let(:tmp_dir) do
+ tmp_dir = Dir.mktmpdir
+ allow(Dir).to receive(:mktmpdir).and_return(tmp_dir)
+ tmp_dir
+ end
+
+ before do
+ # Stub all the other calls for `remove_entry`
+ allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
+ end
+
+ it "tries for #{seconds}" do
+ expect(Retriable).to receive(:retriable).with(a_hash_including(max_elapsed_time: seconds))
+
+ described_class.using_tmp_keychain {}
+ end
+
+ it 'tries at least 2 times to remove the tmp dir before raising', :aggregate_failures do
+ expect(Retriable).to receive(:sleep).at_least(2).times
+ expect(FileUtils).to receive(:remove_entry).with(tmp_dir).at_least(2).times.and_raise('Deletion failed')
+
+ expect { described_class.using_tmp_keychain { } }.to raise_error(described_class::CleanupError)
+ end
+
+ it 'does not attempt multiple times when the deletion succeeds' do
+ expect(Retriable).to receive(:sleep).once
+ expect(FileUtils).to receive(:remove_entry).with(tmp_dir).once.and_raise('Deletion failed')
+ expect(FileUtils).to receive(:remove_entry).with(tmp_dir).and_call_original
+
+ expect { described_class.using_tmp_keychain { } }.not_to raise_error
+
+ expect(File.exist?(tmp_dir)).to be false
+ end
+
+ it 'does not retry when the feature flag is disabled' do
+ stub_feature_flags(gpg_cleanup_retries: false)
+
+ expect(FileUtils).to receive(:remove_entry).with(tmp_dir, true).and_call_original
+ expect(Retriable).not_to receive(:retriable)
+
+ described_class.using_tmp_keychain {}
+ end
+ end
+
+ it_behaves_like 'multiple deletion attempts of the tmp-dir', described_class::FG_CLEANUP_RUNTIME_S
+
+ context 'when running in Sidekiq' do
+ before do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ end
+
+ it_behaves_like 'multiple deletion attempts of the tmp-dir', described_class::BG_CLEANUP_RUNTIME_S
+ 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
new file mode 100644
index 00000000000..8d7826c0a56
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do
+ subject { described_class.new }
+
+ let(:mock_request) { OpenStruct.new(env: {}) }
+
+ describe ".parameters" do
+ describe 'when no exception is available' do
+ it 'returns an empty hash' do
+ expect(subject.parameters(mock_request, nil)).to eq({})
+ end
+ end
+
+ describe 'when an exception is available' do
+ let(:exception) { RuntimeError.new('This is a test') }
+ let(:mock_request) do
+ OpenStruct.new(
+ env: {
+ ::API::Helpers::API_EXCEPTION_ENV => exception
+ }
+ )
+ end
+
+ let(:expected) do
+ {
+ exception: {
+ class: 'RuntimeError',
+ message: 'This is a test'
+ }
+ }
+ end
+
+ it 'returns the correct fields' do
+ expect(subject.parameters(mock_request, nil)).to eq(expected)
+ end
+
+ context 'with backtrace' do
+ before do
+ current_backtrace = caller
+ allow(exception).to receive(:backtrace).and_return(current_backtrace)
+ expected[:exception][:backtrace] = Gitlab::Profiler.clean_backtrace(current_backtrace)
+ end
+
+ it 'includes the backtrace' do
+ expect(subject.parameters(mock_request, nil)).to eq(expected)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/filterable_array_connection_spec.rb b/spec/lib/gitlab/graphql/connections/filterable_array_connection_spec.rb
new file mode 100644
index 00000000000..1fda84f777e
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/filterable_array_connection_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::FilterableArrayConnection do
+ let(:callback) { proc { |nodes| nodes } }
+ let(:all_nodes) { Gitlab::Graphql::FilterableArray.new(callback, 1, 2, 3, 4, 5) }
+ let(:arguments) { {} }
+ subject(:connection) do
+ described_class.new(all_nodes, arguments, max_page_size: 3)
+ end
+
+ describe '#paged_nodes' do
+ let(:paged_nodes) { subject.paged_nodes }
+
+ it_behaves_like "connection with paged nodes"
+
+ context 'when callback filters some nodes' do
+ let(:callback) { proc { |nodes| nodes[1..-1] } }
+
+ it 'does not return filtered elements' do
+ expect(subject.paged_nodes).to contain_exactly(all_nodes[1], all_nodes[2])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition_spec.rb
new file mode 100644
index 00000000000..d943540fe1f
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/conditions/not_null_condition_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::Conditions::NotNullCondition do
+ describe '#build' do
+ let(:condition) { described_class.new(Issue.arel_table, %w(relative_position id), [1500, 500], ['>', '>'], before_or_after) }
+
+ context 'when there is only one ordering field' do
+ let(:condition) { described_class.new(Issue.arel_table, ['id'], [500], ['>'], :after) }
+
+ it 'generates a single condition sql' do
+ expected_sql = <<~SQL
+ ("issues"."id" > 500)
+ SQL
+
+ expect(condition.build.squish).to eq expected_sql.squish
+ end
+ end
+
+ context 'when :after' do
+ let(:before_or_after) { :after }
+
+ it 'generates :after sql' do
+ expected_sql = <<~SQL
+ ("issues"."relative_position" > 1500)
+ OR (
+ "issues"."relative_position" = 1500
+ AND
+ "issues"."id" > 500
+ )
+ OR ("issues"."relative_position" IS NULL)
+ SQL
+
+ expect(condition.build.squish).to eq expected_sql.squish
+ end
+ end
+
+ context 'when :before' do
+ let(:before_or_after) { :before }
+
+ it 'generates :before sql' do
+ expected_sql = <<~SQL
+ ("issues"."relative_position" > 1500)
+ OR (
+ "issues"."relative_position" = 1500
+ AND
+ "issues"."id" > 500
+ )
+ SQL
+
+ expect(condition.build.squish).to eq expected_sql.squish
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/conditions/null_condition_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/conditions/null_condition_spec.rb
new file mode 100644
index 00000000000..7fce94adb81
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/conditions/null_condition_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::Conditions::NullCondition do
+ describe '#build' do
+ let(:condition) { described_class.new(Issue.arel_table, %w(relative_position id), [nil, 500], [nil, '>'], before_or_after) }
+
+ context 'when :after' do
+ let(:before_or_after) { :after }
+
+ it 'generates sql' do
+ expected_sql = <<~SQL
+ (
+ "issues"."relative_position" IS NULL
+ AND
+ "issues"."id" > 500
+ )
+ SQL
+
+ expect(condition.build.squish).to eq expected_sql.squish
+ end
+ end
+
+ context 'when :before' do
+ let(:before_or_after) { :before }
+
+ it 'generates :before sql' do
+ expected_sql = <<~SQL
+ (
+ "issues"."relative_position" IS NULL
+ AND
+ "issues"."id" > 500
+ )
+ OR ("issues"."relative_position" IS NOT NULL)
+ SQL
+
+ expect(condition.build.squish).to eq expected_sql.squish
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb
new file mode 100644
index 00000000000..9dda2a41ec6
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/connection_spec.rb
@@ -0,0 +1,281 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::Connection do
+ let(:nodes) { Project.all.order(id: :asc) }
+ let(:arguments) { {} }
+ subject(:connection) do
+ described_class.new(nodes, arguments, max_page_size: 3)
+ end
+
+ def encoded_cursor(node)
+ described_class.new(nodes, {}).cursor_from_node(node)
+ end
+
+ def decoded_cursor(cursor)
+ JSON.parse(Base64Bp.urlsafe_decode64(cursor))
+ end
+
+ describe '#cursor_from_nodes' do
+ let(:project) { create(:project) }
+ let(:cursor) { connection.cursor_from_node(project) }
+
+ it 'returns an encoded ID' do
+ expect(decoded_cursor(cursor)).to eq('id' => project.id.to_s)
+ end
+
+ context 'when an order is specified' do
+ let(:nodes) { Project.order(:updated_at) }
+
+ it 'returns the encoded value of the order' do
+ expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
+ end
+
+ it 'includes the :id even when not specified in the order' do
+ expect(decoded_cursor(cursor)).to include('id' => project.id.to_s)
+ end
+ end
+
+ context 'when multiple orders are specified' do
+ let(:nodes) { Project.order(:updated_at).order(:created_at) }
+
+ it 'returns the encoded value of the order' do
+ expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
+ end
+ end
+
+ context 'when multiple orders with SQL are specified' do
+ let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) }
+
+ it 'returns the encoded value of the order' do
+ expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s)
+ end
+ end
+ end
+
+ describe '#sliced_nodes' do
+ let(:projects) { create_list(:project, 4) }
+
+ context 'when before is passed' do
+ let(:arguments) { { before: encoded_cursor(projects[1]) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+ end
+ end
+
+ context 'when after is passed' do
+ let(:arguments) { { after: encoded_cursor(projects[1]) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+ end
+ end
+
+ context 'when both before and after are passed' do
+ let(:arguments) do
+ {
+ after: encoded_cursor(projects[1]),
+ before: encoded_cursor(projects[3])
+ }
+ end
+
+ it 'returns the expected set' do
+ expect(subject.sliced_nodes).to contain_exactly(projects[2])
+ end
+ end
+
+ context 'when multiple orders are defined' do
+ let!(:project1) { create(:project, last_repository_check_at: 10.days.ago) } # Asc: project5 Desc: project3
+ let!(:project2) { create(:project, last_repository_check_at: nil) } # Asc: project1 Desc: project1
+ let!(:project3) { create(:project, last_repository_check_at: 5.days.ago) } # Asc: project3 Desc: project5
+ let!(:project4) { create(:project, last_repository_check_at: nil) } # Asc: project2 Desc: project2
+ let!(:project5) { create(:project, last_repository_check_at: 20.days.ago) } # Asc: project4 Desc: project4
+
+ context 'when ascending' do
+ let(:nodes) do
+ Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :asc).order(id: :asc)
+ end
+
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'returns projects in ascending order' do
+ expect(subject.sliced_nodes).to eq([project5, project1, project3, project2, project4])
+ end
+ end
+
+ context 'when before cursor value is NULL' do
+ let(:arguments) { { before: encoded_cursor(project4) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project5, project1, project3, project2])
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(project3) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project5, project1])
+ end
+ end
+
+ context 'when after cursor value is NULL' do
+ let(:arguments) { { after: encoded_cursor(project2) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project4])
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(project1) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project3, project2, project4])
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project5) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project1, project3, project2])
+ end
+ end
+ end
+
+ context 'when descending' do
+ let(:nodes) do
+ Project.order(Arel.sql('projects.last_repository_check_at IS NULL')).order(last_repository_check_at: :desc).order(id: :asc)
+ end
+
+ context 'when no cursor is passed' do
+ let(:arguments) { {} }
+
+ it 'only returns projects in descending order' do
+ expect(subject.sliced_nodes).to eq([project3, project1, project5, project2, project4])
+ end
+ end
+
+ context 'when before cursor value is NULL' do
+ let(:arguments) { { before: encoded_cursor(project4) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project3, project1, project5, project2])
+ end
+ end
+
+ context 'when before cursor value is not NULL' do
+ let(:arguments) { { before: encoded_cursor(project5) } }
+
+ it 'returns all projects before the cursor' do
+ expect(subject.sliced_nodes).to eq([project3, project1])
+ end
+ end
+
+ context 'when after cursor value is NULL' do
+ let(:arguments) { { after: encoded_cursor(project2) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project4])
+ end
+ end
+
+ context 'when after cursor value is not NULL' do
+ let(:arguments) { { after: encoded_cursor(project1) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project5, project2, project4])
+ end
+ end
+
+ context 'when before and after cursor' do
+ let(:arguments) { { before: encoded_cursor(project4), after: encoded_cursor(project3) } }
+
+ it 'returns all projects after the cursor' do
+ expect(subject.sliced_nodes).to eq([project1, project5, project2])
+ end
+ end
+ end
+ end
+
+ # TODO Enable this as part of below issue
+ # https://gitlab.com/gitlab-org/gitlab/issues/32933
+ # context 'when an invalid cursor is provided' do
+ # let(:arguments) { { before: 'invalidcursor' } }
+ #
+ # it 'raises an error' do
+ # expect { expect(subject.sliced_nodes) }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ # end
+ # end
+
+ # TODO Remove this as part of below issue
+ # https://gitlab.com/gitlab-org/gitlab/issues/32933
+ context 'when an old style cursor is provided' do
+ let(:arguments) { { before: Base64Bp.urlsafe_encode64(projects[1].id.to_s, padding: false) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+ end
+ end
+
+ describe '#paged_nodes' do
+ let_it_be(:all_nodes) { create_list(:project, 5) }
+ let(:paged_nodes) { subject.paged_nodes }
+
+ it_behaves_like "connection with paged nodes"
+
+ context 'when both are passed' do
+ let(:arguments) { { first: 2, last: 2 } }
+
+ it 'raises an error' do
+ expect { paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ context 'when primary key is not in original order' do
+ let(:nodes) { Project.order(last_repository_check_at: :desc) }
+
+ it 'is added to end' do
+ sliced = subject.sliced_nodes
+ last_order_name = sliced.order_values.last.expr.name
+
+ expect(last_order_name).to eq sliced.primary_key
+ end
+ end
+
+ context 'when there is no primary key' do
+ let(:nodes) { NoPrimaryKey.all }
+
+ it 'raises an error' do
+ expect(NoPrimaryKey.primary_key).to be_nil
+ expect { subject.sliced_nodes }.to raise_error(ArgumentError, 'Relation must have a primary key')
+ 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/connections/keyset/legacy_keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb
new file mode 100644
index 00000000000..aaf28fed684
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/legacy_keyset_connection_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+# TODO https://gitlab.com/gitlab-org/gitlab/issues/35104
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::LegacyKeysetConnection do
+ describe 'old keyset_connection' do
+ let(:described_class) { Gitlab::Graphql::Connections::Keyset::Connection }
+ let(:nodes) { Project.all.order(id: :asc) }
+ let(:arguments) { {} }
+ subject(:connection) do
+ described_class.new(nodes, arguments, max_page_size: 3)
+ end
+
+ before do
+ stub_feature_flags(graphql_keyset_pagination: false)
+ end
+
+ def encoded_property(value)
+ Base64Bp.urlsafe_encode64(value.to_s, padding: false)
+ end
+
+ describe '#cursor_from_nodes' do
+ let(:project) { create(:project) }
+
+ it 'returns an encoded ID' do
+ expect(connection.cursor_from_node(project))
+ .to eq(encoded_property(project.id))
+ end
+
+ context 'when an order was specified' do
+ let(:nodes) { Project.order(:updated_at) }
+
+ it 'returns the encoded value of the order' do
+ expect(connection.cursor_from_node(project))
+ .to eq(encoded_property(project.updated_at))
+ end
+ end
+ end
+
+ describe '#sliced_nodes' do
+ let(:projects) { create_list(:project, 4) }
+
+ context 'when before is passed' do
+ let(:arguments) { { before: encoded_property(projects[1].id) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+ end
+ end
+
+ context 'when after is passed' do
+ let(:arguments) { { after: encoded_property(projects[1].id) } }
+
+ it 'only returns the project before the selected one' do
+ expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
+ end
+
+ context 'when the sort order is descending' do
+ let(:nodes) { Project.all.order(id: :desc) }
+
+ it 'returns the correct nodes' do
+ expect(subject.sliced_nodes).to contain_exactly(projects.first)
+ end
+ end
+ end
+
+ context 'when both before and after are passed' do
+ let(:arguments) do
+ {
+ after: encoded_property(projects[1].id),
+ before: encoded_property(projects[3].id)
+ }
+ end
+
+ it 'returns the expected set' do
+ expect(subject.sliced_nodes).to contain_exactly(projects[2])
+ end
+ end
+ end
+
+ describe '#paged_nodes' do
+ let!(:projects) { create_list(:project, 5) }
+
+ it 'returns the collection limited to max page size' do
+ expect(subject.paged_nodes.size).to eq(3)
+ end
+
+ it 'is a loaded memoized array' do
+ expect(subject.paged_nodes).to be_an(Array)
+ expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
+ end
+
+ context 'when `first` is passed' do
+ let(:arguments) { { first: 2 } }
+
+ it 'returns only the first elements' do
+ expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second)
+ end
+ end
+
+ context 'when `last` is passed' do
+ let(:arguments) { { last: 2 } }
+
+ it 'returns only the last elements' do
+ expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4])
+ end
+ end
+
+ context 'when both are passed' do
+ let(:arguments) { { first: 2, last: 2 } }
+
+ it 'raises an error' do
+ expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/order_info_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/order_info_spec.rb
new file mode 100644
index 00000000000..17ddcaefeeb
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/order_info_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::OrderInfo do
+ describe '#build_order_list' do
+ let(:order_list) { described_class.build_order_list(relation) }
+
+ context 'when multiple orders with SQL is specified' do
+ let(:relation) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) }
+
+ it 'ignores the SQL order' do
+ expect(order_list.count).to eq 2
+ expect(order_list.first.attribute_name).to eq 'updated_at'
+ expect(order_list.first.operator_for(:after)).to eq '>'
+ expect(order_list.last.attribute_name).to eq 'id'
+ expect(order_list.last.operator_for(:after)).to eq '>'
+ end
+ end
+
+ context 'when order contains NULLS LAST' do
+ let(:relation) { Project.order(Arel.sql('projects.updated_at Asc Nulls Last')).order(:id) }
+
+ it 'does not ignore the SQL order' do
+ expect(order_list.count).to eq 2
+ expect(order_list.first.attribute_name).to eq 'projects.updated_at'
+ expect(order_list.first.operator_for(:after)).to eq '>'
+ expect(order_list.last.attribute_name).to eq 'id'
+ expect(order_list.last.operator_for(:after)).to eq '>'
+ end
+ end
+
+ context 'when order contains invalid formatted NULLS LAST ' do
+ let(:relation) { Project.order(Arel.sql('projects.updated_at created_at Asc Nulls Last')).order(:id) }
+
+ it 'ignores the SQL order' do
+ expect(order_list.count).to eq 1
+ end
+ end
+ end
+
+ describe '#validate_ordering' do
+ let(:order_list) { described_class.build_order_list(relation) }
+
+ context 'when number of ordering fields is 0' do
+ let(:relation) { Project.all }
+
+ it 'raises an error' do
+ expect { described_class.validate_ordering(relation, order_list) }
+ .to raise_error(ArgumentError, 'A minimum of 1 ordering field is required')
+ end
+ end
+
+ context 'when number of ordering fields is over 2' do
+ let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc).order(:id) }
+
+ it 'raises an error' do
+ expect { described_class.validate_ordering(relation, order_list) }
+ .to raise_error(ArgumentError, 'A maximum of 2 ordering fields are allowed')
+ end
+ end
+
+ context 'when the second (or first) column is nullable' do
+ let(:relation) { Project.order(last_repository_check_at: :desc).order(updated_at: :desc) }
+
+ it 'raises an error' do
+ expect { described_class.validate_ordering(relation, order_list) }
+ .to raise_error(ArgumentError, "Column `updated_at` must not allow NULL")
+ end
+ end
+
+ context 'for last ordering field' do
+ let(:relation) { Project.order(namespace_id: :desc) }
+
+ it 'raises error if primary key is not last field' do
+ expect { described_class.validate_ordering(relation, order_list) }
+ .to raise_error(ArgumentError, "Last ordering field must be the primary key, `#{relation.primary_key}`")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset/query_builder_spec.rb b/spec/lib/gitlab/graphql/connections/keyset/query_builder_spec.rb
new file mode 100644
index 00000000000..59e153d9e07
--- /dev/null
+++ b/spec/lib/gitlab/graphql/connections/keyset/query_builder_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Connections::Keyset::QueryBuilder do
+ context 'when number of ordering fields is 0' do
+ it 'raises an error' do
+ expect { described_class.new(Issue.arel_table, [], {}, :after) }
+ .to raise_error(ArgumentError, 'No ordering scopes have been supplied')
+ end
+ end
+
+ describe '#conditions' do
+ let(:relation) { Issue.order(relative_position: :desc).order(:id) }
+ let(:order_list) { Gitlab::Graphql::Connections::Keyset::OrderInfo.build_order_list(relation) }
+ let(:builder) { described_class.new(arel_table, order_list, decoded_cursor, before_or_after) }
+ let(:before_or_after) { :after }
+
+ context 'when only a single ordering' do
+ let(:relation) { Issue.order(id: :desc) }
+
+ context 'when the value is nil' do
+ let(:decoded_cursor) { { 'id' => nil } }
+
+ it 'raises an error' do
+ expect { builder.conditions }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Before/after cursor invalid: `nil` was provided as only sortable value')
+ end
+ end
+
+ context 'when value is not nil' do
+ let(:decoded_cursor) { { 'id' => 100 } }
+ let(:conditions) { builder.conditions }
+
+ context 'when :after' do
+ it 'generates the correct condition' do
+ expect(conditions.strip).to eq '("issues"."id" < 100)'
+ end
+ end
+
+ context 'when :before' do
+ let(:before_or_after) { :before }
+
+ it 'generates the correct condition' do
+ expect(conditions.strip).to eq '("issues"."id" > 100)'
+ end
+ end
+ end
+ end
+
+ context 'when two orderings' do
+ let(:decoded_cursor) { { 'relative_position' => 1500, 'id' => 100 } }
+
+ context 'when no values are nil' do
+ context 'when :after' do
+ it 'generates the correct condition' do
+ conditions = builder.conditions
+
+ expect(conditions).to include '"issues"."relative_position" < 1500'
+ expect(conditions).to include '"issues"."id" > 100'
+ expect(conditions).to include 'OR ("issues"."relative_position" IS NULL)'
+ end
+ end
+
+ context 'when :before' do
+ let(:before_or_after) { :before }
+
+ it 'generates the correct condition' do
+ conditions = builder.conditions
+
+ expect(conditions).to include '("issues"."relative_position" > 1500)'
+ expect(conditions).to include '"issues"."id" < 100'
+ expect(conditions).to include '"issues"."relative_position" = 1500'
+ end
+ end
+ end
+
+ context 'when first value is nil' do
+ let(:decoded_cursor) { { 'relative_position' => nil, 'id' => 100 } }
+
+ context 'when :after' do
+ it 'generates the correct condition' do
+ conditions = builder.conditions
+
+ expect(conditions).to include '"issues"."relative_position" IS NULL'
+ expect(conditions).to include '"issues"."id" > 100'
+ end
+ end
+
+ context 'when :before' do
+ let(:before_or_after) { :before }
+
+ it 'generates the correct condition' do
+ conditions = builder.conditions
+
+ expect(conditions).to include '"issues"."relative_position" IS NULL'
+ expect(conditions).to include '"issues"."id" < 100'
+ expect(conditions).to include 'OR ("issues"."relative_position" IS NOT NULL)'
+ end
+ end
+ end
+ end
+ end
+
+ def arel_table
+ Issue.arel_table
+ end
+end
diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
deleted file mode 100644
index 4eb121794e1..00000000000
--- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Graphql::Connections::KeysetConnection do
- let(:nodes) { Project.all.order(id: :asc) }
- let(:arguments) { {} }
- subject(:connection) do
- described_class.new(nodes, arguments, max_page_size: 3)
- end
-
- def encoded_property(value)
- Base64Bp.urlsafe_encode64(value.to_s, padding: false)
- end
-
- describe '#cursor_from_nodes' do
- let(:project) { create(:project) }
-
- it 'returns an encoded ID' do
- expect(connection.cursor_from_node(project))
- .to eq(encoded_property(project.id))
- end
-
- context 'when an order was specified' do
- let(:nodes) { Project.order(:updated_at) }
-
- it 'returns the encoded value of the order' do
- expect(connection.cursor_from_node(project))
- .to eq(encoded_property(project.updated_at))
- end
- end
- end
-
- describe '#sliced_nodes' do
- let(:projects) { create_list(:project, 4) }
-
- context 'when before is passed' do
- let(:arguments) { { before: encoded_property(projects[1].id) } }
-
- it 'only returns the project before the selected one' do
- expect(subject.sliced_nodes).to contain_exactly(projects.first)
- end
-
- context 'when the sort order is descending' do
- let(:nodes) { Project.all.order(id: :desc) }
-
- it 'returns the correct nodes' do
- expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
- end
- end
- end
-
- context 'when after is passed' do
- let(:arguments) { { after: encoded_property(projects[1].id) } }
-
- it 'only returns the project before the selected one' do
- expect(subject.sliced_nodes).to contain_exactly(*projects[2..-1])
- end
-
- context 'when the sort order is descending' do
- let(:nodes) { Project.all.order(id: :desc) }
-
- it 'returns the correct nodes' do
- expect(subject.sliced_nodes).to contain_exactly(projects.first)
- end
- end
- end
-
- context 'when both before and after are passed' do
- let(:arguments) do
- {
- after: encoded_property(projects[1].id),
- before: encoded_property(projects[3].id)
- }
- end
-
- it 'returns the expected set' do
- expect(subject.sliced_nodes).to contain_exactly(projects[2])
- end
- end
- end
-
- describe '#paged_nodes' do
- let!(:projects) { create_list(:project, 5) }
-
- it 'returns the collection limited to max page size' do
- expect(subject.paged_nodes.size).to eq(3)
- end
-
- it 'is a loaded memoized array' do
- expect(subject.paged_nodes).to be_an(Array)
- expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
- end
-
- context 'when `first` is passed' do
- let(:arguments) { { first: 2 } }
-
- it 'returns only the first elements' do
- expect(subject.paged_nodes).to contain_exactly(projects.first, projects.second)
- end
- end
-
- context 'when `last` is passed' do
- let(:arguments) { { last: 2 } }
-
- it 'returns only the last elements' do
- expect(subject.paged_nodes).to contain_exactly(projects[3], projects[4])
- end
- end
-
- context 'when both are passed' do
- let(:arguments) { { first: 2, last: 2 } }
-
- it 'raises an error' do
- expect { subject.paged_nodes }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb
deleted file mode 100644
index 136027736c3..00000000000
--- a/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Graphql::Loaders::PipelineForShaLoader do
- include GraphqlHelpers
-
- describe '#find_last' do
- it 'batch-resolves latest pipeline' do
- project = create(:project, :repository)
- pipeline1 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
- pipeline2 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
- pipeline3 = create(:ci_pipeline, project: project, ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
-
- result = batch_sync(max_queries: 1) do
- [pipeline1.sha, pipeline3.sha].map { |sha| described_class.new(project, sha).find_last }
- end
-
- expect(result).to contain_exactly(pipeline2, pipeline3)
- end
- end
-end
diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb
index 53a91a35ec9..570b0cb7401 100644
--- a/spec/lib/gitlab/group_search_results_spec.rb
+++ b/spec/lib/gitlab/group_search_results_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GroupSearchResults do
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 8e253b51597..ce7f2c4530d 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do
subject.bulk_migrate(start: ids.min, finish: ids.max)
end
- it 'has all projects migrated and set as writable' do
+ it 'has all projects migrated and set as writable', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
subject.bulk_migrate(start: ids.min, finish: ids.max)
end
@@ -79,7 +79,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do
subject.bulk_rollback(start: ids.min, finish: ids.max)
end
- it 'has all projects rolledback and set as writable' do
+ it 'has all projects rolledback and set as writable', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
subject.bulk_rollback(start: ids.min, finish: ids.max)
end
@@ -108,7 +108,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do
expect { subject.migrate(project) }.not_to raise_error
end
- it 'migrates project storage' do
+ it 'migrates project storage', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
subject.migrate(project)
end
@@ -154,7 +154,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do
expect { subject.rollback(project) }.not_to raise_error
end
- it 'rolls-back project storage' do
+ it 'rolls-back project storage', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
subject.rollback(project)
end
diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb
new file mode 100644
index 00000000000..91441a7ddc3
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/master_check_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+require_relative './simple_check_shared'
+
+describe Gitlab::HealthChecks::MasterCheck do
+ let(:result_class) { Gitlab::HealthChecks::Result }
+
+ SUCCESS_CODE = 100
+ FAILURE_CODE = 101
+
+ before do
+ described_class.register_master
+ end
+
+ after do
+ described_class.finish_master
+ end
+
+ describe '#readiness' do
+ context 'when master is running' do
+ it 'worker does return success' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(SUCCESS_CODE)
+ end
+ end
+
+ context 'when master finishes early' do
+ before do
+ described_class.send(:close_write)
+ end
+
+ it 'worker does return failure' do
+ _, child_status = run_worker
+
+ expect(child_status.exitstatus).to eq(FAILURE_CODE)
+ end
+ end
+
+ def run_worker
+ pid = fork do
+ described_class.register_worker
+
+ exit(described_class.readiness.success ? SUCCESS_CODE : FAILURE_CODE)
+ end
+
+ Process.wait2(pid)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 4676db6b8d8..5a45d724b83 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Highlight do
diff --git a/spec/lib/gitlab/http_io_spec.rb b/spec/lib/gitlab/http_io_spec.rb
index 788bddb8f59..f30528916dc 100644
--- a/spec/lib/gitlab/http_io_spec.rb
+++ b/spec/lib/gitlab/http_io_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::HttpIO do
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index d3f9be845dd..192816ad057 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::HTTP do
diff --git a/spec/lib/gitlab/i18n_spec.rb b/spec/lib/gitlab/i18n_spec.rb
index 785035d993f..2664423af88 100644
--- a/spec/lib/gitlab/i18n_spec.rb
+++ b/spec/lib/gitlab/i18n_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::I18n do
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index 1e583f4cee2..9c7972d4bde 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Identifier do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 4fd61383c6b..8f627fcc24d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -29,6 +29,9 @@ issues:
- prometheus_alerts
- prometheus_alert_events
- self_managed_prometheus_alert_events
+- zoom_meetings
+- vulnerability_links
+- related_vulnerabilities
events:
- author
- project
@@ -119,6 +122,7 @@ merge_requests:
- pipelines_for_merge_request
- merge_request_assignees
- suggestions
+- unresolved_notes
- assignees
- reviews
- approval_rules
@@ -338,6 +342,7 @@ project:
- triggers
- pipeline_schedules
- environments
+- environments_for_dashboard
- deployments
- project_feature
- auto_devops
@@ -421,6 +426,12 @@ project:
- pages_metadatum
- alerts_service
- grafana_integration
+- remove_source_branch_after_merge
+- deleting_user
+- upstream_projects
+- downstream_projects
+- upstream_project_subscriptions
+- downstream_project_subscriptions
award_emoji:
- awardable
- user
@@ -528,4 +539,6 @@ versions: &version
- issue
- designs
- actions
+zoom_meetings:
+- issue
design_versions: *version
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 934e676d020..b190a1007a0 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -132,10 +132,6 @@ describe Gitlab::ImportExport::FastHashSerializer do
end
it 'has no when YML attributes but only the DB column' do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:ci_yaml_file)
- .and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
-
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
subject
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
index 71fd5a51c3b..5752fd8fa0d 100644
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -47,7 +47,7 @@ describe 'forked project import' do
end
end
- it 'can access the MR' do
+ it 'can access the MR', :sidekiq_might_not_need_inline do
project.merge_requests.first.fetch_ref!
expect(project.repository.ref_exists?('refs/merge-requests/1/head')).to be_truthy
diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
index 6a803c48b34..1a5cb7806a3 100644
--- a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
+++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::GroupProjectObjectBuilder do
let(:project) do
- create(:project,
+ create(:project, :repository,
:builds_disabled,
:issues_disabled,
name: 'project',
@@ -11,8 +11,8 @@ describe Gitlab::ImportExport::GroupProjectObjectBuilder do
end
context 'labels' do
- it 'finds the right group label' do
- group_label = create(:group_label, 'name': 'group label', 'group': project.group)
+ it 'finds the existing group label' do
+ group_label = create(:group_label, name: 'group label', group: project.group)
expect(described_class.build(Label,
'title' => 'group label',
@@ -31,8 +31,8 @@ describe Gitlab::ImportExport::GroupProjectObjectBuilder do
end
context 'milestones' do
- it 'finds the right group milestone' do
- milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group)
+ it 'finds the existing group milestone' do
+ milestone = create(:milestone, name: 'group milestone', group: project.group)
expect(described_class.build(Milestone,
'title' => 'group milestone',
@@ -49,4 +49,30 @@ describe Gitlab::ImportExport::GroupProjectObjectBuilder do
expect(milestone.persisted?).to be true
end
end
+
+ context 'merge_request' do
+ it 'finds the existing merge_request' do
+ merge_request = create(:merge_request, title: 'MergeRequest', iid: 7, target_project: project, source_project: project)
+ expect(described_class.build(MergeRequest,
+ 'title' => 'MergeRequest',
+ 'source_project_id' => project.id,
+ 'target_project_id' => project.id,
+ 'source_branch' => 'SourceBranch',
+ 'iid' => 7,
+ 'target_branch' => 'TargetBranch',
+ 'author_id' => project.creator.id)).to eq(merge_request)
+ end
+
+ it 'creates a new merge_request' do
+ merge_request = described_class.build(MergeRequest,
+ 'title' => 'MergeRequest',
+ 'iid' => 8,
+ 'source_project_id' => project.id,
+ 'target_project_id' => project.id,
+ 'source_branch' => 'SourceBranch',
+ 'target_branch' => 'TargetBranch',
+ 'author_id' => project.creator.id)
+ expect(merge_request.persisted?).to be true
+ end
+ 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..b856441981a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group_tree_saver_spec.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::GroupTreeSaver do
+ describe 'saves the group tree into a json object' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) }
+ let(:export_path) { "#{Dir.tmpdir}/group_tree_saver_spec" }
+ let(:user) { create(:user) }
+ let!(:group) { setup_group }
+
+ before do
+ group.add_maintainer(user)
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves group successfully' do
+ expect(group_tree_saver.save).to be true
+ end
+
+ context ':export_fast_serialize feature flag checks' do
+ before do
+ expect(Gitlab::ImportExport::Reader).to receive(:new).with(shared: shared, config: group_config).and_return(reader)
+ expect(reader).to receive(:group_tree).and_return(group_tree)
+ end
+
+ let(:reader) { instance_double('Gitlab::ImportExport::Reader') }
+ let(:group_config) { Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.group_config_file).to_h }
+ let(:group_tree) do
+ {
+ include: [{ milestones: { include: [] } }],
+ preload: { milestones: nil }
+ }
+ end
+
+ context 'when :export_fast_serialize feature is enabled' do
+ let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
+
+ before do
+ stub_feature_flags(export_fast_serialize: true)
+
+ expect(Gitlab::ImportExport::FastHashSerializer).to receive(:new).with(group, group_tree).and_return(serializer)
+ end
+
+ it 'uses FastHashSerializer' do
+ expect(serializer).to receive(:execute)
+
+ group_tree_saver.save
+ end
+ end
+
+ context 'when :export_fast_serialize feature is disabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: false)
+ end
+
+ it 'is serialized via built-in `as_json`' do
+ expect(group).to receive(:as_json).with(group_tree).and_call_original
+
+ group_tree_saver.save
+ end
+ end
+ end
+
+ # It is mostly duplicated in
+ # `spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb`
+ # except:
+ # context 'with description override' do
+ # context 'group members' do
+ # ^ These are specific for the groupTreeSaver
+ context 'JSON' do
+ let(:saved_group_json) do
+ group_tree_saver.save
+ group_json(group_tree_saver.full_path)
+ end
+
+ it 'saves the correct json' do
+ expect(saved_group_json).to include({ 'description' => 'description', 'visibility_level' => 20 })
+ end
+
+ it 'has milestones' do
+ expect(saved_group_json['milestones']).not_to be_empty
+ end
+
+ it 'has labels' do
+ expect(saved_group_json['labels']).not_to be_empty
+ end
+
+ it 'has boards' do
+ expect(saved_group_json['boards']).not_to be_empty
+ end
+
+ it 'has group members' do
+ expect(saved_group_json['members']).not_to be_empty
+ end
+
+ it 'has priorities associated to labels' do
+ expect(saved_group_json['labels'].first['priorities']).not_to be_empty
+ end
+
+ it 'has badges' do
+ expect(saved_group_json['badges']).not_to be_empty
+ end
+
+ context 'group children' do
+ let(:children) { group.children }
+
+ it 'exports group children' do
+ expect(saved_group_json['children'].length).to eq(children.count)
+ end
+
+ it 'exports group children of children' do
+ expect(saved_group_json['children'].first['children'].length).to eq(children.first.children.count)
+ end
+ end
+
+ context 'group members' do
+ let(:user2) { create(:user, email: 'group@member.com') }
+ let(:member_emails) do
+ saved_group_json['members'].map do |pm|
+ pm['user']['email']
+ end
+ end
+
+ before do
+ group.add_developer(user2)
+ end
+
+ it 'exports group members as group owner' do
+ group.add_owner(user)
+
+ expect(member_emails).to include('group@member.com')
+ end
+
+ context 'as admin' do
+ let(:user) { create(:admin) }
+
+ it 'exports group members as admin' do
+ expect(member_emails).to include('group@member.com')
+ end
+
+ it 'exports group members' do
+ member_types = saved_group_json['members'].map { |pm| pm['source_type'] }
+
+ expect(member_types).to all(eq('Namespace'))
+ end
+ end
+ end
+
+ context 'group attributes' do
+ it 'does not contain the runners token' do
+ expect(saved_group_json).not_to include("runners_token" => 'token')
+ end
+ end
+ end
+ end
+
+ def setup_group
+ group = create(:group, description: 'description')
+ sub_group = create(:group, description: 'description', parent: group)
+ create(:group, description: 'description', parent: sub_group)
+ create(:milestone, group: group)
+ create(:group_badge, group: group)
+ group_label = create(:group_label, group: group)
+ create(:label_priority, label: group_label, priority: 1)
+ create(:board, group: group)
+ create(:group_badge, group: group)
+
+ group
+ end
+
+ def group_json(filename)
+ JSON.parse(IO.read(filename))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
index 40a5f2294a2..a6b0dc758cd 100644
--- a/spec/lib/gitlab/import_export/import_export_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -6,17 +6,17 @@ describe Gitlab::ImportExport do
let(:project) { create(:project, :public, path: 'project-path', namespace: group) }
it 'contains the project path' do
- expect(described_class.export_filename(project: project)).to include(project.path)
+ expect(described_class.export_filename(exportable: project)).to include(project.path)
end
it 'contains the namespace path' do
- expect(described_class.export_filename(project: project)).to include(project.namespace.full_path.tr('/', '_'))
+ expect(described_class.export_filename(exportable: project)).to include(project.namespace.full_path.tr('/', '_'))
end
it 'does not go over a certain length' do
project.path = 'a' * 100
- expect(described_class.export_filename(project: project).length).to be < 70
+ expect(described_class.export_filename(exportable: project).length).to be < 70
end
end
end
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 ebd2c6089ce..459b1eed1a7 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do
+ include ImportExport::CommonUtil
+
let(:shared) { project.import_export_shared }
describe 'restore project tree' do
@@ -16,7 +18,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
RSpec::Mocks.with_temporary_scope do
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
- allow(@shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/')
+
+ setup_import_export_config('complex')
allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
@@ -207,10 +210,27 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(@project.project_badges.count).to eq(2)
end
+ it 'has snippets' do
+ expect(@project.snippets.count).to eq(1)
+ end
+
+ it 'has award emoji for a snippet' do
+ award_emoji = @project.snippets.first.award_emoji
+
+ expect(award_emoji.map(&:name)).to contain_exactly('thumbsup', 'coffee')
+ end
+
it 'restores the correct service' do
expect(CustomIssueTrackerService.first).not_to be_nil
end
+ it 'restores zoom meetings' do
+ meetings = @project.issues.first.zoom_meetings
+
+ expect(meetings.count).to eq(1)
+ expect(meetings.first.url).to eq('https://zoom.us/j/123456789')
+ end
+
context 'Merge requests' do
it 'always has the new project as a target' do
expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
@@ -250,9 +270,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
it 'has the correct number of pipelines and statuses' do
- expect(@project.ci_pipelines.size).to eq(5)
+ expect(@project.ci_pipelines.size).to eq(6)
- @project.ci_pipelines.zip([2, 2, 2, 2, 2])
+ @project.ci_pipelines.order(:id).zip([2, 2, 2, 2, 2, 0])
.each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size)
end
@@ -261,7 +281,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'when restoring hierarchy of pipeline, stages and jobs' do
it 'restores pipelines' do
- expect(Ci::Pipeline.all.count).to be 5
+ expect(Ci::Pipeline.all.count).to be 6
end
it 'restores pipeline stages' do
@@ -307,21 +327,33 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
end
- context 'Light JSON' do
+ context 'project.json file access check' do
let(:user) { create(:user) }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
- before do
- allow(shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/')
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ setup_symlink(tmpdir, 'project.json')
+ allow(shared).to receive(:export_path).and_call_original
+
+ expect(project_tree_restorer.restore).to eq(false)
+ expect(shared.errors).to include('Incorrect JSON format')
+ end
end
+ end
+
+ context 'Light JSON' do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+ let(:restored_project_json) { project_tree_restorer.restore }
context 'with a simple project' do
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json")
-
- restored_project_json
+ setup_import_export_config('light')
+ expect(restored_project_json).to eq(true)
end
it_behaves_like 'restores project correctly',
@@ -332,19 +364,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
first_issue_labels: 1,
services: 1
- context 'project.json file access check' do
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
-
- restored_project_json
-
- expect(shared.errors).to be_empty
- end
- end
- end
-
context 'when there is an existing build with build token' do
before do
create(:ci_build, token: 'abcd')
@@ -360,6 +379,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'when the project has overridden params in import data' do
+ before do
+ setup_import_export_config('light')
+ end
+
it 'handles string versions of visibility_level' do
# Project needs to be in a group for visibility level comparison
# to happen
@@ -368,24 +391,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } })
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
it 'overwrites the params stored in the JSON' do
project.create_import_data(data: { override_params: { description: "Overridden" } })
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.description).to eq("Overridden")
end
it 'does not allow setting params that are excluded from import_export settings' do
project.create_import_data(data: { override_params: { lfs_enabled: true } })
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.lfs_enabled).to be_falsey
end
@@ -401,7 +421,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project.create_import_data(data: { override_params: disabled_access_levels })
- restored_project_json
+ expect(restored_project_json).to eq(true)
aggregate_failures do
access_level_keys.each do |key|
@@ -422,9 +442,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.group.json")
-
- restored_project_json
+ setup_import_export_config('group')
+ expect(restored_project_json).to eq(true)
end
it_behaves_like 'restores project correctly',
@@ -456,11 +475,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
- project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json")
+ setup_import_export_config('light')
end
it 'does not import any templated services' do
- restored_project_json
+ expect(restored_project_json).to eq(true)
expect(project.services.where(template: true).count).to eq(0)
end
@@ -470,8 +489,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.labels.count).to eq(1)
end
@@ -480,8 +498,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.group.milestones.count).to eq(1)
expect(project.milestones.count).to eq(0)
end
@@ -497,13 +514,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
group: create(:group))
end
- it 'preserves the project milestone IID' do
- project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json")
+ before do
+ setup_import_export_config('milestone-iid')
+ end
+ it 'preserves the project milestone IID' do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.milestones.count).to eq(2)
expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
@@ -511,19 +529,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'with external authorization classification labels' do
+ before do
+ setup_import_export_config('light')
+ end
+
it 'converts empty external classification authorization labels to nil' do
project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } })
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.external_authorization_classification_label).to be_nil
end
it 'preserves valid external classification authorization labels' do
project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } })
- restored_project_json
-
+ expect(restored_project_json).to eq(true)
expect(project.external_authorization_classification_label).to eq("foobar")
end
end
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 ff46e062a5d..97d8b155826 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -203,7 +203,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end
it 'has no when YML attributes but only the DB column' do
- allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
saved_project_json
diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
index 472bf55d37e..d62f5725f9e 100644
--- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::ImportExport::RelationRenameService do
+ include ImportExport::CommonUtil
+
let(:renames) do
{
'example_relation1' => 'new_example_relation1',
@@ -21,12 +23,12 @@ describe Gitlab::ImportExport::RelationRenameService do
context 'when importing' do
let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) }
- let(:import_path) { 'spec/fixtures/lib/gitlab/import_export' }
- let(:file_content) { IO.read("#{import_path}/project.json") }
- let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
+ let(:file_content) { IO.read(File.join(shared.export_path, 'project.json')) }
+ let(:json_file) { ActiveSupport::JSON.decode(file_content) }
before do
- allow(shared).to receive(:export_path).and_return(import_path)
+ setup_import_export_config('complex')
+
allow(ActiveSupport::JSON).to receive(:decode).and_call_original
allow(ActiveSupport::JSON).to receive(:decode).with(file_content).and_return(json_file)
end
@@ -94,15 +96,20 @@ describe Gitlab::ImportExport::RelationRenameService do
let(:export_content_path) { project_tree_saver.full_path }
let(:export_content_hash) { ActiveSupport::JSON.decode(File.read(export_content_path)) }
let(:injected_hash) { renames.values.product([{}]).to_h }
+ let(:relation_tree_saver) { Gitlab::ImportExport::RelationTreeSaver.new }
let(:project_tree_saver) do
Gitlab::ImportExport::ProjectTreeSaver.new(
project: project, current_user: user, shared: shared)
end
+ before do
+ allow(project_tree_saver).to receive(:tree_saver).and_return(relation_tree_saver)
+ end
+
it 'adds old relationships to the exported file' do
# we inject relations with new names that should be rewritten
- expect(project_tree_saver).to receive(:serialize_project_tree).and_wrap_original do |method, *args|
+ expect(relation_tree_saver).to receive(:serialize).and_wrap_original do |method, *args|
method.call(*args).merge(injected_hash)
end
diff --git a/spec/lib/gitlab/import_export/relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/relation_tree_saver_spec.rb
new file mode 100644
index 00000000000..2fc26c0e3d4
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_tree_saver_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationTreeSaver do
+ let(:exportable) { create(:group) }
+ let(:relation_tree_saver) { described_class.new }
+ let(:tree) { {} }
+
+ describe '#serialize' do
+ context 'when :export_fast_serialize feature is enabled' do
+ let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
+
+ before do
+ stub_feature_flags(export_fast_serialize: true)
+ end
+
+ it 'uses FastHashSerializer' do
+ expect(Gitlab::ImportExport::FastHashSerializer)
+ .to receive(:new)
+ .with(exportable, tree)
+ .and_return(serializer)
+
+ expect(serializer).to receive(:execute)
+
+ relation_tree_saver.serialize(exportable, tree)
+ end
+ end
+
+ context 'when :export_fast_serialize feature is disabled' do
+ before do
+ stub_feature_flags(export_fast_serialize: false)
+ end
+
+ it 'is serialized via built-in `as_json`' do
+ expect(exportable).to receive(:as_json).with(tree)
+
+ relation_tree_saver.serialize(exportable, tree)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 8ae571a69ef..04fe985cdb5 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -185,6 +185,7 @@ MergeRequest:
- merge_when_pipeline_succeeds
- merge_user_id
- merge_commit_sha
+- squash_commit_sha
- in_progress_merge_commit_sha
- lock_version
- milestone_id
@@ -512,6 +513,7 @@ Project:
- request_access_enabled
- has_external_wiki
- only_allow_merge_if_all_discussions_are_resolved
+- remove_source_branch_after_merge
- auto_cancel_pending_pipelines
- printing_merge_request_link_enabled
- resolve_outdated_diff_discussions
@@ -537,7 +539,6 @@ Project:
- external_webhook_token
- pages_https_only
- merge_requests_disable_committers_approval
-- merge_requests_require_code_owner_approval
- require_password_to_approve
ProjectTracingSetting:
- external_url
@@ -752,4 +753,12 @@ DesignManagement::Version:
- created_at
- sha
- issue_id
-- user_id
+- author_id
+ZoomMeeting:
+- id
+- issue_id
+- project_id
+- issue_status
+- url
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
index d185ff2dfcc..aca63953677 100644
--- a/spec/lib/gitlab/import_export/saver_spec.rb
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::Saver do
let!(:project) { create(:project, :public, name: 'project') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { project.import_export_shared }
- subject { described_class.new(project: project, shared: shared) }
+ subject { described_class.new(exportable: project, shared: shared) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
index 62669836973..fc011f7e1be 100644
--- a/spec/lib/gitlab/import_export/shared_spec.rb
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::ImportExport::Shared do
context 'with a repository on disk' do
let(:project) { create(:project, :repository) }
- let(:base_path) { %(/tmp/project_exports/#{project.disk_path}/) }
+ let(:base_path) { %(/tmp/gitlab_exports/#{project.disk_path}/) }
describe '#archive_path' do
it 'uses a random hash to avoid conflicts' do
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 8060b5d4448..265241dc2af 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ImportSources do
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 2db62ab983a..598336d0b31 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Gitlab::IncomingEmail do
diff --git a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
index 6532579b1c9..7f20ae98b06 100644
--- a/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
+++ b/spec/lib/gitlab/insecure_key_fingerprint_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::InsecureKeyFingerprint do
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
new file mode 100644
index 00000000000..c2674638743
--- /dev/null
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::InstrumentationHelper do
+ using RSpec::Parameterized::TableSyntax
+
+ 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.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.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.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
+ 0 | nil | "2019-10-23T12:13:16.000+0200" | nil
+ -1 | nil | "2019-10-23T12:13:16.000+0200" | nil
+ "2019-06-01T02:00:00.000+0000" | nil | "2019-06-01T00:00:00.000+0000" | 0
+ Time.at(1571999233) | nil | "2019-10-25T12:29:16.000+0200" | 123
+ end
+
+ with_them do
+ let(:job) { { 'enqueued_at' => enqueued_at, 'created_at' => created_at } }
+
+ it "returns the correct duration" do
+ Timecop.freeze(Time.iso8601(time_now)) do
+ expect(described_class.queue_duration_for_job(job)).to eq(expected_duration)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 032467b8b4e..7632bc3060a 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::IssuableMetadata do
diff --git a/spec/lib/gitlab/issuable_sorter_spec.rb b/spec/lib/gitlab/issuable_sorter_spec.rb
index 5bd76bc6081..486e9539b92 100644
--- a/spec/lib/gitlab/issuable_sorter_spec.rb
+++ b/spec/lib/gitlab/issuable_sorter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::IssuableSorter do
diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb
index c262fdfcb61..9380aa53470 100644
--- a/spec/lib/gitlab/issuables_count_for_state_spec.rb
+++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::IssuablesCountForState do
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index b0b4fdc09bc..efa7fd4b975 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::JobWaiter do
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index 3d4f9b5db86..5d544198c40 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::JsonLogger do
diff --git a/spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb b/spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb
new file mode 100644
index 00000000000..f701643860a
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/config_maps/aws_node_auth_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth do
+ describe '#generate' do
+ let(:role) { 'arn:aws:iam::123456789012:role/node-instance-role' }
+
+ let(:name) { 'aws-auth' }
+ let(:namespace) { 'kube-system' }
+ let(:role_config) do
+ [{
+ 'rolearn' => role,
+ 'username' => 'system:node:{{EC2PrivateDNSName}}',
+ 'groups' => [
+ 'system:bootstrappers',
+ 'system:nodes'
+ ]
+ }]
+ end
+
+ subject { described_class.new(role).generate }
+
+ it 'builds a Kubeclient Resource' do
+ expect(subject).to be_a(Kubeclient::Resource)
+
+ expect(subject.metadata.name).to eq(name)
+ expect(subject.metadata.namespace).to eq(namespace)
+
+ expect(YAML.safe_load(subject.data.mapRoles)).to eq(role_config)
+ end
+ 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 9eb3322f1a6..e5a361bdab3 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -86,33 +86,6 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
end
end
- context 'when there is no repository' do
- let(:repository) { nil }
-
- it_behaves_like 'helm commands' do
- let(:commands) do
- <<~EOS
- helm init --upgrade
- for i in $(seq 1 30); do helm version #{tls_flags} && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)
- #{helm_install_command}
- EOS
- end
-
- let(:helm_install_command) do
- <<~EOS.squish
- helm upgrade app-name chart-name
- --install
- --reset-values
- #{tls_flags}
- --version 1.2.3
- --set rbac.create\\=false,rbac.enabled\\=false
- --namespace gitlab-managed-apps
- -f /data/helm/app-name/config/values.yaml
- EOS
- end
- end
- end
-
context 'when there is a pre-install script' do
let(:preinstall) { ['/bin/date', '/bin/true'] }
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 64cadcc011c..e1b4bd0b664 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,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.14.3-kube-1.11.10')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.16.1-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_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index a7ea942960b..31bfd20449d 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Kubernetes do
diff --git a/spec/lib/gitlab/language_detection_spec.rb b/spec/lib/gitlab/language_detection_spec.rb
index 9636fbd401b..f558ce0d527 100644
--- a/spec/lib/gitlab/language_detection_spec.rb
+++ b/spec/lib/gitlab/language_detection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::LanguageDetection do
diff --git a/spec/lib/gitlab/lazy_spec.rb b/spec/lib/gitlab/lazy_spec.rb
index 37a3ac74316..19758a18589 100644
--- a/spec/lib/gitlab/lazy_spec.rb
+++ b/spec/lib/gitlab/lazy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::Lazy do
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
index af5df1fab43..697bedf7362 100644
--- a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -136,7 +136,7 @@ describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_cachi
describe '.find_all_paths' do
let(:all_dashboard_paths) { described_class.find_all_paths(project) }
- let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Default', default: true } }
+ let(:system_dashboard) { { path: system_dashboard_path, display_name: 'Default', default: true, system_dashboard: true } }
it 'includes only the system dashboard by default' do
expect(all_dashboard_paths).to eq([system_dashboard])
@@ -147,7 +147,7 @@ describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_cachi
let(:project) { project_with_dashboard(dashboard_path) }
it 'includes system and project dashboards' do
- project_dashboard = { path: dashboard_path, display_name: 'test.yml', default: false }
+ project_dashboard = { path: dashboard_path, display_name: 'test.yml', default: false, system_dashboard: false }
expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
index e2ce1869810..4fa136bc405 100644
--- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -25,6 +25,14 @@ describe Gitlab::Metrics::Dashboard::Processor do
end
end
+ context 'when the dashboard is not present' do
+ let(:dashboard_yml) { nil }
+
+ it 'returns nil' do
+ expect(dashboard).to be_nil
+ end
+ end
+
context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
diff --git a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
index 095d0a2df78..0d4562f78f1 100644
--- a/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
@@ -75,6 +75,17 @@ describe Gitlab::Metrics::Dashboard::ServiceSelector do
it { is_expected.to be Metrics::Dashboard::CustomMetricEmbedService }
end
+
+ context 'with a grafana link' do
+ let(:arguments) do
+ {
+ embedded: true,
+ grafana_url: 'https://grafana.example.com'
+ }
+ end
+
+ it { is_expected.to be Metrics::Dashboard::GrafanaMetricEmbedService }
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
new file mode 100644
index 00000000000..5c2ec6dae6b
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
+ include GrafanaApiHelpers
+
+ let_it_be(:namespace) { create(:namespace, name: 'foo') }
+ 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(:dashboard) { described_class.new(project, {}, params).transform! }
+
+ let(:params) do
+ {
+ grafana_dashboard: grafana_dashboard,
+ datasource: datasource,
+ grafana_url: valid_grafana_dashboard_link('https://grafana.example.com')
+ }
+ end
+
+ context 'when the query and resources are configured correctly' do
+ let(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
+
+ it 'generates a gitlab-yml formatted dashboard' do
+ expect(dashboard).to eq(expected_dashboard)
+ end
+ end
+
+ context 'when the inputs are invalid' do
+ shared_examples_for 'processing error' do
+ it 'raises a processing error' do
+ expect { dashboard }
+ .to raise_error(Gitlab::Metrics::Dashboard::Stages::InputFormatValidator::DashboardProcessingError)
+ end
+ end
+
+ context 'when the datasource is not proxyable' do
+ before do
+ params[:datasource][:access] = 'not-proxy'
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "panelId" is not specified' do
+ before do
+ params[:grafana_url].gsub!('panelId=8', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "from" is not specified' do
+ before do
+ params[:grafana_url].gsub!('from=1570397739557', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "to" is not specified' do
+ before do
+ params[:grafana_url].gsub!('to=1570484139557', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the panel is not a graph' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:type] = 'singlestat'
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the panel is not a line graph' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:lines] = false
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the query dashboard includes undefined variables' do
+ before do
+ params[:grafana_url].gsub!('&var-instance=localhost:9121', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the expression contains unsupported global variables' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])'
+ end
+
+ it_behaves_like 'processing error'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
index e0dc6d98efc..daaf66cba46 100644
--- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -3,13 +3,41 @@
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Url do
- describe '#regex' do
- it 'returns a regular expression' do
- expect(described_class.regex).to be_a Regexp
- end
+ shared_examples_for 'a regex which matches the expected url' do
+ it { is_expected.to be_a Regexp }
it 'matches a metrics dashboard link with named params' do
- url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
+ expect(subject).to match url
+
+ subject.match(url) do |m|
+ expect(m.named_captures).to eq expected_params
+ end
+ end
+ end
+
+ shared_examples_for 'does not match non-matching urls' do
+ it 'does not match other gitlab urls that contain the term metrics' do
+ url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
+
+ expect(subject).not_to match url
+ end
+
+ it 'does not match other gitlab urls' do
+ url = Gitlab.config.gitlab.url
+
+ expect(subject).not_to match url
+ end
+
+ it 'does not match non-gitlab urls' do
+ url = 'https://www.super_awesome_site.com/'
+
+ expect(subject).not_to match url
+ end
+ end
+
+ describe '#regex' do
+ let(:url) do
+ Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
'foo',
'bar',
1,
@@ -18,8 +46,10 @@ describe Gitlab::Metrics::Dashboard::Url do
group: 'awesome group',
anchor: 'title'
)
+ end
- expected_params = {
+ let(:expected_params) do
+ {
'url' => url,
'namespace' => 'foo',
'project' => 'bar',
@@ -27,31 +57,40 @@ describe Gitlab::Metrics::Dashboard::Url do
'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
'anchor' => '#title'
}
-
- expect(described_class.regex).to match url
-
- described_class.regex.match(url) do |m|
- expect(m.named_captures).to eq expected_params
- end
end
- it 'does not match other gitlab urls that contain the term metrics' do
- url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
+ subject { described_class.regex }
- expect(described_class.regex).not_to match url
- end
+ it_behaves_like 'a regex which matches the expected url'
+ it_behaves_like 'does not match non-matching urls'
+ end
- it 'does not match other gitlab urls' do
- url = Gitlab.config.gitlab.url
+ describe '#grafana_regex' do
+ let(:url) do
+ Gitlab::Routing.url_helpers.namespace_project_grafana_api_metrics_dashboard_url(
+ 'foo',
+ 'bar',
+ start: '2019-08-02T05:43:09.000Z',
+ dashboard: 'config/prometheus/common_metrics.yml',
+ group: 'awesome group',
+ anchor: 'title'
+ )
+ end
- expect(described_class.regex).not_to match url
+ let(:expected_params) do
+ {
+ 'url' => url,
+ 'namespace' => 'foo',
+ 'project' => 'bar',
+ 'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z',
+ 'anchor' => '#title'
+ }
end
- it 'does not match non-gitlab urls' do
- url = 'https://www.super_awesome_site.com/'
+ subject { described_class.grafana_regex }
- expect(described_class.regex).not_to match url
- end
+ it_behaves_like 'a regex which matches the expected url'
+ it_behaves_like 'does not match non-matching urls'
end
describe '#build_dashboard_url' do
diff --git a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
index 99349934e63..f22993cf057 100644
--- a/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/web_exporter_spec.rb
@@ -4,61 +4,41 @@ require 'spec_helper'
describe Gitlab::Metrics::Exporter::WebExporter do
let(:exporter) { described_class.new }
-
- context 'when blackout seconds is used' do
- let(:blackout_seconds) { 0 }
- let(:readiness_probe) { exporter.send(:readiness_probe).execute }
-
- before do
- stub_config(
- monitoring: {
- web_exporter: {
- enabled: true,
- port: 0,
- address: '127.0.0.1',
- blackout_seconds: blackout_seconds
- }
+ let(:readiness_probe) { exporter.send(:readiness_probe).execute }
+
+ before do
+ stub_config(
+ monitoring: {
+ web_exporter: {
+ enabled: true,
+ port: 0,
+ address: '127.0.0.1'
}
- )
-
- exporter.start
- end
-
- after do
- exporter.stop
- end
+ }
+ )
- context 'when running server' do
- it 'readiness probe returns succesful status' do
- expect(readiness_probe.http_status).to eq(200)
- expect(readiness_probe.json).to include(status: 'ok')
- expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
- end
- end
-
- context 'when blackout seconds is 10s' do
- let(:blackout_seconds) { 10 }
+ exporter.start
+ end
- it 'readiness probe returns a failure status' do
- # during sleep we check the status of readiness probe
- expect(exporter).to receive(:sleep).with(10) do
- expect(readiness_probe.http_status).to eq(503)
- expect(readiness_probe.json).to include(status: 'failed')
- expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'failed' }])
- end
+ after do
+ exporter.stop
+ end
- exporter.stop
- end
+ context 'when running server' do
+ it 'readiness probe returns succesful status' do
+ expect(readiness_probe.http_status).to eq(200)
+ expect(readiness_probe.json).to include(status: 'ok')
+ expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'ok' }])
end
+ end
- context 'when blackout is disabled' do
- let(:blackout_seconds) { 0 }
-
- it 'readiness probe returns a failure status' do
- expect(exporter).not_to receive(:sleep)
+ describe '#mark_as_not_running!' do
+ it 'readiness probe returns a failure status' do
+ exporter.mark_as_not_running!
- exporter.stop
- end
+ expect(readiness_probe.http_status).to eq(503)
+ expect(readiness_probe.json).to include(status: 'failed')
+ expect(readiness_probe.json).to include('web_exporter' => [{ 'status': 'failed' }])
end
end
end
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index f48cd096a98..335670278c4 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -31,7 +31,7 @@ describe Gitlab::Metrics::RequestsRackMiddleware do
end
it 'measures execution time' do
- expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: 200, method: 'get' }, a_positive_execution_time)
+ expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: '200', method: 'get' }, a_positive_execution_time)
Timecop.scale(3600) { subject.call(env) }
end
@@ -69,7 +69,7 @@ describe Gitlab::Metrics::RequestsRackMiddleware do
expected_labels = []
described_class::HTTP_METHODS.each do |method, statuses|
statuses.each do |status|
- expected_labels << { method: method, status: status.to_i }
+ expected_labels << { method: method, status: status.to_s }
end
end
diff --git a/spec/lib/gitlab/pagination/offset_pagination_spec.rb b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
new file mode 100644
index 00000000000..9c7dd385726
--- /dev/null
+++ b/spec/lib/gitlab/pagination/offset_pagination_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Pagination::OffsetPagination do
+ let(:resource) { Project.all }
+ let(:custom_port) { 8080 }
+ let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" }
+
+ before do
+ stub_config_setting(port: custom_port)
+ end
+
+ let(:request_context) { double("request_context") }
+
+ subject do
+ described_class.new(request_context)
+ end
+
+ describe '#paginate' do
+ let(:value) { spy('return value') }
+ let(:base_query) { { foo: 'bar', bar: 'baz' } }
+ let(:query) { base_query }
+
+ before do
+ allow(request_context).to receive(:header).and_return(value)
+ allow(request_context).to receive(:params).and_return(query)
+ allow(request_context).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}"))
+ end
+
+ context 'when resource can be paginated' do
+ before do
+ create_list(:project, 3)
+ end
+
+ describe 'first page' do
+ shared_examples 'response with pagination headers' do
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Prev-Page', '')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).not_to include('rel="prev"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ shared_examples 'paginated response' do
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 2
+ end
+
+ it 'executes only one SELECT COUNT query' do
+ expect { subject.paginate(resource) }.to make_queries_matching(/SELECT COUNT/, 1)
+ end
+ end
+
+ let(:query) { base_query.merge(page: 1, per_page: 2) }
+
+ context 'when the api_kaminari_count_with_limit feature flag is unset' do
+ it_behaves_like 'paginated response'
+ it_behaves_like 'response with pagination headers'
+ end
+
+ context 'when the api_kaminari_count_with_limit feature flag is disabled' do
+ before do
+ stub_feature_flags(api_kaminari_count_with_limit: false)
+ end
+
+ it_behaves_like 'paginated response'
+ it_behaves_like 'response with pagination headers'
+ end
+
+ context 'when the api_kaminari_count_with_limit feature flag is enabled' do
+ before do
+ stub_feature_flags(api_kaminari_count_with_limit: true)
+ end
+
+ context 'when resources count is less than MAX_COUNT_LIMIT' do
+ before do
+ stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 4)
+ end
+
+ it_behaves_like 'paginated response'
+ it_behaves_like 'response with pagination headers'
+ end
+
+ context 'when resources count is more than MAX_COUNT_LIMIT' do
+ before do
+ stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 2)
+ end
+
+ it_behaves_like 'paginated response'
+
+ it 'does not return the X-Total and X-Total-Pages headers' do
+ expect_no_header('X-Total')
+ expect_no_header('X-Total-Pages')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '2')
+ expect_header('X-Prev-Page', '')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).not_to include('rel="last"')
+ expect(val).not_to include('rel="prev"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+ end
+ end
+
+ describe 'second page' do
+ let(:query) { base_query.merge(page: 2, per_page: 2) }
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 1
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '3')
+ expect_header('X-Total-Pages', '2')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '2')
+ expect_header('X-Next-Page', '')
+ expect_header('X-Prev-Page', '1')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
+ expect(val).not_to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+
+ context 'if order' do
+ it 'is not present it adds default order(:id) if no order is present' do
+ resource.order_values = []
+
+ paginated_relation = subject.paginate(resource)
+
+ expect(resource.order_values).to be_empty
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.first).to be_ascending
+ expect(paginated_relation.order_values.first.expr.name).to eq 'id'
+ end
+
+ it 'is present it does not add anything' do
+ paginated_relation = subject.paginate(resource.order(created_at: :desc))
+
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq 'created_at'
+ end
+ end
+ end
+
+ context 'when resource empty' do
+ describe 'first page' do
+ let(:query) { base_query.merge(page: 1, per_page: 2) }
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 0
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '0')
+ expect_header('X-Total-Pages', '1')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '')
+ expect_header('X-Prev-Page', '')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
+ expect(val).not_to include('rel="prev"')
+ expect(val).not_to include('rel="next"')
+ expect(val).not_to include('page=0')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+ end
+ end
+
+ def expect_header(*args, &block)
+ expect(subject).to receive(:header).with(*args, &block)
+ end
+
+ def expect_no_header(*args, &block)
+ expect(subject).not_to receive(:header).with(*args)
+ end
+
+ def expect_message(method)
+ expect(subject).to receive(method)
+ .at_least(:once).and_return(value)
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/project_creator_spec.rb b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
index e9455b866ac..fd17284eea2 100644
--- a/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::PhabricatorImport::ProjectCreator do
subject(:creator) { described_class.new(user, params) }
describe '#execute' do
- it 'creates a project correctly and schedule an import' do
+ it 'creates a project correctly and schedule an import', :sidekiq_might_not_need_inline do
expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
expect(importer).to receive(:execute)
end
diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb
index 82ccb42f8a6..6e5c36172e2 100644
--- a/spec/lib/gitlab/project_authorizations_spec.rb
+++ b/spec/lib/gitlab/project_authorizations_spec.rb
@@ -3,48 +3,55 @@
require 'spec_helper'
describe Gitlab::ProjectAuthorizations do
- let(:group) { create(:group) }
- let!(:owned_project) { create(:project) }
- let!(:other_project) { create(:project) }
- let!(:group_project) { create(:project, namespace: group) }
-
- let(:user) { owned_project.namespace.owner }
-
def map_access_levels(rows)
rows.each_with_object({}) do |row, hash|
hash[row.project_id] = row.access_level
end
end
- before do
- other_project.add_reporter(user)
- group.add_developer(user)
- end
-
- let(:authorizations) do
+ subject(:authorizations) do
described_class.new(user).calculate
end
- it 'returns the correct number of authorizations' do
- expect(authorizations.length).to eq(3)
- end
+ context 'user added to group and project' do
+ let(:group) { create(:group) }
+ let!(:other_project) { create(:project) }
+ let!(:group_project) { create(:project, namespace: group) }
+ let!(:owned_project) { create(:project) }
+ let(:user) { owned_project.namespace.owner }
- it 'includes the correct projects' do
- expect(authorizations.pluck(:project_id))
- .to include(owned_project.id, other_project.id, group_project.id)
- end
+ before do
+ other_project.add_reporter(user)
+ group.add_developer(user)
+ end
+
+ it 'returns the correct number of authorizations' do
+ expect(authorizations.length).to eq(3)
+ end
- it 'includes the correct access levels' do
- mapping = map_access_levels(authorizations)
+ it 'includes the correct projects' do
+ expect(authorizations.pluck(:project_id))
+ .to include(owned_project.id, other_project.id, group_project.id)
+ end
+
+ it 'includes the correct access levels' do
+ mapping = map_access_levels(authorizations)
- expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
- expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
- expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ expect(mapping[owned_project.id]).to eq(Gitlab::Access::MAINTAINER)
+ expect(mapping[other_project.id]).to eq(Gitlab::Access::REPORTER)
+ expect(mapping[group_project.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
end
context 'with nested groups' do
+ let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:nested_project) { create(:project, namespace: nested_group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_developer(user)
+ end
it 'includes nested groups' do
expect(authorizations.pluck(:project_id)).to include(nested_project.id)
@@ -64,4 +71,114 @@ describe Gitlab::ProjectAuthorizations do
expect(mapping[nested_project.id]).to eq(Gitlab::Access::MAINTAINER)
end
end
+
+ context 'with shared groups' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ 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(:shared_group_parent) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ let_it_be(:project_parent) { create(:project, group: shared_group_parent) }
+ let_it_be(:project) { create(:project, group: shared_group) }
+ let_it_be(:project_child) { create(:project, group: shared_group_child) }
+
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group)
+ end
+
+ context 'when feature flag share_group_with_group is enabled' do
+ before do
+ stub_feature_flags(share_group_with_group: true)
+ end
+
+ context 'group user' do
+ let(:user) { group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER)
+ expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ 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 'group user' do
+ let(:user) { group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'creates proper authorizations' do
+ mapping = map_access_levels(authorizations)
+
+ expect(mapping[project_parent.id]).to be_nil
+ expect(mapping[project.id]).to be_nil
+ expect(mapping[project_child.id]).to be_nil
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index d6e50c672e6..99078f19361 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -79,20 +79,20 @@ describe Gitlab::ProjectSearchResults do
end
it 'finds by name' do
- expect(results.map(&:filename)).to include(expected_file_by_name)
+ expect(results.map(&:path)).to include(expected_file_by_path)
end
- it "loads all blobs for filename matches in single batch" do
+ it "loads all blobs for path matches in single batch" do
expect(Gitlab::Git::Blob).to receive(:batch).once.and_call_original
expected = project.repository.search_files_by_name(query, 'master')
- expect(results.map(&:filename)).to include(*expected)
+ expect(results.map(&:path)).to include(*expected)
end
it 'finds by content' do
- blob = results.select { |result| result.filename == expected_file_by_content }.flatten.last
+ blob = results.select { |result| result.path == expected_file_by_content }.flatten.last
- expect(blob.filename).to eq(expected_file_by_content)
+ expect(blob.path).to eq(expected_file_by_content)
end
end
@@ -146,7 +146,7 @@ describe Gitlab::ProjectSearchResults do
let(:blob_type) { 'blobs' }
let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) }
let(:private_project) { create(:project, :public, :repository, :repository_private) }
- let(:expected_file_by_name) { 'files/images/wm.svg' }
+ let(:expected_file_by_path) { 'files/images/wm.svg' }
let(:expected_file_by_content) { 'CHANGELOG' }
end
@@ -169,7 +169,7 @@ describe Gitlab::ProjectSearchResults 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) }
- let(:expected_file_by_name) { 'Files/Title.md' }
+ let(:expected_file_by_path) { 'Files/Title.md' }
let(:expected_file_by_content) { 'CHANGELOG.md' }
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 83acd979a80..5559b1e4291 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -22,7 +22,8 @@ describe Gitlab::ProjectTemplate do
described_class.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll'),
described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'),
described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'),
- described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo')
+ described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo'),
+ described_class.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg')
]
expect(described_class.all).to be_an(Array)
diff --git a/spec/lib/gitlab/prometheus/internal_spec.rb b/spec/lib/gitlab/prometheus/internal_spec.rb
new file mode 100644
index 00000000000..884bdcb4e9b
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/internal_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Internal do
+ let(:listen_address) { 'localhost:9090' }
+
+ let(:prometheus_settings) do
+ {
+ enable: true,
+ listen_address: listen_address
+ }
+ end
+
+ before do
+ stub_config(prometheus: prometheus_settings)
+ end
+
+ describe '.uri' do
+ shared_examples 'returns valid uri' do |uri_string|
+ it do
+ expect(described_class.uri).to eq(uri_string)
+ expect { Addressable::URI.parse(described_class.uri) }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'returns valid uri', 'http://localhost:9090'
+
+ context 'with non default prometheus address' do
+ let(:listen_address) { 'https://localhost:9090' }
+
+ it_behaves_like 'returns valid uri', 'https://localhost:9090'
+
+ context 'with :9090 symbol' do
+ let(:listen_address) { :':9090' }
+
+ it_behaves_like 'returns valid uri', 'http://localhost:9090'
+ end
+
+ context 'with 0.0.0.0:9090' do
+ let(:listen_address) { '0.0.0.0:9090' }
+
+ it_behaves_like 'returns valid uri', 'http://localhost:9090'
+ end
+ end
+
+ context 'when listen_address is nil' do
+ let(:listen_address) { nil }
+
+ it 'does not fail' do
+ expect(described_class.uri).to eq(nil)
+ end
+ end
+
+ context 'when prometheus listen address is blank in gitlab.yml' do
+ let(:listen_address) { '' }
+
+ it 'does not configure prometheus' do
+ expect(described_class.uri).to eq(nil)
+ end
+ end
+ end
+
+ describe 'prometheus_enabled?' do
+ it 'returns correct value' do
+ expect(described_class.prometheus_enabled?).to eq(true)
+ end
+
+ context 'when prometheus setting is disabled in gitlab.yml' do
+ let(:prometheus_settings) do
+ {
+ enable: false,
+ listen_address: listen_address
+ }
+ end
+
+ it 'returns correct value' do
+ expect(described_class.prometheus_enabled?).to eq(false)
+ end
+ end
+
+ context 'when prometheus setting is not present in gitlab.yml' do
+ before do
+ allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting)
+ end
+
+ it 'does not fail' do
+ expect(described_class.prometheus_enabled?).to eq(false)
+ end
+ end
+ end
+
+ describe '.listen_address' do
+ it 'returns correct value' do
+ expect(described_class.listen_address).to eq(listen_address)
+ end
+
+ context 'when prometheus setting is not present in gitlab.yml' do
+ before do
+ allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting)
+ end
+
+ it 'does not fail' do
+ expect(described_class.listen_address).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
index 7f6283715f2..6361893c53c 100644
--- a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
@@ -13,14 +13,19 @@ describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
context 'verify queries' do
before do
- allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
- allow(client).to receive(:query_range)
+ create(:prometheus_metric,
+ :common,
+ identifier: :system_metrics_knative_function_invocation_count,
+ query: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_app=~"%{function_name}.*"}[1m])*60))')
end
it 'has the query, but no data' do
- results = subject.query(serverless_func.id)
+ expect(client).to receive(:query_range).with(
+ 'sum(ceil(rate(istio_requests_total{destination_service_namespace="test-ns", destination_app=~"test-name.*"}[1m])*60))',
+ hash_including(:start, :stop)
+ )
- expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))')
+ subject.query(serverless_func.id)
end
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index b557baed258..1397add9f5a 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -66,6 +66,15 @@ describe Gitlab::Regex do
end
describe '.aws_account_id_regex' do
+ subject { described_class.aws_account_id_regex }
+
+ it { is_expected.to match('123456789012') }
+ it { is_expected.not_to match('12345678901') }
+ it { is_expected.not_to match('1234567890123') }
+ it { is_expected.not_to match('12345678901a') }
+ end
+
+ describe '.aws_arn_regex' do
subject { described_class.aws_arn_regex }
it { is_expected.to match('arn:aws:iam::123456789012:role/role-name') }
@@ -75,4 +84,14 @@ describe Gitlab::Regex do
it { is_expected.not_to match('123456789012') }
it { is_expected.not_to match('role/role-name') }
end
+
+ describe '.utc_date_regex' do
+ subject { described_class.utc_date_regex }
+
+ it { is_expected.to match('2019-10-20') }
+ it { is_expected.to match('1990-01-01') }
+ it { is_expected.not_to match('11-1234-90') }
+ it { is_expected.not_to match('aa-1234-cc') }
+ it { is_expected.not_to match('9/9/2018') }
+ end
end
diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb
index a575f6e2f11..07842faa638 100644
--- a/spec/lib/gitlab/search/found_blob_spec.rb
+++ b/spec/lib/gitlab/search/found_blob_spec.rb
@@ -15,7 +15,6 @@ describe Gitlab::Search::FoundBlob do
is_expected.to be_an described_class
expect(subject.id).to be_nil
expect(subject.path).to eq('CHANGELOG')
- expect(subject.filename).to eq('CHANGELOG')
expect(subject.basename).to eq('CHANGELOG')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(188)
@@ -25,12 +24,12 @@ describe Gitlab::Search::FoundBlob do
it 'does not parse content if not needed' do
expect(subject).not_to receive(:parse_search_result)
expect(subject.project_id).to eq(project.id)
- expect(subject.binary_filename).to eq('CHANGELOG')
+ expect(subject.binary_path).to eq('CHANGELOG')
end
it 'parses content only once when needed' do
expect(subject).to receive(:parse_search_result).once.and_call_original
- expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.path).to eq('CHANGELOG')
expect(subject.startline).to eq(188)
end
@@ -38,7 +37,7 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/project::function1.yaml')
+ expect(subject.path).to eq('testdata/project::function1.yaml')
expect(subject.basename).to eq('testdata/project::function1')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -50,7 +49,7 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.path).to eq('testdata/foo.txt')
expect(subject.basename).to eq('testdata/foo')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -62,7 +61,7 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { "master:testdata/foo.txt\x001\x00blah\x001\x00foo" }
it 'returns a valid FoundBlob' do
- expect(subject.filename).to eq('testdata/foo.txt')
+ expect(subject.path).to eq('testdata/foo.txt')
expect(subject.basename).to eq('testdata/foo')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -74,7 +73,7 @@ describe Gitlab::Search::FoundBlob do
let(:results) { project.repository.search_files_by_content('Role models', 'master') }
it 'returns a valid FoundBlob that ends with an empty line' do
- expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
+ expect(subject.path).to eq('files/markdown/ruby-style-guide.md')
expect(subject.basename).to eq('files/markdown/ruby-style-guide')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -87,7 +86,7 @@ describe Gitlab::Search::FoundBlob do
let(:results) { project.repository.search_files_by_content('файл', 'master') }
it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/russian.rb')
+ expect(subject.path).to eq('encoding/russian.rb')
expect(subject.basename).to eq('encoding/russian')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -99,7 +98,7 @@ describe Gitlab::Search::FoundBlob do
let(:results) { project.repository.search_files_by_content('webhook', 'master') }
it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/テスト.txt')
+ expect(subject.path).to eq('encoding/テスト.txt')
expect(subject.basename).to eq('encoding/テスト')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(3)
@@ -111,7 +110,7 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { (+"master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n").force_encoding(Encoding::ASCII_8BIT) }
it 'returns results as UTF-8' do
- expect(subject.filename).to eq('encoding/iso8859.txt')
+ expect(subject.path).to eq('encoding/iso8859.txt')
expect(subject.basename).to eq('encoding/iso8859')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
@@ -124,7 +123,6 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
it { expect(subject.path).to eq('CONTRIBUTE.md') }
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
it { expect(subject.basename).to eq('CONTRIBUTE') }
end
@@ -132,7 +130,6 @@ describe Gitlab::Search::FoundBlob do
let(:search_result) { "master:a/b/c.md\x005\x00a b c\n" }
it { expect(subject.path).to eq('a/b/c.md') }
- it { expect(subject.filename).to eq('a/b/c.md') }
it { expect(subject.basename).to eq('a/b/c') }
end
end
@@ -141,7 +138,7 @@ describe Gitlab::Search::FoundBlob do
context 'when file is under directory' do
let(:path) { 'a/b/c.md' }
- subject { described_class.new(blob_filename: path, project: project, ref: 'master') }
+ subject { described_class.new(blob_path: path, project: project, ref: 'master') }
before do
allow(Gitlab::Git::Blob).to receive(:batch).and_return([
@@ -150,7 +147,6 @@ describe Gitlab::Search::FoundBlob do
end
it { expect(subject.path).to eq('a/b/c.md') }
- it { expect(subject.filename).to eq('a/b/c.md') }
it { expect(subject.basename).to eq('a/b/c') }
context 'when filename has multiple extensions' do
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index a17e9a31212..eefc548a4d9 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -310,18 +310,18 @@ describe Gitlab::Shell do
let(:disk_path) { "#{project.disk_path}.git" }
it 'returns true when the command succeeds' do
- expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(true)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(true)
expect(gitlab_shell.remove_repository(project.repository_storage, project.disk_path)).to be(true)
- expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(false)
end
it 'keeps the namespace directory' do
gitlab_shell.remove_repository(project.repository_storage, project.disk_path)
- expect(gitlab_shell.exists?(project.repository_storage, disk_path)).to be(false)
- expect(gitlab_shell.exists?(project.repository_storage, project.disk_path.gsub(project.name, ''))).to be(true)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, disk_path)).to be(false)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, project.disk_path.gsub(project.name, ''))).to be(true)
end
end
@@ -332,18 +332,18 @@ describe Gitlab::Shell do
old_path = project2.disk_path
new_path = "project/new_path"
- expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(true)
- expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(false)
+ expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{old_path}.git")).to be(true)
+ expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{new_path}.git")).to be(false)
expect(gitlab_shell.mv_repository(project2.repository_storage, old_path, new_path)).to be_truthy
- expect(gitlab_shell.exists?(project2.repository_storage, "#{old_path}.git")).to be(false)
- expect(gitlab_shell.exists?(project2.repository_storage, "#{new_path}.git")).to be(true)
+ expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{old_path}.git")).to be(false)
+ expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{new_path}.git")).to be(true)
end
it 'returns false when the command fails' do
expect(gitlab_shell.mv_repository(project2.repository_storage, project2.disk_path, '')).to be_falsy
- expect(gitlab_shell.exists?(project2.repository_storage, "#{project2.disk_path}.git")).to be(true)
+ expect(TestEnv.storage_dir_exists?(project2.repository_storage, "#{project2.disk_path}.git")).to be(true)
end
end
@@ -401,68 +401,48 @@ describe Gitlab::Shell do
describe '#add_namespace' do
it 'creates a namespace' do
- subject.add_namespace(storage, "mepmep")
+ Gitlab::GitalyClient::NamespaceService.allow { subject.add_namespace(storage, "mepmep") }
- expect(subject.exists?(storage, "mepmep")).to be(true)
+ expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(true)
end
end
- describe '#exists?' do
- context 'when the namespace does not exist' do
+ describe '#repository_exists?' do
+ context 'when the repository does not exist' do
it 'returns false' do
- expect(subject.exists?(storage, "non-existing")).to be(false)
+ expect(subject.repository_exists?(storage, "non-existing.git")).to be(false)
end
end
- context 'when the namespace exists' do
+ context 'when the repository exists' do
it 'returns true' do
- subject.add_namespace(storage, "mepmep")
+ project = create(:project, :repository, :legacy_storage)
- expect(subject.exists?(storage, "mepmep")).to be(true)
+ expect(subject.repository_exists?(storage, project.repository.disk_path + ".git")).to be(true)
end
end
end
- describe '#repository_exists?' do
- context 'when the storage path does not exist' do
- subject { described_class.new.repository_exists?(storage, "non-existing.git") }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when the repository does not exist' do
- let(:project) { create(:project, :repository, :legacy_storage) }
-
- subject { described_class.new.repository_exists?(storage, "#{project.repository.disk_path}-some-other-repo.git") }
-
- it { is_expected.to be_falsey }
- end
-
- context 'when the repository exists' do
- let(:project) { create(:project, :repository, :legacy_storage) }
-
- subject { described_class.new.repository_exists?(storage, "#{project.repository.disk_path}.git") }
-
- it { is_expected.to be_truthy }
- end
- end
-
describe '#remove' do
it 'removes the namespace' do
- subject.add_namespace(storage, "mepmep")
- subject.rm_namespace(storage, "mepmep")
+ Gitlab::GitalyClient::NamespaceService.allow do
+ subject.add_namespace(storage, "mepmep")
+ subject.rm_namespace(storage, "mepmep")
+ end
- expect(subject.exists?(storage, "mepmep")).to be(false)
+ expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false)
end
end
describe '#mv_namespace' do
it 'renames the namespace' do
- subject.add_namespace(storage, "mepmep")
- subject.mv_namespace(storage, "mepmep", "2mep")
+ Gitlab::GitalyClient::NamespaceService.allow do
+ subject.add_namespace(storage, "mepmep")
+ subject.mv_namespace(storage, "mepmep", "2mep")
+ end
- expect(subject.exists?(storage, "mepmep")).to be(false)
- expect(subject.exists?(storage, "2mep")).to be(true)
+ expect(TestEnv.storage_dir_exists?(storage, "mepmep")).to be(false)
+ expect(TestEnv.storage_dir_exists?(storage, "2mep")).to be(true)
end
end
end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index 46fbc069efb..cb870cc996b 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::SidekiqLogging::StructuredLogger do
describe '#call' do
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
index 8410467ef1f..27eea963402 100644
--- a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do
end
end
- it 'injects into payload the correlation id' do
+ it 'injects into payload the correlation id', :sidekiq_might_not_need_inline do
expect_any_instance_of(described_class).to receive(:call).and_call_original
expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
index 806112fcb16..0d8cff3a295 100644
--- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
@@ -1,69 +1,108 @@
# frozen_string_literal: true
-require 'spec_helper'
+require 'fast_spec_helper'
describe Gitlab::SidekiqMiddleware::Metrics do
+ let(:middleware) { described_class.new }
+ let(:concurrency_metric) { double('concurrency metric') }
+
+ let(:queue_duration_seconds) { double('queue duration seconds metric') }
+ let(:completion_seconds_metric) { double('completion seconds metric') }
+ let(:user_execution_seconds_metric) { double('user execution seconds metric') }
+ let(:failed_total_metric) { double('failed total metric') }
+ let(:retried_total_metric) { double('retried total metric') }
+ let(:running_jobs_metric) { double('running jobs metric') }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_queue_duration_seconds, anything, anything, anything).and_return(queue_duration_seconds)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
+ allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
+ allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
+
+ allow(concurrency_metric).to receive(:set)
+ end
+
+ describe '#initialize' do
+ it 'sets general metrics' do
+ expect(concurrency_metric).to receive(:set).with({}, Sidekiq.options[:concurrency].to_i)
+
+ middleware
+ end
+ end
+
+ it 'ignore user execution when measured 0' do
+ allow(completion_seconds_metric).to receive(:observe)
+
+ expect(user_execution_seconds_metric).not_to receive(:observe)
+ end
+
describe '#call' do
- let(:middleware) { described_class.new }
let(:worker) { double(:worker) }
- let(:completion_seconds_metric) { double('completion seconds metric') }
- let(:user_execution_seconds_metric) { double('user execution seconds metric') }
- let(:failed_total_metric) { double('failed total metric') }
- let(:retried_total_metric) { double('retried total metric') }
- let(:running_jobs_metric) { double('running jobs metric') }
+ let(:job) { {} }
+ let(:job_status) { :done }
+ let(:labels) { { queue: :test } }
+ let(:labels_with_job_status) { { queue: :test, job_status: job_status } }
- before do
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything, anything).and_return(completion_seconds_metric)
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_cpu_seconds, anything, anything, anything).and_return(user_execution_seconds_metric)
- allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
- allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
- allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric)
+ let(:thread_cputime_before) { 1 }
+ let(:thread_cputime_after) { 2 }
+ let(:thread_cputime_duration) { thread_cputime_after - thread_cputime_before }
- allow(running_jobs_metric).to receive(:increment)
- end
+ let(:monotonic_time_before) { 11 }
+ let(:monotonic_time_after) { 20 }
+ let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
- it 'yields block' do
- allow(completion_seconds_metric).to receive(:observe)
- allow(user_execution_seconds_metric).to receive(:observe)
+ let(:queue_duration_for_job) { 0.01 }
- expect { |b| middleware.call(worker, {}, :test, &b) }.to yield_control.once
- end
-
- it 'sets metrics' do
- labels = { queue: :test }
- allow(middleware).to receive(:get_thread_cputime).and_return(1, 3)
+ before do
+ allow(middleware).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
+ allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
+ allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
- expect(user_execution_seconds_metric).to receive(:observe).with(labels, 2)
expect(running_jobs_metric).to receive(:increment).with(labels, 1)
expect(running_jobs_metric).to receive(:increment).with(labels, -1)
- expect(completion_seconds_metric).to receive(:observe).with(labels, kind_of(Numeric))
- middleware.call(worker, {}, :test) { nil }
+ expect(queue_duration_seconds).to receive(:observe).with(labels, queue_duration_for_job) if queue_duration_for_job
+ expect(user_execution_seconds_metric).to receive(:observe).with(labels_with_job_status, thread_cputime_duration)
+ expect(completion_seconds_metric).to receive(:observe).with(labels_with_job_status, monotonic_time_duration)
+ end
+
+ it 'yields block' do
+ expect { |b| middleware.call(worker, job, :test, &b) }.to yield_control.once
+ end
+
+ it 'sets queue specific metrics' do
+ middleware.call(worker, job, :test) { nil }
end
- it 'ignore user execution when measured 0' do
- allow(completion_seconds_metric).to receive(:observe)
- allow(middleware).to receive(:get_thread_cputime).and_return(0, 0)
+ context 'when job_duration is not available' do
+ let(:queue_duration_for_job) { nil }
- expect(user_execution_seconds_metric).not_to receive(:observe)
+ it 'does not set the queue_duration_seconds histogram' do
+ middleware.call(worker, job, :test) { nil }
+ end
end
context 'when job is retried' do
- it 'sets sidekiq_jobs_retried_total metric' do
- allow(completion_seconds_metric).to receive(:observe)
- expect(user_execution_seconds_metric).to receive(:observe)
+ let(:job) { { 'retry_count' => 1 } }
+ it 'sets sidekiq_jobs_retried_total metric' do
expect(retried_total_metric).to receive(:increment)
- middleware.call(worker, { 'retry_count' => 1 }, :test) { nil }
+ middleware.call(worker, job, :test) { nil }
end
end
context 'when error is raised' do
+ let(:job_status) { :fail }
+
it 'sets sidekiq_jobs_failed_total and reraises' do
- expect(failed_total_metric).to receive(:increment)
- expect { middleware.call(worker, {}, :test) { raise } }.to raise_error
+ expect(failed_total_metric).to receive(:increment).with(labels, 1)
+
+ expect { middleware.call(worker, job, :test) { raise StandardError, "Failed" } }.to raise_error(StandardError, "Failed")
end
end
end
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index dc412c80e68..5a8c721a634 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -115,5 +115,10 @@ describe Gitlab::SlashCommands::Command do
let(:params) { { text: 'issue move #78291 to gitlab/gitlab-ci' } }
it { is_expected.to eq(Gitlab::SlashCommands::IssueMove) }
end
+
+ context 'IssueComment is triggered' do
+ let(:params) { { text: "issue comment #503\ncomment body" } }
+ it { is_expected.to eq(Gitlab::SlashCommands::IssueComment) }
+ end
end
end
diff --git a/spec/lib/gitlab/slash_commands/issue_comment_spec.rb b/spec/lib/gitlab/slash_commands/issue_comment_spec.rb
new file mode 100644
index 00000000000..c6f56d10d1f
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/issue_comment_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::IssueComment do
+ describe '#execute' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { issue.author }
+ let(:chat_name) { double(:chat_name, user: user) }
+ let(:regex_match) { described_class.match("issue comment #{issue.iid}\nComment body") }
+
+ subject { described_class.new(project, chat_name).execute(regex_match) }
+
+ context 'when the issue exists' do
+ context 'when project is private' do
+ let(:project) { create(:project) }
+
+ context 'when the user is not a member of the project' do
+ let(:chat_name) { double(:chat_name, user: create(:user)) }
+
+ it 'does not allow the user to comment' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match('not found')
+ expect(issue.reload.notes.count).to be_zero
+ end
+ end
+ end
+
+ context 'when the user is not a member of the project' do
+ let(:chat_name) { double(:chat_name, user: create(:user)) }
+
+ context 'when the discussion is locked in the issue' do
+ before do
+ issue.update!(discussion_locked: true)
+ end
+
+ it 'does not allow the user to comment' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match('You are not allowed')
+ expect(issue.reload.notes.count).to be_zero
+ end
+ end
+ end
+
+ context 'when the user can comment on the issue' do
+ context 'when comment body exists' do
+ it 'creates a new comment' do
+ expect { subject }.to change { issue.notes.count }.by(1)
+ end
+
+ it 'a new comment has a correct body' do
+ subject
+
+ expect(issue.notes.last.note).to eq('Comment body')
+ end
+ end
+
+ context 'when comment body does not exist' do
+ let(:regex_match) { described_class.match("issue comment #{issue.iid}") }
+
+ it 'does not create a new comment' do
+ expect { subject }.not_to change { issue.notes.count }
+ end
+
+ it 'displays the errors' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("- Note can't be blank")
+ end
+ end
+ end
+ end
+
+ context 'when the issue does not exist' do
+ let(:regex_match) { described_class.match("issue comment 2343242\nComment body") }
+
+ it 'returns not found' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match('not found')
+ end
+ end
+ end
+
+ describe '.match' do
+ subject(:match) { described_class.match(command) }
+
+ context 'when a command has an issue ID' do
+ context 'when command has a comment body' do
+ let(:command) { "issue comment 503\nComment body" }
+
+ it 'matches an issue ID' do
+ expect(match[:iid]).to eq('503')
+ end
+
+ it 'matches an note body' do
+ expect(match[:note_body]).to eq('Comment body')
+ end
+ end
+ end
+
+ context 'when a command has a reference prefix for issue ID' do
+ let(:command) { "issue comment #503\nComment body" }
+
+ it 'matches an issue ID' do
+ expect(match[:iid]).to eq('503')
+ end
+ end
+
+ context 'when a command does not have an issue ID' do
+ let(:command) { 'issue comment' }
+
+ it 'does not match' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
index c7b83467660..804184a7173 100644
--- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
+++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb
@@ -22,6 +22,16 @@ describe Gitlab::SlashCommands::Presenters::Access do
end
end
+ describe '#generic_access_denied' do
+ subject { described_class.new.generic_access_denied }
+
+ it { is_expected.to be_a(Hash) }
+
+ it_behaves_like 'displays an error message' do
+ let(:error_message) { 'You are not allowed to perform the given chatops command.' }
+ end
+ end
+
describe '#deactivated' do
subject { described_class.new.deactivated }
diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb
new file mode 100644
index 00000000000..b5ef417cb93
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/presenters/issue_comment_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Presenters::IssueComment do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:note) { create(:note, project: project, noteable: issue) }
+ let(:author) { note.author }
+
+ describe '#present' do
+ let(:attachment) { subject[:attachments].first }
+ subject { described_class.new(note).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'sets ephemeral response type' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+
+ it 'sets the title' do
+ expect(attachment[:title]).to eq("#{issue.title} · #{issue.to_reference}")
+ end
+
+ it 'sets the fallback text' do
+ expect(attachment[:fallback]).to eq("New comment on #{issue.to_reference}: #{issue.title}")
+ end
+
+ it 'sets the fields' do
+ expect(attachment[:fields]).to eq([{ title: 'Comment', value: note.note }])
+ end
+
+ it 'sets the color' do
+ expect(attachment[:color]).to eq('#38ae67')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sourcegraph_spec.rb b/spec/lib/gitlab/sourcegraph_spec.rb
new file mode 100644
index 00000000000..e081ae32175
--- /dev/null
+++ b/spec/lib/gitlab/sourcegraph_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Sourcegraph do
+ let_it_be(:user) { create(:user) }
+ let(:feature_scope) { true }
+
+ before do
+ Feature.enable(:sourcegraph, feature_scope)
+ end
+
+ describe '.feature_conditional?' do
+ subject { described_class.feature_conditional? }
+
+ context 'when feature is enabled globally' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when feature is enabled only to a resource' do
+ let(:feature_scope) { user }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '.feature_available?' do
+ subject { described_class.feature_available? }
+
+ context 'when feature is enabled globally' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature is enabled only to a resource' do
+ let(:feature_scope) { user }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '.feature_enabled?' do
+ let(:current_user) { nil }
+
+ subject { described_class.feature_enabled?(current_user) }
+
+ context 'when feature is enabled globally' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature is enabled only to a resource' do
+ let(:feature_scope) { user }
+
+ context 'for the same resource' do
+ let(:current_user) { user }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for a different resource' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb
index 20e36c224b0..b15be56dd6d 100644
--- a/spec/lib/gitlab/sql/recursive_cte_spec.rb
+++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::SQL::RecursiveCTE do
[rel1.except(:order).to_sql, rel2.except(:order).to_sql]
end
- expect(sql).to eq("#{name} AS (#{sql1}\nUNION\n#{sql2})")
+ expect(sql).to eq("#{name} AS ((#{sql1})\nUNION\n(#{sql2}))")
end
end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index f8f6da19fa5..f736614ae53 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::SQL::Union do
it 'returns a String joining relations together using a UNION' do
union = described_class.new([relation_1, relation_2])
- expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
+ expect(union.to_sql).to eq("(#{to_sql(relation_1)})\nUNION\n(#{to_sql(relation_2)})")
end
it 'skips Model.none segements' do
@@ -22,7 +22,7 @@ describe Gitlab::SQL::Union do
union = described_class.new([empty_relation, relation_1, relation_2])
expect {User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
- expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
+ expect(union.to_sql).to eq("(#{to_sql(relation_1)})\nUNION\n(#{to_sql(relation_2)})")
end
it 'uses UNION ALL when removing duplicates is disabled' do
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index 50488dba48c..dc877f20cae 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -8,19 +8,23 @@ describe Gitlab::Tracking do
stub_application_setting(snowplow_enabled: true)
stub_application_setting(snowplow_collector_hostname: 'gitfoo.com')
stub_application_setting(snowplow_cookie_domain: '.gitfoo.com')
- stub_application_setting(snowplow_site_id: '_abc123_')
+ stub_application_setting(snowplow_app_id: '_abc123_')
+ stub_application_setting(snowplow_iglu_registry_url: 'https://example.org')
end
describe '.snowplow_options' do
it 'returns useful client options' do
- expect(described_class.snowplow_options(nil)).to eq(
+ expected_fields = {
namespace: 'gl',
hostname: 'gitfoo.com',
cookieDomain: '.gitfoo.com',
appId: '_abc123_',
formTracking: true,
- linkClickTracking: true
- )
+ linkClickTracking: true,
+ igluRegistryUrl: 'https://example.org'
+ }
+
+ expect(subject.snowplow_options(nil)).to match(expected_fields)
end
it 'enables features using feature flags' do
@@ -29,11 +33,12 @@ describe Gitlab::Tracking do
:additional_snowplow_tracking,
'_group_'
).and_return(false)
-
- expect(described_class.snowplow_options('_group_')).to include(
+ addition_feature_fields = {
formTracking: false,
linkClickTracking: false
- )
+ }
+
+ expect(subject.snowplow_options('_group_')).to include(addition_feature_fields)
end
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 7a01f7d1de8..96ebeb8ff76 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
@@ -34,22 +34,54 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
it_behaves_like 'counter examples'
end
+ describe 'previews counter' do
+ let(:setting_enabled) { true }
+
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: setting_enabled)
+ 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'
+ 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)
+
+ 2.times { described_class.increment_previews_count }
+
+ expect(described_class.total_previews_count).to eq(0)
+ end
+ end
+ end
+
describe '.totals' do
commits = 5
merge_requests = 3
views = 2
+ previews = 4
before do
+ stub_application_setting(web_ide_clientside_preview_enabled: true)
+
commits.times { described_class.increment_commits_count }
merge_requests.times { described_class.increment_merge_requests_count }
views.times { described_class.increment_views_count }
+ previews.times { described_class.increment_previews_count }
end
it 'can report all totals' do
expect(described_class.totals).to include(
web_ide_commits: commits,
web_ide_views: views,
- web_ide_merge_requests: merge_requests
+ web_ide_merge_requests: merge_requests,
+ web_ide_previews: previews
)
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index f2e864472c5..484684eeb65 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -17,21 +17,41 @@ describe Gitlab::UsageData do
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true)
+ create(:service, project: projects[2], type: 'MattermostService', active: true)
+ create(:service, project: projects[2], type: 'JenkinsService', active: true)
+ create(:service, project: projects[2], type: 'CustomIssueTrackerService', active: true)
create(:project_error_tracking_setting, project: projects[0])
create(:project_error_tracking_setting, project: projects[1], enabled: false)
-
- gcp_cluster = create(:cluster, :provided_by_gcp)
- create(:cluster, :provided_by_user)
- create(:cluster, :provided_by_user, :disabled)
+ create_list(:issue, 4, project: projects[0])
+ 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)
+ create(:zoom_meeting, project: projects[0], issue: projects[0].issues[2], issue_status: :added)
+ create_list(:zoom_meeting, 2, project: projects[0], issue: projects[0].issues[2], issue_status: :removed)
+
+ # Enabled clusters
+ gcp_cluster = create(:cluster_provider_gcp, :created).cluster
+ create(:cluster_provider_aws, :created)
+ create(:cluster_platform_kubernetes)
create(:cluster, :group)
+
+ # Disabled clusters
+ create(:cluster, :disabled)
create(:cluster, :group, :disabled)
create(:cluster, :group, :disabled)
+
+ # Applications
create(:clusters_applications_helm, :installed, cluster: gcp_cluster)
create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
create(:clusters_applications_cert_manager, :installed, cluster: gcp_cluster)
create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_crossplane, :installed, cluster: gcp_cluster)
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_elastic_stack, :installed, cluster: gcp_cluster)
+
+ create(:grafana_integration, project: projects[0], enabled: true)
+ create(:grafana_integration, project: projects[1], enabled: true)
+ create(:grafana_integration, project: projects[2], enabled: false)
ProjectFeature.first.update_attribute('repository_access_level', 0)
end
@@ -64,6 +84,8 @@ describe Gitlab::UsageData do
avg_cycle_analytics
influxdb_metrics_enabled
prometheus_metrics_enabled
+ web_ide_clientside_preview_enabled
+ ingress_modsecurity_enabled
))
end
@@ -81,6 +103,7 @@ describe Gitlab::UsageData do
web_ide_views
web_ide_commits
web_ide_merge_requests
+ web_ide_previews
navbar_searches
cycle_analytics_views
productivity_analytics_views
@@ -112,17 +135,23 @@ describe Gitlab::UsageData do
clusters_disabled
project_clusters_disabled
group_clusters_disabled
+ clusters_platforms_eks
clusters_platforms_gke
clusters_platforms_user
clusters_applications_helm
clusters_applications_ingress
clusters_applications_cert_managers
clusters_applications_prometheus
+ clusters_applications_crossplane
clusters_applications_runner
clusters_applications_knative
+ clusters_applications_elastic_stack
in_review_folder
+ grafana_integrated_projects
groups
issues
+ issues_with_associated_zoom_link
+ issues_using_zoom_quick_actions
keys
label_lists
labels
@@ -139,6 +168,9 @@ describe Gitlab::UsageData do
projects_jira_cloud_active
projects_slack_notifications_active
projects_slack_slash_active
+ projects_custom_issue_tracker_active
+ projects_jenkins_active
+ projects_mattermost_active
projects_prometheus_active
projects_with_repositories_enabled
projects_with_error_tracking_enabled
@@ -172,24 +204,33 @@ describe Gitlab::UsageData do
expect(count_data[:projects_jira_cloud_active]).to eq(2)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
+ expect(count_data[:projects_custom_issue_tracker_active]).to eq(1)
+ expect(count_data[:projects_jenkins_active]).to eq(1)
+ expect(count_data[:projects_mattermost_active]).to eq(1)
expect(count_data[:projects_with_repositories_enabled]).to eq(3)
expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
+ expect(count_data[:issues_with_associated_zoom_link]).to eq(2)
+ expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
- expect(count_data[:clusters_enabled]).to eq(7)
- expect(count_data[:project_clusters_enabled]).to eq(6)
+ expect(count_data[:clusters_enabled]).to eq(4)
+ expect(count_data[:project_clusters_enabled]).to eq(3)
expect(count_data[:group_clusters_enabled]).to eq(1)
expect(count_data[:clusters_disabled]).to eq(3)
expect(count_data[:project_clusters_disabled]).to eq(1)
expect(count_data[:group_clusters_disabled]).to eq(2)
expect(count_data[:group_clusters_enabled]).to eq(1)
+ expect(count_data[:clusters_platforms_eks]).to eq(1)
expect(count_data[:clusters_platforms_gke]).to eq(1)
expect(count_data[:clusters_platforms_user]).to eq(1)
expect(count_data[:clusters_applications_helm]).to eq(1)
expect(count_data[:clusters_applications_ingress]).to eq(1)
expect(count_data[:clusters_applications_cert_managers]).to eq(1)
+ expect(count_data[:clusters_applications_crossplane]).to eq(1)
expect(count_data[:clusters_applications_prometheus]).to eq(1)
expect(count_data[:clusters_applications_runner]).to eq(1)
expect(count_data[:clusters_applications_knative]).to eq(1)
+ expect(count_data[:clusters_applications_elastic_stack]).to eq(1)
+ expect(count_data[:grafana_integrated_projects]).to eq(2)
end
it 'works when queries time out' do
@@ -232,6 +273,7 @@ describe Gitlab::UsageData do
expect(subject[:container_registry_enabled]).to eq(Gitlab.config.registry.enabled)
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?)
end
end
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index c25bd14fcba..4e7c43a6856 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -148,7 +148,7 @@ describe Gitlab::UserAccess do
)
end
- it 'allows users that have push access to the canonical project to push to the MR branch' do
+ it 'allows users that have push access to the canonical project to push to the MR branch', :sidekiq_might_not_need_inline do
canonical_project.add_developer(user)
expect(access.can_push_to_branch?('awesome-feature')).to be_truthy
diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb
index 47dfc04f46f..ccd202b33f7 100644
--- a/spec/lib/gitlab/utils/deep_size_spec.rb
+++ b/spec/lib/gitlab/utils/deep_size_spec.rb
@@ -42,4 +42,10 @@ describe Gitlab::Utils::DeepSize do
end
end
end
+
+ describe '.human_default_max_size' do
+ it 'returns 1 MB' do
+ expect(described_class.human_default_max_size).to eq('1 MB')
+ end
+ end
end
diff --git a/spec/lib/gitlab/visibility_level_checker_spec.rb b/spec/lib/gitlab/visibility_level_checker_spec.rb
index 325ac3c6f31..fc929d5cbbf 100644
--- a/spec/lib/gitlab/visibility_level_checker_spec.rb
+++ b/spec/lib/gitlab/visibility_level_checker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::VisibilityLevelChecker do
diff --git a/spec/lib/gitlab/wiki_file_finder_spec.rb b/spec/lib/gitlab/wiki_file_finder_spec.rb
index fdd95d5e6e6..aeba081f3d3 100644
--- a/spec/lib/gitlab/wiki_file_finder_spec.rb
+++ b/spec/lib/gitlab/wiki_file_finder_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::WikiFileFinder do
it_behaves_like 'file finder' do
subject { described_class.new(project, project.wiki.default_branch) }
- let(:expected_file_by_name) { 'Files/Title.md' }
+ let(:expected_file_by_path) { 'Files/Title.md' }
let(:expected_file_by_content) { 'CHANGELOG.md' }
end
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 6bf837f1d3f..9362ff72fbc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -96,6 +96,48 @@ describe Gitlab do
end
end
+ describe '.canary?' do
+ it 'is true when CANARY env var is set to true' do
+ stub_env('CANARY', '1')
+
+ expect(described_class.canary?).to eq true
+ end
+
+ it 'is false when CANARY env var is set to false' do
+ stub_env('CANARY', '0')
+
+ expect(described_class.canary?).to eq false
+ end
+ end
+
+ describe '.com_and_canary?' do
+ it 'is true when on .com and canary' do
+ allow(described_class).to receive_messages(com?: true, canary?: true)
+
+ expect(described_class.com_and_canary?).to eq true
+ end
+
+ it 'is false when on .com but not on canary' do
+ allow(described_class).to receive_messages(com?: true, canary?: false)
+
+ expect(described_class.com_and_canary?).to eq false
+ end
+ end
+
+ describe '.com_but_not_canary?' do
+ it 'is false when on .com and canary' do
+ allow(described_class).to receive_messages(com?: true, canary?: true)
+
+ expect(described_class.com_but_not_canary?).to eq false
+ end
+
+ it 'is true when on .com but not on canary' do
+ allow(described_class).to receive_messages(com?: true, canary?: false)
+
+ expect(described_class.com_but_not_canary?).to eq true
+ end
+ end
+
describe '.dev_env_org_or_com?' do
it 'is true when on .com' do
allow(described_class).to receive_messages(com?: true, org?: false)
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index 0f7f57095df..473ad639ead 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -104,7 +104,8 @@ describe GoogleApi::CloudPlatform::Client do
enabled: legacy_abac
},
ip_allocation_policy: {
- use_ip_aliases: true
+ use_ip_aliases: true,
+ cluster_ipv4_cidr_block: '/16'
},
addons_config: addons_config
}
diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb
index bd93a3c59a2..699344e940e 100644
--- a/spec/lib/grafana/client_spec.rb
+++ b/spec/lib/grafana/client_spec.rb
@@ -35,7 +35,7 @@ describe Grafana::Client do
it 'does not follow redirects' do
expect { subject }.to raise_exception(
Grafana::Client::Error,
- 'Grafana response status code: 302'
+ 'Grafana response status code: 302, Message: {}'
)
expect(redirect_req_stub).to have_been_requested
@@ -67,6 +67,30 @@ describe Grafana::Client do
end
end
+ describe '#get_dashboard' do
+ let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/dashboards/uid/FndfgnX' }
+
+ subject do
+ client.get_dashboard(uid: 'FndfgnX')
+ end
+
+ it_behaves_like 'calls grafana api'
+ it_behaves_like 'no redirects'
+ it_behaves_like 'handles exceptions'
+ end
+
+ describe '#get_datasource' do
+ let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/datasources/name/Test%20Name' }
+
+ subject do
+ client.get_datasource(name: 'Test Name')
+ end
+
+ it_behaves_like 'calls grafana api'
+ it_behaves_like 'no redirects'
+ it_behaves_like 'handles exceptions'
+ end
+
describe '#proxy_datasource' do
let(:grafana_api_url) do
'https://grafanatest.com/-/grafana-project/' \
diff --git a/spec/lib/omni_auth/strategies/saml_spec.rb b/spec/lib/omni_auth/strategies/saml_spec.rb
index 3c59de86d98..73e86872308 100644
--- a/spec/lib/omni_auth/strategies/saml_spec.rb
+++ b/spec/lib/omni_auth/strategies/saml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe OmniAuth::Strategies::SAML, type: :strategy do
diff --git a/spec/lib/prometheus/pid_provider_spec.rb b/spec/lib/prometheus/pid_provider_spec.rb
index ba843b27254..6fdc11b14c4 100644
--- a/spec/lib/prometheus/pid_provider_spec.rb
+++ b/spec/lib/prometheus/pid_provider_spec.rb
@@ -18,7 +18,17 @@ describe Prometheus::PidProvider do
expect(Sidekiq).to receive(:server?).and_return(true)
end
- it { is_expected.to eq 'sidekiq' }
+ context 'in a clustered setup' do
+ before do
+ stub_env('SIDEKIQ_WORKER_ID', '123')
+ end
+
+ it { is_expected.to eq 'sidekiq_123' }
+ end
+
+ context 'in a single process setup' do
+ it { is_expected.to eq 'sidekiq' }
+ end
end
context 'when running in Unicorn mode' do
diff --git a/spec/lib/quality/helm_client_spec.rb b/spec/lib/quality/helm_client_spec.rb
index 7abb9688d5a..da5ba4c4d99 100644
--- a/spec/lib/quality/helm_client_spec.rb
+++ b/spec/lib/quality/helm_client_spec.rb
@@ -107,5 +107,25 @@ RSpec.describe Quality::HelmClient do
expect(subject.delete(release_name: release_name)).to eq('')
end
+
+ context 'with multiple release names' do
+ let(:release_name) { ['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 "#{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 "#{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/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb
index 4e77dcc97e6..5bac102ac41 100644
--- a/spec/lib/quality/kubernetes_client_spec.rb
+++ b/spec/lib/quality/kubernetes_client_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Quality::KubernetesClient do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(kubectl --namespace "#{namespace}" delete ) \
'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized --wait=true -l release=\"#{release_name}\""])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
@@ -23,11 +23,59 @@ RSpec.describe Quality::KubernetesClient do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(kubectl --namespace "#{namespace}" delete ) \
'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized --wait=true -l release=\"#{release_name}\""])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
expect { subject.cleanup(release_name: release_name) }.to output.to_stdout
end
+
+ context 'with multiple releases' do
+ let(:release_name) { ['my-release', 'my-release-2'] }
+
+ it 'raises an error if the Kubernetes command fails' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl --namespace "#{namespace}" delete ) \
+ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
+ "--now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})'"])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+
+ expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
+ end
+
+ it 'calls kubectl with the correct arguments' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl --namespace "#{namespace}" delete ) \
+ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
+ "--now --ignore-not-found --include-uninitialized --wait=true -l 'release in (#{release_name.join(', ')})'"])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ # We're not verifying the output here, just silencing it
+ expect { subject.cleanup(release_name: release_name) }.to output.to_stdout
+ end
+ end
+
+ context 'with `wait: false`' do
+ it 'raises an error if the Kubernetes command fails' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl --namespace "#{namespace}" delete ) \
+ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
+ "--now --ignore-not-found --include-uninitialized --wait=false -l release=\"#{release_name}\""])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
+
+ expect { subject.cleanup(release_name: release_name, wait: false) }.to raise_error(described_class::CommandFailedError)
+ end
+
+ it 'calls kubectl with the correct arguments' do
+ expect(Gitlab::Popen).to receive(:popen_with_detail)
+ .with([%(kubectl --namespace "#{namespace}" delete ) \
+ 'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
+ "--now --ignore-not-found --include-uninitialized --wait=false -l release=\"#{release_name}\""])
+ .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
+
+ # We're not verifying the output here, just silencing it
+ expect { subject.cleanup(release_name: release_name, wait: false) }.to output.to_stdout
+ end
+ end
end
end
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index ca2b17b44e0..8101664d34f 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -192,6 +192,15 @@ describe Sentry::Client do
end
end
+ context 'sentry api response too large' do
+ it 'raises exception' do
+ deep_size = double('Gitlab::Utils::DeepSize', valid?: false)
+ allow(Gitlab::Utils::DeepSize).to receive(:new).with(sentry_api_response).and_return(deep_size)
+
+ expect { subject }.to raise_error(Sentry::Client::ResponseInvalidSizeError, 'Sentry API response is too big. Limit is 1 MB.')
+ end
+ end
+
it_behaves_like 'maps exceptions'
end
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
index 86153071cd3..fcbffb52849 100644
--- a/spec/mailers/abuse_report_mailer_spec.rb
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AbuseReportMailer do
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
index 2ad572bb5c7..541acc47172 100644
--- a/spec/mailers/emails/merge_requests_spec.rb
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'email_spec'
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index c52e3c2191d..e360e38256e 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'email_spec'
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 1f7be415e35..d340f207dc7 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'email_spec'
diff --git a/spec/mailers/emails/releases_spec.rb b/spec/mailers/emails/releases_spec.rb
index 19f404db2a6..c614c009434 100644
--- a/spec/mailers/emails/releases_spec.rb
+++ b/spec/mailers/emails/releases_spec.rb
@@ -18,6 +18,7 @@ describe Emails::Releases do
context 'when the release has a name' do
it 'shows the correct subject' do
+ release.name = 'beta-1'
expected_subject = "#{release.project.name} | New release: #{release.name} - #{release.tag}"
is_expected.to have_subject(expected_subject)
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1991bac0229..cafb96898b3 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'email_spec'
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
index 757d3dfa797..1fd4d28ca53 100644
--- a/spec/mailers/repository_check_mailer_spec.rb
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCheckMailer do
diff --git a/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb
index 5c6f213e15b..f4155eab1bf 100644
--- a/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb
+++ b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180122154930_schedule_set_confidential_note_events_on_services.rb')
@@ -30,7 +32,7 @@ describe ScheduleSetConfidentialNoteEventsOnServices, :migration, :sidekiq do
end
end
- it 'correctly processes services' do
+ it 'correctly processes services', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
expect(services_table.where(confidential_note_events: nil).count).to eq 4
expect(services_table.where(confidential_note_events: true).count).to eq 1
diff --git a/spec/migrations/active_record/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb
index bc246f88685..617e31f359b 100644
--- a/spec/migrations/active_record/schema_spec.rb
+++ b/spec/migrations/active_record/schema_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# Check consistency of db/schema.rb version, migrations' timestamps, and the latest migration timestamp
diff --git a/spec/migrations/add_default_and_free_plans_spec.rb b/spec/migrations/add_default_and_free_plans_spec.rb
new file mode 100644
index 00000000000..ae40b5b10c2
--- /dev/null
+++ b/spec/migrations/add_default_and_free_plans_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20191023152913_add_default_and_free_plans.rb')
+
+describe AddDefaultAndFreePlans, :migration do
+ describe 'migrate' do
+ let(:plans) { table(:plans) }
+
+ context 'when on Gitlab.com' do
+ before do
+ expect(Gitlab).to receive(:com?) { true }
+ end
+
+ it 'creates free and default plans' do
+ expect { migrate! }.to change { plans.count }.by 2
+
+ expect(plans.last(2).pluck(:name)).to eq %w[free default]
+ end
+ end
+
+ context 'when on self-hosted' do
+ before do
+ expect(Gitlab).to receive(:com?) { false }
+ end
+
+ it 'creates only a default plan' do
+ expect { migrate! }.to change { plans.count }.by 1
+
+ expect(plans.last.name).to eq 'default'
+ end
+ end
+ end
+end
diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb
index 2500e2f8333..9932113a003 100644
--- a/spec/migrations/add_foreign_keys_to_todos_spec.rb
+++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_todos.rb')
diff --git a/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
index 6fd3cb1f44e..24ae939afa7 100644
--- a/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
+++ b/spec/migrations/add_not_null_constraint_to_project_mirror_data_foreign_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180508100222_add_not_null_constraint_to_project_mirror_data_foreign_key.rb')
diff --git a/spec/migrations/add_pages_access_level_to_project_feature_spec.rb b/spec/migrations/add_pages_access_level_to_project_feature_spec.rb
index 3946602c5be..a5e2bf2de71 100644
--- a/spec/migrations/add_pages_access_level_to_project_feature_spec.rb
+++ b/spec/migrations/add_pages_access_level_to_project_feature_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180423204600_add_pages_access_level_to_project_feature.rb')
diff --git a/spec/migrations/add_pipeline_build_foreign_key_spec.rb b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
index e9413f52f19..bb40ead9b93 100644
--- a/spec/migrations/add_pipeline_build_foreign_key_spec.rb
+++ b/spec/migrations/add_pipeline_build_foreign_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180420010016_add_pipeline_build_foreign_key.rb')
diff --git a/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
index bf299b70a29..8b128ff5ab8 100644
--- a/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
+++ b/spec/migrations/add_unique_constraint_to_project_features_project_id_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180511174224_add_unique_constraint_to_project_features_project_id.rb')
diff --git a/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
index b8c3a3eda4e..ae53b4e6443 100644
--- a/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
+++ b/spec/migrations/assure_commits_count_for_merge_request_diff_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180425131009_assure_commits_count_for_merge_request_diff.rb')
diff --git a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
index 65a918d5440..913b4d3f114 100644
--- a/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
+++ b/spec/migrations/backfill_store_project_full_path_in_repo_spec.rb
@@ -20,7 +20,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do
describe '#up' do
shared_examples_for 'writes the full path to git config' do
- it 'writes the git config' do
+ it 'writes the git config', :sidekiq_might_not_need_inline do
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
allow(repository_service).to receive(:cleanup)
expect(repository_service).to receive(:set_config).with('gitlab.fullpath' => expected_path)
@@ -29,7 +29,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do
migration.up
end
- it 'retries in case of failure' do
+ it 'retries in case of failure', :sidekiq_might_not_need_inline do
repository_service = spy(:repository_service)
allow(Gitlab::GitalyClient::RepositoryService).to receive(:new).and_return(repository_service)
@@ -40,7 +40,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do
migration.up
end
- it 'cleans up repository before writing the config' do
+ it 'cleans up repository before writing the config', :sidekiq_might_not_need_inline do
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service|
expect(repository_service).to receive(:cleanup).ordered
expect(repository_service).to receive(:set_config).ordered
@@ -87,7 +87,7 @@ describe BackfillStoreProjectFullPathInRepo, :migration do
context 'project in group' do
let!(:project) { projects.create!(namespace_id: group.id, name: 'baz', path: 'baz') }
- it 'deletes the gitlab full config value' do
+ it 'deletes the gitlab full config value', :sidekiq_might_not_need_inline do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService)
.to receive(:delete_config).with(['gitlab.fullpath'])
diff --git a/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb
index 7e61ab9b52e..699708ad1d4 100644
--- a/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb
+++ b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180531220618_change_default_value_for_dsa_key_restriction.rb')
diff --git a/spec/migrations/cleanup_build_stage_migration_spec.rb b/spec/migrations/cleanup_build_stage_migration_spec.rb
index 4d4d02aaa94..532212810c8 100644
--- a/spec/migrations/cleanup_build_stage_migration_spec.rb
+++ b/spec/migrations/cleanup_build_stage_migration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180420010616_cleanup_build_stage_migration.rb')
diff --git a/spec/migrations/cleanup_environments_external_url_spec.rb b/spec/migrations/cleanup_environments_external_url_spec.rb
index 07ddaf3d38f..bc20f936593 100644
--- a/spec/migrations/cleanup_environments_external_url_spec.rb
+++ b/spec/migrations/cleanup_environments_external_url_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20181108091549_cleanup_environments_external_url.rb')
diff --git a/spec/migrations/cleanup_stages_position_migration_spec.rb b/spec/migrations/cleanup_stages_position_migration_spec.rb
index dde5a777487..649fda1bb4e 100644
--- a/spec/migrations/cleanup_stages_position_migration_spec.rb
+++ b/spec/migrations/cleanup_stages_position_migration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180604123514_cleanup_stages_position_migration.rb')
diff --git a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
index 3fd4c5bc8d6..5df08a74e56 100644
--- a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
+++ b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb')
diff --git a/spec/migrations/drop_duplicate_protected_tags_spec.rb b/spec/migrations/drop_duplicate_protected_tags_spec.rb
index acfb6850722..7f0c7efbf66 100644
--- a/spec/migrations/drop_duplicate_protected_tags_spec.rb
+++ b/spec/migrations/drop_duplicate_protected_tags_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180711103851_drop_duplicate_protected_tags.rb')
diff --git a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
index abf39317188..327fb09ffec 100644
--- a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
+++ b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180216121030_enqueue_verify_pages_domain_workers')
diff --git a/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb
index cf5c10f77e1..50ecf083f27 100644
--- a/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb
+++ b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20181030135124_fill_empty_finished_at_in_deployments')
diff --git a/spec/migrations/fill_file_store_spec.rb b/spec/migrations/fill_file_store_spec.rb
index 5ff7aa56ce2..806c9283634 100644
--- a/spec/migrations/fill_file_store_spec.rb
+++ b/spec/migrations/fill_file_store_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180424151928_fill_file_store')
@@ -21,7 +23,7 @@ describe FillFileStore, :migration do
uploads.create!(size: 10, path: 'path', uploader: 'uploader', mount_point: 'file_name', store: nil)
end
- it 'correctly migrates nullified file_store/store column' do
+ it 'correctly migrates nullified file_store/store column', :sidekiq_might_not_need_inline do
expect(job_artifacts.where(file_store: nil).count).to eq(1)
expect(lfs_objects.where(file_store: nil).count).to eq(1)
expect(uploads.where(store: nil).count).to eq(1)
diff --git a/spec/migrations/fill_productivity_analytics_start_date_spec.rb b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
new file mode 100644
index 00000000000..7cbba9ef20e
--- /dev/null
+++ b/spec/migrations/fill_productivity_analytics_start_date_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20191004081520_fill_productivity_analytics_start_date.rb')
+
+describe FillProductivityAnalyticsStartDate, :migration do
+ let(:settings_table) { table('application_settings') }
+ let(:metrics_table) { table('merge_request_metrics') }
+
+ before do
+ settings_table.create!
+ end
+
+ context 'with NO productivity analytics data available' do
+ it 'sets start_date to NOW' do
+ expect { migrate! }.to change {
+ settings_table.first&.productivity_analytics_start_date
+ }.to(be_like_time(Time.now))
+ end
+ end
+
+ context 'with productivity analytics data available' do
+ before do
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics DISABLE TRIGGER ALL')
+ metrics_table.create!(merged_at: Time.parse('2019-09-09'), commits_count: nil, merge_request_id: 3)
+ metrics_table.create!(merged_at: Time.parse('2019-10-10'), commits_count: 5, merge_request_id: 1)
+ metrics_table.create!(merged_at: Time.parse('2019-11-11'), commits_count: 10, merge_request_id: 2)
+ ActiveRecord::Base.connection.execute('ALTER TABLE merge_request_metrics ENABLE TRIGGER ALL')
+ end
+ end
+
+ it 'set start_date to earliest merged_at value with PA data available' do
+ expect { migrate! }.to change {
+ settings_table.first&.productivity_analytics_start_date
+ }.to(be_like_time(Time.parse('2019-10-10')))
+ end
+ end
+end
diff --git a/spec/migrations/fix_wrong_pages_access_level_spec.rb b/spec/migrations/fix_wrong_pages_access_level_spec.rb
index 75ac5d919b2..73d8218b95c 100644
--- a/spec/migrations/fix_wrong_pages_access_level_spec.rb
+++ b/spec/migrations/fix_wrong_pages_access_level_spec.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190703185326_fix_wrong_pages_access_level.rb')
-describe FixWrongPagesAccessLevel, :migration, :sidekiq, schema: 20190628185004 do
+describe FixWrongPagesAccessLevel, :migration, :sidekiq_might_not_need_inline, schema: 20190628185004 do
using RSpec::Parameterized::TableSyntax
let(:migration_class) { described_class::MIGRATION }
diff --git a/spec/migrations/generate_lets_encrypt_private_key_spec.rb b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
index 773bf5222f0..7746ba46446 100644
--- a/spec/migrations/generate_lets_encrypt_private_key_spec.rb
+++ b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20190524062810_generate_lets_encrypt_private_key.rb')
diff --git a/spec/migrations/generate_missing_routes_spec.rb b/spec/migrations/generate_missing_routes_spec.rb
index 30ad135d4df..a4a25951ff0 100644
--- a/spec/migrations/generate_missing_routes_spec.rb
+++ b/spec/migrations/generate_missing_routes_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180702134423_generate_missing_routes.rb')
diff --git a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
index a1f243651b5..4e7438fc182 100644
--- a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20181219145520_migrate_cluster_configure_worker_sidekiq_queue.rb')
diff --git a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
index 66555118a43..d54aac50dc8 100644
--- a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180306074045_migrate_create_trace_artifact_sidekiq_queue.rb')
diff --git a/spec/migrations/migrate_legacy_artifacts_to_job_artifacts_spec.rb b/spec/migrations/migrate_legacy_artifacts_to_job_artifacts_spec.rb
index df82672f254..98bbe0ed5a2 100644
--- a/spec/migrations/migrate_legacy_artifacts_to_job_artifacts_spec.rb
+++ b/spec/migrations/migrate_legacy_artifacts_to_job_artifacts_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180816161409_migrate_legacy_artifacts_to_job_artifacts.rb')
@@ -40,7 +42,7 @@ describe MigrateLegacyArtifactsToJobArtifacts, :migration, :sidekiq do
end
end
- it 'migrates legacy artifacts to ci_job_artifacts table' do
+ it 'migrates legacy artifacts to ci_job_artifacts table', :sidekiq_might_not_need_inline do
migrate!
expect(job_artifacts.order(:job_id, :file_type).pluck('project_id, job_id, file_type, file_store, size, expire_at, file, file_sha256, file_location'))
diff --git a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
index 6ce04805e5d..6a188f34854 100644
--- a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180603190921_migrate_object_storage_upload_sidekiq_queue.rb')
diff --git a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
index 94de208e53e..d8f39ce4e71 100644
--- a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_migrator_sidekiq_queue.rb')
diff --git a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
index 976f3ce07d7..e517eef1320 100644
--- a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180307012445_migrate_update_head_pipeline_for_merge_request_sidekiq_queue.rb')
diff --git a/spec/migrations/move_limits_from_plans_spec.rb b/spec/migrations/move_limits_from_plans_spec.rb
new file mode 100644
index 00000000000..693d6ecb2c1
--- /dev/null
+++ b/spec/migrations/move_limits_from_plans_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20191030152934_move_limits_from_plans.rb')
+
+describe MoveLimitsFromPlans, :migration do
+ let(:plans) { table(:plans) }
+ let(:plan_limits) { table(:plan_limits) }
+
+ let!(:early_adopter_plan) { plans.create(name: 'early_adopter', title: 'Early adopter', active_pipelines_limit: 10, pipeline_size_limit: 11, active_jobs_limit: 12) }
+ let!(:gold_plan) { plans.create(name: 'gold', title: 'Gold', active_pipelines_limit: 20, pipeline_size_limit: 21, active_jobs_limit: 22) }
+ let!(:silver_plan) { plans.create(name: 'silver', title: 'Silver', active_pipelines_limit: 30, pipeline_size_limit: 31, active_jobs_limit: 32) }
+ let!(:bronze_plan) { plans.create(name: 'bronze', title: 'Bronze', active_pipelines_limit: 40, pipeline_size_limit: 41, active_jobs_limit: 42) }
+ let!(:free_plan) { plans.create(name: 'free', title: 'Free', active_pipelines_limit: 50, pipeline_size_limit: 51, active_jobs_limit: 52) }
+ let!(:other_plan) { plans.create(name: 'other', title: 'Other', active_pipelines_limit: nil, pipeline_size_limit: nil, active_jobs_limit: 0) }
+
+ describe 'migrate' do
+ it 'populates plan_limits from all the records in plans' do
+ expect { migrate! }.to change { plan_limits.count }.by 6
+ end
+
+ it 'copies plan limits and plan.id into to plan_limits table' do
+ migrate!
+
+ new_data = plan_limits.pluck(:plan_id, :ci_active_pipelines, :ci_pipeline_size, :ci_active_jobs)
+ expected_data = [
+ [early_adopter_plan.id, 10, 11, 12],
+ [gold_plan.id, 20, 21, 22],
+ [silver_plan.id, 30, 31, 32],
+ [bronze_plan.id, 40, 41, 42],
+ [free_plan.id, 50, 51, 52],
+ [other_plan.id, 0, 0, 0]
+ ]
+ expect(new_data).to contain_exactly(*expected_data)
+ end
+ end
+end
diff --git a/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
index 441c4295a40..ad1bcf37732 100644
--- a/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
+++ b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180220150310_remove_empty_extern_uid_auth0_identities.rb')
diff --git a/spec/migrations/remove_empty_github_service_templates_spec.rb b/spec/migrations/remove_empty_github_service_templates_spec.rb
new file mode 100644
index 00000000000..c128c8538db
--- /dev/null
+++ b/spec/migrations/remove_empty_github_service_templates_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20191021101942_remove_empty_github_service_templates.rb')
+
+describe RemoveEmptyGithubServiceTemplates, :migration do
+ subject(:migration) { described_class.new }
+
+ let(:services) do
+ table(:services).tap do |klass|
+ klass.class_eval do
+ serialize :properties, JSON
+ end
+ end
+ end
+
+ before do
+ services.delete_all
+
+ create_service(properties: nil)
+ create_service(properties: {})
+ create_service(properties: { some: :value })
+ create_service(properties: {}, template: false)
+ create_service(properties: {}, type: 'SomeType')
+ end
+
+ def all_service_properties
+ services.where(template: true, type: 'GithubService').pluck(:properties)
+ end
+
+ it 'correctly migrates up and down service templates' do
+ reversible_migration do |migration|
+ migration.before -> do
+ expect(services.count).to eq(5)
+
+ expect(all_service_properties)
+ .to match(a_collection_containing_exactly(nil, {}, { 'some' => 'value' }))
+ end
+
+ migration.after -> do
+ expect(services.count).to eq(4)
+
+ expect(all_service_properties)
+ .to match(a_collection_containing_exactly(nil, { 'some' => 'value' }))
+ end
+ end
+ end
+
+ def create_service(params)
+ data = { template: true, type: 'GithubService' }
+ data.merge!(params)
+
+ services.create!(data)
+ end
+end
diff --git a/spec/migrations/remove_redundant_pipeline_stages_spec.rb b/spec/migrations/remove_redundant_pipeline_stages_spec.rb
index 8325f986594..ad905d7eb8a 100644
--- a/spec/migrations/remove_redundant_pipeline_stages_spec.rb
+++ b/spec/migrations/remove_redundant_pipeline_stages_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180119121225_remove_redundant_pipeline_stages.rb')
diff --git a/spec/migrations/reschedule_builds_stages_migration_spec.rb b/spec/migrations/reschedule_builds_stages_migration_spec.rb
index 3bfd9dd9f6b..f9707d8f90b 100644
--- a/spec/migrations/reschedule_builds_stages_migration_spec.rb
+++ b/spec/migrations/reschedule_builds_stages_migration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180405101928_reschedule_builds_stages_migration')
diff --git a/spec/migrations/reschedule_commits_count_for_merge_request_diff_spec.rb b/spec/migrations/reschedule_commits_count_for_merge_request_diff_spec.rb
index 26489ef58bd..a62650c44fb 100644
--- a/spec/migrations/reschedule_commits_count_for_merge_request_diff_spec.rb
+++ b/spec/migrations/reschedule_commits_count_for_merge_request_diff_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180309121820_reschedule_commits_count_for_merge_request_diff')
diff --git a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
index 6d155f78342..ff859d07ff2 100644
--- a/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
+++ b/spec/migrations/schedule_digest_personal_access_tokens_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180913142237_schedule_digest_personal_access_tokens.rb')
@@ -32,7 +34,7 @@ describe ScheduleDigestPersonalAccessTokens, :migration, :sidekiq do
end
end
- it 'schedules background migrations' do
+ it 'schedules background migrations', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
plain_text_token = 'token IS NOT NULL'
diff --git a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
index 54f3e264df0..a0241f1d20c 100644
--- a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
+++ b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb')
@@ -32,7 +34,7 @@ describe ScheduleFillValidTimeForPagesDomainCertificates, :migration, :sidekiq d
end
end
- it 'sets certificate valid_not_before/not_after' do
+ it 'sets certificate valid_not_before/not_after', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
migrate!
diff --git a/spec/migrations/schedule_runners_token_encryption_spec.rb b/spec/migrations/schedule_runners_token_encryption_spec.rb
index 97ff6c128f3..6b9538c4d17 100644
--- a/spec/migrations/schedule_runners_token_encryption_spec.rb
+++ b/spec/migrations/schedule_runners_token_encryption_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20181121111200_schedule_runners_token_encryption')
diff --git a/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb
index fa4ddd5fbc7..845b0515177 100644
--- a/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb
+++ b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180104131052_schedule_set_confidential_note_events_on_webhooks.rb')
@@ -30,7 +32,7 @@ describe ScheduleSetConfidentialNoteEventsOnWebhooks, :migration, :sidekiq do
end
end
- it 'correctly processes web hooks' do
+ it 'correctly processes web hooks', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 4
expect(web_hooks_table.where(confidential_note_events: true).count).to eq 1
diff --git a/spec/migrations/schedule_stages_index_migration_spec.rb b/spec/migrations/schedule_stages_index_migration_spec.rb
index 710264da375..9ebc648f9d8 100644
--- a/spec/migrations/schedule_stages_index_migration_spec.rb
+++ b/spec/migrations/schedule_stages_index_migration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180420080616_schedule_stages_index_migration')
diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
index bc94f8820bd..4f841e8ce04 100644
--- a/spec/migrations/schedule_sync_issuables_state_id_spec.rb
+++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
@@ -33,7 +33,7 @@ describe ScheduleSyncIssuablesStateId, :migration, :sidekiq do
describe '#up' do
context 'issues' do
- it 'migrates state column to integer' do
+ it 'migrates state column to integer', :sidekiq_might_not_need_inline do
opened_issue = issues.create!(description: 'first', state: 'opened')
closed_issue = issues.create!(description: 'second', state: 'closed')
invalid_state_issue = issues.create!(description: 'fourth', state: 'not valid')
@@ -55,7 +55,7 @@ describe ScheduleSyncIssuablesStateId, :migration, :sidekiq do
end
context 'merge requests' do
- it 'migrates state column to integer' do
+ it 'migrates state column to integer', :sidekiq_might_not_need_inline do
opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: project.id, target_branch: 'feature1', source_branch: 'master')
closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: project.id, target_branch: 'feature2', source_branch: 'master')
merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: project.id, target_branch: 'feature3', source_branch: 'master')
diff --git a/spec/migrations/schedule_to_archive_legacy_traces_spec.rb b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb
index d3eac3c45ea..a81fb1494c7 100644
--- a/spec/migrations/schedule_to_archive_legacy_traces_spec.rb
+++ b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180529152628_schedule_to_archive_legacy_traces')
@@ -23,7 +25,7 @@ describe ScheduleToArchiveLegacyTraces, :migration do
create_legacy_trace(@build_running, 'This job is not done yet')
end
- it 'correctly archive legacy traces' do
+ it 'correctly archive legacy traces', :sidekiq_might_not_need_inline do
expect(job_artifacts.count).to eq(0)
expect(File.exist?(legacy_trace_path(@build_success))).to be_truthy
expect(File.exist?(legacy_trace_path(@build_failed))).to be_truthy
diff --git a/spec/migrations/truncate_user_fullname_spec.rb b/spec/migrations/truncate_user_fullname_spec.rb
index 17fd4d9f688..65b870de7b8 100644
--- a/spec/migrations/truncate_user_fullname_spec.rb
+++ b/spec/migrations/truncate_user_fullname_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20190325080727_truncate_user_fullname.rb')
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
index 83d6ff754c5..9d18618f638 100644
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -16,8 +16,16 @@ describe Analytics::CycleAnalytics::ProjectStage do
end
end
- it_behaves_like "cycle analytics stage" do
+ it_behaves_like 'cycle analytics stage' do
let(:parent) { create(:project) }
let(:parent_name) { :project }
end
+
+ context 'relative positioning' do
+ it_behaves_like 'a class that supports relative positioning' do
+ let(:project) { create(:project) }
+ let(:factory) { :cycle_analytics_project_stage }
+ let(:default_params) { { project: project } }
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 7bef3d30064..ba3b99f4421 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe ApplicationSetting do
+ using RSpec::Parameterized::TableSyntax
+
subject(:setting) { described_class.create_from_defaults }
it { include(CacheableAttributes) }
@@ -64,6 +66,24 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
+ context 'when snowplow is enabled' do
+ before do
+ setting.snowplow_enabled = true
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:snowplow_collector_hostname) }
+ it { is_expected.to allow_value("snowplow.gitlab.com").for(:snowplow_collector_hostname) }
+ it { is_expected.not_to allow_value('/example').for(:snowplow_collector_hostname) }
+ it { is_expected.to allow_value('https://example.org').for(:snowplow_iglu_registry_url) }
+ it { is_expected.not_to allow_value('not-a-url').for(:snowplow_iglu_registry_url) }
+ it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) }
+ end
+
+ context 'when snowplow is not enabled' do
+ it { is_expected.to allow_value(nil).for(:snowplow_collector_hostname) }
+ it { is_expected.to allow_value(nil).for(:snowplow_iglu_registry_url) }
+ end
+
context "when user accepted let's encrypt terms of service" do
before do
setting.update(lets_encrypt_terms_of_service_accepted: true)
@@ -72,6 +92,37 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
end
+ describe 'EKS integration' do
+ before do
+ setting.eks_integration_enabled = eks_enabled
+ end
+
+ context 'integration is disabled' do
+ let(:eks_enabled) { false }
+
+ it { is_expected.to allow_value(nil).for(:eks_account_id) }
+ it { is_expected.to allow_value(nil).for(:eks_access_key_id) }
+ it { is_expected.to allow_value(nil).for(:eks_secret_access_key) }
+ end
+
+ context 'integration is enabled' do
+ let(:eks_enabled) { true }
+
+ it { is_expected.to allow_value('123456789012').for(:eks_account_id) }
+ it { is_expected.not_to allow_value(nil).for(:eks_account_id) }
+ it { is_expected.not_to allow_value('123').for(:eks_account_id) }
+ it { is_expected.not_to allow_value('12345678901a').for(:eks_account_id) }
+
+ it { is_expected.to allow_value('access-key-id-12').for(:eks_access_key_id) }
+ it { is_expected.not_to allow_value('a' * 129).for(:eks_access_key_id) }
+ it { is_expected.not_to allow_value('short-key').for(:eks_access_key_id) }
+ it { is_expected.not_to allow_value(nil).for(:eks_access_key_id) }
+
+ it { is_expected.to allow_value('secret-access-key').for(:eks_secret_access_key) }
+ it { is_expected.not_to allow_value(nil).for(:eks_secret_access_key) }
+ end
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -446,6 +497,15 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:static_objects_external_storage_auth_token) }
end
end
+
+ context 'sourcegraph settings' do
+ it 'is invalid if sourcegraph is enabled and no url is provided' do
+ allow(subject).to receive(:sourcegraph_enabled).and_return(true)
+
+ expect(subject.sourcegraph_url).to be_nil
+ is_expected.to be_invalid
+ end
+ end
end
context 'restrict creating duplicates' do
@@ -534,5 +594,24 @@ describe ApplicationSetting do
end
end
+ describe '#sourcegraph_url_is_com?' do
+ where(:url, :is_com) do
+ 'https://sourcegraph.com' | true
+ 'https://sourcegraph.com/' | true
+ 'https://www.sourcegraph.com' | true
+ 'shttps://www.sourcegraph.com' | false
+ 'https://sourcegraph.example.com/' | false
+ 'https://sourcegraph.org/' | false
+ end
+
+ with_them do
+ it 'matches the url with sourcegraph.com' do
+ setting.sourcegraph_url = url
+
+ expect(setting.sourcegraph_url_is_com?).to eq(is_com)
+ end
+ end
+ end
+
it_behaves_like 'application settings examples'
end
diff --git a/spec/models/aws/role_spec.rb b/spec/models/aws/role_spec.rb
index c40752e40a6..d4165567146 100644
--- a/spec/models/aws/role_spec.rb
+++ b/spec/models/aws/role_spec.rb
@@ -31,4 +31,56 @@ describe Aws::Role do
end
end
end
+
+ describe 'callbacks' do
+ describe '#ensure_role_external_id!' do
+ subject { role.validate }
+
+ context 'for a new record' do
+ let(:role) { build(:aws_role, role_external_id: nil) }
+
+ it 'calls #ensure_role_external_id!' do
+ expect(role).to receive(:ensure_role_external_id!)
+
+ subject
+ end
+ end
+
+ context 'for an existing record' do
+ let(:role) { create(:aws_role) }
+
+ it 'does not call #ensure_role_external_id!' do
+ expect(role).not_to receive(:ensure_role_external_id!)
+
+ subject
+ end
+ end
+ end
+ end
+
+ describe '#ensure_role_external_id!' do
+ let(:role) { build(:aws_role, role_external_id: external_id) }
+
+ subject { role.ensure_role_external_id! }
+
+ context 'role_external_id is blank' do
+ let(:external_id) { nil }
+
+ it 'generates an external ID and assigns it to the record' do
+ subject
+
+ expect(role.role_external_id).to be_present
+ end
+ end
+
+ context 'role_external_id is already set' do
+ let(:external_id) { 'external-id' }
+
+ it 'does not change the existing external id' do
+ subject
+
+ expect(role.role_external_id).to eq external_id
+ end
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 058305bc04e..24fa3b9b1ea 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -206,6 +206,35 @@ describe Ci::Build do
end
end
+ describe '.with_exposed_artifacts' do
+ subject { described_class.with_exposed_artifacts }
+
+ let!(:job1) { create(:ci_build) }
+ let!(:job2) { create(:ci_build, options: options) }
+ let!(:job3) { create(:ci_build) }
+
+ context 'when some jobs have exposed artifacs and some not' do
+ let(:options) { { artifacts: { expose_as: 'test', paths: ['test'] } } }
+
+ before do
+ job1.ensure_metadata.update!(has_exposed_artifacts: nil)
+ job3.ensure_metadata.update!(has_exposed_artifacts: false)
+ end
+
+ it 'selects only the jobs with exposed artifacts' do
+ is_expected.to eq([job2])
+ end
+ end
+
+ context 'when job does not expose artifacts' do
+ let(:options) { nil }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.with_reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
@@ -1558,7 +1587,7 @@ describe Ci::Build do
end
end
- describe '#retries_max' do
+ describe '#options_retry_max' do
context 'with retries max config option' do
subject { create(:ci_build, options: { retry: { max: 1 } }) }
@@ -1568,7 +1597,7 @@ describe Ci::Build do
end
it 'returns the number of configured max retries' do
- expect(subject.retries_max).to eq 1
+ expect(subject.options_retry_max).to eq 1
end
end
@@ -1578,7 +1607,7 @@ describe Ci::Build do
end
it 'returns the number of configured max retries' do
- expect(subject.retries_max).to eq 1
+ expect(subject.options_retry_max).to eq 1
end
end
end
@@ -1586,16 +1615,16 @@ describe Ci::Build do
context 'without retries max config option' do
subject { create(:ci_build) }
- it 'returns zero' do
- expect(subject.retries_max).to eq 0
+ it 'returns nil' do
+ expect(subject.options_retry_max).to be_nil
end
end
context 'when build is degenerated' do
subject { create(:ci_build, :degenerated) }
- it 'returns zero' do
- expect(subject.retries_max).to eq 0
+ it 'returns nil' do
+ expect(subject.options_retry_max).to be_nil
end
end
@@ -1603,17 +1632,17 @@ describe Ci::Build do
subject { create(:ci_build, options: { retry: 1 }) }
it 'returns the number of configured max retries' do
- expect(subject.retries_max).to eq 1
+ expect(subject.options_retry_max).to eq 1
end
end
end
- describe '#retry_when' do
+ describe '#options_retry_when' do
context 'with retries when config option' do
subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) }
it 'returns the configured when' do
- expect(subject.retry_when).to eq ['some_reason']
+ expect(subject.options_retry_when).to eq ['some_reason']
end
end
@@ -1621,7 +1650,7 @@ describe Ci::Build do
subject { create(:ci_build) }
it 'returns always array' do
- expect(subject.retry_when).to eq ['always']
+ expect(subject.options_retry_when).to eq ['always']
end
end
@@ -1629,72 +1658,38 @@ describe Ci::Build do
subject { create(:ci_build, options: { retry: 1 }) }
it 'returns always array' do
- expect(subject.retry_when).to eq ['always']
+ expect(subject.options_retry_when).to eq ['always']
end
end
end
describe '#retry_failure?' do
- subject { create(:ci_build) }
+ using RSpec::Parameterized::TableSyntax
- context 'when retries max is zero' do
- before do
- expect(subject).to receive(:retries_max).at_least(:once).and_return(0)
- end
+ let(:build) { create(:ci_build) }
- it 'returns false' do
- expect(subject.retry_failure?).to eq false
- end
- end
+ subject { build.retry_failure? }
- context 'when retries max equals retries count' do
- before do
- expect(subject).to receive(:retries_max).at_least(:once).and_return(1)
- expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
- end
-
- it 'returns false' do
- expect(subject.retry_failure?).to eq false
- end
+ where(:description, :retry_count, :options, :failure_reason, :result) do
+ "retries are disabled" | 0 | { max: 0 } | nil | false
+ "max equals count" | 2 | { max: 2 } | nil | false
+ "max is higher than count" | 1 | { max: 2 } | nil | true
+ "matching failure reason" | 0 | { when: %w[api_failure], max: 2 } | :api_failure | true
+ "not matching with always" | 0 | { when: %w[always], max: 2 } | :api_failure | true
+ "not matching reason" | 0 | { when: %w[script_error], max: 2 } | :api_failure | false
+ "scheduler failure override" | 1 | { when: %w[scheduler_failure], max: 1 } | :scheduler_failure | false
+ "default for scheduler failure" | 1 | {} | :scheduler_failure | true
end
- context 'when retries max is higher than retries count' do
+ with_them do
before do
- expect(subject).to receive(:retries_max).at_least(:once).and_return(2)
- expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
- end
+ allow(build).to receive(:retries_count) { retry_count }
- context 'and retry when is always' do
- before do
- expect(subject).to receive(:retry_when).at_least(:once).and_return(['always'])
- end
-
- it 'returns true' do
- expect(subject.retry_failure?).to eq true
- end
- end
-
- context 'and retry when includes the failure_reason' do
- before do
- expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
- expect(subject).to receive(:retry_when).at_least(:once).and_return(['some_reason'])
- end
-
- it 'returns true' do
- expect(subject.retry_failure?).to eq true
- end
+ build.options[:retry] = options
+ build.failure_reason = failure_reason
end
- context 'and retry when does not include failure_reason' do
- before do
- expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
- expect(subject).to receive(:retry_when).at_least(:once).and_return(['some', 'other failure'])
- end
-
- it 'returns false' do
- expect(subject.retry_failure?).to eq false
- end
- end
+ it { is_expected.to eq(result) }
end
end
end
@@ -1844,6 +1839,14 @@ describe Ci::Build do
expect(build.metadata.read_attribute(:config_options)).to be_nil
end
end
+
+ context 'when options include artifacts:expose_as' do
+ let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }) }
+
+ it 'saves the presence of expose_as into build metadata' do
+ expect(build.metadata).to have_exposed_artifacts
+ end
+ end
end
describe '#other_manual_actions' do
@@ -2218,7 +2221,7 @@ describe Ci::Build do
{ key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
{ key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false },
{ key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false },
- { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true, masked: false },
+ { key: 'CI_CONFIG_PATH', value: pipeline.config_path, public: true, masked: false },
{ key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false },
{ key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true, masked: false },
{ key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false },
@@ -2664,11 +2667,17 @@ describe Ci::Build do
it { is_expected.to include(deployment_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 } }
+
+ it { is_expected.to include(ci_config_path) }
+ end
+
context 'when project has custom CI config path' do
let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true, masked: false } }
before do
- project.update(ci_config_path: 'custom')
+ expect_any_instance_of(Project).to receive(:ci_config_path) { 'custom' }
end
it { is_expected.to include(ci_config_path) }
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 59db347582b..96d81f4cc49 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
@@ -63,7 +65,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
before do
- build_trace_chunk.send(:unsafe_set_data!, 'Sample data in redis')
+ build_trace_chunk.send(:unsafe_set_data!, +'Sample data in redis')
end
it { is_expected.to eq('Sample data in redis') }
@@ -71,7 +73,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when data_store is database' do
let(:data_store) { :database }
- let(:raw_data) { 'Sample data in database' }
+ let(:raw_data) { +'Sample data in database' }
it { is_expected.to eq('Sample data in database') }
end
@@ -80,7 +82,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :fog }
before do
- build_trace_chunk.send(:unsafe_set_data!, 'Sample data in fog')
+ build_trace_chunk.send(:unsafe_set_data!, +'Sample data in fog')
end
it { is_expected.to eq('Sample data in fog') }
@@ -90,7 +92,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
describe '#append' do
subject { build_trace_chunk.append(new_data, offset) }
- let(:new_data) { 'Sample new data' }
+ let(:new_data) { +'Sample new data' }
let(:offset) { 0 }
let(:merged_data) { data + new_data.to_s }
@@ -143,7 +145,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when new_data is empty' do
- let(:new_data) { '' }
+ let(:new_data) { +'' }
it 'does not append' do
subject
@@ -172,7 +174,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
shared_examples_for 'Scheduling sidekiq worker to flush data to persist store' do
context 'when new data fulfilled chunk size' do
- let(:new_data) { 'a' * described_class::CHUNK_SIZE }
+ let(:new_data) { +'a' * described_class::CHUNK_SIZE }
it 'schedules trace chunk flush worker' do
expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
@@ -180,7 +182,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject
end
- it 'migrates data to object storage' do
+ it 'migrates data to object storage', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
subject
@@ -194,7 +196,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
shared_examples_for 'Scheduling no sidekiq worker' do
context 'when new data fulfilled chunk size' do
- let(:new_data) { 'a' * described_class::CHUNK_SIZE }
+ let(:new_data) { +'a' * described_class::CHUNK_SIZE }
it 'does not schedule trace chunk flush worker' do
expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
@@ -219,7 +221,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
context 'when there are no data' do
- let(:data) { '' }
+ let(:data) { +'' }
it 'has no data' do
expect(build_trace_chunk.data).to be_empty
@@ -230,7 +232,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when there are some data' do
- let(:data) { 'Sample data in redis' }
+ let(:data) { +'Sample data in redis' }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -249,7 +251,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :database }
context 'when there are no data' do
- let(:data) { '' }
+ let(:data) { +'' }
it 'has no data' do
expect(build_trace_chunk.data).to be_empty
@@ -260,7 +262,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when there are some data' do
- let(:raw_data) { 'Sample data in database' }
+ let(:raw_data) { +'Sample data in database' }
let(:data) { raw_data }
it 'has data' do
@@ -276,7 +278,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :fog }
context 'when there are no data' do
- let(:data) { '' }
+ let(:data) { +'' }
it 'has no data' do
expect(build_trace_chunk.data).to be_empty
@@ -287,7 +289,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when there are some data' do
- let(:data) { 'Sample data in fog' }
+ let(:data) { +'Sample data in fog' }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -332,7 +334,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when data_store is redis' do
let(:data_store) { :redis }
- let(:data) { 'Sample data in redis' }
+ let(:data) { +'Sample data in redis' }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -343,7 +345,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when data_store is database' do
let(:data_store) { :database }
- let(:raw_data) { 'Sample data in database' }
+ let(:raw_data) { +'Sample data in database' }
let(:data) { raw_data }
it_behaves_like 'truncates'
@@ -351,7 +353,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when data_store is fog' do
let(:data_store) { :fog }
- let(:data) { 'Sample data in fog' }
+ let(:data) { +'Sample data in fog' }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -368,7 +370,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
context 'when data exists' do
- let(:data) { 'Sample data in redis' }
+ let(:data) { +'Sample data in redis' }
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@@ -386,7 +388,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :database }
context 'when data exists' do
- let(:raw_data) { 'Sample data in database' }
+ let(:raw_data) { +'Sample data in database' }
let(:data) { raw_data }
it { is_expected.to eq(data.bytesize) }
@@ -401,7 +403,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :fog }
context 'when data exists' do
- let(:data) { 'Sample data in fog' }
+ let(:data) { +'Sample data in fog' }
let(:key) { "tmp/builds/#{build.id}/chunks/#{chunk_index}.log" }
before do
@@ -443,7 +445,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data size reached CHUNK_SIZE' do
- let(:data) { 'a' * described_class::CHUNK_SIZE }
+ let(:data) { +'a' * described_class::CHUNK_SIZE }
it 'persists the data' do
expect(build_trace_chunk.redis?).to be_truthy
@@ -463,7 +465,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data size has not reached CHUNK_SIZE' do
- let(:data) { 'Sample data in redis' }
+ let(:data) { +'Sample data in redis' }
it 'does not persist the data and the orignal data is intact' do
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
@@ -492,7 +494,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data size reached CHUNK_SIZE' do
- let(:data) { 'a' * described_class::CHUNK_SIZE }
+ let(:data) { +'a' * described_class::CHUNK_SIZE }
it 'persists the data' do
expect(build_trace_chunk.database?).to be_truthy
@@ -512,7 +514,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data size has not reached CHUNK_SIZE' do
- let(:data) { 'Sample data in database' }
+ let(:data) { +'Sample data in database' }
it 'does not persist the data and the orignal data is intact' do
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
@@ -561,7 +563,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data size has not reached CHUNK_SIZE' do
- let(:data) { 'Sample data in fog' }
+ let(:data) { +'Sample data in fog' }
it 'does not raise error' do
expect { subject }.not_to raise_error
@@ -582,7 +584,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
shared_examples_for 'deletes all build_trace_chunk and data in redis' do
- it do
+ it 'deletes all build_trace_chunk and data in redis', :sidekiq_might_not_need_inline do
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3)
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index de0ce9932e8..d24cf3d2115 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -979,141 +979,6 @@ describe Ci::Pipeline, :mailer do
end
describe 'pipeline stages' do
- describe '#stage_seeds' do
- let(:pipeline) { build(:ci_pipeline, config: config) }
- let(:config) { { rspec: { script: 'rake' } } }
-
- it 'returns preseeded stage seeds object' do
- expect(pipeline.stage_seeds)
- .to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
- expect(pipeline.stage_seeds.count).to eq 1
- end
-
- context 'when no refs policy is specified' do
- let(:config) do
- { production: { stage: 'deploy', script: 'cap prod' },
- rspec: { stage: 'test', script: 'rspec' },
- spinach: { stage: 'test', script: 'spinach' } }
- end
-
- it 'correctly fabricates a stage seeds object' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 2
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.second.attributes[:name]).to eq 'deploy'
- expect(seeds.dig(0, 0, :name)).to eq 'rspec'
- expect(seeds.dig(0, 1, :name)).to eq 'spinach'
- expect(seeds.dig(1, 0, :name)).to eq 'production'
- end
- end
-
- context 'when refs policy is specified' do
- let(:pipeline) do
- build(:ci_pipeline, ref: 'feature', tag: true, config: config)
- end
-
- let(:config) do
- { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
- end
-
- it 'returns stage seeds only assigned to master to master' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 1
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
- end
- end
-
- context 'when source policy is specified' do
- let(:pipeline) { build(:ci_pipeline, source: :schedule, config: config) }
-
- let(:config) do
- { production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
- spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } }
- end
-
- it 'returns stage seeds only assigned to schedules' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 1
- expect(seeds.first.attributes[:name]).to eq 'test'
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
- end
- end
-
- context 'when kubernetes policy is specified' do
- let(:config) do
- {
- spinach: { stage: 'test', script: 'spinach' },
- production: {
- stage: 'deploy',
- script: 'cap',
- only: { kubernetes: 'active' }
- }
- }
- end
-
- context 'when kubernetes is active' do
- context 'when user configured kubernetes from CI/CD > Clusters' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
- let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
-
- it 'returns seeds for kubernetes dependent job' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 2
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
- expect(seeds.dig(1, 0, :name)).to eq 'production'
- end
- end
- end
-
- context 'when kubernetes is not active' do
- it 'does not return seeds for kubernetes dependent job' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 1
- expect(seeds.dig(0, 0, :name)).to eq 'spinach'
- end
- end
- end
-
- context 'when variables policy is specified' do
- let(:config) do
- { unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
- feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } }
- end
-
- it 'returns stage seeds only when variables expression is truthy' do
- seeds = pipeline.stage_seeds
-
- expect(seeds.size).to eq 1
- expect(seeds.dig(0, 0, :name)).to eq 'unit'
- end
- end
- end
-
- describe '#seeds_size' do
- context 'when refs policy is specified' do
- let(:config) do
- { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
- end
-
- let(:pipeline) do
- build(:ci_pipeline, ref: 'feature', tag: true, config: config)
- end
-
- it 'returns real seeds size' do
- expect(pipeline.seeds_size).to eq 1
- end
- end
- end
-
describe 'legacy stages' do
before do
create(:commit_status, pipeline: pipeline,
@@ -1346,7 +1211,7 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#duration' do
+ describe '#duration', :sidekiq_might_not_need_inline do
context 'when multiple builds are finished' do
before do
travel_to(current + 30) do
@@ -1422,7 +1287,7 @@ describe Ci::Pipeline, :mailer do
end
describe '#finished_at' do
- it 'updates on transitioning to success' do
+ it 'updates on transitioning to success', :sidekiq_might_not_need_inline do
build.success
expect(pipeline.reload.finished_at).not_to be_nil
@@ -2102,7 +1967,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.not_to include('created', 'preparing', 'pending') }
end
- describe '#status' do
+ describe '#status', :sidekiq_might_not_need_inline do
let(:build) do
create(:ci_build, :created, pipeline: pipeline, name: 'test')
end
@@ -2186,161 +2051,6 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#ci_yaml_file_path' do
- subject { pipeline.ci_yaml_file_path }
-
- %i[unknown_source repository_source].each do |source|
- context source.to_s do
- before do
- pipeline.config_source = described_class.config_sources.fetch(source)
- end
-
- it 'returns the path from project' do
- allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' }
-
- is_expected.to eq('custom/path')
- end
-
- it 'returns default when custom path is nil' do
- allow(pipeline.project).to receive(:ci_config_path) { nil }
-
- is_expected.to eq('.gitlab-ci.yml')
- end
-
- it 'returns default when custom path is empty' do
- allow(pipeline.project).to receive(:ci_config_path) { '' }
-
- is_expected.to eq('.gitlab-ci.yml')
- end
- end
- end
-
- context 'when pipeline is for auto-devops' do
- before do
- pipeline.config_source = 'auto_devops_source'
- end
-
- it 'does not return config file' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#set_config_source' do
- context 'when pipelines does not contain needed data and auto devops is disabled' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it 'defines source to be unknown' do
- pipeline.set_config_source
-
- expect(pipeline).to be_unknown_source
- end
- end
-
- context 'when pipeline contains all needed data' do
- let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: '1234',
- ref: 'master',
- source: :push)
- end
-
- context 'when the repository has a config file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml_for)
- .and_return('config')
- end
-
- it 'defines source to be from repository' do
- pipeline.set_config_source
-
- expect(pipeline).to be_repository_source
- end
-
- context 'when loading an object' do
- let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) }
-
- it 'does not redefine the source' do
- # force to overwrite the source
- pipeline.unknown_source!
-
- expect(new_pipeline).to be_unknown_source
- end
- end
- end
-
- context 'when the repository does not have a config file' do
- let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
-
- context 'auto devops enabled' do
- before do
- allow(project).to receive(:ci_config_path) { 'custom' }
- end
-
- it 'defines source to be auto devops' do
- pipeline.set_config_source
-
- expect(pipeline).to be_auto_devops_source
- end
- end
- end
- end
- end
-
- describe '#ci_yaml_file' do
- let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
-
- context 'the source is unknown' do
- before do
- pipeline.unknown_source!
- end
-
- it 'returns the configuration if found' do
- allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
- .and_return('config')
-
- expect(pipeline.ci_yaml_file).to be_a(String)
- expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
- expect(pipeline.yaml_errors).to be_nil
- end
-
- it 'sets yaml errors if not found' do
- expect(pipeline.ci_yaml_file).to be_nil
- expect(pipeline.yaml_errors)
- .to start_with('Failed to load CI/CD config file')
- end
- end
-
- context 'the source is the repository' do
- before do
- pipeline.repository_source!
- end
-
- it 'returns the configuration if found' do
- allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
- .and_return('config')
-
- expect(pipeline.ci_yaml_file).to be_a(String)
- expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
- expect(pipeline.yaml_errors).to be_nil
- end
- end
-
- context 'when the source is auto_devops_source' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- pipeline.auto_devops_source!
- end
-
- it 'finds the implied config' do
- expect(pipeline.ci_yaml_file).to eq(implied_yml)
- expect(pipeline.yaml_errors).to be_nil
- end
- end
- end
-
describe '#update_status' do
context 'when pipeline is empty' do
it 'updates does not change pipeline status' do
@@ -2675,7 +2385,7 @@ describe Ci::Pipeline, :mailer do
stub_full_request(hook.url, method: :post)
end
- context 'with multiple builds' do
+ context 'with multiple builds', :sidekiq_might_not_need_inline do
context 'when build is queued' do
before do
build_a.enqueue
@@ -2886,24 +2596,19 @@ describe Ci::Pipeline, :mailer do
end
describe '#has_yaml_errors?' do
- context 'when pipeline has errors' do
- let(:pipeline) do
- create(:ci_pipeline, config: { rspec: nil })
+ context 'when yaml_errors is set' do
+ before do
+ pipeline.yaml_errors = 'File not found'
end
- it 'contains yaml errors' do
+ it 'returns true if yaml_errors is set' do
expect(pipeline).to have_yaml_errors
+ expect(pipeline.yaml_errors).to include('File not foun')
end
end
- context 'when pipeline does not have errors' do
- let(:pipeline) do
- create(:ci_pipeline, config: { rspec: { script: 'rake test' } })
- end
-
- it 'does not contain yaml errors' do
- expect(pipeline).not_to have_yaml_errors
- end
+ it 'returns false if yaml_errors is not set' do
+ expect(pipeline).not_to have_yaml_errors
end
end
@@ -2930,7 +2635,7 @@ describe Ci::Pipeline, :mailer do
end
shared_examples 'sending a notification' do
- it 'sends an email' do
+ it 'sends an email', :sidekiq_might_not_need_inline do
should_only_email(pipeline.user, kind: :bcc)
end
end
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index c1933c578bc..6b85f9bb127 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -54,7 +54,7 @@ describe Clusters::Applications::CertManager do
'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
])
expect(subject.postinstall).to eq([
- "for i in $(seq 1 30); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
+ "for i in $(seq 1 90); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
])
end
diff --git a/spec/models/clusters/applications/crossplane_spec.rb b/spec/models/clusters/applications/crossplane_spec.rb
new file mode 100644
index 00000000000..ebc675497f4
--- /dev/null
+++ b/spec/models/clusters/applications/crossplane_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::Crossplane do
+ let(:crossplane) { create(:clusters_applications_crossplane) }
+
+ include_examples 'cluster application core specs', :clusters_applications_crossplane
+ include_examples 'cluster application status specs', :clusters_applications_crossplane
+ include_examples 'cluster application version specs', :clusters_applications_crossplane
+ include_examples 'cluster application initial status specs'
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:stack) }
+ end
+
+ describe '#can_uninstall?' do
+ subject { crossplane.can_uninstall? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#install_command' do
+ let(:stack) { 'gcp' }
+
+ subject { crossplane.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'is initialized with crossplane arguments' do
+ expect(subject.name).to eq('crossplane')
+ expect(subject.chart).to eq('crossplane/crossplane')
+ expect(subject.repository).to eq('https://charts.crossplane.io/alpha')
+ expect(subject.version).to eq('0.4.1')
+ expect(subject).to be_rbac
+ end
+
+ context 'application failed to install previously' do
+ let(:crossplane) { create(:clusters_applications_crossplane, :errored, version: '0.0.1') }
+
+ it 'is initialized with the locked version' do
+ expect(subject.version).to eq('0.4.1')
+ end
+ end
+ end
+
+ describe '#files' do
+ let(:application) { crossplane }
+ let(:values) { subject[:'values.yaml'] }
+
+ subject { application.files }
+
+ it 'includes crossplane specific keys in the values.yaml file' do
+ expect(values).to include('clusterStacks')
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
new file mode 100644
index 00000000000..d0e0dd5ad57
--- /dev/null
+++ b/spec/models/clusters/applications/elastic_stack_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::ElasticStack do
+ include KubernetesHelpers
+
+ include_examples 'cluster application core specs', :clusters_applications_elastic_stack
+ include_examples 'cluster application status specs', :clusters_applications_elastic_stack
+ include_examples 'cluster application version specs', :clusters_applications_elastic_stack
+ include_examples 'cluster application helm specs', :clusters_applications_elastic_stack
+
+ describe '#can_uninstall?' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ subject { elastic_stack.can_uninstall? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#set_initial_status' do
+ before do
+ elastic_stack.set_initial_status
+ end
+
+ context 'when ingress is not installed' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) }
+
+ it { expect(elastic_stack).to be_not_installable }
+ end
+
+ context 'when ingress is installed and external_ip is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ it { expect(elastic_stack).to be_installable }
+ end
+
+ context 'when ingress is installed and external_hostname is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ it { expect(elastic_stack).to be_installable }
+ end
+ end
+
+ describe '#install_command' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ subject { elastic_stack.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ 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.8.0')
+ expect(subject).to be_rbac
+ expect(subject.files).to eq(elastic_stack.files)
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ elastic_stack.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ 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.8.0')
+ end
+ end
+ end
+
+ describe '#uninstall_command' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ subject { elastic_stack.uninstall_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
+
+ it 'is initialized with elastic stack arguments' do
+ expect(subject.name).to eq('elastic-stack')
+ expect(subject).to be_rbac
+ expect(subject.files).to eq(elastic_stack.files)
+ end
+
+ it 'specifies a post delete command to remove custom resource definitions' do
+ expect(subject.postdelete).to eq([
+ 'kubectl delete pvc --selector release\\=elastic-stack'
+ ])
+ end
+ end
+
+ describe '#files' do
+ let!(:ingress) { create(:clusters_applications_ingress, :installed, external_ip: '127.0.0.1') }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: ingress.cluster) }
+
+ let(:values) { subject[:'values.yaml'] }
+
+ subject { elastic_stack.files }
+
+ it 'includes elastic stack specific keys in the values.yaml file' do
+ expect(values).to include('ELASTICSEARCH_HOSTS')
+ end
+ end
+
+ describe '#elasticsearch_client' do
+ context 'cluster is nil' do
+ it 'returns nil' do
+ expect(subject.cluster).to be_nil
+ expect(subject.elasticsearch_client).to be_nil
+ end
+ end
+
+ context "cluster doesn't have kubeclient" do
+ let(:cluster) { create(:cluster) }
+ subject { create(:clusters_applications_elastic_stack, cluster: cluster) }
+
+ it 'returns nil' do
+ expect(subject.elasticsearch_client).to be_nil
+ end
+ end
+
+ context 'cluster has kubeclient' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url }
+ let(:kube_client) { subject.cluster.kubeclient.core_client }
+
+ subject { create(:clusters_applications_elastic_stack, cluster: cluster) }
+
+ before do
+ subject.cluster.platform_kubernetes.namespace = 'a-namespace'
+ stub_kubeclient_discover(cluster.platform_kubernetes.api_url)
+
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
+
+ it 'creates proxy elasticsearch_client' do
+ expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client)
+ end
+
+ it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do
+ expect(Elasticsearch::Client)
+ .to(receive(:new))
+ .with(url: a_valid_url)
+ .and_call_original
+
+ client = subject.elasticsearch_client
+ faraday_connection = client.transport.connections.first.connection
+
+ expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization])
+ expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store)
+ expect(faraday_connection.ssl.verify).to eq(1)
+ end
+
+ context 'when cluster is not reachable' do
+ before do
+ allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ end
+
+ it 'returns nil' do
+ expect(subject.elasticsearch_client).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index be0c6df7ad6..d7ad7867e1a 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -21,7 +21,7 @@ describe Clusters::Applications::Ingress do
describe '#can_uninstall?' do
subject { ingress.can_uninstall? }
- it 'returns true if application_jupyter_nil_or_installable? AND external_ip_or_hostname? are true' do
+ it 'returns true if external ip is set and no application exists' do
ingress.external_ip = 'IP'
is_expected.to be_truthy
@@ -33,6 +33,12 @@ describe Clusters::Applications::Ingress do
is_expected.to be_falsey
end
+ it 'returns false if application_elastic_stack_nil_or_installable? is false' do
+ create(:clusters_applications_elastic_stack, :installed, cluster: ingress.cluster)
+
+ is_expected.to be_falsey
+ end
+
it 'returns false if external_ip_or_hostname? is false' do
is_expected.to be_falsey
end
@@ -150,6 +156,21 @@ describe Clusters::Applications::Ingress do
it 'includes modsecurity core ruleset enablement' do
expect(subject.values).to include("enable-owasp-modsecurity-crs: 'true'")
end
+
+ it 'includes modsecurity.conf content' do
+ expect(subject.values).to include('modsecurity.conf')
+ # Includes file content from Ingress#modsecurity_config_content
+ expect(subject.values).to include('SecAuditLog')
+
+ expect(subject.values).to include('extraVolumes')
+ expect(subject.values).to include('extraVolumeMounts')
+ end
+
+ it 'includes modsecurity sidecar container' do
+ expect(subject.values).to include('modsecurity-log-volume')
+
+ expect(subject.values).to include('extraContainers')
+ end
end
context 'when ingress_modsecurity is disabled' do
@@ -166,6 +187,21 @@ describe Clusters::Applications::Ingress do
it 'excludes modsecurity core ruleset enablement' do
expect(subject.values).not_to include('enable-owasp-modsecurity-crs')
end
+
+ it 'excludes modsecurity.conf content' do
+ expect(subject.values).not_to include('modsecurity.conf')
+ # Excludes file content from Ingress#modsecurity_config_content
+ expect(subject.values).not_to include('SecAuditLog')
+
+ expect(subject.values).not_to include('extraVolumes')
+ expect(subject.values).not_to include('extraVolumeMounts')
+ end
+
+ it 'excludes modsecurity sidecar container' do
+ expect(subject.values).not_to include('modsecurity-log-volume')
+
+ expect(subject.values).not_to include('extraContainers')
+ end
end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 48e3b4d6bae..a163229e15a 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -29,6 +29,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:on_creation?).to(:provider) }
+ it { is_expected.to delegate_method(:knative_pre_installed?).to(:provider) }
it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix }
it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
@@ -55,7 +56,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
let!(:cluster) { create(:cluster, enabled: true) }
before do
- create(:cluster, enabled: false)
+ create(:cluster, :disabled)
end
it { is_expected.to contain_exactly(cluster) }
@@ -64,7 +65,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.disabled' do
subject { described_class.disabled }
- let!(:cluster) { create(:cluster, enabled: false) }
+ let!(:cluster) { create(:cluster, :disabled) }
before do
create(:cluster, enabled: true)
@@ -76,10 +77,10 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.user_provided' do
subject { described_class.user_provided }
- let!(:cluster) { create(:cluster, :provided_by_user) }
+ let!(:cluster) { create(:cluster_platform_kubernetes).cluster }
before do
- create(:cluster, :provided_by_gcp)
+ create(:cluster_provider_gcp, :created)
end
it { is_expected.to contain_exactly(cluster) }
@@ -88,7 +89,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.gcp_provided' do
subject { described_class.gcp_provided }
- let!(:cluster) { create(:cluster, :provided_by_gcp) }
+ let!(:cluster) { create(:cluster_provider_gcp, :created).cluster }
before do
create(:cluster, :provided_by_user)
@@ -100,7 +101,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.gcp_installed' do
subject { described_class.gcp_installed }
- let!(:cluster) { create(:cluster, :provided_by_gcp) }
+ let!(:cluster) { create(:cluster_provider_gcp, :created).cluster }
before do
create(:cluster, :providing_by_gcp)
@@ -112,7 +113,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.aws_provided' do
subject { described_class.aws_provided }
- let!(:cluster) { create(:cluster, :provided_by_aws) }
+ let!(:cluster) { create(:cluster_provider_aws, :created).cluster }
before do
create(:cluster, :provided_by_user)
@@ -124,11 +125,11 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
describe '.aws_installed' do
subject { described_class.aws_installed }
- let!(:cluster) { create(:cluster, :provided_by_aws) }
+ let!(:cluster) { create(:cluster_provider_aws, :created).cluster }
before do
- errored_cluster = create(:cluster, :provided_by_aws)
- errored_cluster.provider.make_errored!("Error message")
+ errored_provider = create(:cluster_provider_aws)
+ errored_provider.make_errored!("Error message")
end
it { is_expected.to contain_exactly(cluster) }
@@ -152,6 +153,16 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '.for_project_namespace' do
+ subject { described_class.for_project_namespace(namespace_id) }
+
+ let!(:cluster) { create(:cluster, :project) }
+ let!(:another_cluster) { create(:cluster, :project) }
+ let(:namespace_id) { cluster.first_project.namespace_id }
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
describe 'validations' do
subject { cluster.valid? }
@@ -504,13 +515,15 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching 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) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, cert_manager, prometheus, runner, jupyter, knative)
+ is_expected.to contain_exactly(helm, ingress, cert_manager, crossplane, prometheus, runner, jupyter, knative, elastic_stack)
end
end
end
@@ -675,12 +688,36 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'the cluster has a provider' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
+ let(:provider_status) { :errored }
before do
cluster.provider.make_errored!
end
- it { is_expected.to eq :errored }
+ it { is_expected.to eq provider_status }
+
+ context 'when cluster cleanup is ongoing' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status_name, :cleanup_status) do
+ provider_status | :cleanup_not_started
+ :cleanup_ongoing | :cleanup_uninstalling_applications
+ :cleanup_ongoing | :cleanup_removing_project_namespaces
+ :cleanup_ongoing | :cleanup_removing_service_account
+ :cleanup_errored | :cleanup_errored
+ end
+
+ with_them do
+ it 'returns cleanup_ongoing when uninstalling applications' do
+ cluster.cleanup_status = described_class
+ .state_machines[:cleanup_status]
+ .states[cleanup_status]
+ .value
+
+ is_expected.to eq status_name
+ end
+ end
+ end
end
context 'there is a cached connection status' do
@@ -704,6 +741,83 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe 'cleanup_status state_machine' do
+ shared_examples 'cleanup_status transition' do
+ let(:cluster) { create(:cluster, from_state) }
+
+ it 'transitions cleanup_status correctly' do
+ expect { subject }.to change { cluster.cleanup_status_name }
+ .from(from_state).to(to_state)
+ end
+
+ it 'schedules a Clusters::Cleanup::*Worker' do
+ expect(expected_worker_class).to receive(:perform_async).with(cluster.id)
+ subject
+ end
+ end
+
+ describe '#start_cleanup!' do
+ let(:expected_worker_class) { Clusters::Cleanup::AppWorker }
+ let(:to_state) { :cleanup_uninstalling_applications }
+
+ subject { cluster.start_cleanup! }
+
+ context 'when cleanup_status is cleanup_not_started' do
+ let(:from_state) { :cleanup_not_started }
+
+ it_behaves_like 'cleanup_status transition'
+ end
+
+ context 'when cleanup_status is errored' do
+ let(:from_state) { :cleanup_errored }
+
+ it_behaves_like 'cleanup_status transition'
+ end
+ end
+
+ describe '#make_cleanup_errored!' do
+ NON_ERRORED_STATES = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
+
+ NON_ERRORED_STATES.each do |state|
+ it "transitions cleanup_status from #{state} to cleanup_errored" do
+ cluster = create(:cluster, state)
+
+ expect { cluster.make_cleanup_errored! }.to change { cluster.cleanup_status_name }
+ .from(state).to(:cleanup_errored)
+ end
+
+ it "sets error message" do
+ cluster = create(:cluster, state)
+
+ expect { cluster.make_cleanup_errored!("Error Message") }.to change { cluster.cleanup_status_reason }
+ .from(nil).to("Error Message")
+ end
+ end
+ end
+
+ describe '#continue_cleanup!' do
+ context 'when cleanup_status is cleanup_uninstalling_applications' do
+ let(:expected_worker_class) { Clusters::Cleanup::ProjectNamespaceWorker }
+ let(:from_state) { :cleanup_uninstalling_applications }
+ let(:to_state) { :cleanup_removing_project_namespaces }
+
+ subject { cluster.continue_cleanup! }
+
+ it_behaves_like 'cleanup_status transition'
+ end
+
+ context 'when cleanup_status is cleanup_removing_project_namespaces' do
+ let(:expected_worker_class) { Clusters::Cleanup::ServiceAccountWorker }
+ let(:from_state) { :cleanup_removing_project_namespaces }
+ let(:to_state) { :cleanup_removing_service_account }
+
+ subject { cluster.continue_cleanup! }
+
+ it_behaves_like 'cleanup_status transition'
+ end
+ end
+ end
+
describe '#connection_status' do
let(:cluster) { create(:cluster) }
let(:status) { :connected }
@@ -804,26 +918,4 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
end
-
- describe '#knative_pre_installed?' do
- subject { cluster.knative_pre_installed? }
-
- context 'with a GCP provider without cloud_run' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
-
- it { is_expected.to be_falsey }
- end
-
- context 'with a GCP provider with cloud_run' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :cloud_run_enabled) }
-
- it { is_expected.to be_truthy }
- end
-
- context 'with a user provider' do
- let(:cluster) { create(:cluster, :provided_by_user) }
-
- it { is_expected.to be_falsey }
- end
- end
end
diff --git a/spec/models/clusters/clusters_hierarchy_spec.rb b/spec/models/clusters/clusters_hierarchy_spec.rb
index fc35b8257e9..1957e1fc5ee 100644
--- a/spec/models/clusters/clusters_hierarchy_spec.rb
+++ b/spec/models/clusters/clusters_hierarchy_spec.rb
@@ -42,6 +42,28 @@ describe Clusters::ClustersHierarchy do
it 'returns clusters for project' do
expect(base_and_ancestors(cluster.project)).to eq([cluster])
end
+
+ context 'cluster has management project' do
+ let(:management_project) { create(:project, namespace: cluster.first_project.namespace) }
+
+ before do
+ cluster.update!(management_project: management_project)
+ end
+
+ context 'management_project is in same namespace as cluster' do
+ it 'returns cluster for management_project' do
+ expect(base_and_ancestors(management_project)).to eq([cluster])
+ end
+ end
+
+ context 'management_project is in a different namespace from cluster' do
+ let(:management_project) { create(:project) }
+
+ it 'returns nothing' do
+ expect(base_and_ancestors(management_project)).to be_empty
+ end
+ end
+ end
end
context 'cluster has management project' do
@@ -50,16 +72,12 @@ describe Clusters::ClustersHierarchy do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
- let(:management_project) { create(:project) }
+ let(:management_project) { create(:project, group: group) }
it 'returns clusters for management_project' do
expect(base_and_ancestors(management_project)).to eq([group_cluster])
end
- it 'returns nothing if include_management_project is false' do
- expect(base_and_ancestors(management_project, include_management_project: false)).to be_empty
- end
-
it 'returns clusters for project' do
expect(base_and_ancestors(project)).to eq([project_cluster, group_cluster])
end
@@ -70,17 +88,21 @@ describe Clusters::ClustersHierarchy do
end
context 'project in nested group with clusters at some levels' do
- let!(:child) { create(:cluster, :group, groups: [child_group], management_project: management_project) }
- let!(:ancestor) { create(:cluster, :group, groups: [ancestor_group]) }
+ let!(:child) { create(:cluster, :group, groups: [child_group]) }
+ let!(:ancestor) { create(:cluster, :group, groups: [ancestor_group], management_project: management_project) }
let(:ancestor_group) { create(:group) }
let(:parent_group) { create(:group, parent: ancestor_group) }
let(:child_group) { create(:group, parent: parent_group) }
let(:project) { create(:project, group: child_group) }
- let(:management_project) { create(:project) }
+ let(:management_project) { create(:project, group: child_group) }
+
+ it 'returns clusters for management_project' do
+ expect(base_and_ancestors(management_project)).to eq([ancestor, child])
+ end
it 'returns clusters for management_project' do
- expect(base_and_ancestors(management_project)).to eq([child])
+ expect(base_and_ancestors(management_project, include_management_project: false)).to eq([child, ancestor])
end
it 'returns clusters for project' do
diff --git a/spec/models/clusters/providers/aws_spec.rb b/spec/models/clusters/providers/aws_spec.rb
index ec8159a7ee0..05d6e63288e 100644
--- a/spec/models/clusters/providers/aws_spec.rb
+++ b/spec/models/clusters/providers/aws_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
describe Clusters::Providers::Aws do
it { is_expected.to belong_to(:cluster) }
- it { is_expected.to belong_to(:created_by_user) }
it { is_expected.to validate_length_of(:key_name).is_at_least(1).is_at_most(255) }
it { is_expected.to validate_length_of(:region).is_at_least(1).is_at_most(255) }
@@ -64,13 +63,72 @@ describe Clusters::Providers::Aws do
before do
expect(provider.access_key_id).to be_present
expect(provider.secret_access_key).to be_present
+ expect(provider.session_token).to be_present
end
- it 'removes access_key_id and secret_access_key' do
+ it 'removes access_key_id, secret_access_key and session_token' do
subject
expect(provider.access_key_id).to be_nil
expect(provider.secret_access_key).to be_nil
+ expect(provider.session_token).to be_nil
end
end
+
+ describe '#api_client' do
+ let(:provider) { create(:cluster_provider_aws) }
+ let(:credentials) { double }
+ let(:client) { double }
+
+ subject { provider.api_client }
+
+ before do
+ allow(provider).to receive(:credentials).and_return(credentials)
+
+ expect(Aws::CloudFormation::Client).to receive(:new)
+ .with(credentials: credentials, region: provider.region)
+ .and_return(client)
+ end
+
+ it { is_expected.to eq client }
+ end
+
+ describe '#credentials' do
+ let(:provider) { create(:cluster_provider_aws) }
+ let(:credentials) { double }
+
+ subject { provider.credentials }
+
+ before do
+ expect(Aws::Credentials).to receive(:new)
+ .with(provider.access_key_id, provider.secret_access_key, provider.session_token)
+ .and_return(credentials)
+ end
+
+ it { is_expected.to eq credentials }
+ end
+
+ describe '#created_by_user' do
+ let(:provider) { create(:cluster_provider_aws) }
+
+ subject { provider.created_by_user }
+
+ it { is_expected.to eq provider.cluster.user }
+ end
+
+ describe '#has_rbac_enabled?' do
+ let(:provider) { create(:cluster_provider_aws) }
+
+ subject { provider.has_rbac_enabled? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#knative_pre_installed?' do
+ let(:provider) { create(:cluster_provider_aws) }
+
+ subject { provider.knative_pre_installed? }
+
+ it { is_expected.to be_falsey }
+ end
end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
index 15e152519b4..e2fd777d131 100644
--- a/spec/models/clusters/providers/gcp_spec.rb
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -78,12 +78,20 @@ describe Clusters::Providers::Gcp do
end
end
- describe '#legacy_abac?' do
- let(:gcp) { build(:cluster_provider_gcp) }
+ describe '#has_rbac_enabled?' do
+ subject { gcp.has_rbac_enabled? }
+
+ context 'when cluster is legacy_abac' do
+ let(:gcp) { create(:cluster_provider_gcp, :abac_enabled) }
+
+ it { is_expected.to be_falsey }
+ end
- subject { gcp }
+ context 'when cluster is not legacy_abac' do
+ let(:gcp) { create(:cluster_provider_gcp) }
- it { is_expected.not_to be_legacy_abac }
+ it { is_expected.to be_truthy }
+ end
end
describe '#knative_pre_installed?' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 95e9b0d0f92..1e1b679a32c 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -522,7 +522,7 @@ describe CommitStatus do
let(:stage) { Ci::Stage.first }
- it 'creates a new stage' do
+ it 'creates a new stage', :sidekiq_might_not_need_inline do
expect { commit_status }.to change { Ci::Stage.count }.by(1)
expect(stage.name).to eq 'test'
@@ -548,7 +548,7 @@ describe CommitStatus do
status: :success)
end
- it 'uses existing stage' do
+ it 'uses existing stage', :sidekiq_might_not_need_inline do
expect { commit_status }.not_to change { Ci::Stage.count }
expect(commit_status.stage_id).to eq stage.id
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index f99bf18768f..9164c3a75c5 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -13,7 +13,11 @@ describe DeploymentPlatform do
end
context 'when project is the cluster\'s management project ' do
- let!(:cluster_with_management_project) { create(:cluster, :provided_by_user, management_project: project) }
+ let(:another_project) { create(:project, namespace: project.namespace) }
+
+ let!(:cluster_with_management_project) do
+ create(:cluster, :provided_by_user, projects: [another_project], management_project: project)
+ end
context 'cluster_management_project feature is enabled' do
it 'returns the cluster with management project' do
@@ -66,7 +70,11 @@ describe DeploymentPlatform do
end
context 'when project is the cluster\'s management project ' do
- let!(:cluster_with_management_project) { create(:cluster, :provided_by_user, management_project: project) }
+ let(:another_project) { create(:project, namespace: project.namespace) }
+
+ let!(:cluster_with_management_project) do
+ create(:cluster, :provided_by_user, projects: [another_project], management_project: project)
+ end
context 'cluster_management_project feature is enabled' do
it 'returns the cluster with management project' do
@@ -130,5 +138,13 @@ describe DeploymentPlatform do
end
end
end
+
+ context 'when instance has configured kubernetes cluster' do
+ let!(:instance_cluster) { create(:cluster, :provided_by_user, :instance) }
+
+ it 'returns the Kubernetes platform' do
+ is_expected.to eq(instance_cluster.platform_kubernetes)
+ end
+ end
end
end
diff --git a/spec/models/concerns/from_union_spec.rb b/spec/models/concerns/from_union_spec.rb
index ee427a667c6..735e14b47ec 100644
--- a/spec/models/concerns/from_union_spec.rb
+++ b/spec/models/concerns/from_union_spec.rb
@@ -15,7 +15,7 @@ describe FromUnion do
it 'selects from the results of the UNION' do
query = model.from_union([model.where(id: 1), model.where(id: 2)])
- expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) users/m)
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) users/m)
end
it 'supports the use of a custom alias for the sub query' do
@@ -24,7 +24,7 @@ describe FromUnion do
alias_as: 'kittens'
)
- expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) kittens/m)
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\nUNION\n\(SELECT.+\)\) kittens/m)
end
it 'supports keeping duplicate rows' do
@@ -34,7 +34,7 @@ describe FromUnion do
)
expect(query.to_sql)
- .to match(/FROM \(SELECT.+UNION ALL.+SELECT.+\) users/m)
+ .to match(/FROM \(\(SELECT.+\)\nUNION ALL\n\(SELECT.+\)\) users/m)
end
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index e8116f0a301..f7bef9e71e2 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -111,6 +111,34 @@ describe Issuable do
end
end
+ describe '.initialize' do
+ it 'maps the state to the right state_id' do
+ described_class::STATE_ID_MAP.each do |key, value|
+ issuable = MergeRequest.new(state: key)
+
+ expect(issuable.state).to eq(key)
+ expect(issuable.state_id).to eq(value)
+ end
+ end
+
+ it 'maps a string version of the state to the right state_id' do
+ described_class::STATE_ID_MAP.each do |key, value|
+ issuable = MergeRequest.new('state' => key)
+
+ expect(issuable.state).to eq(key)
+ expect(issuable.state_id).to eq(value)
+ end
+ end
+
+ it 'gives preference to state_id if present' do
+ issuable = MergeRequest.new('state' => 'opened',
+ 'state_id' => described_class::STATE_ID_MAP['merged'])
+
+ expect(issuable.state).to eq('merged')
+ expect(issuable.state_id).to eq(described_class::STATE_ID_MAP['merged'])
+ end
+ end
+
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index f823ac0165f..e8991a3a015 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -177,50 +177,6 @@ describe Noteable do
end
end
- describe "#discussions_to_be_resolved?" do
- context "when discussions are not resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when discussions are resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(true)
-
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable discussions are resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when some resolvable discussions are not resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.discussions_to_be_resolved?).to be true
- end
- end
- end
- end
-
describe "#discussions_to_be_resolved" do
before do
allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb
index 57c7d2cb767..3f6a2e2410c 100644
--- a/spec/models/concerns/redactable_spec.rb
+++ b/spec/models/concerns/redactable_spec.rb
@@ -7,44 +7,6 @@ describe Redactable do
stub_commonmark_sourcepos_disabled
end
- shared_examples 'model with redactable field' do
- it 'redacts unsubscribe token' do
- model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
-
- model.save!
-
- expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
- end
-
- it 'ignores not hexadecimal tokens' do
- text = 'some text /sent_notifications/token/unsubscribe more text'
- model[field] = text
-
- model.save!
-
- expect(model[field]).to eq text
- end
-
- it 'ignores not matching texts' do
- text = 'some text /sent_notifications/.*/unsubscribe more text'
- model[field] = text
-
- model.save!
-
- expect(model[field]).to eq text
- end
-
- it 'redacts the field when saving the model before creating markdown cache' do
- model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
-
- model.save!
-
- expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
- expect(model[field]).to eq expected
- expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
- end
- end
-
context 'when model is an issue' do
it_behaves_like 'model with redactable field' do
let(:model) { create(:issue) }
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 2f88adf08dd..f189cd7633c 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -133,4 +133,60 @@ describe Subscribable, 'Subscribable' do
end
end
end
+
+ describe '#set_subscription' do
+ shared_examples 'setting subscriptions' do
+ context 'when desired_state is set to true' do
+ context 'when a user is subscribed to the resource' do
+ it 'keeps the user subscribed' do
+ resource.subscriptions.create(user: user_1, subscribed: true, project: resource_project)
+
+ resource.set_subscription(user_1, true, resource_project)
+
+ expect(resource.subscribed?(user_1, resource_project)).to be_truthy
+ end
+ end
+
+ context 'when a user is not subscribed to the resource' do
+ it 'subscribes the user to the resource' do
+ expect { resource.set_subscription(user_1, true, resource_project) }
+ .to change { resource.subscribed?(user_1, resource_project) }
+ .from(false).to(true)
+ end
+ end
+ end
+
+ context 'when desired_state is set to false' do
+ context 'when a user is subscribed to the resource' do
+ it 'unsubscribes the user from the resource' do
+ resource.subscriptions.create(user: user_1, subscribed: true, project: resource_project)
+
+ expect { resource.set_subscription(user_1, false, resource_project) }
+ .to change { resource.subscribed?(user_1, resource_project) }
+ .from(true).to(false)
+ end
+ end
+
+ context 'when a user is not subscribed to the resource' do
+ it 'keeps the user unsubscribed' do
+ resource.set_subscription(user_1, false, resource_project)
+
+ expect(resource.subscribed?(user_1, resource_project)).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'without project' do
+ let(:resource_project) { nil }
+
+ it_behaves_like 'setting subscriptions'
+ end
+
+ context 'with project' do
+ let(:resource_project) { project }
+
+ it_behaves_like 'setting subscriptions'
+ end
+ end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index eea539746a5..0a3065140bf 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -235,4 +235,36 @@ describe ContainerRepository do
expect(repository).not_to be_persisted
end
end
+
+ describe '.for_group_and_its_subgroups' do
+ subject { described_class.for_group_and_its_subgroups(test_group) }
+
+ context 'in a group' do
+ let(:test_group) { group }
+
+ it { is_expected.to contain_exactly(repository) }
+ end
+
+ context 'with a subgroup' do
+ let(:test_group) { create(:group) }
+ let(:another_project) { create(:project, path: 'test', group: test_group) }
+
+ let(:another_repository) do
+ create(:container_repository, name: 'my_image', project: another_project)
+ end
+
+ before do
+ group.parent = test_group
+ group.save
+ end
+
+ it { is_expected.to contain_exactly(repository, another_repository) }
+ end
+
+ context 'group without container_repositories' do
+ let(:test_group) { create(:group) }
+
+ it { is_expected.to eq([]) }
+ end
+ end
end
diff --git a/spec/models/deployment_merge_request_spec.rb b/spec/models/deployment_merge_request_spec.rb
new file mode 100644
index 00000000000..fd5be52d47c
--- /dev/null
+++ b/spec/models/deployment_merge_request_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeploymentMergeRequest do
+ let(:mr) { create(:merge_request, :merged) }
+ let(:deployment) { create(:deployment, :success, project: project) }
+ let(:project) { mr.project }
+
+ subject { described_class.new(deployment: deployment, merge_request: mr) }
+
+ it { is_expected.to belong_to(:deployment).required }
+ it { is_expected.to belong_to(:merge_request).required }
+end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 3a0b3c46ad0..52c19d4814c 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -10,6 +10,8 @@ describe Deployment do
it { is_expected.to belong_to(:cluster).class_name('Clusters::Cluster') }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:deployable) }
+ it { is_expected.to have_many(:deployment_merge_requests) }
+ it { is_expected.to have_many(:merge_requests).through(:deployment_merge_requests) }
it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
it { is_expected.to delegate_method(:commit).to(:project) }
@@ -361,4 +363,82 @@ describe Deployment do
.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#previous_deployment' do
+ it 'returns the previous deployment' do
+ deploy1 = create(:deployment)
+ deploy2 = create(
+ :deployment,
+ project: deploy1.project,
+ environment: deploy1.environment
+ )
+
+ expect(deploy2.previous_deployment).to eq(deploy1)
+ end
+ end
+
+ describe '#link_merge_requests' do
+ it 'links merge requests with a deployment' do
+ deploy = create(:deployment)
+ mr1 = create(
+ :merge_request,
+ :merged,
+ target_project: deploy.project,
+ source_project: deploy.project
+ )
+
+ mr2 = create(
+ :merge_request,
+ :merged,
+ target_project: deploy.project,
+ source_project: deploy.project
+ )
+
+ deploy.link_merge_requests(deploy.project.merge_requests)
+
+ expect(deploy.merge_requests).to include(mr1, mr2)
+ end
+ end
+
+ describe '#previous_environment_deployment' do
+ it 'returns the previous deployment of the same environment' do
+ deploy1 = create(:deployment, :success, ref: 'v1.0.0')
+ deploy2 = create(
+ :deployment,
+ :success,
+ project: deploy1.project,
+ environment: deploy1.environment,
+ ref: 'v1.0.1'
+ )
+
+ expect(deploy2.previous_environment_deployment).to eq(deploy1)
+ end
+
+ it 'ignores deployments that were not successful' do
+ deploy1 = create(:deployment, :failed, ref: 'v1.0.0')
+ deploy2 = create(
+ :deployment,
+ :success,
+ project: deploy1.project,
+ environment: deploy1.environment,
+ ref: 'v1.0.1'
+ )
+
+ expect(deploy2.previous_environment_deployment).to be_nil
+ end
+
+ it 'ignores deployments for different environments' do
+ deploy1 = create(:deployment, :success, ref: 'v1.0.0')
+ preprod = create(:environment, project: deploy1.project, name: 'preprod')
+ deploy2 = create(
+ :deployment,
+ :success,
+ project: deploy1.project,
+ environment: preprod,
+ ref: 'v1.0.1'
+ )
+
+ expect(deploy2.previous_environment_deployment).to be_nil
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 786f3b832c4..47e39e5fbe5 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe Environment, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
using RSpec::Parameterized::TableSyntax
+ include RepoHelpers
let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) }
@@ -259,7 +260,7 @@ describe Environment, :use_clean_rails_memory_store_caching do
let(:head_commit) { project.commit }
let(:commit) { project.commit.parent }
- it 'returns deployment id for the environment' do
+ it 'returns deployment id for the environment', :sidekiq_might_not_need_inline do
expect(environment.first_deployment_for(commit.id)).to eq deployment1
end
@@ -267,7 +268,7 @@ describe Environment, :use_clean_rails_memory_store_caching do
expect(environment.first_deployment_for(head_commit.id)).to eq nil
end
- it 'returns a UTF-8 ref' do
+ it 'returns a UTF-8 ref', :sidekiq_might_not_need_inline do
expect(environment.first_deployment_for(commit.id).ref).to be_utf8
end
end
@@ -505,6 +506,14 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ context 'when there is a deployment record with failed status' do
+ let!(:deployment) { create(:deployment, :failed, environment: environment) }
+
+ it 'returns the previous deployment' do
+ is_expected.to eq(previous_deployment)
+ end
+ end
+
context 'when there is a deployment record with success status' do
let!(:deployment) { create(:deployment, :success, environment: environment) }
@@ -515,6 +524,131 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe '#last_visible_deployment' do
+ subject { environment.last_visible_deployment }
+
+ before do
+ allow_any_instance_of(Deployment).to receive(:create_ref)
+ end
+
+ context 'when there is an old deployment record' do
+ let!(:previous_deployment) { create(:deployment, :success, environment: environment) }
+
+ context 'when there is a deployment record with created status' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+
+ it { is_expected.to eq(previous_deployment) }
+ end
+
+ context 'when there is a deployment record with running status' do
+ let!(:deployment) { create(:deployment, :running, environment: environment) }
+
+ it { is_expected.to eq(deployment) }
+ end
+
+ context 'when there is a deployment record with success status' do
+ let!(:deployment) { create(:deployment, :success, environment: environment) }
+
+ it { is_expected.to eq(deployment) }
+ end
+
+ context 'when there is a deployment record with failed status' do
+ let!(:deployment) { create(:deployment, :failed, environment: environment) }
+
+ it { is_expected.to eq(deployment) }
+ end
+
+ context 'when there is a deployment record with canceled status' do
+ let!(:deployment) { create(:deployment, :canceled, environment: environment) }
+
+ it { is_expected.to eq(deployment) }
+ end
+ end
+ end
+
+ describe '#last_visible_pipeline' do
+ let(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let(:environment) { create(:environment, project: project) }
+ let(:commit) { project.commit }
+
+ let(:success_pipeline) do
+ create(:ci_pipeline, :success, project: project, user: user, sha: commit.sha)
+ end
+
+ let(:failed_pipeline) do
+ create(:ci_pipeline, :failed, project: project, user: user, sha: commit.sha)
+ end
+
+ it 'uses the last deployment even if it failed' do
+ pipeline = create(:ci_pipeline, project: project, user: user, sha: commit.sha)
+ ci_build = create(:ci_build, project: project, pipeline: pipeline)
+ create(:deployment, :failed, project: project, environment: environment, deployable: ci_build, sha: commit.sha)
+
+ last_pipeline = environment.last_visible_pipeline
+
+ expect(last_pipeline).to eq(pipeline)
+ end
+
+ it 'returns nil if there is no deployment' do
+ create(:ci_build, project: project, pipeline: success_pipeline)
+
+ expect(environment.last_visible_pipeline).to be_nil
+ end
+
+ it 'does not return an invisible pipeline' do
+ failed_pipeline = create(:ci_pipeline, project: project, user: user, sha: commit.sha)
+ ci_build_a = create(:ci_build, project: project, pipeline: failed_pipeline)
+ create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_a, sha: commit.sha)
+ pipeline = create(:ci_pipeline, project: project, user: user, sha: commit.sha)
+ ci_build_b = create(:ci_build, project: project, pipeline: pipeline)
+ create(:deployment, :created, project: project, environment: environment, deployable: ci_build_b, sha: commit.sha)
+
+ last_pipeline = environment.last_visible_pipeline
+
+ expect(last_pipeline).to eq(failed_pipeline)
+ end
+
+ context 'for the environment' do
+ it 'returns the last pipeline' do
+ pipeline = create(:ci_pipeline, project: project, user: user, sha: commit.sha)
+ ci_build = create(:ci_build, project: project, pipeline: pipeline)
+ create(:deployment, :success, project: project, environment: environment, deployable: ci_build, sha: commit.sha)
+
+ last_pipeline = environment.last_visible_pipeline
+
+ expect(last_pipeline).to eq(pipeline)
+ end
+
+ context 'with multiple deployments' do
+ it 'returns the last pipeline' do
+ pipeline_a = create(:ci_pipeline, project: project, user: user)
+ pipeline_b = create(:ci_pipeline, project: project, user: user)
+ ci_build_a = create(:ci_build, project: project, pipeline: pipeline_a)
+ ci_build_b = create(:ci_build, project: project, pipeline: pipeline_b)
+ create(:deployment, :success, project: project, environment: environment, deployable: ci_build_a)
+ create(:deployment, :success, project: project, environment: environment, deployable: ci_build_b)
+
+ last_pipeline = environment.last_visible_pipeline
+
+ expect(last_pipeline).to eq(pipeline_b)
+ end
+ end
+
+ context 'with multiple pipelines' do
+ it 'returns the last pipeline' do
+ create(:ci_build, project: project, pipeline: success_pipeline)
+ ci_build_b = create(:ci_build, project: project, pipeline: failed_pipeline)
+ create(:deployment, :failed, project: project, environment: environment, deployable: ci_build_b, sha: commit.sha)
+
+ last_pipeline = environment.last_visible_pipeline
+
+ expect(last_pipeline).to eq(failed_pipeline)
+ end
+ end
+ end
+ end
+
describe '#has_terminals?' do
subject { environment.has_terminals? }
@@ -610,6 +744,12 @@ describe Environment, :use_clean_rails_memory_store_caching do
allow(environment).to receive(:deployment_platform).and_return(double)
end
+ context 'reactive cache configuration' do
+ it 'does not continue to spawn jobs' do
+ expect(described_class.reactive_cache_lifetime).to be < described_class.reactive_cache_refresh_interval
+ end
+ end
+
context 'reactive cache is empty' do
before do
stub_reactive_cache(environment, nil)
@@ -727,6 +867,51 @@ describe Environment, :use_clean_rails_memory_store_caching do
end
end
+ describe '#prometheus_status' do
+ context 'when a cluster is present' do
+ context 'when a deployment platform is present' do
+ let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:environment) { create(:environment, project: cluster.project) }
+
+ subject { environment.prometheus_status }
+
+ context 'when the prometheus application status is :updating' do
+ let!(:prometheus) { create(:clusters_applications_prometheus, :updating, cluster: cluster) }
+
+ it { is_expected.to eq(:updating) }
+ end
+
+ context 'when the prometheus application state is :updated' do
+ let!(:prometheus) { create(:clusters_applications_prometheus, :updated, cluster: cluster) }
+
+ it { is_expected.to eq(:updated) }
+ end
+
+ context 'when the prometheus application is not installed' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when a deployment platform is not present' do
+ let(:cluster) { create(:cluster, :project) }
+ let(:environment) { create(:environment, project: cluster.project) }
+
+ subject { environment.prometheus_status }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'when a cluster is not present' do
+ let(:project) { create(:project, :stubbed_repository) }
+ let(:environment) { create(:environment, project: project) }
+
+ subject { environment.prometheus_status }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
let(:metric_params) { [] }
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 01d331f518b..eea81d7c128 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -113,7 +113,7 @@ describe EnvironmentStatus do
head_pipeline: pipeline)
end
- it 'returns environment status' do
+ it 'returns environment status', :sidekiq_might_not_need_inline do
expect(subject.count).to eq(1)
expect(subject[0].environment).to eq(environment)
expect(subject[0].merge_request).to eq(merge_request)
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 21e381d9fb7..dbd3f8ffab3 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -208,6 +208,28 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
expect(sentry_client).to have_received(:list_issues)
end
end
+
+ context 'when sentry client raises Sentry::Client::ResponseInvalidSizeError' do
+ let(:sentry_client) { spy(:sentry_client) }
+ let(:error_msg) {"Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."}
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(subject).to receive(:sentry_client).and_return(sentry_client)
+ allow(sentry_client).to receive(:list_issues).with(opts)
+ .and_raise(Sentry::Client::ResponseInvalidSizeError, error_msg)
+ end
+
+ it 'returns error' do
+ expect(result).to eq(
+ error: error_msg,
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_INVALID_SIZE
+ )
+ expect(subject).to have_received(:sentry_client)
+ expect(sentry_client).to have_received(:list_issues)
+ end
+ end
end
describe '#list_sentry_projects' do
diff --git a/spec/models/evidence_spec.rb b/spec/models/evidence_spec.rb
index 00788c2c391..8f534517fc1 100644
--- a/spec/models/evidence_spec.rb
+++ b/spec/models/evidence_spec.rb
@@ -27,7 +27,7 @@ describe Evidence do
let(:release) { create(:release, project: project, name: nil) }
it 'creates a valid JSON object' do
- expect(release.name).to be_nil
+ expect(release.name).to eq(release.tag)
expect(summary_json).to match_schema(schema_file)
end
end
diff --git a/spec/models/grafana_integration_spec.rb b/spec/models/grafana_integration_spec.rb
index f8973097a40..615865e17b9 100644
--- a/spec/models/grafana_integration_spec.rb
+++ b/spec/models/grafana_integration_spec.rb
@@ -34,5 +34,36 @@ describe GrafanaIntegration do
internal_url
).for(:grafana_url)
end
+
+ it 'disallows non-booleans in enabled column' do
+ is_expected.not_to allow_value(
+ nil
+ ).for(:enabled)
+ end
+
+ it 'allows booleans in enabled column' do
+ is_expected.to allow_value(
+ true,
+ false
+ ).for(:enabled)
+ end
+ end
+
+ describe '.client' do
+ subject(:grafana_integration) { create(:grafana_integration) }
+
+ context 'with grafana integration disabled' do
+ it 'returns a grafana client' do
+ expect(grafana_integration.client).to be_an_instance_of(::Grafana::Client)
+ end
+ end
+
+ context 'with grafana integration enabled' do
+ it 'returns nil' do
+ grafana_integration.update(enabled: false)
+
+ expect(grafana_integration.client).to be(nil)
+ end
+ end
end
end
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
new file mode 100644
index 00000000000..e4ad5703a10
--- /dev/null
+++ b/spec/models/group_group_link_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupGroupLink do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:shared_group) { create(:group) }
+ let_it_be(:group_group_link) do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: group)
+ end
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:shared_group) }
+ it { is_expected.to belong_to(:shared_with_group) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:shared_group) }
+
+ it do
+ is_expected.to(
+ validate_uniqueness_of(:shared_group_id)
+ .scoped_to(:shared_with_group_id)
+ .with_message('The group has already been shared with this group'))
+ end
+
+ it { is_expected.to validate_presence_of(:shared_with_group) }
+ it { is_expected.to validate_presence_of(:group_access) }
+
+ it do
+ is_expected.to(
+ validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 520421ac5e3..3fa9d71cc7d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -525,6 +525,128 @@ describe Group do
it { expect(subject.parent).to be_kind_of(described_class) }
end
+ describe '#max_member_access_for_user' do
+ context 'group shared with another group' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ 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(:shared_group_parent) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group,
+ 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
+ end
+
+ 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
+ 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
+ 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 }
+
+ 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 }
+
+ 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 }
+
+ 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
+ end
+
+ context 'multiple groups shared with group' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :private) }
+ let(:shared_group_parent) { create(:group, :private) }
+ 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,
+ shared_group: shared_group,
+ group_access: GroupMember::DEVELOPER })
+ create(:group_group_link, { shared_with_group: group,
+ shared_group: shared_group_parent,
+ group_access: GroupMember::MAINTAINER })
+ end
+
+ it 'returns correct access level' do
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::MAINTAINER)
+ end
+ end
+ end
+
describe '#members_with_parents' do
let!(:group) { create(:group, :nested) }
let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) }
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index a4d202dc4f8..94f1b0cba2e 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -16,7 +16,7 @@ describe SystemHook do
end
end
- describe "execute" do
+ describe "execute", :sidekiq_might_not_need_inline do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 18a1a30eee5..0f78cb4d9b1 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -423,6 +423,19 @@ describe Issue do
issue = create(:issue, title: 'testing-issue', confidential: true)
expect(issue.to_branch_name).to match /confidential-issue\z/
end
+
+ context 'issue title longer than 100 characters' do
+ let(:issue) { create(:issue, iid: 999, title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit Mauris sit amet ipsum id lacus custom fringilla convallis') }
+
+ it "truncates branch name to at most 100 characters" do
+ expect(issue.to_branch_name.length).to be <= 100
+ end
+
+ it "truncates dangling parts of the branch name" do
+ # 100 characters would've got us "999-lorem...lacus-custom-fri".
+ expect(issue.to_branch_name).to eq("999-lorem-ipsum-dolor-sit-amet-consectetur-adipiscing-elit-mauris-sit-amet-ipsum-id-lacus-custom")
+ end
+ end
end
describe '#can_be_worked_on?' do
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 47cae5cf197..44445429d3e 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -3,6 +3,18 @@
require 'spec_helper'
describe LfsObject do
+ context 'scopes' do
+ describe '.not_existing_in_project' do
+ it 'contains only lfs objects not linked to the project' do
+ project = create(:project)
+ create(:lfs_objects_project, project: project)
+ other_lfs_object = create(:lfs_object)
+
+ expect(described_class.not_linked_to_project(project)).to contain_exactly(other_lfs_object)
+ end
+ end
+ end
+
it 'has a distinct has_many :projects relation through lfs_objects_projects' do
lfs_object = create(:lfs_object)
project = create(:project)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index b86663fd7d9..0f7f68e0b38 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -378,6 +378,14 @@ describe MergeRequestDiff do
expect(diff_with_commits.commit_shas).not_to be_empty
expect(diff_with_commits.commit_shas).to all(match(/\h{40}/))
end
+
+ context 'with limit attribute' do
+ it 'returns limited number of shas' do
+ expect(diff_with_commits.commit_shas(limit: 2).size).to eq(2)
+ expect(diff_with_commits.commit_shas(limit: 100).size).to eq(29)
+ expect(diff_with_commits.commit_shas.size).to eq(29)
+ end
+ end
end
describe '#compare_with' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index ad79bee8801..b5aa05fd8b4 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -283,6 +283,16 @@ describe MergeRequest do
end
end
+ describe '.by_merge_commit_sha' do
+ it 'returns merge requests that match the given merge commit' do
+ mr = create(:merge_request, :merged, merge_commit_sha: '123abc')
+
+ create(:merge_request, :merged, merge_commit_sha: '123def')
+
+ expect(described_class.by_merge_commit_sha('123abc')).to eq([mr])
+ end
+ end
+
describe '.in_projects' do
it 'returns the merge requests for a set of projects' do
expect(described_class.in_projects(Project.all)).to eq([subject])
@@ -1190,7 +1200,7 @@ describe MergeRequest do
context 'diverged on fork' do
subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
- it 'counts commits that are on target branch but not on source branch' do
+ it 'counts commits that are on target branch but not on source branch', :sidekiq_might_not_need_inline do
expect(subject.diverged_commits_count).to eq(29)
end
end
@@ -1251,13 +1261,49 @@ describe MergeRequest do
end
describe '#commit_shas' do
- before do
- allow(subject.merge_request_diff).to receive(:commit_shas)
- .and_return(['sha1'])
+ context 'persisted merge request' do
+ context 'with a limit' do
+ it 'returns a limited number of commit shas' do
+ expect(subject.commit_shas(limit: 2)).to eq(%w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ end
+ end
+
+ context 'without a limit' do
+ it 'returns all commit shas of the merge request diff' do
+ expect(subject.commit_shas.size).to eq(29)
+ end
+ end
end
- it 'delegates to merge request diff' do
- expect(subject.commit_shas).to eq ['sha1']
+ context 'new merge request' do
+ subject { build(:merge_request) }
+
+ context 'compare commits' do
+ before do
+ subject.compare_commits = [
+ double(sha: 'sha1'), double(sha: 'sha2')
+ ]
+ end
+
+ context 'without a limit' do
+ it 'returns all shas of compare commits' do
+ expect(subject.commit_shas).to eq(%w[sha2 sha1])
+ end
+ end
+
+ context 'with a limit' do
+ it 'returns a limited number of shas' do
+ expect(subject.commit_shas(limit: 1)).to eq(['sha2'])
+ end
+ end
+ end
+
+ it 'returns diff_head_sha as an array' do
+ expect(subject.commit_shas).to eq([subject.diff_head_sha])
+ expect(subject.commit_shas(limit: 2)).to eq([subject.diff_head_sha])
+ end
end
end
@@ -1674,6 +1720,63 @@ describe MergeRequest do
end
end
+ describe '#find_exposed_artifacts' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :with_test_reports, source_project: project) }
+ let(:pipeline) { merge_request.head_pipeline }
+
+ subject { merge_request.find_exposed_artifacts }
+
+ context 'when head pipeline has exposed artifacts' do
+ let!(:job) do
+ create(:ci_build, options: { artifacts: { expose_as: 'artifact', paths: ['ci_artifacts.txt'] } }, 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 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 status and data' do
+ expect(subject[:status]).to eq(:parsed)
+ end
+
+ context 'when an error occurrs' do
+ before do
+ expect_next_instance_of(Ci::FindExposedArtifactsService) do |service|
+ expect(service).to receive(:for_pipeline)
+ .and_raise(StandardError.new)
+ end
+ end
+
+ it 'returns an error message' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
+ context 'when cached results is not latest' do
+ before do
+ allow_next_instance_of(Ci::GenerateExposedArtifactsReportService) do |service|
+ allow(service).to receive(:latest?).and_return(false)
+ end
+ end
+
+ it 'raises and InvalidateReactiveCache error' do
+ expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
+ end
+ end
+ end
+ end
+ end
+
describe '#compare_test_reports' do
subject { merge_request.compare_test_reports }
@@ -1831,7 +1934,7 @@ describe MergeRequest do
context 'when the MR has been merged' do
before do
MergeRequests::MergeService
- .new(subject.target_project, subject.author)
+ .new(subject.target_project, subject.author, { sha: subject.diff_head_sha })
.execute(subject)
end
@@ -2081,6 +2184,13 @@ describe MergeRequest do
expect { execute }.to raise_error(ActiveRecord::StaleObjectError)
end
+
+ it "raises ActiveRecord::LockWaitTimeout after 6 tries" do
+ expect(merge_request).to receive(:with_lock).exactly(6).times.and_raise(ActiveRecord::LockWaitTimeout)
+ expect(RebaseWorker).not_to receive(:perform_async)
+
+ expect { execute }.to raise_error(MergeRequest::RebaseLockTimeout)
+ end
end
describe '#mergeable?' do
@@ -2103,6 +2213,50 @@ describe MergeRequest do
end
end
+ describe '#check_mergeability' do
+ let(:mergeability_service) { double }
+
+ before do
+ allow(MergeRequests::MergeabilityCheckService).to receive(:new) do
+ mergeability_service
+ end
+ end
+
+ context 'if the merge status is unchecked' do
+ before do
+ subject.mark_as_unchecked!
+ end
+
+ it 'executes MergeabilityCheckService' do
+ expect(mergeability_service).to receive(:execute)
+
+ subject.check_mergeability
+ end
+ end
+
+ context 'if the merge status is checked' do
+ context 'and feature flag is enabled' do
+ it 'executes MergeabilityCheckService' do
+ expect(mergeability_service).not_to receive(:execute)
+
+ subject.check_mergeability
+ end
+ end
+
+ context 'and feature flag is disabled' do
+ before do
+ stub_feature_flags(merge_requests_conditional_mergeability_check: false)
+ end
+
+ it 'does not execute MergeabilityCheckService' do
+ expect(mergeability_service).to receive(:execute)
+
+ subject.check_mergeability
+ end
+ end
+ end
+ end
+
describe '#mergeable_state?' do
let(:project) { create(:project, :repository) }
@@ -2203,7 +2357,7 @@ describe MergeRequest do
allow(subject).to receive(:head_pipeline) { pipeline }
end
- it { expect(subject.mergeable_ci_state?).to be_truthy }
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
end
context 'when no pipeline is associated' do
@@ -2327,7 +2481,7 @@ describe MergeRequest do
create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
end
- it 'selects deployed environments' do
+ it 'selects deployed environments', :sidekiq_might_not_need_inline do
expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
end
@@ -2338,7 +2492,7 @@ describe MergeRequest do
create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
end
- it 'selects deployed environments' do
+ it 'selects deployed environments', :sidekiq_might_not_need_inline do
expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
end
end
@@ -2689,7 +2843,7 @@ describe MergeRequest do
describe '#mergeable_with_quick_action?' do
def create_pipeline(status)
- pipeline = create(:ci_pipeline_with_one_job,
+ pipeline = create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
@@ -2804,9 +2958,9 @@ describe MergeRequest do
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
- let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
- let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
- let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) }
+ let!(:first_pipeline) { create(:ci_pipeline, pipeline_arguments) }
+ let!(:last_pipeline) { create(:ci_pipeline, pipeline_arguments) }
+ let!(:last_pipeline_with_other_ref) { create(:ci_pipeline, pipeline_arguments.merge(ref: 'other')) }
it 'returns latest pipeline for the target branch' do
expect(merge_request.base_pipeline).to eq(last_pipeline)
@@ -2932,7 +3086,7 @@ describe MergeRequest do
describe '#unlock_mr' do
subject { create(:merge_request, state: 'locked', merge_jid: 123) }
- it 'updates merge request head pipeline and sets merge_jid to nil' do
+ it 'updates merge request head pipeline and sets merge_jid to nil', :sidekiq_might_not_need_inline do
pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
subject.unlock_mr
@@ -3304,7 +3458,7 @@ describe MergeRequest do
end
end
- describe '.with_open_merge_when_pipeline_succeeds' do
+ describe '.with_auto_merge_enabled' do
let!(:project) { create(:project) }
let!(:fork) { fork_project(project) }
let!(:merge_request1) do
@@ -3316,15 +3470,6 @@ describe MergeRequest do
source_branch: 'feature-1')
end
- let!(:merge_request2) do
- create(:merge_request,
- :merge_when_pipeline_succeeds,
- target_project: project,
- target_branch: 'master',
- source_project: fork,
- source_branch: 'fork-feature-1')
- end
-
let!(:merge_request4) do
create(:merge_request,
target_project: project,
@@ -3333,10 +3478,73 @@ describe MergeRequest do
source_branch: 'fork-feature-2')
end
- let(:query) { described_class.with_open_merge_when_pipeline_succeeds }
+ let(:query) { described_class.with_auto_merge_enabled }
- it { expect(query).to contain_exactly(merge_request1, merge_request2) }
+ it { expect(query).to contain_exactly(merge_request1) }
end
it_behaves_like 'versioned description'
+
+ describe '#commits' do
+ context 'persisted merge request' do
+ context 'with a limit' do
+ it 'returns a limited number of commits' do
+ expect(subject.commits(limit: 2).map(&:sha)).to eq(%w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ expect(subject.commits(limit: 3).map(&:sha)).to eq(%w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ 1b12f15a11fc6e62177bef08f47bc7b5ce50b141
+ ])
+ end
+ end
+
+ context 'without a limit' do
+ it 'returns all commits of the merge request diff' do
+ expect(subject.commits.size).to eq(29)
+ end
+ end
+ end
+
+ context 'new merge request' do
+ subject { build(:merge_request) }
+
+ context 'compare commits' do
+ let(:first_commit) { double }
+ let(:second_commit) { double }
+
+ before do
+ subject.compare_commits = [
+ first_commit, second_commit
+ ]
+ end
+
+ context 'without a limit' do
+ it 'returns all the compare commits' do
+ expect(subject.commits.to_a).to eq([second_commit, first_commit])
+ end
+ end
+
+ context 'with a limit' do
+ it 'returns a limited number of commits' do
+ expect(subject.commits(limit: 1).to_a).to eq([second_commit])
+ end
+ end
+ end
+ end
+ end
+
+ describe '#recent_commits' do
+ before do
+ stub_const("#{MergeRequestDiff}::COMMITS_SAFE_SIZE", 2)
+ end
+
+ it 'returns the safe number of commits' do
+ expect(subject.recent_commits.map(&:sha)).to eq(%w[
+ b83d6e391c22777fca1ed3012fce84f633d7fed0 498214de67004b1da3d820901307bed2a68a8ef6
+ ])
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 120ba67f328..45cd2768708 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -55,6 +55,17 @@ describe Milestone do
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) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 1e06d0fd7b9..c93e6aafd75 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -281,6 +281,44 @@ describe Namespace do
end
end
+ shared_examples 'move_dir without repository storage feature' do |storage_version|
+ let(:namespace) { create(:namespace) }
+ let(:gitlab_shell) { namespace.gitlab_shell }
+ let!(:project) { create(:project_empty_repo, namespace: namespace, storage_version: storage_version) }
+
+ it 'calls namespace service' do
+ expect(gitlab_shell).to receive(:add_namespace).and_return(true)
+ expect(gitlab_shell).to receive(:mv_namespace).and_return(true)
+
+ namespace.move_dir
+ end
+ end
+
+ shared_examples 'move_dir with repository storage feature' do |storage_version|
+ let(:namespace) { create(:namespace) }
+ let(:gitlab_shell) { namespace.gitlab_shell }
+ let!(:project) { create(:project_empty_repo, namespace: namespace, storage_version: storage_version) }
+
+ it 'does not call namespace service' do
+ expect(gitlab_shell).not_to receive(:add_namespace)
+ expect(gitlab_shell).not_to receive(:mv_namespace)
+
+ namespace.move_dir
+ end
+ end
+
+ context 'project is without repository storage feature' do
+ [nil, 0].each do |storage_version|
+ it_behaves_like 'move_dir without repository storage feature', storage_version
+ end
+ end
+
+ context 'project has repository storage feature' do
+ [1, 2].each do |storage_version|
+ it_behaves_like 'move_dir with repository storage feature', storage_version
+ end
+ end
+
context 'with subgroups' do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:new_parent) { create(:group, name: 'new_parent', path: 'new_parent') }
diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb
new file mode 100644
index 00000000000..276c8e22731
--- /dev/null
+++ b/spec/models/personal_snippet_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PersonalSnippet do
+ describe '#embeddable?' do
+ [
+ { snippet: :public, embeddable: true },
+ { snippet: :internal, embeddable: false },
+ { snippet: :private, embeddable: false }
+ ].each do |combination|
+ it 'returns true when snippet is public' do
+ snippet = build(:personal_snippet, combination[:snippet])
+
+ expect(snippet.embeddable?).to eq(combination[:embeddable])
+ end
+ end
+ end
+end
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index 8a847bbe24e..0b4dcc62ff6 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -27,7 +27,7 @@ describe ProjectImportState, type: :model do
expect(project.wiki.repository).to receive(:after_import).and_call_original
end
- it 'imports a project' do
+ it 'imports a project', :sidekiq_might_not_need_inline do
expect(RepositoryImportWorker).to receive(:perform_async).and_call_original
expect { import_state.schedule }.to change { import_state.jid }
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 cf7c7bf7e61..366ef01924e 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -159,6 +159,45 @@ describe ChatMessage::PipelineMessage do
)
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
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 2bde0b93fda..fe0b2fe3440 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -96,7 +96,7 @@ describe ChatMessage::PushMessage do
context 'without markdown' do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<http://url.com/commits/new_tag|new_tag> to ' \
+ '<http://url.com/-/tags/new_tag|new_tag> to ' \
'<http://url.com|project_name>')
expect(subject.attachments).to be_empty
end
@@ -109,10 +109,10 @@ describe ChatMessage::PushMessage do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag) to [project_name](http://url.com)')
+ 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag) to [project_name](http://url.com)')
expect(subject.attachments).to be_empty
expect(subject.activity).to eq(
- title: 'test.user pushed new tag [new_tag](http://url.com/commits/new_tag)',
+ title: 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag)',
subtitle: 'in [project_name](http://url.com)',
text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
image: 'http://someavatar.com'
diff --git a/spec/models/project_services/data_fields_spec.rb b/spec/models/project_services/data_fields_spec.rb
index 146db0ae227..6b388a7222b 100644
--- a/spec/models/project_services/data_fields_spec.rb
+++ b/spec/models/project_services/data_fields_spec.rb
@@ -74,6 +74,12 @@ describe DataFields do
expect(service.url_changed?).to be_falsy
end
end
+
+ describe 'data_fields_present?' do
+ it 'returns true from the issue tracker service' do
+ expect(service.data_fields_present?).to be true
+ end
+ end
end
context 'when data are stored in data_fields' do
@@ -92,6 +98,18 @@ describe DataFields do
end
end
+ context 'when service and data_fields are not persisted' do
+ let(:service) do
+ JiraService.new
+ end
+
+ describe 'data_fields_present?' do
+ it 'returns true' do
+ expect(service.data_fields_present?).to be true
+ end
+ end
+ end
+
context 'when data are stored in properties' do
let(:service) { create(:jira_service, :without_properties_callback, properties: properties) }
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 2e1f6964692..309dc51191b 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -60,7 +60,7 @@ describe IrkerService do
@irker_server.close
end
- it 'sends valid JSON messages to an Irker listener' do
+ it 'sends valid JSON messages to an Irker listener', :sidekiq_might_not_need_inline do
irker.execute(sample_data)
conn = @irker_server.accept
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index e5ac6ca65d6..bc22818ede7 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -65,6 +65,37 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
end
end
+
+ context 'with self-monitoring project and internal Prometheus' do
+ before do
+ service.api_url = 'http://localhost:9090'
+
+ stub_application_setting(instance_administration_project_id: project.id)
+ stub_config(prometheus: { enable: true, listen_address: 'localhost:9090' })
+ end
+
+ it 'allows self-monitoring project to connect to internal Prometheus' do
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be true
+ end
+ end
+ end
+
+ it 'does not allow self-monitoring project to connect to other local URLs' do
+ service.api_url = 'http://localhost:8000'
+
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be false
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index e87b4f41f4d..46025507cb5 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -10,4 +10,25 @@ describe ProjectSnippet do
describe "Validation" do
it { is_expected.to validate_presence_of(:project) }
end
+
+ describe '#embeddable?' do
+ [
+ { project: :public, snippet: :public, embeddable: true },
+ { project: :internal, snippet: :public, embeddable: false },
+ { project: :private, snippet: :public, embeddable: false },
+ { project: :public, snippet: :internal, embeddable: false },
+ { project: :internal, snippet: :internal, embeddable: false },
+ { project: :private, snippet: :internal, embeddable: false },
+ { project: :public, snippet: :private, embeddable: false },
+ { project: :internal, snippet: :private, embeddable: false },
+ { project: :private, snippet: :private, embeddable: false }
+ ].each do |combination|
+ it 'only returns true when both project and snippet are public' do
+ project = create(:project, combination[:project])
+ snippet = build(:project_snippet, combination[:snippet], project: project)
+
+ expect(snippet.embeddable?).to eq(combination[:embeddable])
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1bda3094e75..815ab7aa166 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2029,24 +2029,37 @@ describe Project do
end
describe '#ci_config_path=' do
- let(:project) { create(:project) }
+ using RSpec::Parameterized::TableSyntax
- it 'sets nil' do
- project.update!(ci_config_path: nil)
+ let(:project) { create(:project) }
- expect(project.ci_config_path).to be_nil
+ where(:default_ci_config_path, :project_ci_config_path, :expected_ci_config_path) do
+ nil | :notset | :default
+ nil | nil | :default
+ nil | '' | :default
+ nil | "cust\0om/\0/path" | 'custom//path'
+ '' | :notset | :default
+ '' | nil | :default
+ '' | '' | :default
+ '' | "cust\0om/\0/path" | 'custom//path'
+ 'global/path' | :notset | 'global/path'
+ 'global/path' | nil | :default
+ 'global/path' | '' | :default
+ 'global/path' | "cust\0om/\0/path" | 'custom//path'
end
- it 'sets a string' do
- project.update!(ci_config_path: 'foo/.gitlab_ci.yml')
-
- expect(project.ci_config_path).to eq('foo/.gitlab_ci.yml')
- end
+ with_them do
+ before do
+ stub_application_setting(default_ci_config_path: default_ci_config_path)
- it 'sets a string but removes all null characters' do
- project.update!(ci_config_path: "f\0oo/\0/.gitlab_ci.yml")
+ if project_ci_config_path != :notset
+ project.ci_config_path = project_ci_config_path
+ end
+ end
- expect(project.ci_config_path).to eq('foo//.gitlab_ci.yml')
+ it 'returns the correct path' do
+ expect(project.ci_config_path.presence || :default).to eq(expected_ci_config_path)
+ end
end
end
@@ -3342,22 +3355,6 @@ describe Project do
end
end
- describe '#append_or_update_attribute' do
- let(:project) { create(:project) }
-
- it 'shows full error updating an invalid MR' do
- expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }
- .to raise_error(ActiveRecord::RecordInvalid, /Failed to set merge_requests:/)
- end
-
- it 'updates the project successfully' do
- merge_request = create(:merge_request, target_project: project, source_project: project)
-
- expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }
- .not_to raise_error
- end
- end
-
describe '#update' do
let(:project) { create(:project) }
@@ -4284,22 +4281,25 @@ describe Project do
describe '#check_repository_path_availability' do
let(:project) { build(:project, :repository, :legacy_storage) }
- subject { project.check_repository_path_availability }
context 'when the repository already exists' do
let(:project) { create(:project, :repository, :legacy_storage) }
- it { is_expected.to be_falsey }
+ it 'returns false when repository already exists' do
+ expect(project.check_repository_path_availability).to be_falsey
+ end
end
context 'when the repository does not exist' do
- it { is_expected.to be_truthy }
+ it 'returns false when repository already exists' do
+ expect(project.check_repository_path_availability).to be_truthy
+ end
it 'skips gitlab-shell exists?' do
project.skip_disk_validation = true
expect(project.gitlab_shell).not_to receive(:repository_exists?)
- is_expected.to be_truthy
+ expect(project.check_repository_path_availability).to be_truthy
end
end
end
@@ -4631,7 +4631,7 @@ describe Project do
end
describe '#any_branch_allows_collaboration?' do
- it 'allows access when there are merge requests open allowing collaboration' do
+ it 'allows access when there are merge requests open allowing collaboration', :sidekiq_might_not_need_inline do
expect(project.any_branch_allows_collaboration?(user))
.to be_truthy
end
@@ -4645,7 +4645,7 @@ describe Project do
end
describe '#branch_allows_collaboration?' do
- it 'allows access if the user can merge the merge request' do
+ it 'allows access if the user can merge the merge request', :sidekiq_might_not_need_inline do
expect(project.branch_allows_collaboration?(user, 'awesome-feature-1'))
.to be_truthy
end
@@ -4899,20 +4899,6 @@ describe Project do
end
end
- describe '.find_without_deleted' do
- it 'returns nil if the project is about to be removed' do
- project = create(:project, pending_delete: true)
-
- expect(described_class.find_without_deleted(project.id)).to be_nil
- end
-
- it 'returns a project when it is not about to be removed' do
- project = create(:project)
-
- expect(described_class.find_without_deleted(project.id)).to eq(project)
- end
- end
-
describe '.for_group' do
it 'returns the projects for a given group' do
group = create(:group)
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 0aac325c2b2..f9c7a14f1f3 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe Release do
expect(existing_release_without_name).to be_valid
expect(existing_release_without_name.description).to eq("change")
- expect(existing_release_without_name.name).to be_nil
+ expect(existing_release_without_name.name).not_to be_nil
end
end
@@ -57,14 +57,14 @@ RSpec.describe Release do
subject { release.assets_count }
it 'returns the number of sources' do
- is_expected.to eq(Releases::Source::FORMATS.count)
+ is_expected.to eq(Gitlab::Workhorse::ARCHIVE_FORMATS.count)
end
context 'when a links exists' do
let!(:link) { create(:release_link, release: release) }
it 'counts the link as an asset' do
- is_expected.to eq(1 + Releases::Source::FORMATS.count)
+ is_expected.to eq(1 + Gitlab::Workhorse::ARCHIVE_FORMATS.count)
end
it "excludes sources count when asked" do
@@ -92,7 +92,7 @@ RSpec.describe Release do
end
end
- describe 'evidence' do
+ describe 'evidence', :sidekiq_might_not_need_inline do
describe '#create_evidence!' do
context 'when a release is created' do
it 'creates one Evidence object too' do
@@ -129,4 +129,16 @@ RSpec.describe Release do
end
end
end
+
+ describe '#name' do
+ context 'name is nil' do
+ before do
+ release.update(name: nil)
+ end
+
+ it 'returns tag' do
+ expect(release.name).to eq(release.tag)
+ end
+ end
+ end
end
diff --git a/spec/models/releases/source_spec.rb b/spec/models/releases/source_spec.rb
index c5213196962..c8ac8e31c97 100644
--- a/spec/models/releases/source_spec.rb
+++ b/spec/models/releases/source_spec.rb
@@ -11,7 +11,7 @@ describe Releases::Source do
it 'returns all formats of sources' do
expect(subject.map(&:format))
- .to match_array(described_class::FORMATS)
+ .to match_array(Gitlab::Workhorse::ARCHIVE_FORMATS)
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 63d0bf3f314..79d45da8a1e 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -189,7 +189,7 @@ describe RemoteMirror, :mailer do
remote_mirror.project.add_maintainer(user)
end
- it 'notifies the project maintainers' do
+ it 'notifies the project maintainers', :sidekiq_might_not_need_inline do
perform_enqueued_jobs { subject }
should_email(user)
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 64077b76f01..f58bcbebd67 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -15,6 +15,26 @@ describe Service do
end
describe 'Scopes' do
+ describe '.by_type' do
+ let!(:service1) { create(:jira_service) }
+ let!(:service2) { create(:jira_service) }
+ let!(:service3) { create(:redmine_service) }
+
+ subject { described_class.by_type(type) }
+
+ context 'when type is "JiraService"' do
+ let(:type) { 'JiraService' }
+
+ it { is_expected.to match_array([service1, service2]) }
+ end
+
+ context 'when type is "RedmineService"' do
+ let(:type) { 'RedmineService' }
+
+ it { is_expected.to match_array([service3]) }
+ 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)
diff --git a/spec/models/shard_spec.rb b/spec/models/shard_spec.rb
index 83104711b55..4da86858b54 100644
--- a/spec/models/shard_spec.rb
+++ b/spec/models/shard_spec.rb
@@ -1,4 +1,5 @@
-# frozen_string_literals: true
+# frozen_string_literal: true
+
require 'spec_helper'
describe Shard do
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index f4dcbfbc190..e4cc8931840 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -451,41 +451,4 @@ describe Snippet do
expect(blob.data).to eq(snippet.content)
end
end
-
- describe '#embeddable?' do
- context 'project snippet' do
- [
- { project: :public, snippet: :public, embeddable: true },
- { project: :internal, snippet: :public, embeddable: false },
- { project: :private, snippet: :public, embeddable: false },
- { project: :public, snippet: :internal, embeddable: false },
- { project: :internal, snippet: :internal, embeddable: false },
- { project: :private, snippet: :internal, embeddable: false },
- { project: :public, snippet: :private, embeddable: false },
- { project: :internal, snippet: :private, embeddable: false },
- { project: :private, snippet: :private, embeddable: false }
- ].each do |combination|
- it 'only returns true when both project and snippet are public' do
- project = create(:project, combination[:project])
- snippet = create(:project_snippet, combination[:snippet], project: project)
-
- expect(snippet.embeddable?).to eq(combination[:embeddable])
- end
- end
- end
-
- context 'personal snippet' do
- [
- { snippet: :public, embeddable: true },
- { snippet: :internal, embeddable: false },
- { snippet: :private, embeddable: false }
- ].each do |combination|
- it 'only returns true when snippet is public' do
- snippet = create(:personal_snippet, combination[:snippet])
-
- expect(snippet.embeddable?).to eq(combination[:embeddable])
- end
- end
- end
- end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index e9ea234f75d..f4e073dc38f 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -20,7 +20,7 @@ describe SpamLog do
expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end
- it 'removes the user' do
+ it 'removes the user', :sidekiq_might_not_need_inline do
spam_log = build(:spam_log)
user = spam_log.user
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 487a1c619c6..ea09c6caed3 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -150,6 +150,19 @@ describe Todo do
end
end
+ describe '#done?' do
+ let_it_be(:todo1) { create(:todo, state: :pending) }
+ let_it_be(:todo2) { create(:todo, state: :done) }
+
+ it 'returns true for todos with done state' do
+ expect(todo2.done?).to be_truthy
+ end
+
+ it 'returns false for todos with state pending' do
+ expect(todo1.done?).to be_falsey
+ end
+ end
+
describe '#self_assigned?' do
let(:user_1) { build(:user) }
@@ -208,6 +221,40 @@ describe Todo do
expect(described_class.for_project(project1)).to eq([todo])
end
+
+ it 'returns the todos for many projects' do
+ project1 = create(:project)
+ project2 = create(:project)
+ project3 = create(:project)
+
+ todo1 = create(:todo, project: project1)
+ todo2 = create(:todo, project: project2)
+ create(:todo, project: project3)
+
+ expect(described_class.for_project([project2, project1])).to contain_exactly(todo2, todo1)
+ end
+ end
+
+ describe '.for_undeleted_projects' do
+ let(:project1) { create(:project) }
+ let(:project2) { create(:project) }
+ let(:project3) { create(:project) }
+
+ let!(:todo1) { create(:todo, project: project1) }
+ let!(:todo2) { create(:todo, project: project2) }
+ let!(:todo3) { create(:todo, project: project3) }
+
+ it 'returns the todos for a given project' do
+ expect(described_class.for_undeleted_projects).to contain_exactly(todo1, todo2, todo3)
+ end
+
+ context 'when todo belongs to deleted project' do
+ let(:project2) { create(:project, pending_delete: true) }
+
+ it 'excludes todos of deleted projects' do
+ expect(described_class.for_undeleted_projects).to contain_exactly(todo1, todo3)
+ end
+ end
end
describe '.for_group' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8eb2f9b5bc0..ee7edb1516c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe User do
+describe User, :do_not_mock_admin_mode do
include ProjectForksHelper
include TermsHelper
@@ -2797,10 +2797,26 @@ describe User do
expect(user.full_private_access?).to be_falsy
end
- it 'returns true for admin user' do
- user = build(:user, :admin)
+ context 'for admin user' do
+ include_context 'custom session'
- expect(user.full_private_access?).to be_truthy
+ let(:user) { build(:user, :admin) }
+
+ context 'when admin mode is disabled' do
+ it 'returns false' do
+ expect(user.full_private_access?).to be_falsy
+ end
+ end
+
+ context 'when admin mode is enabled' do
+ before do
+ Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
+ end
+
+ it 'returns true' do
+ expect(user.full_private_access?).to be_truthy
+ end
+ end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 9014276dcf8..a7c28519c5a 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -563,17 +563,6 @@ describe WikiPage do
end
end
- describe '#formatted_content' do
- it 'returns processed content of the page' do
- subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" })
- page = wiki.find_page('RDoc')
-
- expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n")
-
- destroy_page('RDoc')
- end
- end
-
describe '#hook_attrs' do
it 'adds absolute urls for images in the content' do
create_page("test page", "test![WikiPage_Image](/uploads/abc/WikiPage_Image.png)")
diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb
new file mode 100644
index 00000000000..3dad957a1ce
--- /dev/null
+++ b/spec/models/zoom_meeting_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ZoomMeeting do
+ let(:project) { build(:project) }
+
+ describe 'Factory' do
+ subject { build(:zoom_meeting) }
+
+ it { is_expected.to be_valid }
+ end
+
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:issue).required }
+ end
+
+ describe 'scopes' do
+ let(:issue) { create(:issue, project: project) }
+ let!(:added_meeting) { create(:zoom_meeting, :added_to_issue, issue: issue) }
+ let!(:removed_meeting) { create(:zoom_meeting, :removed_from_issue, issue: issue) }
+
+ describe '.added_to_issue' do
+ it 'gets only added meetings' do
+ meetings_added = described_class.added_to_issue.pluck(:id)
+
+ expect(meetings_added).to include(added_meeting.id)
+ expect(meetings_added).not_to include(removed_meeting.id)
+ end
+ end
+ describe '.removed_from_issue' do
+ it 'gets only removed meetings' do
+ meetings_removed = described_class.removed_from_issue.pluck(:id)
+
+ expect(meetings_removed).to include(removed_meeting.id)
+ expect(meetings_removed).not_to include(added_meeting.id)
+ end
+ end
+ end
+
+ describe 'Validations' do
+ describe 'url' do
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to validate_length_of(:url).is_at_most(255) }
+
+ shared_examples 'invalid Zoom URL' do
+ it do
+ expect(subject).to be_invalid
+ expect(subject.errors[:url])
+ .to contain_exactly('must contain one valid Zoom URL')
+ end
+ end
+
+ context 'with non-Zoom URL' do
+ before do
+ subject.url = %{https://non-zoom.url}
+ end
+
+ include_examples 'invalid Zoom URL'
+ end
+
+ context 'with multiple Zoom-URLs' do
+ before do
+ subject.url = %{https://zoom.us/j/123 https://zoom.us/j/456}
+ end
+
+ include_examples 'invalid Zoom URL'
+ end
+ end
+
+ describe 'issue association' do
+ let(:issue) { build(:issue, project: project) }
+
+ subject { build(:zoom_meeting, project: project, issue: issue) }
+
+ context 'for the same project' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'for a different project' do
+ let(:issue) { build(:issue) }
+
+ it do
+ expect(subject).to be_invalid
+ expect(subject.errors[:issue])
+ .to contain_exactly('must associate the same project')
+ end
+ end
+ end
+ end
+
+ describe 'limit number of meetings per issue' do
+ shared_examples 'can add meetings' do
+ it 'can add new Zoom meetings' do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end
+ end
+
+ shared_examples 'can remove meetings' do
+ it 'can remove Zoom meetings' do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+ end
+
+ shared_examples 'cannot add meetings' do
+ it 'fails to add a new meeting' do
+ expect do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end.to raise_error ActiveRecord::RecordNotUnique
+ end
+ end
+
+ let(:issue) { create(:issue, project: project) }
+
+ context 'without meetings' do
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when no other meeting is added' do
+ before do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when meeting is added' do
+ before do
+ create(:zoom_meeting, :added_to_issue, issue: issue)
+ end
+
+ it_behaves_like 'cannot add meetings'
+ end
+
+ context 'when meeting is added to another issue' do
+ let(:another_issue) { create(:issue, project: project) }
+
+ before do
+ create(:zoom_meeting, :added_to_issue, issue: another_issue)
+ end
+
+ it_behaves_like 'can add meetings'
+ end
+
+ context 'when second meeting is removed' do
+ before do
+ create(:zoom_meeting, :removed_from_issue, issue: issue)
+ end
+
+ it_behaves_like 'can remove meetings'
+ end
+ end
+end
diff --git a/spec/policies/application_setting/term_policy_spec.rb b/spec/policies/application_setting/term_policy_spec.rb
index 93b5ebf5f72..21690d4b457 100644
--- a/spec/policies/application_setting/term_policy_spec.rb
+++ b/spec/policies/application_setting/term_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationSetting::TermPolicy do
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index 09be831dcd5..81aee4cfcac 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe BasePolicy do
+describe BasePolicy, :do_not_mock_admin_mode do
include ExternalAuthorizationServiceHelpers
+ include AdminModeHelper
describe '.class_for' do
it 'detects policy class based on the subject ancestors' do
@@ -34,8 +37,42 @@ describe BasePolicy do
it { is_expected.not_to be_allowed(:read_cross_project) }
- it 'allows admins' do
- expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project)
+ context 'for admins' do
+ let(:current_user) { build(:admin) }
+
+ subject { described_class.new(current_user, nil) }
+
+ it 'allowed when in admin mode' do
+ enable_admin_mode!(current_user)
+
+ is_expected.to be_allowed(:read_cross_project)
+ end
+
+ it 'prevented when not in admin mode' do
+ is_expected.not_to be_allowed(:read_cross_project)
+ end
+ end
+ end
+ end
+
+ describe 'full private access' do
+ let(:current_user) { create(:user) }
+
+ subject { described_class.new(current_user, nil) }
+
+ it { is_expected.not_to be_allowed(:read_all_resources) }
+
+ context 'for admins' do
+ let(:current_user) { build(:admin) }
+
+ it 'allowed when in admin mode' do
+ enable_admin_mode!(current_user)
+
+ is_expected.to be_allowed(:read_all_resources)
+ end
+
+ it 'prevented when not in admin mode' do
+ is_expected.not_to be_allowed(:read_all_resources)
end
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 79a616899fa..333f4e560cf 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildPolicy do
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 126d44d1860..293fe1fc5b9 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelinePolicy, :models do
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index 5a56e91cd69..700d7d1af0a 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelineSchedulePolicy, :models do
diff --git a/spec/policies/ci/trigger_policy_spec.rb b/spec/policies/ci/trigger_policy_spec.rb
index e9a85890082..e936277a391 100644
--- a/spec/policies/ci/trigger_policy_spec.rb
+++ b/spec/policies/ci/trigger_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::TriggerPolicy do
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index cc3dde154dc..55c3351a171 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::ClusterPolicy, :models do
diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb
index e7263d49613..aca93d8fe85 100644
--- a/spec/policies/deploy_key_policy_spec.rb
+++ b/spec/policies/deploy_key_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployKeyPolicy do
diff --git a/spec/policies/deploy_token_policy_spec.rb b/spec/policies/deploy_token_policy_spec.rb
index cef5a4a22bc..43e23ee55ac 100644
--- a/spec/policies/deploy_token_policy_spec.rb
+++ b/spec/policies/deploy_token_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployTokenPolicy do
diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb
index 0442b032e89..3d0f250740c 100644
--- a/spec/policies/environment_policy_spec.rb
+++ b/spec/policies/environment_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentPolicy do
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 880f1bcbc05..c18cc245468 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GlobalPolicy do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index aeb09c1dc3a..ae9d125f970 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupPolicy do
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
index 6d34b0a8b4b..18e35308ecd 100644
--- a/spec/policies/issuable_policy_spec.rb
+++ b/spec/policies/issuable_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssuablePolicy, models: true do
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 25267d36ab8..89fcf3c10df 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssuePolicy do
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index af4c9703eb4..287325e96df 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestPolicy do
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
index 909c17fe8b5..c0a5119c550 100644
--- a/spec/policies/namespace_policy_spec.rb
+++ b/spec/policies/namespace_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NamespacePolicy do
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index d18ded8bce9..5aee66275d4 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotePolicy do
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index 097000ceb6a..36b4ac16cf0 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb
@@ -18,6 +20,19 @@ describe PersonalSnippetPolicy do
described_class.new(user, snippet)
end
+ shared_examples 'admin access' do
+ context 'admin user' do
+ subject { permissions(admin_user) }
+
+ it do
+ is_expected.to be_allowed(:read_personal_snippet)
+ is_expected.to be_allowed(:create_note)
+ is_expected.to be_allowed(:award_emoji)
+ is_expected.to be_allowed(*author_permissions)
+ end
+ end
+ end
+
context 'public snippet' do
let(:snippet) { create(:personal_snippet, :public) }
@@ -53,6 +68,8 @@ describe PersonalSnippetPolicy do
is_expected.to be_allowed(*author_permissions)
end
end
+
+ it_behaves_like 'admin access'
end
context 'internal snippet' do
@@ -101,6 +118,8 @@ describe PersonalSnippetPolicy do
is_expected.to be_allowed(*author_permissions)
end
end
+
+ it_behaves_like 'admin access'
end
context 'private snippet' do
@@ -128,17 +147,6 @@ describe PersonalSnippetPolicy do
end
end
- context 'admin user' do
- subject { permissions(admin_user) }
-
- it do
- is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(:create_note)
- is_expected.to be_disallowed(:award_emoji)
- is_expected.to be_disallowed(*author_permissions)
- end
- end
-
context 'external user' do
subject { permissions(external_user) }
@@ -160,5 +168,7 @@ describe PersonalSnippetPolicy do
is_expected.to be_allowed(*author_permissions)
end
end
+
+ it_behaves_like 'admin access'
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index e61a064e82c..ab54d97f2a2 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectPolicy do
@@ -313,6 +315,31 @@ describe ProjectPolicy do
end
end
+ context 'pipeline feature' do
+ let(:project) { create(:project) }
+
+ describe 'for unconfirmed user' do
+ let(:unconfirmed_user) { create(:user, confirmed_at: nil) }
+ subject { described_class.new(unconfirmed_user, project) }
+
+ it 'disallows to modify pipelines' do
+ expect_disallowed(:create_pipeline)
+ expect_disallowed(:update_pipeline)
+ expect_disallowed(:create_pipeline_schedule)
+ end
+ end
+
+ describe 'for confirmed user' do
+ subject { described_class.new(developer, project) }
+
+ it 'allows modify pipelines' do
+ expect_allowed(:create_pipeline)
+ expect_allowed(:update_pipeline)
+ expect_allowed(:create_pipeline_schedule)
+ end
+ end
+ end
+
context 'builds feature' do
context 'when builds are disabled' do
subject { described_class.new(owner, project) }
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index 2e9ef1e89fd..3c68d33b1f3 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
index 1587196754d..ea7fd093e38 100644
--- a/spec/policies/protected_branch_policy_spec.rb
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranchPolicy do
diff --git a/spec/policies/resource_label_event_policy_spec.rb b/spec/policies/resource_label_event_policy_spec.rb
index 9206640ea00..799534d2b08 100644
--- a/spec/policies/resource_label_event_policy_spec.rb
+++ b/spec/policies/resource_label_event_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ResourceLabelEventPolicy do
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 7e0a1824200..9da9d2ce49b 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserPolicy do
diff --git a/spec/presenters/ci/bridge_presenter_spec.rb b/spec/presenters/ci/bridge_presenter_spec.rb
index 986818a7b9e..1c2eeced20c 100644
--- a/spec/presenters/ci/bridge_presenter_spec.rb
+++ b/spec/presenters/ci/bridge_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BridgePresenter do
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index e202f7a9b5f..b6c47f40ceb 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildPresenter do
@@ -267,7 +269,7 @@ describe Ci::BuildPresenter 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) }
+ 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|
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index fa8791f2257..017e94d04f1 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildRunnerPresenter do
diff --git a/spec/presenters/ci/group_variable_presenter_spec.rb b/spec/presenters/ci/group_variable_presenter_spec.rb
index cb58a757564..3b81a425f5b 100644
--- a/spec/presenters/ci/group_variable_presenter_spec.rb
+++ b/spec/presenters/ci/group_variable_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::GroupVariablePresenter do
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index 8cfcd9befb3..eca5d3e05fe 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelinePresenter do
diff --git a/spec/presenters/ci/trigger_presenter_spec.rb b/spec/presenters/ci/trigger_presenter_spec.rb
index 231b539c188..ac3967f4f77 100644
--- a/spec/presenters/ci/trigger_presenter_spec.rb
+++ b/spec/presenters/ci/trigger_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::TriggerPresenter do
diff --git a/spec/presenters/ci/variable_presenter_spec.rb b/spec/presenters/ci/variable_presenter_spec.rb
index e3ce88372ea..70cf2f539b6 100644
--- a/spec/presenters/ci/variable_presenter_spec.rb
+++ b/spec/presenters/ci/variable_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::VariablePresenter do
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 6b988e2645b..8bc5374f2db 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::ClusterPresenter do
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
index 2b7742ddbb8..b02497d4c11 100644
--- a/spec/presenters/commit_status_presenter_spec.rb
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitStatusPresenter do
diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
index b8b68a676e6..ac18d5203e5 100644
--- a/spec/presenters/conversational_development_index/metric_presenter_spec.rb
+++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ConversationalDevelopmentIndex::MetricPresenter do
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
index fa77273f6aa..11a8decc9cc 100644
--- a/spec/presenters/group_clusterable_presenter_spec.rb
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -43,6 +43,12 @@ describe GroupClusterablePresenter do
it { is_expected.to eq(new_group_cluster_path(group)) }
end
+ describe '#authorize_aws_role_path' do
+ subject { presenter.authorize_aws_role_path }
+
+ it { is_expected.to eq(authorize_aws_role_group_clusters_path(group)) }
+ end
+
describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path }
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
index bb66523a83d..382b1881ab7 100644
--- a/spec/presenters/group_member_presenter_spec.rb
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupMemberPresenter do
diff --git a/spec/presenters/instance_clusterable_presenter_spec.rb b/spec/presenters/instance_clusterable_presenter_spec.rb
new file mode 100644
index 00000000000..9f1268379f5
--- /dev/null
+++ b/spec/presenters/instance_clusterable_presenter_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe InstanceClusterablePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:presenter) { described_class.new(instance) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+ let(:instance) { cluster.instance }
+
+ describe '#create_aws_clusters_path' do
+ subject { described_class.new(instance).create_aws_clusters_path }
+
+ it { is_expected.to eq(create_aws_admin_clusters_path) }
+ end
+
+ describe '#authorize_aws_role_path' do
+ subject { described_class.new(instance).authorize_aws_role_path }
+
+ it { is_expected.to eq(authorize_aws_role_admin_clusters_path) }
+ end
+
+ describe '#revoke_aws_role_path' do
+ subject { described_class.new(instance).revoke_aws_role_path }
+
+ it { is_expected.to eq(revoke_aws_role_admin_clusters_path) }
+ end
+
+ describe '#aws_api_proxy_path' do
+ let(:resource) { 'resource' }
+
+ subject { described_class.new(instance).aws_api_proxy_path(resource) }
+
+ it { is_expected.to eq(aws_proxy_admin_clusters_path(resource: resource)) }
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 6408b0bd748..ce437090d43 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestPresenter do
diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb
index 6786a84243f..441c2a50fea 100644
--- a/spec/presenters/project_clusterable_presenter_spec.rb
+++ b/spec/presenters/project_clusterable_presenter_spec.rb
@@ -43,6 +43,12 @@ describe ProjectClusterablePresenter do
it { is_expected.to eq(new_project_cluster_path(project)) }
end
+ describe '#authorize_aws_role_path' do
+ subject { presenter.authorize_aws_role_path }
+
+ it { is_expected.to eq(authorize_aws_role_project_clusters_path(project)) }
+ end
+
describe '#create_user_clusters_path' do
subject { presenter.create_user_clusters_path }
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
index 73ef113a1c5..743c89fc7c2 100644
--- a/spec/presenters/project_member_presenter_spec.rb
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectMemberPresenter do
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 2a00548c2c3..ce095d2225f 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectPresenter do
@@ -310,8 +312,8 @@ describe ProjectPresenter do
project.add_developer(user)
allow(project.repository).to receive(:license_blob).and_return(nil)
- expect(presenter.license_anchor_data).to have_attributes(is_link: true,
- label: a_string_including('Add license'),
+ expect(presenter.license_anchor_data).to have_attributes(is_link: false,
+ label: a_string_including('Add LICENSE'),
link: presenter.add_license_path)
end
end
@@ -320,7 +322,7 @@ describe ProjectPresenter do
it 'returns anchor data' do
allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo'))
- expect(presenter.license_anchor_data).to have_attributes(is_link: true,
+ expect(presenter.license_anchor_data).to have_attributes(is_link: false,
label: a_string_including(presenter.license_short_name),
link: presenter.license_path)
end
@@ -418,6 +420,7 @@ describe ProjectPresenter do
it 'orders the items correctly' do
allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
+ allow(project.repository).to receive(:license_blob).and_return(nil)
allow(project.repository).to receive(:changelog).and_return(nil)
allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo'))
allow(presenter).to receive(:filename_path).and_return('fake/path')
@@ -431,25 +434,54 @@ describe ProjectPresenter do
end
end
- describe '#empty_repo_statistics_buttons' do
- let(:project) { create(:project, :repository) }
+ describe '#repo_statistics_buttons' do
let(:presenter) { described_class.new(project, current_user: user) }
-
subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons }
before do
- project.add_developer(user)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
end
- it 'orders the items correctly in an empty project' do
- expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
- a_string_including('New'),
- a_string_including('README'),
- a_string_including('CHANGELOG'),
- a_string_including('CONTRIBUTING'),
- a_string_including('CI/CD')
- )
+ context 'empty repo' do
+ let(:project) { create(:project, :stubbed_repository)}
+
+ context 'for a guest user' do
+ it 'orders the items correctly' do
+ expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
+ a_string_including('No license')
+ )
+ end
+ end
+
+ context 'for a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'orders the items correctly' do
+ expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
+ a_string_including('New'),
+ a_string_including('README'),
+ a_string_including('LICENSE'),
+ a_string_including('CHANGELOG'),
+ a_string_including('CONTRIBUTING'),
+ a_string_including('CI/CD')
+ )
+ end
+ end
+ end
+
+ context 'initialized repo' do
+ let(:project) { create(:project, :repository) }
+
+ it 'orders the items correctly' do
+ expect(empty_repo_statistics_buttons.map(&:label)).to start_with(
+ a_string_including('README'),
+ a_string_including('License'),
+ a_string_including('CHANGELOG'),
+ a_string_including('CONTRIBUTING')
+ )
+ end
end
end
end
diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
index b4bf39f3cdb..de58733c8ea 100644
--- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
+++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Settings::DeployKeysPresenter do
diff --git a/spec/presenters/release_presenter_spec.rb b/spec/presenters/release_presenter_spec.rb
new file mode 100644
index 00000000000..4d9fa7a4d75
--- /dev/null
+++ b/spec/presenters/release_presenter_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ReleasePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let(:developer) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:user) { developer }
+ let(:release) { create(:release, project: project) }
+ let(:presenter) { described_class.new(release, current_user: user) }
+
+ before do
+ project.add_developer(developer)
+ project.add_guest(guest)
+ end
+
+ describe '#commit_path' do
+ subject { presenter.commit_path }
+
+ it 'returns commit path' do
+ is_expected.to eq(project_commit_path(project, release.commit.id))
+ end
+
+ context 'when commit is not found' do
+ let(:release) { create(:release, project: project, sha: 'not-found') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#tag_path' do
+ subject { presenter.tag_path }
+
+ it 'returns tag path' do
+ is_expected.to eq(project_tag_path(project, release.tag))
+ end
+
+ context 'when user is guest' do
+ let(:user) { guest }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#merge_requests_url' do
+ subject { presenter.merge_requests_url }
+
+ it 'returns merge requests url' do
+ is_expected.to match /#{project_merge_requests_url(project)}/
+ end
+
+ context 'when release_mr_issue_urls feature flag is disabled' do
+ before do
+ stub_feature_flags(release_mr_issue_urls: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#issues_url' do
+ subject { presenter.issues_url }
+
+ it 'returns merge requests url' do
+ is_expected.to match /#{project_issues_url(project)}/
+ end
+
+ context 'when release_mr_issue_urls feature flag is disabled' do
+ before do
+ stub_feature_flags(release_mr_issue_urls: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#edit_url' do
+ subject { presenter.edit_url }
+
+ it 'returns release edit url' do
+ is_expected.to match /#{edit_project_release_url(project, release)}/
+ end
+
+ context 'when release_edit_page feature flag is disabled' do
+ before do
+ stub_feature_flags(release_edit_page: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 100f3d33c7b..3bfca00776f 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::AccessRequests do
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
index 53fc3096751..438d5dbf018 100644
--- a/spec/requests/api/applications_spec.rb
+++ b/spec/requests/api/applications_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Applications, :api do
diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb
index 9bc49bd5982..c8bc7f8a4a2 100644
--- a/spec/requests/api/avatar_spec.rb
+++ b/spec/requests/api/avatar_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Avatar do
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 342fcfa1041..80040cddd4d 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::AwardEmoji do
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 771a78a2d91..ea0a7d4c9b7 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Badges do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 0b9c0c2ebe9..8a67e956165 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Boards do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index f9c8b42afa8..675b06b057c 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Branches do
@@ -117,6 +119,25 @@ describe API::Branches do
it_behaves_like 'repository branches'
end
+
+ it 'does not submit N+1 DB queries', :request_store do
+ create(:protected_branch, name: 'master', project: project)
+
+ # Make sure no setup step query is recorded.
+ get api(route, current_user), params: { per_page: 100 }
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api(route, current_user), params: { per_page: 100 }
+ end
+
+ new_branch_name = 'protected-branch'
+ CreateBranchService.new(project, current_user).execute(new_branch_name, 'master')
+ create(:protected_branch, name: new_branch_name, project: project)
+
+ expect do
+ get api(route, current_user), params: { per_page: 100 }
+ end.not_to exceed_query_limit(control)
+ end
end
context 'when authenticated', 'as a guest' do
@@ -602,7 +623,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Invalid reference name')
+ expect(json_response['message']).to eq('Invalid reference name: new_design3')
end
end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 0b48b79219c..541acb29857 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::BroadcastMessages do
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 6cb02ba2f6b..639b8e96343 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::CommitStatuses do
@@ -278,7 +280,7 @@ describe API::CommitStatuses do
}
end
- it 'update the correct pipeline' do
+ it 'update the correct pipeline', :sidekiq_might_not_need_inline do
subject
expect(first_pipeline.reload.status).to eq('created')
@@ -302,7 +304,7 @@ describe API::CommitStatuses do
expect(json_response['status']).to eq('success')
end
- it 'retries a commit status' do
+ it 'retries a commit status', :sidekiq_might_not_need_inline do
expect(CommitStatus.count).to eq 2
expect(CommitStatus.first).to be_retried
expect(CommitStatus.last.pipeline).to be_success
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 90ff1d12bf1..d8da1c001b0 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'mime/types'
@@ -369,7 +371,7 @@ describe API::Commits do
valid_c_params[:start_project] = public_project.id
end
- it 'adds a new commit to forked_project and returns a 201' do
+ it 'adds a new commit to forked_project and returns a 201', :sidekiq_might_not_need_inline do
expect_request_with_status(201) { post api(url, guest), params: valid_c_params }
.to change { last_commit_id(forked_project, valid_c_params[:branch]) }
.and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
@@ -381,14 +383,14 @@ describe API::Commits do
valid_c_params[:start_project] = public_project.full_path
end
- it 'adds a new commit to forked_project and returns a 201' do
+ it 'adds a new commit to forked_project and returns a 201', :sidekiq_might_not_need_inline do
expect_request_with_status(201) { post api(url, guest), params: valid_c_params }
.to change { last_commit_id(forked_project, valid_c_params[:branch]) }
.and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
end
end
- context 'when branch already exists' do
+ context 'when branch already exists', :sidekiq_might_not_need_inline do
before do
valid_c_params.delete(:start_branch)
valid_c_params[:branch] = 'master'
@@ -835,7 +837,7 @@ describe API::Commits do
}
end
- it 'allows pushing to the source branch of the merge request' do
+ it 'allows pushing to the source branch of the merge request', :sidekiq_might_not_need_inline do
post api(url, user), params: push_params('feature')
expect(response).to have_gitlab_http_status(:created)
@@ -1087,6 +1089,20 @@ describe API::Commits do
expect(json_response.first.keys).to include 'diff'
end
+ context 'when hard limits are lower than the number of files' do
+ before do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 1)
+ end
+
+ it 'respects the limit' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to be <= 1
+ end
+ end
+
context 'when ref does not exist' do
let(:commit_id) { 'unknown' }
@@ -1360,6 +1376,12 @@ describe API::Commits do
it_behaves_like '400 response' do
let(:request) { post api(route, current_user), params: { branch: 'markdown' } }
end
+
+ it 'includes an error_code in the response' do
+ post api(route, current_user), params: { branch: 'markdown' }
+
+ expect(json_response['error_code']).to eq 'empty'
+ end
end
context 'when ref contains a dot' do
@@ -1417,7 +1439,7 @@ describe API::Commits do
let(:project_id) { forked_project.id }
- it 'allows access from a maintainer that to the source branch' do
+ it 'allows access from a maintainer that to the source branch', :sidekiq_might_not_need_inline do
post api(route, user), params: { branch: 'feature' }
expect(response).to have_gitlab_http_status(:created)
@@ -1519,6 +1541,19 @@ describe API::Commits do
let(:request) { post api(route, current_user) }
end
end
+
+ context 'when commit is already reverted in the target branch' do
+ it 'includes an error_code in the response' do
+ # First one actually reverts
+ post api(route, current_user), params: { branch: 'markdown' }
+
+ # Second one is redundant and should be empty
+ post api(route, current_user), params: { branch: 'markdown' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error_code']).to eq 'empty'
+ end
+ end
end
context 'when authenticated', 'as a developer' do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index e0cc18abcca..4579ccfad80 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::DeployKeys do
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index ad7be531979..26849c0991d 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -12,9 +12,9 @@ describe API::Deployments do
describe 'GET /projects/:id/deployments' do
let(:project) { create(:project) }
- let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now) }
- let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) }
- let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago) }
+ let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now, updated_at: Time.now) }
+ let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago, updated_at: 2.hours.ago) }
+ let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago, updated_at: 1.hour.ago) }
context 'as member of the project' do
it 'returns projects deployments sorted by id asc' do
@@ -57,6 +57,8 @@ describe API::Deployments do
'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3]
'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3]
'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2]
+ 'updated_at' | 'asc' | [:deployment_2, :deployment_3, :deployment_1]
+ 'updated_at' | 'desc' | [:deployment_1, :deployment_3, :deployment_2]
end
with_them do
@@ -137,14 +139,42 @@ describe API::Deployments do
expect(response).to have_gitlab_http_status(500)
end
+
+ it 'links any merged merge requests to the deployment' do
+ mr = create(
+ :merge_request,
+ :merged,
+ target_project: project,
+ source_project: project,
+ target_branch: 'master',
+ source_branch: 'foo'
+ )
+
+ post(
+ api("/projects/#{project.id}/deployments", user),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ deploy = project.deployments.last
+
+ expect(deploy.merge_requests).to eq([mr])
+ end
end
context 'as a developer' do
- it 'creates a new deployment' do
- developer = create(:user)
+ let(:developer) { create(:user) }
+ before do
project.add_developer(developer)
+ end
+ it 'creates a new deployment' do
post(
api("/projects/#{project.id}/deployments", developer),
params: {
@@ -161,6 +191,32 @@ describe API::Deployments do
expect(json_response['sha']).to eq(sha)
expect(json_response['ref']).to eq('master')
end
+
+ it 'links any merged merge requests to the deployment' do
+ mr = create(
+ :merge_request,
+ :merged,
+ target_project: project,
+ source_project: project,
+ target_branch: 'master',
+ source_branch: 'foo'
+ )
+
+ post(
+ api("/projects/#{project.id}/deployments", developer),
+ params: {
+ environment: 'production',
+ sha: sha,
+ ref: 'master',
+ tag: false,
+ status: 'success'
+ }
+ )
+
+ deploy = project.deployments.last
+
+ expect(deploy.merge_requests).to eq([mr])
+ end
end
context 'as non member' do
@@ -182,7 +238,7 @@ describe API::Deployments do
end
describe 'PUT /projects/:id/deployments/:deployment_id' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, :failed, project: project) }
let(:environment) { create(:environment, project: project) }
let(:deploy) do
@@ -191,7 +247,8 @@ describe API::Deployments do
:failed,
project: project,
environment: environment,
- deployable: nil
+ deployable: nil,
+ sha: project.commit.sha
)
end
@@ -216,6 +273,26 @@ describe API::Deployments do
expect(response).to have_gitlab_http_status(200)
expect(json_response['status']).to eq('success')
end
+
+ it 'links merge requests when the deployment status changes to success', :sidekiq_inline do
+ mr = create(
+ :merge_request,
+ :merged,
+ target_project: project,
+ source_project: project,
+ target_branch: 'master',
+ source_branch: 'foo'
+ )
+
+ put(
+ api("/projects/#{project.id}/deployments/#{deploy.id}", user),
+ params: { status: 'success' }
+ )
+
+ deploy = project.deployments.last
+
+ expect(deploy.merge_requests).to eq([mr])
+ end
end
context 'as a developer' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 0420201efe3..68f7d407b54 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Discussions do
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index cfee3f6c0f8..2a34e623a7e 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'doorkeeper access' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 745f3c55ac8..aa273e97209 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Environments do
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 992fd5e9c66..9f8d254a00c 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Events do
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 57a57e69a00..dfd14f89dbf 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Features do
@@ -118,14 +120,13 @@ describe API::Features do
post api("/features/#{feature_name}", admin), params: { value: 'true', user: user.username, feature_group: 'perf_team' }
expect(response).to have_gitlab_http_status(201)
- expect(json_response).to eq(
- 'name' => 'my_feature',
- 'state' => 'conditional',
- 'gates' => [
- { 'key' => 'boolean', 'value' => false },
- { 'key' => 'groups', 'value' => ['perf_team'] },
- { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
- ])
+ expect(json_response['name']).to eq('my_feature')
+ expect(json_response['state']).to eq('conditional')
+ expect(json_response['gates']).to contain_exactly(
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'groups', 'value' => ['perf_team'] },
+ { 'key' => 'actors', 'value' => ["User:#{user.id}"] }
+ )
end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 21b67357543..ec18156f49f 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Files do
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
new file mode 100644
index 00000000000..82deba0d92c
--- /dev/null
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Query current user todos' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ 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(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('todos'.classify)}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for('currentUser', {}, query_graphql_field('todos', {}, fields))
+ end
+
+ subject { graphql_data.dig('currentUser', 'todos', 'nodes') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'contains the expected ids' 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)
+ )
+ end
+
+ it 'returns Todos for all target types' do
+ is_expected.to include(
+ a_hash_including('targetType' => 'COMMIT'),
+ a_hash_including('targetType' => 'ISSUE'),
+ a_hash_including('targetType' => 'MERGEREQUEST')
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/current_user_query_spec.rb b/spec/requests/api/graphql/current_user_query_spec.rb
new file mode 100644
index 00000000000..9db638ea59e
--- /dev/null
+++ b/spec/requests/api/graphql/current_user_query_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting project information' do
+ include GraphqlHelpers
+
+ let(:query) do
+ graphql_query_for('currentUser', {}, 'name')
+ end
+
+ subject { graphql_data['currentUser'] }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ context 'when there is a current_user' do
+ set(:current_user) { create(:user) }
+
+ it_behaves_like 'a working graphql query'
+
+ it { is_expected.to include('name' => current_user.name) }
+ end
+
+ context 'when there is no current_user' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a working graphql query'
+
+ it { is_expected.to be_nil }
+ end
+end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index 1e799a0a42a..2aeb75a10b4 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'GitlabSchema configurations' do
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
new file mode 100644
index 00000000000..8f908b7bf88
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting assignees of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:assignee) { create(:user) }
+ let(:assignee2) { create(:user) }
+ let(:input) { { assignee_usernames: [assignee.username] } }
+ let(:expected_result) do
+ [{ 'username' => assignee.username }]
+ end
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_set_assignees, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ assignees {
+ nodes {
+ username
+ }
+ }
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_set_assignees)
+ end
+
+ def mutation_assignee_nodes
+ mutation_response['mergeRequest']['assignees']['nodes']
+ end
+
+ before do
+ project.add_developer(current_user)
+ project.add_developer(assignee)
+ project.add_developer(assignee2)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'does not allow members without the right permission to add assignees' do
+ user = create(:user)
+ project.add_guest(user)
+
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ context 'with assignees already assigned' do
+ before do
+ merge_request.assignees = [assignee2]
+ merge_request.save!
+ end
+
+ it 'replaces the assignee' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_assignee_nodes).to match_array(expected_result)
+ end
+ end
+
+ context 'when passing an empty list of assignees' do
+ let(:input) { { assignee_usernames: [] } }
+
+ before do
+ merge_request.assignees = [assignee2]
+ merge_request.save!
+ end
+
+ it 'removes assignee' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_assignee_nodes).to eq([])
+ end
+ end
+
+ context 'when passing append as true' do
+ let(:input) { { assignee_usernames: [assignee2.username], operation_mode: Types::MutationOperationModeEnum.enum[:append] } }
+
+ before do
+ # In CE, APPEND is a NOOP as you can't have multiple assignees
+ # We test multiple assignment in EE specs
+ stub_licensed_features(multiple_merge_request_assignees: false)
+
+ merge_request.assignees = [assignee]
+ merge_request.save!
+ end
+
+ it 'does not replace the assignee in CE' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_assignee_nodes).to match_array(expected_result)
+ end
+ end
+
+ context 'when passing remove as true' do
+ let(:input) { { assignee_usernames: [assignee.username], operation_mode: Types::MutationOperationModeEnum.enum[:remove] } }
+ let(:expected_result) { [] }
+
+ before do
+ merge_request.assignees = [assignee]
+ merge_request.save!
+ end
+
+ it 'removes the users in the list, while adding none' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_assignee_nodes).to match_array(expected_result)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
new file mode 100644
index 00000000000..2112ff0dc74
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting labels of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:label) { create(:label, project: project) }
+ let(:label2) { create(:label, project: project) }
+ let(:input) { { label_ids: [GitlabSchema.id_from_object(label).to_s] } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_set_labels, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ labels {
+ nodes {
+ id
+ }
+ }
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_set_labels)
+ end
+
+ def mutation_label_nodes
+ mutation_response['mergeRequest']['labels']['nodes']
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'sets the merge request labels, removing existing ones' do
+ merge_request.update(labels: [label2])
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_label_nodes.count).to eq(1)
+ expect(mutation_label_nodes[0]['id']).to eq(label.to_global_id.to_s)
+ end
+
+ context 'when passing label_ids empty array as input' do
+ let(:input) { { label_ids: [] } }
+
+ it 'removes the merge request labels' do
+ merge_request.update!(labels: [label])
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_label_nodes.count).to eq(0)
+ end
+ end
+
+ context 'when passing operation_mode as APPEND' do
+ let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:append], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
+
+ before do
+ merge_request.update!(labels: [label2])
+ end
+
+ it 'sets the labels, without removing others' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_label_nodes.count).to eq(2)
+ expect(mutation_label_nodes).to contain_exactly({ 'id' => label.to_global_id.to_s }, { 'id' => label2.to_global_id.to_s })
+ end
+ end
+
+ context 'when passing operation_mode as REMOVE' do
+ let(:input) { { operation_mode: Types::MutationOperationModeEnum.enum[:remove], label_ids: [GitlabSchema.id_from_object(label).to_s] } }
+
+ before do
+ merge_request.update!(labels: [label, label2])
+ end
+
+ it 'removes the labels, without removing others' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_label_nodes.count).to eq(1)
+ expect(mutation_label_nodes[0]['id']).to eq(label2.to_global_id.to_s)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
new file mode 100644
index 00000000000..c45da613591
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_locked_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting locked status of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:input) { { locked: true } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_set_locked, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ discussionLocked
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_set_locked)['mergeRequest']['discussionLocked']
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'marks the merge request as WIP' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(true)
+ end
+
+ it 'does not do anything if the merge request was already locked' do
+ merge_request.update!(discussion_locked: true)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(true)
+ end
+
+ context 'when passing locked false as input' do
+ let(:input) { { locked: false } }
+
+ it 'does not do anything if the merge request was not marked locked' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(false)
+ end
+
+ it 'unmarks the merge request as locked' do
+ merge_request.update!(discussion_locked: true)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(false)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
new file mode 100644
index 00000000000..bd558edf9c5
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_milestone_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting milestone of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:input) { { milestone_id: GitlabSchema.id_from_object(milestone).to_s } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_set_milestone, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ milestone {
+ id
+ }
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_set_milestone)
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'sets the merge request milestone' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['mergeRequest']['milestone']['id']).to eq(milestone.to_global_id.to_s)
+ end
+
+ context 'when passing milestone_id nil as input' do
+ let(:input) { { milestone_id: nil } }
+
+ it 'removes the merge request milestone' do
+ merge_request.update!(milestone: milestone)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['mergeRequest']['milestone']).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb
new file mode 100644
index 00000000000..975735bf246
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_subscription_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting subscribed status of a merge request' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:input) { { subscribed_state: true } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: merge_request.iid.to_s
+ }
+ graphql_mutation(:merge_request_set_subscription, variables.merge(input),
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ mergeRequest {
+ id
+ subscribed
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed']
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns an error if the user is not allowed to update the merge request' do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+
+ it 'marks the merge request as WIP' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(true)
+ end
+
+ context 'when passing subscribe false as input' do
+ let(:input) { { subscribed_state: false } }
+
+ it 'unmarks the merge request as subscribed' do
+ merge_request.subscribe(current_user, project)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response).to eq(false)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
index bbc477ba485..4492c51dbd7 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Setting WIP status of a merge request' do
diff --git a/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
new file mode 100644
index 00000000000..fabbb3aeb49
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/todos/mark_done_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Marking todos done' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:other_user) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
+ let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
+
+ let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
+
+ let(:input) { { id: todo1.to_global_id.to_s } }
+
+ let(:mutation) do
+ graphql_mutation(:todo_mark_done, input,
+ <<-QL.strip_heredoc
+ clientMutationId
+ errors
+ todo {
+ id
+ state
+ }
+ QL
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:todo_mark_done)
+ end
+
+ it 'marks a single todo as done' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(todo1.reload.state).to eq('done')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+
+ todo = mutation_response['todo']
+ expect(todo['id']).to eq(todo1.to_global_id.to_s)
+ expect(todo['state']).to eq('done')
+ end
+
+ context 'when todo is already marked done' do
+ let(:input) { { id: todo2.to_global_id.to_s } }
+
+ it 'has the expected response' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+
+ todo = mutation_response['todo']
+ expect(todo['id']).to eq(todo2.to_global_id.to_s)
+ expect(todo['state']).to eq('done')
+ end
+ end
+
+ context 'when todo does not belong to requesting user' do
+ let(:input) { { id: other_user_todo.to_global_id.to_s } }
+ let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' }
+
+ it 'contains the expected error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+ expect(errors).not_to be_blank
+ expect(errors.first['message']).to eq(access_error)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+ end
+ end
+
+ context 'when using an invalid gid' do
+ let(:input) { { id: 'invalid_gid' } }
+ let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
+
+ it 'contains the expected error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ errors = json_response['errors']
+ expect(errors).not_to be_blank
+ expect(errors.first['message']).to eq(invalid_gid_error)
+
+ expect(todo1.reload.state).to eq('pending')
+ expect(todo2.reload.state).to eq('done')
+ expect(other_user_todo.reload.state).to eq('pending')
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 4f9f916f22e..4ce7a3912a3 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'getting an issue list for a project' do
@@ -62,7 +64,7 @@ describe 'getting an issue list for a project' do
end
end
- it "is expected to check permissions on the first issue only" do
+ it 'is expected to check permissions on the first issue only' do
allow(Ability).to receive(:allowed?).and_call_original
# Newest first, we only want to see the newest checked
expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
@@ -114,4 +116,141 @@ describe 'getting an issue list for a project' do
end
end
end
+
+ describe 'sorting and pagination' do
+ let(:start_cursor) { graphql_data['project']['issues']['pageInfo']['startCursor'] }
+ let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] }
+
+ 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
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ 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
+ 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]
+ 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']
+
+ expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid]
+ end
+ 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 } } }"
+ )
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ 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]
+ end
+
+ context 'when paginating' do
+ let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' }
+
+ it 'sorts issues' do
+ expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid]
+
+ 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']
+
+ expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]
+ end
+ end
+ end
+ end
+ end
+
+ def grab_iids(data = issues_data)
+ data.map do |issue|
+ issue.dig('node', 'iid').to_i
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 74820d39102..70c21666799 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'getting merge request information nested in a project' do
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 0727ada4691..fbb22958d51 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'getting project information' do
diff --git a/spec/requests/api/group_boards_spec.rb b/spec/requests/api/group_boards_spec.rb
index b400a7f55ef..232ec9aca32 100644
--- a/spec/requests/api/group_boards_spec.rb
+++ b/spec/requests/api/group_boards_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::GroupBoards do
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index 46e3dd650cc..97465647a87 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -286,12 +286,15 @@ describe API::GroupClusters do
let(:update_params) do
{
domain: domain,
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
let(:domain) { 'new-domain.com' }
let(:platform_kubernetes_attributes) { {} }
+ let(:management_project) { create(:project, group: group) }
+ let(:management_project_id) { management_project.id }
let(:cluster) do
create(:cluster, :group, :provided_by_gcp,
@@ -308,6 +311,8 @@ describe API::GroupClusters do
context 'authorized user' do
before do
+ management_project.add_maintainer(current_user)
+
put api("/groups/#{group.id}/clusters/#{cluster.id}", current_user), params: update_params
cluster.reload
@@ -320,6 +325,7 @@ describe API::GroupClusters do
it 'updates cluster attributes' do
expect(cluster.domain).to eq('new-domain.com')
+ expect(cluster.management_project).to eq(management_project)
end
end
@@ -332,6 +338,7 @@ describe API::GroupClusters do
it 'does not update cluster attributes' do
expect(cluster.domain).to eq('old-domain.com')
+ expect(cluster.management_project).to be_nil
end
it 'returns validation errors' do
@@ -339,6 +346,18 @@ describe API::GroupClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with a GCP cluster' do
context 'when user tries to change GCP specific fields' do
let(:platform_kubernetes_attributes) do
diff --git a/spec/requests/api/group_container_repositories_spec.rb b/spec/requests/api/group_container_repositories_spec.rb
index 0a41e455d01..785006253d8 100644
--- a/spec/requests/api/group_container_repositories_spec.rb
+++ b/spec/requests/api/group_container_repositories_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
describe API::GroupContainerRepositories do
- set(:group) { create(:group, :private) }
- set(:project) { create(:project, :private, group: group) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, :private, group: group) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:guest) { create(:user) }
let(:root_repository) { create(:container_repository, :root, project: project) }
let(:test_repository) { create(:container_repository, project: project) }
@@ -44,6 +44,8 @@ describe API::GroupContainerRepositories do
let(:object) { group }
end
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories'
+
context 'with invalid group id' do
let(:url) { '/groups/123412341234/registry/repositories' }
diff --git a/spec/requests/api/group_export_spec.rb b/spec/requests/api/group_export_spec.rb
new file mode 100644
index 00000000000..ac4853e5388
--- /dev/null
+++ b/spec/requests/api/group_export_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::GroupExport do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ let(:path) { "/groups/#{group.id}/export" }
+ let(:download_path) { "/groups/#{group.id}/export/download" }
+
+ let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |import_export|
+ expect(import_export).to receive(:storage_path).and_return(export_path)
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ describe 'GET /groups/:group_id/export/download' do
+ let(:upload) { ImportExportUpload.new(group: group) }
+
+ before do
+ stub_uploads_object_storage(ImportExportUploader)
+
+ group.add_owner(user)
+ end
+
+ context 'when export file exists' do
+ before do
+ upload.export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz', "`/tar.gz")
+ upload.save!
+ end
+
+ it 'downloads exported group archive' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ context 'when export_file.file does not exist' do
+ before do
+ expect_next_instance_of(ImportExportUploader) do |uploader|
+ expect(uploader).to receive(:file).and_return(nil)
+ end
+ end
+
+ it 'returns 404' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when export file does not exist' do
+ it 'returns 404' do
+ get api(download_path, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /groups/:group_id/export' do
+ context 'when user is a group owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'accepts download' do
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(202)
+ end
+ end
+
+ context 'when user is not a group owner' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'forbids the request' do
+ post api(path, user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
index 6980eb7f55d..3e9b6246434 100644
--- a/spec/requests/api/group_milestones_spec.rb
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::GroupMilestones do
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index d50bae3dc47..abdc3a40360 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::GroupVariables do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 902a5ec2a86..cb97398805a 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Groups do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a1a007811fe..bbfe40041a1 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'raven/transports/dummy'
require_relative '../../../config/initializers/sentry'
diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb
index 68df02d4d8d..3ff7102479c 100644
--- a/spec/requests/api/import_github_spec.rb
+++ b/spec/requests/api/import_github_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ImportGithub do
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 01a2e33c0d9..fcff2cde730 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Internal::Base do
@@ -316,6 +318,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-get-all-lfs-pointers-go' => 'true', 'gitaly-feature-inforef-uploadpack-cache' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
@@ -335,6 +338,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-get-all-lfs-pointers-go' => 'true', 'gitaly-feature-inforef-uploadpack-cache' => 'true')
expect(user.reload.last_activity_on).to be_nil
end
end
@@ -407,7 +411,6 @@ describe API::Internal::Base do
context "custom action" do
let(:access_checker) { double(Gitlab::GitAccess) }
- let(:message) { 'CustomActionError message' }
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
@@ -418,8 +421,8 @@ describe API::Internal::Base do
}
}
end
-
- let(:custom_action_result) { Gitlab::GitAccessResult::CustomAction.new(payload, message) }
+ let(:console_messages) { ['informational message'] }
+ let(:custom_action_result) { Gitlab::GitAccessResult::CustomAction.new(payload, console_messages) }
before do
project.add_guest(user)
@@ -446,8 +449,8 @@ describe API::Internal::Base do
expect(response).to have_gitlab_http_status(300)
expect(json_response['status']).to be_truthy
- expect(json_response['message']).to eql(message)
expect(json_response['payload']).to eql(payload)
+ expect(json_response['gl_console_messages']).to eql(console_messages)
expect(user.reload.last_activity_on).to be_nil
end
end
@@ -577,6 +580,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
+ expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-get-all-lfs-pointers-go' => 'true', 'gitaly-feature-inforef-uploadpack-cache' => 'true')
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 89ee6f896f9..020e7659a4c 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Jobs do
@@ -595,7 +597,7 @@ describe API::Jobs do
context 'find proper job' do
shared_examples 'a valid file' do
- context 'when artifacts are stored locally' do
+ context 'when artifacts are stored locally', :sidekiq_might_not_need_inline do
let(:download_headers) do
{ 'Content-Transfer-Encoding' => 'binary',
'Content-Disposition' =>
@@ -674,7 +676,7 @@ describe API::Jobs do
let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
let(:public_builds) { true }
- it 'allows to access artifacts' do
+ it 'allows to access artifacts', :sidekiq_might_not_need_inline do
expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h)
.to include('Content-Type' => 'application/json',
@@ -711,7 +713,7 @@ describe API::Jobs do
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:public_builds) { true }
- it 'returns a specific artifact file for a valid path' do
+ it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
expect(Gitlab::Workhorse)
.to receive(:send_artifacts_entry)
.and_call_original
@@ -732,7 +734,7 @@ describe API::Jobs do
sha: project.commit('improve/awesome').sha)
end
- it 'returns a specific artifact file for a valid path' do
+ it 'returns a specific artifact file for a valid path', :sidekiq_might_not_need_inline do
get_artifact_file(artifact, 'improve/awesome')
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index f37d84fddef..6802a0cfdab 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Keys do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 7089da3d351..d027738c8db 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Labels do
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index f52cdf1c459..46d23bd16b9 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Lint do
diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb
index 0cf5c5677b9..99263f2fc1e 100644
--- a/spec/requests/api/markdown_spec.rb
+++ b/spec/requests/api/markdown_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe API::Markdown do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index eb55d747179..f2942020e16 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Members do
@@ -24,7 +26,7 @@ describe API::Members do
shared_examples 'GET /:source_type/:id/members/(all)' do |source_type, all|
let(:members_url) do
- "/#{source_type.pluralize}/#{source.id}/members".tap do |url|
+ (+"/#{source_type.pluralize}/#{source.id}/members").tap do |url|
url << "/all" if all
end
end
@@ -149,9 +151,15 @@ describe API::Members do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.map { |u| u['id'] }).to eq [developer.id, maintainer.id, nested_user.id, project_user.id, linked_group_user.id]
- expect(json_response.map { |u| u['access_level'] }).to eq [Gitlab::Access::DEVELOPER, Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER,
- Gitlab::Access::DEVELOPER, Gitlab::Access::DEVELOPER]
+
+ expected_users_and_access_levels = [
+ [developer.id, Gitlab::Access::DEVELOPER],
+ [maintainer.id, Gitlab::Access::OWNER],
+ [nested_user.id, Gitlab::Access::DEVELOPER],
+ [project_user.id, Gitlab::Access::DEVELOPER],
+ [linked_group_user.id, Gitlab::Access::DEVELOPER]
+ ]
+ expect(json_response.map { |u| [u['id'], u['access_level']] }).to match_array(expected_users_and_access_levels)
end
it 'finds all group members including inherited members' do
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 8a67d98fc4c..9de76c2fe50 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 05160a33e61..c96c80b6998 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe API::MergeRequests do
@@ -10,7 +12,7 @@ describe API::MergeRequests do
let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
@@ -699,16 +701,20 @@ describe API::MergeRequests do
expect(json_response.first['id']).to eq merge_request_closed.id
end
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new do
- get api("/projects/#{project.id}/merge_requests", user)
- end.count
+ context 'a project which enforces all discussions to be resolved' do
+ let!(:project) { create(:project, :repository, only_allow_merge_if_all_discussions_are_resolved: true) }
- create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time)
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{project.id}/merge_requests", user)
+ end.count
- expect do
- get api("/projects/#{project.id}/merge_requests", user)
- end.not_to exceed_query_limit(control)
+ create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time)
+
+ expect do
+ get api("/projects/#{project.id}/merge_requests", user)
+ end.not_to exceed_query_limit(control)
+ end
end
end
@@ -775,6 +781,8 @@ describe API::MergeRequests do
expect(json_response['merge_error']).to eq(merge_request.merge_error)
expect(json_response['user']['can_merge']).to be_truthy
expect(json_response).not_to include('rebase_in_progress')
+ expect(json_response['has_conflicts']).to be_falsy
+ expect(json_response['blocking_discussions_resolved']).to be_truthy
end
it 'exposes description and title html when render_html is true' do
@@ -921,7 +929,7 @@ describe API::MergeRequests do
allow_collaboration: true)
end
- it 'includes the `allow_collaboration` field' do
+ it 'includes the `allow_collaboration` field', :sidekiq_might_not_need_inline do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(json_response['allow_collaboration']).to be_truthy
@@ -1035,14 +1043,12 @@ describe API::MergeRequests do
describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do
before do
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:ci_yaml_file)
- .and_return(YAML.dump({
- rspec: {
- script: 'ls',
- only: ['merge_requests']
- }
- }))
+ stub_ci_pipeline_yaml_file(YAML.dump({
+ rspec: {
+ script: 'ls',
+ only: ['merge_requests']
+ }
+ }))
end
let(:project) do
@@ -1326,7 +1332,7 @@ describe API::MergeRequests do
context 'accepts remove_source_branch parameter' do
let(:params) do
{ title: 'Test merge_request',
- source_branch: 'markdown',
+ source_branch: 'feature_conflict',
target_branch: 'master',
author: user }
end
@@ -1406,7 +1412,7 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(400)
end
- it 'allows setting `allow_collaboration`' do
+ it 'allows setting `allow_collaboration`', :sidekiq_might_not_need_inline do
post api("/projects/#{forked_project.id}/merge_requests", user2),
params: { title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, allow_collaboration: true }
expect(response).to have_gitlab_http_status(201)
@@ -1438,7 +1444,7 @@ describe API::MergeRequests do
end
end
- it "returns 201 when target_branch is specified and for the same project" do
+ it "returns 201 when target_branch is specified and for the same project", :sidekiq_might_not_need_inline do
post api("/projects/#{forked_project.id}/merge_requests", user2),
params: { title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id }
expect(response).to have_gitlab_http_status(201)
@@ -1486,7 +1492,7 @@ describe API::MergeRequests do
end
describe "PUT /projects/:id/merge_requests/:merge_request_iid/merge" do
- let(:pipeline) { create(:ci_pipeline_without_jobs) }
+ let(:pipeline) { create(:ci_pipeline) }
it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
@@ -1633,6 +1639,21 @@ describe API::MergeRequests do
expect(source_repository.branch_exists?(source_branch)).to be_falsy
end
end
+
+ context "performing a ff-merge with squash" do
+ let(:merge_request) { create(:merge_request, :rebased, source_project: project, squash: true) }
+
+ before do
+ project.update(merge_requests_ff_only_enabled: true)
+ end
+
+ it "records the squash commit SHA and returns it in the response" do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['squash_commit_sha'].length).to eq(40)
+ end
+ end
end
describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref", :clean_gitlab_redis_shared_state do
@@ -2152,6 +2173,16 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(409)
end
+
+ it "returns 409 if rebase can't lock the row" do
+ allow_any_instance_of(MergeRequest).to receive(:with_lock).and_raise(ActiveRecord::LockWaitTimeout)
+ expect(RebaseWorker).not_to receive(:perform_async)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/rebase", user)
+
+ expect(response).to have_gitlab_http_status(409)
+ expect(json_response['message']).to eq(MergeRequest::REBASE_LOCK_MESSAGE)
+ end
end
describe 'Time tracking' do
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 2e376109b42..e0bf1509be3 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Namespaces do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 6c1e30791d2..e57d7699892 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Notes do
diff --git a/spec/requests/api/notification_settings_spec.rb b/spec/requests/api/notification_settings_spec.rb
index 4ed667ad0dc..09fc0197c58 100644
--- a/spec/requests/api/notification_settings_spec.rb
+++ b/spec/requests/api/notification_settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::NotificationSettings do
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 3811ec751de..8d7b3fa3c09 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'OAuth tokens' do
diff --git a/spec/requests/api/pages/internal_access_spec.rb b/spec/requests/api/pages/internal_access_spec.rb
index 28abe1a8456..821a210a414 100644
--- a/spec/requests/api/pages/internal_access_spec.rb
+++ b/spec/requests/api/pages/internal_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Internal Project Pages Access" do
diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb
index 6af441caf74..ec84762b05a 100644
--- a/spec/requests/api/pages/private_access_spec.rb
+++ b/spec/requests/api/pages/private_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Private Project Pages Access" do
diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb
index d99224eca5b..67b8cfb8fbc 100644
--- a/spec/requests/api/pages/public_access_spec.rb
+++ b/spec/requests/api/pages/public_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Public Project Pages Access" do
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index 326b724666d..6b774e9335e 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -1,15 +1,22 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::PagesDomains do
- set(:project) { create(:project, path: 'my.project', pages_https_only: false) }
- set(:user) { create(:user) }
- set(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, path: 'my.project', pages_https_only: false) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
- set(:pages_domain) { create(:pages_domain, :without_key, :without_certificate, domain: 'www.domain.test', project: project) }
- set(:pages_domain_secure) { create(:pages_domain, domain: 'ssl.domain.test', project: project) }
- set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, domain: 'expired.domain.test', project: project) }
+ let_it_be(:pages_domain) { create(:pages_domain, :without_key, :without_certificate, domain: 'www.domain.test', project: project) }
+ let_it_be(:pages_domain_secure) { create(:pages_domain, domain: 'ssl.domain.test', project: project) }
+ let_it_be(:pages_domain_with_letsencrypt) { create(:pages_domain, :letsencrypt, domain: 'letsencrypt.domain.test', project: project) }
+ let_it_be(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, domain: 'expired.domain.test', project: project) }
let(:pages_domain_params) { build(:pages_domain, :without_key, :without_certificate, domain: 'www.other-domain.test').slice(:domain) }
+ let(:pages_domain_with_letsencrypt_params) do
+ build(:pages_domain, :without_key, :without_certificate, domain: 'www.other-domain.test', auto_ssl_enabled: true)
+ .slice(:domain, :auto_ssl_enabled)
+ end
let(:pages_domain_secure_params) { build(:pages_domain, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
@@ -20,6 +27,7 @@ describe API::PagesDomains do
let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
+ let(:route_letsencrypt_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_with_letsencrypt.domain}" }
before do
allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
@@ -45,9 +53,10 @@ describe API::PagesDomains do
expect(response).to match_response_schema('public_api/v4/pages_domain_basics')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(3)
+ expect(json_response.size).to eq(4)
expect(json_response.last).to have_key('domain')
expect(json_response.last).to have_key('project_id')
+ expect(json_response.last).to have_key('auto_ssl_enabled')
expect(json_response.last).to have_key('certificate_expiration')
expect(json_response.last['certificate_expiration']['expired']).to be true
expect(json_response.first).not_to have_key('certificate_expiration')
@@ -71,7 +80,7 @@ describe API::PagesDomains do
expect(response).to match_response_schema('public_api/v4/pages_domains')
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(3)
+ expect(json_response.size).to eq(4)
expect(json_response.map { |pages_domain| pages_domain['domain'] }).to include(pages_domain.domain)
expect(json_response.last).to have_key('domain')
end
@@ -164,6 +173,7 @@ describe API::PagesDomains do
expect(json_response['url']).to eq(pages_domain_secure.url)
expect(json_response['certificate']['subject']).to eq(pages_domain_secure.subject)
expect(json_response['certificate']['expired']).to be false
+ expect(json_response['auto_ssl_enabled']).to be false
end
it 'returns pages domain with an expired certificate' do
@@ -173,6 +183,18 @@ describe API::PagesDomains do
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(json_response['certificate']['expired']).to be true
end
+
+ it 'returns pages domain with letsencrypt' do
+ get api(route_letsencrypt_domain, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(json_response['domain']).to eq(pages_domain_with_letsencrypt.domain)
+ expect(json_response['url']).to eq(pages_domain_with_letsencrypt.url)
+ expect(json_response['certificate']['subject']).to eq(pages_domain_with_letsencrypt.subject)
+ expect(json_response['certificate']['expired']).to be false
+ expect(json_response['auto_ssl_enabled']).to be true
+ end
end
context 'when domain is vacant' do
@@ -244,6 +266,7 @@ describe API::PagesDomains do
expect(pages_domain.domain).to eq(params[:domain])
expect(pages_domain.certificate).to be_nil
expect(pages_domain.key).to be_nil
+ expect(pages_domain.auto_ssl_enabled).to be false
end
it 'creates a new secure pages domain' do
@@ -255,6 +278,29 @@ describe API::PagesDomains do
expect(pages_domain.domain).to eq(params_secure[:domain])
expect(pages_domain.certificate).to eq(params_secure[:certificate])
expect(pages_domain.key).to eq(params_secure[:key])
+ expect(pages_domain.auto_ssl_enabled).to be false
+ end
+
+ it 'creates domain with letsencrypt enabled' do
+ post api(route, user), params: pages_domain_with_letsencrypt_params
+ pages_domain = PagesDomain.find_by(domain: json_response['domain'])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.domain).to eq(pages_domain_with_letsencrypt_params[:domain])
+ expect(pages_domain.auto_ssl_enabled).to be true
+ end
+
+ it 'creates domain with letsencrypt enabled and provided certificate' do
+ post api(route, user), params: params_secure.merge(auto_ssl_enabled: true)
+ pages_domain = PagesDomain.find_by(domain: json_response['domain'])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.domain).to eq(params_secure[:domain])
+ expect(pages_domain.certificate).to eq(params_secure[:certificate])
+ expect(pages_domain.key).to eq(params_secure[:key])
+ expect(pages_domain.auto_ssl_enabled).to be true
end
it 'fails to create pages domain without key' do
@@ -321,13 +367,14 @@ describe API::PagesDomains do
shared_examples_for 'put pages domain' do
it 'updates pages domain removing certificate' do
- put api(route_secure_domain, user)
+ put api(route_secure_domain, user), params: { certificate: nil, key: nil }
pages_domain_secure.reload
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
expect(pages_domain_secure.certificate).to be_nil
expect(pages_domain_secure.key).to be_nil
+ expect(pages_domain_secure.auto_ssl_enabled).to be false
end
it 'updates pages domain adding certificate' do
@@ -340,6 +387,37 @@ describe API::PagesDomains do
expect(pages_domain.key).to eq(params_secure[:key])
end
+ it 'updates pages domain adding certificate with letsencrypt' do
+ put api(route_domain, user), params: params_secure.merge(auto_ssl_enabled: true)
+ pages_domain.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.certificate).to eq(params_secure[:certificate])
+ expect(pages_domain.key).to eq(params_secure[:key])
+ expect(pages_domain.auto_ssl_enabled).to be true
+ end
+
+ it 'updates pages domain enabling letsencrypt' do
+ put api(route_domain, user), params: { auto_ssl_enabled: true }
+ pages_domain.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain.auto_ssl_enabled).to be true
+ end
+
+ it 'updates pages domain disabling letsencrypt while preserving the certificate' do
+ put api(route_letsencrypt_domain, user), params: { auto_ssl_enabled: false }
+ pages_domain_with_letsencrypt.reload
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pages_domain/detail')
+ expect(pages_domain_with_letsencrypt.auto_ssl_enabled).to be false
+ expect(pages_domain_with_letsencrypt.key).to be
+ expect(pages_domain_with_letsencrypt.certificate).to be
+ end
+
it 'updates pages domain with expired certificate' do
put api(route_expired_domain, user), params: params_secure
pages_domain_expired.reload
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 072bd02f2ac..5c8ccce2e37 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::PipelineSchedules do
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 3ac63dc381b..cce52cfc1ca 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -673,7 +673,7 @@ describe API::Pipelines do
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'authorized user' do
- it 'retries failed builds' do
+ it 'retries failed builds', :sidekiq_might_not_need_inline do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index a7b919de2ef..04e59238877 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -281,11 +281,14 @@ describe API::ProjectClusters do
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { 'new-namespace' }
let(:platform_kubernetes_attributes) { { namespace: namespace } }
+ let(:management_project) { create(:project, namespace: project.namespace) }
+ let(:management_project_id) { management_project.id }
let(:update_params) do
{
domain: 'new-domain.com',
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
@@ -310,6 +313,8 @@ describe API::ProjectClusters do
context 'authorized user' do
before do
+ management_project.add_maintainer(current_user)
+
put api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: update_params
cluster.reload
@@ -323,6 +328,7 @@ describe API::ProjectClusters do
it 'updates cluster attributes' do
expect(cluster.domain).to eq('new-domain.com')
expect(cluster.platform_kubernetes.namespace).to eq('new-namespace')
+ expect(cluster.management_project).to eq(management_project)
end
end
@@ -336,6 +342,7 @@ describe API::ProjectClusters do
it 'does not update cluster attributes' do
expect(cluster.domain).not_to eq('new_domain.com')
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
+ expect(cluster.management_project).not_to eq(management_project)
end
it 'returns validation errors' do
@@ -343,6 +350,18 @@ describe API::ProjectClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with a GCP cluster' do
context 'when user tries to change GCP specific fields' do
let(:platform_kubernetes_attributes) do
diff --git a/spec/requests/api/project_container_repositories_spec.rb b/spec/requests/api/project_container_repositories_spec.rb
index 3ac7ff7656b..d04db134db0 100644
--- a/spec/requests/api/project_container_repositories_spec.rb
+++ b/spec/requests/api/project_container_repositories_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectContainerRepositories do
@@ -44,6 +46,7 @@ describe API::ProjectContainerRepositories do
it_behaves_like 'rejected container repository access', :guest, :forbidden
it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories'
it_behaves_like 'returns repositories for allowed users', :reporter, 'project' do
let(:object) { project }
@@ -55,6 +58,7 @@ describe API::ProjectContainerRepositories do
it_behaves_like 'rejected container repository access', :developer, :forbidden
it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_repository'
context 'for maintainer' do
let(:api_user) { maintainer }
@@ -83,6 +87,8 @@ describe API::ProjectContainerRepositories do
stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest))
end
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'list_tags'
+
it 'returns a list of tags' do
subject
@@ -109,6 +115,7 @@ describe API::ProjectContainerRepositories do
it_behaves_like 'rejected container repository access', :developer, :forbidden
it_behaves_like 'rejected container repository access', :anonymous, :not_found
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'delete_tag_bulk'
end
context 'for maintainer' do
@@ -220,6 +227,7 @@ describe API::ProjectContainerRepositories do
it 'properly removes tag' do
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {})
subject
@@ -235,6 +243,7 @@ describe API::ProjectContainerRepositories do
it 'properly removes tag' do
expect(service).to receive(:execute).with(root_repository) { { status: :success } }
expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(root_repository.project, api_user, tags: %w[rootA]) { service }
+ expect(Gitlab::Tracking).to receive(:event).with(described_class.name, 'delete_tag', {})
subject
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
index 8c2db6e4c62..d466dca9884 100644
--- a/spec/requests/api/project_events_spec.rb
+++ b/spec/requests/api/project_events_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectEvents do
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 7de8935097a..605ff888234 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectExport do
@@ -370,7 +372,7 @@ describe API::ProjectExport do
end
context 'when overriding description' do
- it 'starts' do
+ it 'starts', :sidekiq_might_not_need_inline do
params = { description: "Foo" }
expect_any_instance_of(Projects::ImportExport::ExportService).to receive(:execute)
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b88a8b95201..06c09b100ac 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectHooks, 'ProjectHooks' do
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index d2b1fb063b8..866adbd424e 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectImport do
@@ -153,7 +155,7 @@ describe API::ProjectImport do
expect(import_project.import_data.data['override_params']).to be_empty
end
- it 'correctly overrides params during the import' do
+ it 'correctly overrides params during the import', :sidekiq_might_not_need_inline do
override_params = { 'description' => 'Hello world' }
perform_enqueued_jobs do
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 895f05a98e8..df6d83c1e65 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectMilestones do
diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb
index 2857715cdbe..cdd44f71649 100644
--- a/spec/requests/api/project_snapshots_spec.rb
+++ b/spec/requests/api/project_snapshots_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectSnapshots do
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index ef0cabad4b0..cac3f07d0d0 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectSnippets do
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index 80e5033dab4..2bf864afe87 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProjectTemplates do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 99d2a68ef53..f1447536e0f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
shared_examples 'languages and percentages JSON response' do
@@ -15,7 +17,7 @@ shared_examples 'languages and percentages JSON response' do
end
context "when the languages haven't been detected yet" do
- it 'returns expected language values' do
+ it 'returns expected language values', :sidekiq_might_not_need_inline do
get api("/projects/#{project.id}/languages", user)
expect(response).to have_gitlab_http_status(:ok)
@@ -360,6 +362,30 @@ describe API::Projects do
end
end
+ context 'and using id_after' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { id_after: project2.id } }
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3].select { |p| p.id > project2.id } }
+ end
+ end
+
+ context 'and using id_before' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { id_before: project2.id } }
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id } }
+ end
+ end
+
+ context 'and using both id_after and id_before' do
+ it_behaves_like 'projects response' do
+ let(:filter) { { id_before: project2.id, id_after: public_project.id } }
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3].select { |p| p.id < project2.id && p.id > public_project.id } }
+ end
+ end
+
context 'and membership=true' do
it_behaves_like 'projects response' do
let(:filter) { { membership: true } }
@@ -606,6 +632,7 @@ describe API::Projects do
merge_requests_enabled: false,
wiki_enabled: false,
resolve_outdated_diff_discussions: false,
+ remove_source_branch_after_merge: true,
only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false,
@@ -722,6 +749,22 @@ describe API::Projects do
expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
end
+ it 'sets a project as not removing source branches' do
+ project = attributes_for(:project, remove_source_branch_after_merge: false)
+
+ post api('/projects', user), params: project
+
+ expect(json_response['remove_source_branch_after_merge']).to be_falsey
+ end
+
+ it 'sets a project as removing source branches' do
+ project = attributes_for(:project, remove_source_branch_after_merge: true)
+
+ post api('/projects', user), params: project
+
+ expect(json_response['remove_source_branch_after_merge']).to be_truthy
+ end
+
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
@@ -829,6 +872,63 @@ describe API::Projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
+ context 'and using id_after' do
+ let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
+
+ it 'only returns projects with id_after filter given' do
+ get api("/users/#{user4.id}/projects?id_after=#{public_project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(another_public_project.id)
+ end
+
+ it 'returns both projects without a id_after filter' do
+ get api("/users/#{user4.id}/projects", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
+ end
+ end
+
+ context 'and using id_before' do
+ let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
+
+ it 'only returns projects with id_before filter given' do
+ get api("/users/#{user4.id}/projects?id_before=#{another_public_project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
+ end
+
+ it 'returns both projects without a id_before filter' do
+ get api("/users/#{user4.id}/projects", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id, another_public_project.id)
+ end
+ end
+
+ context 'and using both id_before and id_after' do
+ let!(:more_projects) { create_list(:project, 5, :public, creator_id: user4.id, namespace: user4.namespace) }
+
+ it 'only returns projects with id matching the range' do
+ get api("/users/#{user4.id}/projects?id_after=#{more_projects.first.id}&id_before=#{more_projects.last.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(*more_projects[1..-2].map(&:id))
+ end
+ end
+
it 'returns projects filtered by username' do
get api("/users/#{user4.username}/projects/", user)
@@ -980,6 +1080,22 @@ describe API::Projects do
expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
end
+ it 'sets a project as not removing source branches' do
+ project = attributes_for(:project, remove_source_branch_after_merge: false)
+
+ post api("/projects/user/#{user.id}", admin), params: project
+
+ expect(json_response['remove_source_branch_after_merge']).to be_falsey
+ end
+
+ it 'sets a project as removing source branches' do
+ project = attributes_for(:project, remove_source_branch_after_merge: true)
+
+ post api("/projects/user/#{user.id}", admin), params: project
+
+ expect(json_response['remove_source_branch_after_merge']).to be_truthy
+ end
+
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
post api("/projects/user/#{user.id}", admin), params: project
@@ -1157,6 +1273,7 @@ describe API::Projects do
expect(json_response['wiki_access_level']).to be_present
expect(json_response['builds_access_level']).to be_present
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
+ expect(json_response['remove_source_branch_after_merge']).to be_truthy
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index f90558d77a9..67ce704b3f3 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProtectedBranches do
diff --git a/spec/requests/api/protected_tags_spec.rb b/spec/requests/api/protected_tags_spec.rb
index 41363dcc1c3..5a962cd5667 100644
--- a/spec/requests/api/protected_tags_spec.rb
+++ b/spec/requests/api/protected_tags_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::ProtectedTags do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 99d0ceee76b..bf05587fe03 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Releases do
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 6f4bb525c89..ba301147d43 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'mime/types'
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 70a95663aea..6138036b0af 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Runner, :clean_gitlab_redis_shared_state do
@@ -312,7 +314,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
let(:root_namespace) { create(:namespace) }
let(:namespace) { create(:namespace, parent: root_namespace) }
let(:project) { create(:project, namespace: namespace, shared_runners_enabled: false) }
- let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
let(:runner) { create(:ci_runner, :project, projects: [project]) }
let(:job) do
create(:ci_build, :artifacts, :extended_options,
@@ -610,7 +612,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when job is made for merge request' do
- let(:pipeline) { create(:ci_pipeline_without_jobs, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
+ let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
let(:merge_request) { create(:merge_request) }
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index d26fbee6957..8daba204d50 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Runners do
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 8abdcaa2e0e..24d7f1e313c 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Search do
@@ -436,6 +438,7 @@ describe API::Search do
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(2)
+ expect(json_response.first['path']).to eq('PROCESS.md')
expect(json_response.first['filename']).to eq('PROCESS.md')
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 7153fcc99d7..a080b59173f 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe API::Services do
@@ -100,7 +102,7 @@ describe API::Services do
expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
end
- it "returns empty hash if properties and data fields are empty" do
+ it "returns empty hash or nil values if properties and data fields are empty" do
# deprecated services are not valid for update
initialized_service.update_attribute(:properties, {})
@@ -112,7 +114,7 @@ describe API::Services do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['properties'].keys).to be_empty
+ expect(json_response['properties'].values.compact).to be_empty
end
it "returns error when authenticated but not a project owner" do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index f3bfb258029..b7586307929 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Settings, 'Settings' do
@@ -16,6 +18,10 @@ describe API::Settings, 'Settings' do
expect(json_response['password_authentication_enabled']).to be_truthy
expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil
+ expect(json_response['default_ci_config_path']).to be_nil
+ expect(json_response['sourcegraph_enabled']).to be_falsey
+ expect(json_response['sourcegraph_url']).to be_nil
+ expect(json_response['sourcegraph_public_only']).to be_truthy
expect(json_response['default_project_visibility']).to be_a String
expect(json_response['default_snippet_visibility']).to be_a String
expect(json_response['default_group_visibility']).to be_a String
@@ -42,17 +48,22 @@ describe API::Settings, 'Settings' do
storages = Gitlab.config.repositories.storages
.merge({ 'custom' => 'tmp/tests/custom_repositories' })
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ Feature.get(:sourcegraph).enable
end
it "updates application settings" do
put api("/application/settings", admin),
params: {
+ default_ci_config_path: 'debian/salsa-ci.yml',
default_projects_limit: 3,
default_project_creation: 2,
password_authentication_enabled_for_web: false,
repository_storages: ['custom'],
plantuml_enabled: true,
plantuml_url: 'http://plantuml.example.com',
+ sourcegraph_enabled: true,
+ sourcegraph_url: 'https://sourcegraph.com',
+ sourcegraph_public_only: false,
default_snippet_visibility: 'internal',
restricted_visibility_levels: ['public'],
default_artifacts_expire_in: '2 days',
@@ -78,12 +89,16 @@ describe API::Settings, 'Settings' do
}
expect(response).to have_gitlab_http_status(200)
+ expect(json_response['default_ci_config_path']).to eq('debian/salsa-ci.yml')
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
expect(json_response['password_authentication_enabled_for_web']).to be_falsey
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['plantuml_enabled']).to be_truthy
expect(json_response['plantuml_url']).to eq('http://plantuml.example.com')
+ expect(json_response['sourcegraph_enabled']).to be_truthy
+ expect(json_response['sourcegraph_url']).to eq('https://sourcegraph.com')
+ expect(json_response['sourcegraph_public_only']).to eq(false)
expect(json_response['default_snippet_visibility']).to eq('internal')
expect(json_response['restricted_visibility_levels']).to eq(['public'])
expect(json_response['default_artifacts_expire_in']).to eq('2 days')
@@ -176,7 +191,8 @@ describe API::Settings, 'Settings' do
snowplow_collector_hostname: "snowplow.example.com",
snowplow_cookie_domain: ".example.com",
snowplow_enabled: true,
- snowplow_site_id: "site_id"
+ snowplow_app_id: "app_id",
+ snowplow_iglu_registry_url: 'https://example.com'
}
end
@@ -220,6 +236,61 @@ describe API::Settings, 'Settings' do
end
end
+ context 'EKS integration settings' do
+ let(:attribute_names) { settings.keys.map(&:to_s) }
+ let(:sensitive_attributes) { %w(eks_secret_access_key) }
+ let(:exposed_attributes) { attribute_names - sensitive_attributes }
+
+ let(:settings) do
+ {
+ eks_integration_enabled: true,
+ eks_account_id: '123456789012',
+ eks_access_key_id: 'access-key-id-12',
+ eks_secret_access_key: 'secret-access-key'
+ }
+ end
+
+ it 'includes attributes in the API' do
+ get api("/application/settings", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ exposed_attributes.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+
+ it 'does not include sensitive attributes in the API' do
+ get api("/application/settings", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ sensitive_attributes.each do |attribute|
+ expect(json_response.keys).not_to include(attribute)
+ end
+ end
+
+ it 'allows updating the settings' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(200)
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+
+ context 'EKS integration is enabled but params are blank' do
+ let(:settings) { Hash[eks_integration_enabled: true] }
+
+ it 'does not update the settings' do
+ put api("/application/settings", admin), params: settings
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to include('eks_account_id is missing')
+ expect(json_response['error']).to include('eks_access_key_id is missing')
+ expect(json_response['error']).to include('eks_secret_access_key is missing')
+ end
+ end
+ end
+
context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
@@ -294,5 +365,14 @@ describe API::Settings, 'Settings' do
expect(json_response['domain_blacklist']).to eq(['domain3.com', '*.domain4.com'])
end
end
+
+ context "missing sourcegraph_url value when sourcegraph_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), params: { sourcegraph_enabled: true }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('sourcegraph_url is missing')
+ end
+ end
end
end
diff --git a/spec/requests/api/sidekiq_metrics_spec.rb b/spec/requests/api/sidekiq_metrics_spec.rb
index fff9adb7f57..438b1475c54 100644
--- a/spec/requests/api/sidekiq_metrics_spec.rb
+++ b/spec/requests/api/sidekiq_metrics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::SidekiqMetrics do
@@ -23,6 +25,10 @@ describe API::SidekiqMetrics do
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_a Hash
+ expect(json_response['jobs']).to be_a Hash
+ expect(json_response['jobs'].keys)
+ .to contain_exactly(*%w[processed failed enqueued dead])
+ expect(json_response['jobs'].values).to all(be_an(Integer))
end
it 'defines the `compound_metrics` endpoint' do
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index e7eaaea2418..36d2a0d7ea7 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Snippets do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index 0e2f3face71..79790b1e999 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::SystemHooks do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index c4f4a2cb889..3c6ec631664 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Tags do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index d1e16ab9ca9..b6ba417d892 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Templates do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 9f0d5ad5d12..4121a0f3f3a 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Todos do
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8ea3d16a41f..fd1104fa978 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Triggers do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ee4e783e9ac..1a1e80f1ce3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Users do
@@ -633,32 +635,6 @@ describe API::Users do
end
end
- describe "GET /users/sign_up" do
- context 'when experimental signup_flow is active' do
- before do
- stub_experiment(signup_flow: true)
- end
-
- it "shows sign up page" do
- get "/users/sign_up"
- expect(response).to have_gitlab_http_status(200)
- expect(response).to render_template(:new)
- end
- end
-
- context 'when experimental signup_flow is not active' do
- before do
- stub_experiment(signup_flow: false)
- end
-
- it "redirects to sign in page" do
- get "/users/sign_up"
- expect(response).to have_gitlab_http_status(302)
- expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
- end
- end
- end
-
describe "PUT /users/:id" do
let!(:admin_user) { create(:admin) }
@@ -1277,7 +1253,7 @@ describe API::Users do
admin
end
- it "deletes user" do
+ it "deletes user", :sidekiq_might_not_need_inline do
perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
expect(response).to have_gitlab_http_status(204)
@@ -1312,7 +1288,7 @@ describe API::Users do
end
context "hard delete disabled" do
- it "moves contributions to the ghost user" do
+ it "moves contributions to the ghost user", :sidekiq_might_not_need_inline do
perform_enqueued_jobs { delete api("/users/#{user.id}", admin) }
expect(response).to have_gitlab_http_status(204)
@@ -1322,7 +1298,7 @@ describe API::Users do
end
context "hard delete enabled" do
- it "removes contributions" do
+ it "removes contributions", :sidekiq_might_not_need_inline do
perform_enqueued_jobs { delete api("/users/#{user.id}?hard_delete=true", admin) }
expect(response).to have_gitlab_http_status(204)
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 69f105b71a8..dfecd43cbfa 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Variables do
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index e06f8bbc095..e2117ca45ee 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Version do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 97de26650db..310caa92eb9 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# For every API endpoint we test 3 states of wikis:
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index e58f1b7d9dc..1b17d492b0c 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Git HTTP requests' do
@@ -87,7 +89,7 @@ describe 'Git HTTP requests' do
end
shared_examples_for 'pulls are allowed' do
- it do
+ it 'allows pulls' do
download(path, env) do |response|
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
@@ -96,7 +98,7 @@ describe 'Git HTTP requests' do
end
shared_examples_for 'pushes are allowed' do
- it do
+ it 'allows pushes', :sidekiq_might_not_need_inline do
upload(path, env) do |response|
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
@@ -450,16 +452,22 @@ describe 'Git HTTP requests' do
context "when authentication fails" do
context "when the user is IP banned" do
before do
- Gitlab.config.rack_attack.git_basic_auth['enabled'] = true
+ stub_rack_attack_setting(enabled: true, ip_whitelist: [])
end
- it "responds with status 401" do
+ it "responds with status 403" do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
- allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return('1.2.3.4')
+ expect(Gitlab::AuthLogger).to receive(:error).with({
+ message: 'Rack_Attack',
+ env: :blocklist,
+ remote_ip: '127.0.0.1',
+ request_method: 'GET',
+ path: "/#{path}/info/refs?service=git-upload-pack"
+ })
clone_get(path, env)
- expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
@@ -493,7 +501,7 @@ describe 'Git HTTP requests' do
context "when the user isn't blocked" do
before do
- Gitlab.config.rack_attack.git_basic_auth['enabled'] = true
+ stub_rack_attack_setting(enabled: true, bantime: 1.minute, findtime: 5.minutes, maxretry: 2, ip_whitelist: [])
end
it "resets the IP in Rack Attack on download" do
@@ -652,9 +660,11 @@ describe 'Git HTTP requests' do
response.status
end
+ include_context 'rack attack cache store'
+
it "repeated attempts followed by successful attempt" do
options = Gitlab.config.rack_attack.git_basic_auth
- maxretry = options[:maxretry] - 1
+ maxretry = options[:maxretry]
ip = '1.2.3.4'
allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip)
@@ -666,12 +676,6 @@ describe 'Git HTTP requests' do
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
-
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
-
- Rack::Attack::Allow2Ban.reset(ip, options)
end
end
@@ -843,8 +847,8 @@ describe 'Git HTTP requests' do
get "/#{project.full_path}/blob/master/info/refs"
end
- it "returns not found" do
- expect(response).to have_gitlab_http_status(:not_found)
+ it "redirects" do
+ expect(response).to have_gitlab_http_status(302)
end
end
end
diff --git a/spec/requests/groups/milestones_controller_spec.rb b/spec/requests/groups/milestones_controller_spec.rb
index af19d931284..977cccad29f 100644
--- a/spec/requests/groups/milestones_controller_spec.rb
+++ b/spec/requests/groups/milestones_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::MilestonesController do
diff --git a/spec/requests/groups/registry/repositories_controller_spec.rb b/spec/requests/groups/registry/repositories_controller_spec.rb
new file mode 100644
index 00000000000..35fdeaab604
--- /dev/null
+++ b/spec/requests/groups/registry/repositories_controller_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::Registry::RepositoriesController do
+ let_it_be(:group, reload: true) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+
+ group.add_reporter(user)
+ login_as(user)
+ end
+
+ describe 'GET groups/:group_id/-/container_registries.json' do
+ it 'avoids N+1 queries' do
+ project = create(:project, group: group)
+ create(:container_repository, project: project)
+ endpoint = group_container_registries_path(group, format: :json)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { get(endpoint) }.count
+
+ create_list(:project, 2, group: group).each do |project|
+ create_list(:container_repository, 2, project: project)
+ end
+
+ expect { get(endpoint) }.not_to exceed_all_query_limit(control_count)
+
+ # sanity check that response is 200
+ expect(response).to have_http_status(200)
+ repositories = json_response
+ expect(repositories.count).to eq(5)
+ end
+ end
+end
diff --git a/spec/requests/health_controller_spec.rb b/spec/requests/health_controller_spec.rb
new file mode 100644
index 00000000000..61412815039
--- /dev/null
+++ b/spec/requests/health_controller_spec.rb
@@ -0,0 +1,227 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe HealthController do
+ include StubENV
+
+ let(:token) { Gitlab::CurrentSettings.health_check_access_token }
+ let(:whitelisted_ip) { '1.1.1.1' }
+ let(:not_whitelisted_ip) { '2.2.2.2' }
+ let(:params) { {} }
+ let(:headers) { {} }
+
+ before do
+ allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip])
+ stub_storage_settings({}) # Hide the broken storage
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ shared_context 'endpoint querying database' do
+ it 'does query database' do
+ control_count = ActiveRecord::QueryRecorder.new { subject }.count
+
+ expect(control_count).not_to be_zero
+ end
+ end
+
+ shared_context 'endpoint not querying database' do
+ it 'does not query database' do
+ control_count = ActiveRecord::QueryRecorder.new { subject }.count
+
+ expect(control_count).to be_zero
+ end
+ end
+
+ shared_context 'endpoint not found' do
+ it 'responds with resource not found' do
+ subject
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'GET /-/health' do
+ subject { get '/-/health', params: params, headers: headers }
+
+ shared_context 'endpoint responding with health data' do
+ it 'responds with health checks data' do
+ subject
+
+ expect(response.status).to eq(200)
+ expect(response.body).to eq('GitLab OK')
+ end
+ end
+
+ context 'accessed from whitelisted ip' do
+ before do
+ stub_remote_addr(whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint responding with health data'
+ it_behaves_like 'endpoint not querying database'
+ end
+
+ context 'accessed from not whitelisted ip' do
+ before do
+ stub_remote_addr(not_whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint not querying database'
+ it_behaves_like 'endpoint not found'
+ end
+ end
+
+ describe 'GET /-/readiness' do
+ subject { get '/-/readiness', params: params, headers: headers }
+
+ shared_context 'endpoint responding with readiness data' do
+ context 'when requesting instance-checks' do
+ it 'responds with readiness checks data' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { true }
+
+ subject
+
+ expect(json_response).to include({ 'status' => 'ok' })
+ expect(json_response['master_check']).to contain_exactly({ 'status' => 'ok' })
+ end
+
+ it 'responds with readiness checks data when a failure happens' do
+ expect(Gitlab::HealthChecks::MasterCheck).to receive(:check) { false }
+
+ subject
+
+ expect(json_response).to include({ 'status' => 'failed' })
+ expect(json_response['master_check']).to contain_exactly(
+ { 'status' => 'failed', 'message' => 'unexpected Master check result: false' })
+
+ expect(response.status).to eq(503)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ end
+ end
+
+ context 'when requesting all checks' do
+ before do
+ params.merge!(all: true)
+ end
+
+ it 'responds with readiness checks data' do
+ subject
+
+ expect(json_response['db_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['queues_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['shared_state_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['gitaly_check']).to contain_exactly(
+ { 'status' => 'ok', 'labels' => { 'shard' => 'default' } })
+ end
+
+ it 'responds with readiness checks data when a failure happens' do
+ allow(Gitlab::HealthChecks::Redis::RedisCheck).to receive(:readiness).and_return(
+ Gitlab::HealthChecks::Result.new('redis_check', false, "check error"))
+
+ subject
+
+ expect(json_response['cache_check']).to contain_exactly({ 'status' => 'ok' })
+ expect(json_response['redis_check']).to contain_exactly(
+ { 'status' => 'failed', 'message' => 'check error' })
+
+ expect(response.status).to eq(503)
+ expect(response.headers['X-GitLab-Custom-Error']).to eq(1)
+ end
+ end
+ end
+
+ context 'accessed from whitelisted ip' do
+ before do
+ stub_remote_addr(whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint not querying database'
+ it_behaves_like 'endpoint responding with readiness data'
+
+ context 'when requesting all checks' do
+ before do
+ params.merge!(all: true)
+ end
+
+ it_behaves_like 'endpoint querying database'
+ end
+ end
+
+ context 'accessed from not whitelisted ip' do
+ before do
+ stub_remote_addr(not_whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint not querying database'
+ it_behaves_like 'endpoint not found'
+ end
+
+ context 'accessed with valid token' do
+ context 'token passed in request header' do
+ let(:headers) { { TOKEN: token } }
+
+ it_behaves_like 'endpoint responding with readiness data'
+ it_behaves_like 'endpoint querying database'
+ end
+
+ context 'token passed as URL param' do
+ let(:params) { { token: token } }
+
+ it_behaves_like 'endpoint responding with readiness data'
+ it_behaves_like 'endpoint querying database'
+ end
+ end
+ end
+
+ describe 'GET /-/liveness' do
+ subject { get '/-/liveness', params: params, headers: headers }
+
+ shared_context 'endpoint responding with liveness data' do
+ it 'responds with liveness checks data' do
+ subject
+
+ expect(json_response).to eq('status' => 'ok')
+ end
+ end
+
+ context 'accessed from whitelisted ip' do
+ before do
+ stub_remote_addr(whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint not querying database'
+ it_behaves_like 'endpoint responding with liveness data'
+ end
+
+ context 'accessed from not whitelisted ip' do
+ before do
+ stub_remote_addr(not_whitelisted_ip)
+ end
+
+ it_behaves_like 'endpoint not querying database'
+ it_behaves_like 'endpoint not found'
+
+ context 'accessed with valid token' do
+ context 'token passed in request header' do
+ let(:headers) { { TOKEN: token } }
+
+ it_behaves_like 'endpoint responding with liveness data'
+ it_behaves_like 'endpoint querying database'
+ end
+
+ context 'token passed as URL param' do
+ let(:params) { { token: token } }
+
+ it_behaves_like 'endpoint responding with liveness data'
+ it_behaves_like 'endpoint querying database'
+ end
+ end
+ end
+ end
+
+ def stub_remote_addr(ip)
+ headers.merge!(REMOTE_ADDR: ip)
+ end
+end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 8b2c698fee1..c1f99115612 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JwtController do
diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb
index 11436e5cd0c..41f54162266 100644
--- a/spec/requests/lfs_locks_api_spec.rb
+++ b/spec/requests/lfs_locks_api_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Git LFS File Locking API' do
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
index 3873e754060..bb1c25d686e 100644
--- a/spec/requests/oauth_tokens_spec.rb
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'OAuth Tokens requests' do
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index dfa17c5ff27..bac1a4e18c8 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'OpenID Connect requests' do
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 25390f8a23e..93a1aafde23 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'cycle analytics events' do
@@ -48,7 +50,7 @@ describe 'cycle analytics events' do
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
- it 'lists the test events' do
+ it 'lists the test events', :sidekiq_might_not_need_inline do
get project_cycle_analytics_test_path(project, format: :json)
expect(json_response['events']).not_to be_empty
@@ -64,14 +66,14 @@ describe 'cycle analytics events' do
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
- it 'lists the staging events' do
+ it 'lists the staging events', :sidekiq_might_not_need_inline do
get project_cycle_analytics_staging_path(project, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
end
- it 'lists the production events' do
+ it 'lists the production events', :sidekiq_might_not_need_inline do
get project_cycle_analytics_production_path(project, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
@@ -81,7 +83,7 @@ describe 'cycle analytics events' do
end
context 'specific branch' do
- it 'lists the test events' do
+ it 'lists the test events', :sidekiq_might_not_need_inline do
branch = project.merge_requests.first.source_branch
get project_cycle_analytics_test_path(project, format: :json, branch: branch)
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index ca8720cd414..4d5055a7e27 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Rack Attack global throttles' do
@@ -20,6 +22,7 @@ describe 'Rack Attack global throttles' do
}
end
+ let(:request_method) { 'GET' }
let(:requests_per_period) { 1 }
let(:period_in_seconds) { 10000 }
let(:period) { period_in_seconds.seconds }
@@ -81,7 +84,7 @@ describe 'Rack Attack global throttles' do
expect(response).to have_http_status 200
end
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).at_least(:once).and_return('1.2.3.4')
# would be over limit for the same IP
get url_that_does_not_require_authentication
@@ -141,15 +144,15 @@ describe 'Rack Attack global throttles' do
let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
- let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
- let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+ 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)] }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'with the token in the headers' do
- let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
- let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+ let(:request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
it_behaves_like 'rate-limited token-authenticated requests'
end
@@ -168,15 +171,15 @@ describe 'Rack Attack global throttles' do
let(:api_partial_url) { '/todos' }
context 'with the token in the query string' do
- let(:get_args) { [api(api_partial_url, oauth_access_token: token)] }
- let(:other_user_get_args) { [api(api_partial_url, oauth_access_token: other_user_token)] }
+ let(:request_args) { [api(api_partial_url, oauth_access_token: token)] }
+ let(:other_user_request_args) { [api(api_partial_url, oauth_access_token: other_user_token)] }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'with the token in the headers' do
- let(:get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
- let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
+ let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
+ let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
it_behaves_like 'rate-limited token-authenticated requests'
end
@@ -188,8 +191,8 @@ describe 'Rack Attack global throttles' do
let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
context 'with the token in the query string' do
- let(:get_args) { [rss_url(user), params: nil] }
- let(:other_user_get_args) { [rss_url(other_user), params: nil] }
+ let(:request_args) { [rss_url(user), params: nil] }
+ let(:other_user_request_args) { [rss_url(other_user), params: nil] }
it_behaves_like 'rate-limited token-authenticated requests'
end
@@ -204,10 +207,13 @@ describe 'Rack Attack global throttles' do
end
describe 'protected paths' do
+ let(:request_method) { 'POST' }
+
context 'unauthenticated requests' do
let(:protected_path_that_does_not_require_authentication) do
- '/users/confirmation'
+ '/users/sign_in'
end
+ let(:post_params) { { user: { login: 'username', password: 'password' } } }
before do
settings_to_set[:throttle_protected_paths_requests_per_period] = requests_per_period # 1
@@ -222,7 +228,7 @@ describe 'Rack Attack global throttles' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get protected_path_that_does_not_require_authentication
+ post protected_path_that_does_not_require_authentication, params: post_params
expect(response).to have_http_status 200
end
end
@@ -236,11 +242,11 @@ describe 'Rack Attack global throttles' do
it 'rejects requests over the rate limit' do
requests_per_period.times do
- get protected_path_that_does_not_require_authentication
+ post protected_path_that_does_not_require_authentication, params: post_params
expect(response).to have_http_status 200
end
- expect_rejection { get protected_path_that_does_not_require_authentication }
+ expect_rejection { post protected_path_that_does_not_require_authentication, params: post_params }
end
context 'when Omnibus throttle is present' do
@@ -251,7 +257,7 @@ describe 'Rack Attack global throttles' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get protected_path_that_does_not_require_authentication
+ post protected_path_that_does_not_require_authentication, params: post_params
expect(response).to have_http_status 200
end
end
@@ -265,11 +271,11 @@ describe 'Rack Attack global throttles' do
let(:other_user) { create(:user) }
let(:other_user_token) { create(:personal_access_token, user: other_user) }
let(:throttle_setting_prefix) { 'throttle_protected_paths' }
- let(:api_partial_url) { '/users' }
+ let(:api_partial_url) { '/user/emails' }
let(:protected_paths) do
[
- '/api/v4/users'
+ '/api/v4/user/emails'
]
end
@@ -279,22 +285,22 @@ describe 'Rack Attack global throttles' do
end
context 'with the token in the query string' do
- let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
- let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+ 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)] }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'with the token in the headers' do
- let(:get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
- let(:other_user_get_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
+ let(:request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(token)) }
+ let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, personal_access_token_headers(other_user_token)) }
it_behaves_like 'rate-limited token-authenticated requests'
end
context 'when Omnibus throttle is present' do
- let(:get_args) { [api(api_partial_url, personal_access_token: token)] }
- let(:other_user_get_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
+ 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
@@ -308,8 +314,8 @@ describe 'Rack Attack global throttles' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get(*get_args)
- expect(response).to have_http_status 200
+ post(*request_args)
+ expect(response).not_to have_http_status 429
end
end
end
@@ -318,7 +324,7 @@ describe 'Rack Attack global throttles' do
describe 'web requests authenticated with regular login' do
let(:throttle_setting_prefix) { 'throttle_protected_paths' }
let(:user) { create(:user) }
- let(:url_that_requires_authentication) { '/dashboard/snippets' }
+ let(:url_that_requires_authentication) { '/users/confirmation' }
let(:protected_paths) do
[
@@ -348,8 +354,8 @@ describe 'Rack Attack global throttles' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ post url_that_requires_authentication
+ expect(response).not_to have_http_status 429
end
end
end
diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb
index 851affbcf88..36ccfc6b400 100644
--- a/spec/requests/request_profiler_spec.rb
+++ b/spec/requests/request_profiler_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Request Profiler' do
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 77baaef7afd..a82bdfe3ce8 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block
diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb
index 28b3e79c1ff..ea172698764 100644
--- a/spec/routing/environments_spec.rb
+++ b/spec/routing/environments_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'environments routing' do
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index c6b101ae908..2a8454a276d 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Groups", "routing" do
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 3fdede7914d..7e78a1c0cd2 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# Shared examples for a resource inside a Project
diff --git a/spec/routing/notifications_routing_spec.rb b/spec/routing/notifications_routing_spec.rb
index 54ed87b5520..8c2b29aabcb 100644
--- a/spec/routing/notifications_routing_spec.rb
+++ b/spec/routing/notifications_routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe "notifications routing" do
diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb
index 2c3bc08f1a1..70470032930 100644
--- a/spec/routing/openid_connect_spec.rb
+++ b/spec/routing/openid_connect_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index acdbf064a73..561c2b572ec 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'project routing' do
@@ -786,4 +788,10 @@ describe 'project routing' do
expect(put("/gitlab/gitlabhq/-/deploy_tokens/1/revoke")).to route_to("projects/deploy_tokens#revoke", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
end
+
+ describe Projects::UsagePingController, '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
+ end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1b982fa7744..6f67cdb1222 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
# user GET /users/:username/
@@ -275,6 +277,33 @@ describe "Authentication", "routing" do
it "PUT /users/password" do
expect(put("/users/password")).to route_to('passwords#update')
end
+
+ context 'with LDAP configured' do
+ include LdapHelpers
+
+ let(:ldap_settings) { { enabled: true } }
+
+ before do
+ stub_ldap_setting(ldap_settings)
+ Rails.application.reload_routes!
+ end
+
+ after(:all) do
+ Rails.application.reload_routes!
+ end
+
+ it 'POST /users/auth/ldapmain/callback' do
+ expect(post("/users/auth/ldapmain/callback")).to route_to('ldap/omniauth_callbacks#ldapmain')
+ end
+
+ context 'with LDAP sign-in disabled' do
+ let(:ldap_settings) { { enabled: true, prevent_ldap_sign_in: true } }
+
+ it 'prevents POST /users/auth/ldapmain/callback' do
+ expect(post("/users/auth/ldapmain/callback")).not_to be_routable
+ end
+ end
+ end
end
describe HealthCheckController, 'routing' do
diff --git a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
index ac7b1575ec0..62f6c7a3414 100644
--- a/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
+++ b/spec/rubocop/cop/avoid_break_from_strong_memoize_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
index a5c280a7adc..133d286ccd2 100644
--- a/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
+++ b/spec/rubocop/cop/avoid_return_from_blocks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb
index b0bc40552b3..ac8aa56e040 100644
--- a/spec/rubocop/cop/destroy_all_spec.rb
+++ b/spec/rubocop/cop/destroy_all_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
index 7f689b196c5..7af98b66218 100644
--- a/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
+++ b/spec/rubocop/cop/gitlab/finder_with_find_by_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/gitlab/httparty_spec.rb b/spec/rubocop/cop/gitlab/httparty_spec.rb
index 510839a21d7..42da97679ec 100644
--- a/spec/rubocop/cop/gitlab/httparty_spec.rb
+++ b/spec/rubocop/cop/gitlab/httparty_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
index 8e2d5f70353..9cb55ced1fa 100644
--- a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
+++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
index 21fc4584654..ae9466368d2 100644
--- a/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
+++ b/spec/rubocop/cop/gitlab/predicate_memoization_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
index 7b5235a3da7..8e027ad59f7 100644
--- a/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
+++ b/spec/rubocop/cop/group_public_or_visible_to_user_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
index f5109287876..39965646aff 100644
--- a/spec/rubocop/cop/include_sidekiq_worker_spec.rb
+++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
index cc933ce12c8..d09de4c6614 100644
--- a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
+++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
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 1df1fffb94e..419d74c298a 100644
--- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
index 9c1ebcc0ced..9812e64216f 100644
--- a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb
index 0b56fe8ed83..03348ecc744 100644
--- a/spec/rubocop/cop/migration/add_reference_spec.rb
+++ b/spec/rubocop/cop/migration/add_reference_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/add_timestamps_spec.rb b/spec/rubocop/cop/migration/add_timestamps_spec.rb
index 33f1bb85af8..a3314d878e5 100644
--- a/spec/rubocop/cop/migration/add_timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/add_timestamps_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/datetime_spec.rb b/spec/rubocop/cop/migration/datetime_spec.rb
index f2d9483d8d3..0a771003100 100644
--- a/spec/rubocop/cop/migration/datetime_spec.rb
+++ b/spec/rubocop/cop/migration/datetime_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/hash_index_spec.rb b/spec/rubocop/cop/migration/hash_index_spec.rb
index 5d53dde9a79..e8b05a94653 100644
--- a/spec/rubocop/cop/migration/hash_index_spec.rb
+++ b/spec/rubocop/cop/migration/hash_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb
index f1a64f431bd..bc2fa04ce64 100644
--- a/spec/rubocop/cop/migration/remove_column_spec.rb
+++ b/spec/rubocop/cop/migration/remove_column_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
index a23d5d022e3..9de4c756f12 100644
--- a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb
index bbf2227e512..d343d27484a 100644
--- a/spec/rubocop/cop/migration/remove_index_spec.rb
+++ b/spec/rubocop/cop/migration/remove_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
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
index ba8cd2c6c4a..b3c5b855004 100644
--- a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
index 1c4f18fbcc3..915b73ed5a7 100644
--- a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
+++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/timestamps_spec.rb b/spec/rubocop/cop/migration/timestamps_spec.rb
index cafe255dc9a..d03c75e7cfc 100644
--- a/spec/rubocop/cop/migration/timestamps_spec.rb
+++ b/spec/rubocop/cop/migration/timestamps_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
index cba01400d85..f72efaf2eb2 100644
--- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
+++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/migration/update_large_table_spec.rb b/spec/rubocop/cop/migration/update_large_table_spec.rb
index 5e08eb4f772..0463b6550a8 100644
--- a/spec/rubocop/cop/migration/update_large_table_spec.rb
+++ b/spec/rubocop/cop/migration/update_large_table_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/project_path_helper_spec.rb b/spec/rubocop/cop/project_path_helper_spec.rb
index 84e6eb7d87f..1b69030c798 100644
--- a/spec/rubocop/cop/project_path_helper_spec.rb
+++ b/spec/rubocop/cop/project_path_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/rspec/any_instance_of_spec.rb b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
new file mode 100644
index 00000000000..b16f8ac189c
--- /dev/null
+++ b/spec/rubocop/cop/rspec/any_instance_of_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/any_instance_of'
+
+describe RuboCop::Cop::RSpec::AnyInstanceOf do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when calling allow_any_instance_of' do
+ let(:source) do
+ <<~SRC
+ allow_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
+ SRC
+ end
+ let(:corrected_source) do
+ <<~SRC
+ allow_next_instance_of(User) do |instance|
+ allow(instance).to receive(:invalidate_issue_cache_counts)
+ end
+ SRC
+ end
+
+ it 'registers an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'can autocorrect the source' do
+ expect(autocorrect_source(source)).to eq(corrected_source)
+ end
+ end
+
+ context 'when calling expect_any_instance_of' do
+ let(:source) do
+ <<~SRC
+ expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
+ SRC
+ end
+ let(:corrected_source) do
+ <<~SRC
+ expect_next_instance_of(User) do |instance|
+ expect(instance).to receive(:invalidate_issue_cache_counts).with(args).and_return(double)
+ end
+ SRC
+ end
+
+ it 'registers an offence' do
+ inspect_source(source)
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'can autocorrect the source' do
+ expect(autocorrect_source(source)).to eq(corrected_source)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
index 621afbad3ba..2a2bd1434d6 100644
--- a/spec/rubocop/cop/rspec/env_assignment_spec.rb
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
index 94324bc615d..20013519db4 100644
--- a/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
+++ b/spec/rubocop/cop/rspec/factories_in_migration_specs_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
index 7f237d5ffbb..c10fd7bd32b 100644
--- a/spec/rubocop/cop/sidekiq_options_queue_spec.rb
+++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
diff --git a/spec/serializers/blob_entity_spec.rb b/spec/serializers/blob_entity_spec.rb
index c0687d0232e..7e3a0a87bd5 100644
--- a/spec/serializers/blob_entity_spec.rb
+++ b/spec/serializers/blob_entity_spec.rb
@@ -15,8 +15,16 @@ describe BlobEntity do
context 'as json' do
subject { entity.as_json }
- it 'exposes needed attributes' do
- expect(subject).to include(:readable_text, :url)
+ it 'contains needed attributes' do
+ expect(subject).to include({
+ id: blob.id,
+ path: blob.path,
+ name: blob.name,
+ mode: "100644",
+ readable_text: true,
+ icon: "file-text-o",
+ url: "/#{project.full_path}/blob/master/bar/branch-test.txt"
+ })
end
end
end
diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb
index 68c5c665ed6..80f5bc8f159 100644
--- a/spec/serializers/diff_file_base_entity_spec.rb
+++ b/spec/serializers/diff_file_base_entity_spec.rb
@@ -5,15 +5,15 @@ require 'spec_helper'
describe DiffFileBaseEntity do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
+ let(:entity) { described_class.new(diff_file, options).as_json }
context 'diff for a changed submodule' do
let(:commit_sha_with_changed_submodule) do
"cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
end
let(:commit) { project.commit(commit_sha_with_changed_submodule) }
- let(:diff_file) { commit.diffs.diff_files.to_a.last }
let(:options) { { request: {}, submodule_links: Gitlab::SubmoduleLinks.new(repository) } }
- let(:entity) { described_class.new(diff_file, options).as_json }
+ let(:diff_file) { commit.diffs.diff_files.to_a.last }
it do
expect(entity[:submodule]).to eq(true)
@@ -23,4 +23,15 @@ describe DiffFileBaseEntity do
)
end
end
+
+ context 'contains raw sizes for the blob' do
+ let(:commit) { project.commit('png-lfs') }
+ let(:options) { { request: {} } }
+ let(:diff_file) { commit.diffs.diff_files.to_a.second }
+
+ it do
+ expect(entity[:old_size]).to eq(1219696)
+ expect(entity[:new_size]).to eq(132)
+ end
+ end
end
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 0c2e7c1e3eb..65b62f8aa16 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -11,7 +11,8 @@ describe DiffFileEntity do
let(:diff_refs) { commit.diff_refs }
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
- let(:entity) { described_class.new(diff_file, request: {}) }
+ let(:options) { {} }
+ let(:entity) { described_class.new(diff_file, options.reverse_merge(request: {})) }
subject { entity.as_json }
@@ -23,7 +24,7 @@ describe DiffFileEntity do
let(:user) { create(:user) }
let(:request) { EntityRequest.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
+ let(:entity) { described_class.new(diff_file, options.merge(request: request, merge_request: merge_request)) }
let(:exposed_urls) { %i(edit_path view_path context_lines_path) }
it_behaves_like 'diff file entity'
@@ -49,6 +50,8 @@ describe DiffFileEntity do
end
context '#parallel_diff_lines' do
+ let(:options) { { diff_view: :parallel } }
+
it 'exposes parallel diff lines correctly' do
response = subject
diff --git a/spec/serializers/issuable_sidebar_extras_entity_spec.rb b/spec/serializers/issuable_sidebar_extras_entity_spec.rb
new file mode 100644
index 00000000000..a1a7c554b49
--- /dev/null
+++ b/spec/serializers/issuable_sidebar_extras_entity_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssuableSidebarExtrasEntity do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:resource) { create(:issue, project: project) }
+ let(:request) { double('request', current_user: user) }
+
+ subject { described_class.new(resource, request: request).as_json }
+
+ it 'have subscribe attributes' do
+ expect(subject).to include(:participants,
+ :project_emails_disabled,
+ :subscribe_disabled_description,
+ :subscribed,
+ :assignees)
+ end
+end
diff --git a/spec/serializers/job_artifact_report_entity_spec.rb b/spec/serializers/job_artifact_report_entity_spec.rb
index eef5c16d0fb..3cd12f0e9fe 100644
--- a/spec/serializers/job_artifact_report_entity_spec.rb
+++ b/spec/serializers/job_artifact_report_entity_spec.rb
@@ -22,7 +22,7 @@ describe JobArtifactReportEntity do
end
it 'exposes download path' do
- expect(subject[:download_path]).to include("jobs/#{report.job.id}/artifacts/download")
+ expect(subject[:download_path]).to include("jobs/#{report.job.id}/artifacts/download?file_type=#{report.file_type}")
end
end
end
diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb
index 062f17963c0..59ec0b22158 100644
--- a/spec/serializers/merge_request_diff_entity_spec.rb
+++ b/spec/serializers/merge_request_diff_entity_spec.rb
@@ -7,14 +7,15 @@ describe MergeRequestDiffEntity do
let(:request) { EntityRequest.new(project: project) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
let(:merge_request_diffs) { merge_request.merge_request_diffs }
+ let(:merge_request_diff) { merge_request_diffs.first }
let(:entity) do
- described_class.new(merge_request_diffs.first, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
+ described_class.new(merge_request_diff, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs)
end
- context 'as json' do
- subject { entity.as_json }
+ subject { entity.as_json }
+ context 'as json' do
it 'exposes needed attributes' do
expect(subject).to include(
:version_index, :created_at, :commits_count,
@@ -23,4 +24,16 @@ describe MergeRequestDiffEntity do
)
end
end
+
+ describe '#short_commit_sha' do
+ it 'returns short sha' do
+ expect(subject[:short_commit_sha]).to eq('b83d6e39')
+ end
+
+ it 'returns nil if head_commit_sha does not exist' do
+ allow(merge_request_diff).to receive(:head_commit_sha).and_return(nil)
+
+ expect(subject[:short_commit_sha]).to eq(nil)
+ end
+ end
end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 4872b23d26b..35940ac062e 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -358,4 +358,26 @@ describe MergeRequestWidgetEntity do
end
end
end
+
+ describe 'exposed_artifacts_path' do
+ context 'when merge request has exposed artifacts' do
+ before do
+ expect(resource).to receive(:has_exposed_artifacts?).and_return(true)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:exposed_artifacts_path]).to be_present
+ end
+ end
+
+ context 'when merge request has no exposed artifacts' do
+ before do
+ expect(resource).to receive(:has_exposed_artifacts?).and_return(false)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:exposed_artifacts_path]).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index b180ede51eb..9ce7c265e43 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -115,7 +115,7 @@ describe PipelineDetailsEntity do
context 'when pipeline has YAML errors' do
let(:pipeline) do
- create(:ci_pipeline, config: { rspec: { invalid: :value } })
+ create(:ci_pipeline, yaml_errors: 'Some error occurred')
end
it 'contains information about error' do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index ce5264ec8bb..7661c8acc13 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -180,7 +180,7 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-foss/issues/46368
- expected_queries = Gitlab.ee? ? 44 : 41
+ expected_queries = Gitlab.ee? ? 41 : 38
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
diff --git a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index ccbb4e7c30d..f2cda999932 100644
--- a/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -13,8 +13,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
end
let(:pipeline) do
- create(:ci_pipeline_with_one_job, ref: mr_merge_if_green_enabled.source_branch,
- project: project)
+ create(:ci_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project)
end
let(:service) do
@@ -226,7 +225,7 @@ describe AutoMerge::MergeWhenPipelineSucceedsService do
test.drop
end
- it 'merges when all stages succeeded' do
+ it 'merges when all stages succeeded', :sidekiq_might_not_need_inline do
expect(MergeWorker).to receive(:perform_async)
build.success
diff --git a/spec/services/ci/cancel_user_pipelines_service_spec.rb b/spec/services/ci/cancel_user_pipelines_service_spec.rb
index 251f21feaef..b18bf48a50a 100644
--- a/spec/services/ci/cancel_user_pipelines_service_spec.rb
+++ b/spec/services/ci/cancel_user_pipelines_service_spec.rb
@@ -12,7 +12,7 @@ describe Ci::CancelUserPipelinesService do
let(:pipeline) { create(:ci_pipeline, :running, user: user) }
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
- it 'cancels all running pipelines and related jobs' do
+ it 'cancels all running pipelines and related jobs', :sidekiq_might_not_need_inline do
subject
expect(pipeline.reload).to be_canceled
diff --git a/spec/services/ci/create_pipeline_service/cache_spec.rb b/spec/services/ci/create_pipeline_service/cache_spec.rb
new file mode 100644
index 00000000000..4e0567132ff
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/cache_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CreatePipelineService do
+ context 'cache' do
+ let(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:source) { :push }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+ let(:pipeline) { service.execute(source) }
+ let(:job) { pipeline.builds.find_by(name: 'job') }
+ let(:project) { create(:project, :custom_repo, files: files) }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'with cache:key' do
+ let(:files) { { 'some-file' => '' } }
+
+ let(:config) do
+ <<~EOY
+ job:
+ script:
+ - ls
+ cache:
+ key: 'a-key'
+ paths: ['logs/', 'binaries/']
+ untracked: true
+ EOY
+ end
+
+ it 'uses the provided key' do
+ expected = {
+ 'key' => 'a-key',
+ 'paths' => ['logs/', 'binaries/'],
+ 'policy' => 'pull-push',
+ 'untracked' => true
+ }
+
+ expect(pipeline).to be_persisted
+ expect(job.cache).to match(a_collection_including(expected))
+ end
+ end
+
+ context 'with cache:key:files' do
+ let(:config) do
+ <<~EOY
+ job:
+ script:
+ - ls
+ cache:
+ paths:
+ - logs/
+ key:
+ files:
+ - file.lock
+ - missing-file.lock
+ EOY
+ end
+
+ context 'when file.lock exists' do
+ let(:files) { { 'file.lock' => '' } }
+
+ it 'builds a cache key' do
+ expected = {
+ 'key' => /[a-f0-9]{40}/,
+ 'paths' => ['logs/'],
+ 'policy' => 'pull-push'
+ }
+
+ expect(pipeline).to be_persisted
+ expect(job.cache).to match(a_collection_including(expected))
+ end
+ end
+
+ context 'when file.lock does not exist' do
+ let(:files) { { 'some-file' => '' } }
+
+ it 'uses default cache key' do
+ expected = {
+ 'key' => /default/,
+ 'paths' => ['logs/'],
+ 'policy' => 'pull-push'
+ }
+
+ expect(pipeline).to be_persisted
+ expect(job.cache).to match(a_collection_including(expected))
+ end
+ end
+ end
+
+ context 'with cache:key:files and prefix' do
+ let(:config) do
+ <<~EOY
+ job:
+ script:
+ - ls
+ cache:
+ paths:
+ - logs/
+ key:
+ files:
+ - file.lock
+ prefix: '$ENV_VAR'
+ EOY
+ end
+
+ context 'when file.lock exists' do
+ let(:files) { { 'file.lock' => '' } }
+
+ it 'builds a cache key' do
+ expected = {
+ 'key' => /\$ENV_VAR-[a-f0-9]{40}/,
+ 'paths' => ['logs/'],
+ 'policy' => 'pull-push'
+ }
+
+ expect(pipeline).to be_persisted
+ expect(job.cache).to match(a_collection_including(expected))
+ end
+ end
+
+ context 'when file.lock does not exist' do
+ let(:files) { { 'some-file' => '' } }
+
+ it 'uses default cache key' do
+ expected = {
+ 'key' => /\$ENV_VAR-default/,
+ 'paths' => ['logs/'],
+ 'policy' => 'pull-push'
+ }
+
+ expect(pipeline).to be_persisted
+ expect(job.cache).to match(a_collection_including(expected))
+ end
+ end
+ end
+
+ context 'with too many files' do
+ let(:files) { { 'some-file' => '' } }
+
+ let(:config) do
+ <<~EOY
+ job:
+ script:
+ - ls
+ cache:
+ paths: ['logs/', 'binaries/']
+ untracked: true
+ key:
+ files:
+ - file.lock
+ - other-file.lock
+ - extra-file.lock
+ prefix: 'some-prefix'
+ EOY
+ end
+
+ it 'has errors' do
+ expect(pipeline).to be_persisted
+ expect(pipeline.yaml_errors).to eq("jobs:job:cache:key:files config has too many items (maximum is 2)")
+ expect(job).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 40a3b115cb5..c922266647b 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Ci::CreatePipelineService do
- context 'rules' do
- let(:user) { create(:admin) }
- let(:ref) { 'refs/heads/master' }
- let(:source) { :push }
- let(:service) { described_class.new(project, user, { ref: ref }) }
- let(:pipeline) { service.execute(source) }
- let(:build_names) { pipeline.builds.pluck(:name) }
+ let(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:source) { :push }
+ let(:project) { create(:project, :repository) }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+ let(:pipeline) { service.execute(source) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+ context 'job:rules' do
before do
stub_ci_pipeline_yaml_file(config)
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
@@ -41,6 +41,7 @@ describe Ci::CreatePipelineService do
start_in: 4 hours
EOY
end
+
let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
@@ -91,4 +92,259 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ context 'when workflow:rules are used' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'with a single regex-matching if: clause' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ - if: $CI_COMMIT_REF_NAME =~ /wip$/
+ when: never
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'matching the first rule in the list' do
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+
+ context 'matching the last rule in the list' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+
+ context 'matching the when:never rule' do
+ let(:ref) { 'refs/heads/wip' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches errors' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+ end
+
+ context 'matching no rules in the list' do
+ let(:ref) { 'refs/heads/fix' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches errors' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+ end
+ end
+
+ context 'when root variables are used' do
+ let(:config) do
+ <<-EOY
+ variables:
+ VARIABLE: value
+
+ workflow:
+ rules:
+ - if: $VARIABLE
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'matching the first rule in the list' do
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+ end
+
+ context 'with a multiple regex-matching if: clause' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ - if: $CI_COMMIT_REF_NAME =~ /^feature/ && $CI_COMMIT_REF_NAME =~ /conflict$/
+ when: never
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+
+ regular-job:
+ script: 'echo Hello, World!'
+ EOY
+ end
+
+ context 'with partial match' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+
+ context 'with complete match' do
+ let(:ref) { 'refs/heads/feature_conflict' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches errors' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+ end
+ end
+
+ context 'with job rules' do
+ let(:config) do
+ <<-EOY
+ workflow:
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /master/
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+
+ regular-job:
+ script: 'echo Hello, World!'
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /wip/
+ - if: $CI_COMMIT_REF_NAME =~ /feature/
+ EOY
+ end
+
+ context 'where workflow passes and the job fails' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches an error about no job in the pipeline' do
+ expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
+ end
+
+ context 'with workflow:rules shut off' do
+ before do
+ stub_feature_flags(workflow_rules: false)
+ end
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches an error about no job in the pipeline' do
+ expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
+ end
+ end
+ end
+
+ context 'where workflow passes and the job passes' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+
+ context 'with workflow:rules shut off' do
+ before do
+ stub_feature_flags(workflow_rules: false)
+ end
+
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+ end
+
+ context 'where workflow fails and the job fails' do
+ let(:ref) { 'refs/heads/fix' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches an error about workflow rules' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+
+ context 'with workflow:rules shut off' do
+ before do
+ stub_feature_flags(workflow_rules: false)
+ end
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches an error about job rules' do
+ expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
+ end
+ end
+ end
+
+ context 'where workflow fails and the job passes' do
+ let(:ref) { 'refs/heads/wip' }
+
+ it 'does not save the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'attaches an error about workflow rules' do
+ expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
+ end
+
+ context 'with workflow:rules shut off' do
+ before do
+ stub_feature_flags(workflow_rules: false)
+ end
+
+ it 'saves the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'sets the pipeline state to pending' do
+ expect(pipeline).to be_pending
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index fd5f72c4c46..de0f4841215 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -65,6 +65,7 @@ describe Ci::CreatePipelineService do
expect(pipeline.iid).not_to be_nil
expect(pipeline.repository_source?).to be true
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
+ expect(pipeline.yaml_errors).not_to be_present
end
it 'increments the prometheus counter' do
@@ -97,7 +98,7 @@ describe Ci::CreatePipelineService do
end
context 'when the head pipeline sha equals merge request sha' do
- it 'updates head pipeline of each merge request' do
+ it 'updates head pipeline of each merge request', :sidekiq_might_not_need_inline do
merge_request_1
merge_request_2
@@ -140,7 +141,7 @@ describe Ci::CreatePipelineService do
let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- it 'updates head pipeline for merge request' do
+ it 'updates head pipeline for merge request', :sidekiq_might_not_need_inline do
merge_request = create(:merge_request, source_branch: 'feature',
target_branch: "master",
source_project: project,
@@ -172,7 +173,7 @@ describe Ci::CreatePipelineService do
stub_ci_pipeline_yaml_file('some invalid syntax')
end
- it 'updates merge request head pipeline reference' do
+ it 'updates merge request head pipeline reference', :sidekiq_might_not_need_inline do
merge_request = create(:merge_request, source_branch: 'master',
target_branch: 'feature',
source_project: project)
@@ -192,7 +193,7 @@ describe Ci::CreatePipelineService do
.and_return('some commit [ci skip]')
end
- it 'updates merge request head pipeline' do
+ it 'updates merge request head pipeline', :sidekiq_might_not_need_inline do
merge_request = create(:merge_request, source_branch: 'master',
target_branch: 'feature',
source_project: project)
@@ -218,21 +219,21 @@ describe Ci::CreatePipelineService do
expect(pipeline.reload).to have_attributes(status: 'pending', auto_canceled_by_id: nil)
end
- it 'auto cancel pending non-HEAD pipelines' do
+ it 'auto cancel pending non-HEAD pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit
pipeline
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: pipeline.id)
end
- it 'cancels running outdated pipelines' do
+ it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit.run
head_pipeline = execute_service
expect(pipeline_on_previous_commit.reload).to have_attributes(status: 'canceled', auto_canceled_by_id: head_pipeline.id)
end
- it 'cancel created outdated pipelines' do
+ it 'cancel created outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit.update(status: 'created')
pipeline
@@ -346,7 +347,7 @@ describe Ci::CreatePipelineService do
context 'when only interruptible builds are running' do
context 'when build marked explicitly by interruptible is running' do
- it 'cancels running outdated pipelines' do
+ it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit
.builds
.find_by_name('build_1_2')
@@ -360,7 +361,7 @@ describe Ci::CreatePipelineService do
end
context 'when build that is not marked as interruptible is running' do
- it 'cancels running outdated pipelines' do
+ it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit
.builds
.find_by_name('build_2_1')
@@ -376,7 +377,7 @@ describe Ci::CreatePipelineService do
end
context 'when an uninterruptible build is running' do
- it 'does not cancel running outdated pipelines' do
+ it 'does not cancel running outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit
.builds
.find_by_name('build_3_1')
@@ -391,7 +392,7 @@ describe Ci::CreatePipelineService do
end
context 'when an build is waiting on an interruptible scheduled task' do
- it 'cancels running outdated pipelines' do
+ it 'cancels running outdated pipelines', :sidekiq_might_not_need_inline do
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
pipeline_on_previous_commit
@@ -407,7 +408,7 @@ describe Ci::CreatePipelineService do
end
context 'when a uninterruptible build has finished' do
- it 'does not cancel running outdated pipelines' do
+ it 'does not cancel running outdated pipelines', :sidekiq_might_not_need_inline do
pipeline_on_previous_commit
.builds
.find_by_name('build_3_1')
@@ -474,6 +475,66 @@ describe Ci::CreatePipelineService do
end
end
+ context 'config evaluation' do
+ context 'when config is in a file in repository' do
+ before do
+ content = YAML.dump(rspec: { script: 'echo' })
+ stub_ci_pipeline_yaml_file(content)
+ end
+
+ it 'pull it from the repository' do
+ pipeline = execute_service
+ expect(pipeline).to be_repository_source
+ expect(pipeline.builds.map(&:name)).to eq ['rspec']
+ end
+ end
+
+ context 'when config is from Auto-DevOps' do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(true)
+ end
+
+ it 'pull it from Auto-DevOps' do
+ pipeline = execute_service
+ expect(pipeline).to be_auto_devops_source
+ expect(pipeline.builds.map(&:name)).to eq %w[test code_quality build]
+ end
+ end
+
+ context 'when config is not found' do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ end
+
+ it 'attaches errors to the pipeline' do
+ pipeline = execute_service
+
+ expect(pipeline.errors.full_messages).to eq ['Missing .gitlab-ci.yml file']
+ expect(pipeline).not_to be_persisted
+ end
+ end
+
+ context 'when an unexpected error is raised' do
+ before do
+ expect(Gitlab::Ci::YamlProcessor).to receive(:new)
+ .and_raise(RuntimeError, 'undefined failure')
+ end
+
+ it 'saves error in pipeline' do
+ pipeline = execute_service
+
+ expect(pipeline.yaml_errors).to include('Undefined error')
+ end
+
+ it 'logs error' do
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original
+
+ execute_service
+ end
+ end
+ end
+
context 'when yaml is invalid' do
let(:ci_yaml) { 'invalid: file: fiile' }
let(:message) { 'Message' }
@@ -539,6 +600,25 @@ describe Ci::CreatePipelineService do
end
end
+ context 'when an unexpected error is raised' do
+ before do
+ expect(Gitlab::Ci::YamlProcessor).to receive(:new)
+ .and_raise(RuntimeError, 'undefined failure')
+ end
+
+ it 'saves error in pipeline' do
+ pipeline = execute_service
+
+ expect(pipeline.yaml_errors).to include('Undefined error')
+ end
+
+ it 'logs error' do
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original
+
+ execute_service
+ end
+ end
+
context 'when commit contains a [ci skip] directive' do
let(:message) { "some message[ci skip]" }
@@ -773,8 +853,8 @@ describe Ci::CreatePipelineService do
it 'correctly creates builds with auto-retry value configured' do
expect(pipeline).to be_persisted
- expect(rspec_job.retries_max).to eq 2
- expect(rspec_job.retry_when).to eq ['always']
+ expect(rspec_job.options_retry_max).to eq 2
+ expect(rspec_job.options_retry_when).to eq ['always']
end
end
@@ -783,8 +863,8 @@ describe Ci::CreatePipelineService do
it 'correctly creates builds with auto-retry value configured' do
expect(pipeline).to be_persisted
- expect(rspec_job.retries_max).to eq 2
- expect(rspec_job.retry_when).to eq ['runner_system_failure']
+ expect(rspec_job.options_retry_max).to eq 2
+ expect(rspec_job.options_retry_when).to eq ['runner_system_failure']
end
end
end
@@ -1236,7 +1316,7 @@ describe Ci::CreatePipelineService do
let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- it 'creates a legacy detached merge request pipeline in the forked project' do
+ it 'creates a legacy detached merge request pipeline in the forked project', :sidekiq_might_not_need_inline do
expect(pipeline).to be_persisted
expect(project.ci_pipelines).to eq([pipeline])
expect(target_project.ci_pipelines).to be_empty
diff --git a/spec/services/ci/find_exposed_artifacts_service_spec.rb b/spec/services/ci/find_exposed_artifacts_service_spec.rb
new file mode 100644
index 00000000000..f6309822fe0
--- /dev/null
+++ b/spec/services/ci/find_exposed_artifacts_service_spec.rb
@@ -0,0 +1,147 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::FindExposedArtifactsService do
+ include Gitlab::Routing
+
+ let(:metadata) do
+ Gitlab::Ci::Build::Artifacts::Metadata
+ .new(metadata_file_stream, path, { recursive: true })
+ end
+
+ let(:metadata_file_stream) do
+ File.open(Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz')
+ end
+
+ let_it_be(:project) { create(:project) }
+ let(:user) { nil }
+
+ after do
+ metadata_file_stream&.close
+ end
+
+ def create_job_with_artifacts(options)
+ create(:ci_build, pipeline: pipeline, options: options).tap do |job|
+ create(:ci_job_artifact, :metadata, job: job)
+ end
+ end
+
+ describe '#for_pipeline' do
+ shared_examples 'finds a single match' do
+ it 'returns the artifact with exact location' do
+ expect(subject).to eq([{
+ text: 'Exposed artifact',
+ url: file_project_job_artifacts_path(project, job, 'other_artifacts_0.1.2/doc_sample.txt'),
+ job_name: job.name,
+ job_path: project_job_path(project, job)
+ }])
+ end
+ end
+
+ shared_examples 'finds multiple matches' do
+ it 'returns the path to the artifacts browser' do
+ expect(subject).to eq([{
+ text: 'Exposed artifact',
+ url: browse_project_job_artifacts_path(project, job),
+ job_name: job.name,
+ job_path: project_job_path(project, job)
+ }])
+ end
+ end
+
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ subject { described_class.new(project, user).for_pipeline(pipeline) }
+
+ context 'with jobs having at most 1 matching exposed artifact' do
+ let!(:job) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'Exposed artifact',
+ paths: ['other_artifacts_0.1.2/doc_sample.txt', 'something-else.html']
+ })
+ end
+
+ it_behaves_like 'finds a single match'
+ end
+
+ context 'with jobs having more than 1 matching exposed artifacts' do
+ let!(:job) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'Exposed artifact',
+ paths: [
+ 'ci_artifacts.txt',
+ 'other_artifacts_0.1.2/doc_sample.txt',
+ 'something-else.html'
+ ]
+ })
+ end
+
+ it_behaves_like 'finds multiple matches'
+ end
+
+ context 'with jobs having more than 1 matching exposed artifacts inside a directory' do
+ let!(:job) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'Exposed artifact',
+ paths: ['tests_encoding/']
+ })
+ end
+
+ it_behaves_like 'finds multiple matches'
+ end
+
+ context 'with jobs having paths with glob expression' do
+ let!(:job) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'Exposed artifact',
+ paths: ['other_artifacts_0.1.2/doc_sample.txt', 'tests_encoding/*.*']
+ })
+ end
+
+ it_behaves_like 'finds a single match' # because those with * are ignored
+ end
+
+ context 'limiting results' do
+ let!(:job1) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'artifact 1',
+ paths: ['ci_artifacts.txt']
+ })
+ end
+
+ let!(:job2) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'artifact 2',
+ paths: ['tests_encoding/']
+ })
+ end
+
+ let!(:job3) do
+ create_job_with_artifacts(artifacts: {
+ expose_as: 'should not be exposed',
+ paths: ['other_artifacts_0.1.2/doc_sample.txt']
+ })
+ end
+
+ subject { described_class.new(project, user).for_pipeline(pipeline, limit: 2) }
+
+ it 'returns first 2 results' do
+ expect(subject).to eq([
+ {
+ text: 'artifact 1',
+ url: file_project_job_artifacts_path(project, job1, 'ci_artifacts.txt'),
+ job_name: job1.name,
+ job_path: project_job_path(project, job1)
+ },
+ {
+ text: 'artifact 2',
+ url: browse_project_job_artifacts_path(project, job2),
+ job_name: job2.name,
+ job_path: project_job_path(project, job2)
+ }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 05adec8b745..991f8cdfac5 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -27,7 +27,7 @@ describe Ci::ProcessPipelineService, '#execute' do
create_build('deploy', stage_idx: 2)
end
- it 'processes a pipeline' do
+ it 'processes a pipeline', :sidekiq_might_not_need_inline do
expect(process_pipeline).to be_truthy
succeed_pending
@@ -58,7 +58,7 @@ describe Ci::ProcessPipelineService, '#execute' do
create_build('test_job', stage_idx: 1, allow_failure: true)
end
- it 'automatically triggers a next stage when build finishes' do
+ it 'automatically triggers a next stage when build finishes', :sidekiq_might_not_need_inline do
expect(process_pipeline).to be_truthy
expect(builds_statuses).to eq ['pending']
@@ -72,7 +72,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when optional manual actions are defined' do
+ context 'when optional manual actions are defined', :sidekiq_might_not_need_inline do
before do
create_build('build', stage_idx: 0)
create_build('test', stage_idx: 1)
@@ -241,7 +241,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when delayed jobs are defined' do
+ context 'when delayed jobs are defined', :sidekiq_might_not_need_inline do
context 'when the scene is timed incremental rollout' do
before do
create_build('build', stage_idx: 0)
@@ -458,7 +458,7 @@ describe Ci::ProcessPipelineService, '#execute' do
process_pipeline
end
- it 'skips second stage and continues on third stage' do
+ it 'skips second stage and continues on third stage', :sidekiq_might_not_need_inline do
expect(all_builds_statuses).to eq(%w[pending created created])
builds.first.success
@@ -502,7 +502,7 @@ describe Ci::ProcessPipelineService, '#execute' do
play_manual_action('deploy')
end
- it 'queues the action and pipeline' do
+ it 'queues the action and pipeline', :sidekiq_might_not_need_inline do
expect(all_builds_statuses).to eq(%w[pending])
expect(pipeline.reload).to be_pending
@@ -510,7 +510,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when blocking manual actions are defined' do
+ context 'when blocking manual actions are defined', :sidekiq_might_not_need_inline do
before do
create_build('code:test', stage_idx: 0)
create_build('staging:deploy', stage_idx: 1, when: 'manual')
@@ -618,7 +618,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when second stage has only on_failure jobs' do
+ context 'when second stage has only on_failure jobs', :sidekiq_might_not_need_inline do
before do
create_build('check', stage_idx: 0)
create_build('build', stage_idx: 1, when: 'on_failure')
@@ -636,7 +636,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when failed build in the middle stage is retried' do
+ context 'when failed build in the middle stage is retried', :sidekiq_might_not_need_inline do
context 'when failed build is the only unsuccessful build in the stage' do
before do
create_build('build:1', stage_idx: 0)
@@ -683,7 +683,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when builds with auto-retries are configured' do
+ context 'when builds with auto-retries are configured', :sidekiq_might_not_need_inline do
before do
create_build('build:1', stage_idx: 0, user: user, options: { script: 'aa', retry: 2 })
create_build('test:1', stage_idx: 1, user: user, when: :on_failure)
@@ -712,7 +712,7 @@ describe Ci::ProcessPipelineService, '#execute' do
end
end
- context 'when pipeline with needs is created' do
+ context 'when pipeline with needs is created', :sidekiq_might_not_need_inline do
let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0) }
let!(:mac_build) { create_build('mac:build', stage: 'build', stage_idx: 0) }
let!(:linux_rspec) { create_build('linux:rspec', stage: 'test', stage_idx: 1) }
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 2f2c525ccc4..04334fb8915 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -502,6 +502,57 @@ module Ci
end
end
+ context 'when build has data integrity problem' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ before do
+ pending_job.update_columns(options: "string")
+ end
+
+ subject { execute(specific_runner, {}) }
+
+ it 'does drop the build and logs both failures' do
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
+ .with(anything, a_hash_including(extra: a_hash_including(build_id: pending_job.id)))
+ .twice
+ .and_call_original
+
+ expect(subject).to be_nil
+
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_data_integrity_failure
+ end
+ end
+
+ context 'when build fails to be run!' do
+ let!(:pending_job) do
+ create(:ci_build, :pending, pipeline: pipeline)
+ end
+
+ before do
+ expect_any_instance_of(Ci::Build).to receive(:run!)
+ .and_raise(RuntimeError, 'scheduler error')
+ end
+
+ subject { execute(specific_runner, {}) }
+
+ it 'does drop the build and logs failure' do
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
+ .with(anything, a_hash_including(extra: a_hash_including(build_id: pending_job.id)))
+ .once
+ .and_call_original
+
+ expect(subject).to be_nil
+
+ pending_job.reload
+ expect(pending_job).to be_failed
+ expect(pending_job).to be_scheduler_failure
+ end
+ end
+
context 'when an exception is raised during a persistent ref creation' do
before do
allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false }
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 8dd573c3698..bdacb9ce071 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -132,6 +132,34 @@ describe Clusters::Applications::CreateService do
expect(subject.hostname).to eq('example.com')
end
end
+
+ context 'elastic stack application' do
+ let(:params) do
+ {
+ application: 'elastic_stack',
+ kibana_hostname: 'example.com'
+ }
+ end
+
+ before do
+ create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
+ expect_any_instance_of(Clusters::Applications::ElasticStack)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_elastic_stack)
+ end
+
+ it 'sets the kibana_hostname' do
+ expect(subject.kibana_hostname).to eq('example.com')
+ end
+ end
end
context 'invalid application' do
diff --git a/spec/services/clusters/aws/fetch_credentials_service_spec.rb b/spec/services/clusters/aws/fetch_credentials_service_spec.rb
new file mode 100644
index 00000000000..726d1c30603
--- /dev/null
+++ b/spec/services/clusters/aws/fetch_credentials_service_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Aws::FetchCredentialsService do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:provider) { create(:cluster_provider_aws) }
+
+ let(:gitlab_access_key_id) { 'gitlab-access-key-id' }
+ let(:gitlab_secret_access_key) { 'gitlab-secret-access-key' }
+
+ let(:region) { 'us-east-1' }
+ let(:gitlab_credentials) { Aws::Credentials.new(gitlab_access_key_id, gitlab_secret_access_key) }
+ let(:sts_client) { Aws::STS::Client.new(credentials: gitlab_credentials, region: region) }
+ let(:assumed_role) { instance_double(Aws::AssumeRoleCredentials, credentials: assumed_role_credentials) }
+
+ let(:assumed_role_credentials) { double }
+
+ subject { described_class.new(provision_role, region: region, provider: provider).execute }
+
+ context 'provision role is configured' do
+ let(:provision_role) { create(:aws_role, user: user) }
+
+ before do
+ stub_application_setting(eks_access_key_id: gitlab_access_key_id)
+ stub_application_setting(eks_secret_access_key: gitlab_secret_access_key)
+
+ expect(Aws::Credentials).to receive(:new)
+ .with(gitlab_access_key_id, gitlab_secret_access_key)
+ .and_return(gitlab_credentials)
+
+ expect(Aws::STS::Client).to receive(:new)
+ .with(credentials: gitlab_credentials, region: region)
+ .and_return(sts_client)
+
+ expect(Aws::AssumeRoleCredentials).to receive(:new)
+ .with(
+ client: sts_client,
+ role_arn: provision_role.role_arn,
+ role_session_name: session_name,
+ external_id: provision_role.role_external_id
+ ).and_return(assumed_role)
+ end
+
+ context 'provider is specified' do
+ let(:session_name) { "gitlab-eks-cluster-#{provider.cluster_id}-user-#{user.id}" }
+
+ it { is_expected.to eq assumed_role_credentials }
+ end
+
+ context 'provider is not specifed' do
+ let(:provider) { nil }
+ let(:session_name) { "gitlab-eks-autofill-user-#{user.id}" }
+
+ it { is_expected.to eq assumed_role_credentials }
+ end
+ end
+
+ context 'provision role is not configured' do
+ let(:provision_role) { nil }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::MissingRoleError, 'AWS provisioning role not configured')
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/aws/finalize_creation_service_spec.rb b/spec/services/clusters/aws/finalize_creation_service_spec.rb
new file mode 100644
index 00000000000..8d7341483e3
--- /dev/null
+++ b/spec/services/clusters/aws/finalize_creation_service_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Aws::FinalizeCreationService do
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_aws, :creating) }
+ let(:platform) { provider.cluster.platform_kubernetes }
+
+ let(:create_service_account_service) { double(execute: true) }
+ let(:fetch_token_service) { double(execute: gitlab_token) }
+ let(:kube_client) { double(create_config_map: true) }
+ let(:cluster_stack) { double(outputs: [endpoint_output, cert_output, node_role_output]) }
+ let(:node_auth_config_map) { double }
+
+ let(:endpoint_output) { double(output_key: 'ClusterEndpoint', output_value: api_url) }
+ let(:cert_output) { double(output_key: 'ClusterCertificate', output_value: Base64.encode64(ca_pem)) }
+ let(:node_role_output) { double(output_key: 'NodeInstanceRole', output_value: node_role) }
+
+ let(:api_url) { 'https://kubernetes.example.com' }
+ let(:ca_pem) { File.read(Rails.root.join('spec/fixtures/clusters/sample_cert.pem')) }
+ let(:gitlab_token) { 'gitlab-token' }
+ let(:iam_token) { 'iam-token' }
+ let(:node_role) { 'arn::aws::iam::123456789012:role/node-role' }
+
+ subject { described_class.new.execute(provider) }
+
+ before do
+ allow(Clusters::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:gitlab_creator)
+ .with(kube_client, rbac: true)
+ .and_return(create_service_account_service)
+
+ allow(Clusters::Kubernetes::FetchKubernetesTokenService).to receive(:new)
+ .with(
+ kube_client,
+ Clusters::Kubernetes::GITLAB_ADMIN_TOKEN_NAME,
+ Clusters::Kubernetes::GITLAB_SERVICE_ACCOUNT_NAMESPACE)
+ .and_return(fetch_token_service)
+
+ allow(Gitlab::Kubernetes::KubeClient).to receive(:new)
+ .with(
+ api_url,
+ auth_options: { bearer_token: iam_token },
+ ssl_options: {
+ verify_ssl: OpenSSL::SSL::VERIFY_PEER,
+ cert_store: instance_of(OpenSSL::X509::Store)
+ },
+ http_proxy_uri: nil
+ )
+ .and_return(kube_client)
+
+ allow(provider.api_client).to receive(:describe_stacks)
+ .with(stack_name: provider.cluster.name)
+ .and_return(double(stacks: [cluster_stack]))
+
+ allow(Kubeclient::AmazonEksCredentials).to receive(:token)
+ .with(provider.credentials, provider.cluster.name)
+ .and_return(iam_token)
+
+ allow(Gitlab::Kubernetes::ConfigMaps::AwsNodeAuth).to receive(:new)
+ .with(node_role).and_return(double(generate: node_auth_config_map))
+ end
+
+ it 'configures the provider and platform' do
+ subject
+
+ expect(provider).to be_created
+ expect(platform.api_url).to eq(api_url)
+ expect(platform.ca_pem).to eq(ca_pem)
+ expect(platform.token).to eq(gitlab_token)
+ expect(platform).to be_rbac
+ end
+
+ it 'calls the create_service_account_service' do
+ expect(create_service_account_service).to receive(:execute).once
+
+ subject
+ end
+
+ it 'configures cluster node authentication' do
+ expect(kube_client).to receive(:create_config_map).with(node_auth_config_map).once
+
+ subject
+ end
+
+ describe 'error handling' do
+ shared_examples 'provision error' do |message|
+ it "sets the status to :errored with an appropriate error message" do
+ subject
+
+ expect(provider).to be_errored
+ expect(provider.status_reason).to include(message)
+ end
+ end
+
+ context 'failed to request stack details from AWS' do
+ before do
+ allow(provider.api_client).to receive(:describe_stacks)
+ .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, "Error message"))
+ end
+
+ include_examples 'provision error', 'Failed to fetch CloudFormation stack'
+ end
+
+ context 'failed to create auth config map' do
+ before do
+ allow(kube_client).to receive(:create_config_map)
+ .and_raise(Kubeclient::HttpError.new(500, 'Error', nil))
+ end
+
+ include_examples 'provision error', 'Failed to run Kubeclient'
+ end
+
+ context 'failed to save records' do
+ before do
+ allow(provider.cluster).to receive(:save!)
+ .and_raise(ActiveRecord::RecordInvalid)
+ end
+
+ include_examples 'provision error', 'Failed to configure EKS provider'
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/aws/provision_service_spec.rb b/spec/services/clusters/aws/provision_service_spec.rb
new file mode 100644
index 00000000000..927ffaef002
--- /dev/null
+++ b/spec/services/clusters/aws/provision_service_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Aws::ProvisionService do
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_aws) }
+
+ let(:provision_role) { create(:aws_role, user: provider.created_by_user) }
+ let(:client) { instance_double(Aws::CloudFormation::Client, create_stack: true) }
+ let(:cloudformation_template) { double }
+ let(:credentials) do
+ instance_double(
+ Aws::Credentials,
+ access_key_id: 'key',
+ secret_access_key: 'secret',
+ session_token: 'token'
+ )
+ end
+
+ let(:parameters) do
+ [
+ { parameter_key: 'ClusterName', parameter_value: provider.cluster.name },
+ { parameter_key: 'ClusterRole', parameter_value: provider.role_arn },
+ { parameter_key: 'ClusterControlPlaneSecurityGroup', parameter_value: provider.security_group_id },
+ { parameter_key: 'VpcId', parameter_value: provider.vpc_id },
+ { parameter_key: 'Subnets', parameter_value: provider.subnet_ids.join(',') },
+ { parameter_key: 'NodeAutoScalingGroupDesiredCapacity', parameter_value: provider.num_nodes.to_s },
+ { parameter_key: 'NodeInstanceType', parameter_value: provider.instance_type },
+ { parameter_key: 'KeyName', parameter_value: provider.key_name }
+ ]
+ end
+
+ subject { described_class.new.execute(provider) }
+
+ before do
+ allow(Clusters::Aws::FetchCredentialsService).to receive(:new)
+ .with(provision_role, provider: provider, region: provider.region)
+ .and_return(double(execute: credentials))
+
+ allow(provider).to receive(:api_client)
+ .and_return(client)
+
+ allow(File).to receive(:read)
+ .with(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
+ .and_return(cloudformation_template)
+ end
+
+ it 'updates the provider status to :creating and configures the provider with credentials' do
+ subject
+
+ expect(provider).to be_creating
+ expect(provider.access_key_id).to eq 'key'
+ expect(provider.secret_access_key).to eq 'secret'
+ expect(provider.session_token).to eq 'token'
+ end
+
+ it 'creates a CloudFormation stack' do
+ expect(client).to receive(:create_stack).with(
+ stack_name: provider.cluster.name,
+ template_body: cloudformation_template,
+ parameters: parameters,
+ capabilities: ["CAPABILITY_IAM"]
+ )
+
+ subject
+ end
+
+ it 'schedules a worker to monitor creation status' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+ .with(Clusters::Aws::VerifyProvisionStatusService::INITIAL_INTERVAL, provider.cluster_id)
+
+ subject
+ end
+
+ describe 'error handling' do
+ shared_examples 'provision error' do |message|
+ it "sets the status to :errored with an appropriate error message" do
+ subject
+
+ expect(provider).to be_errored
+ expect(provider.status_reason).to include(message)
+ end
+ end
+
+ context 'invalid state transition' do
+ before do
+ allow(provider).to receive(:make_creating).and_return(false)
+ end
+
+ include_examples 'provision error', 'Failed to update provider record'
+ end
+
+ context 'AWS role is not configured' do
+ before do
+ allow(Clusters::Aws::FetchCredentialsService).to receive(:new)
+ .and_raise(Clusters::Aws::FetchCredentialsService::MissingRoleError)
+ end
+
+ include_examples 'provision error', 'Amazon role is not configured'
+ end
+
+ context 'AWS credentials are not configured' do
+ before do
+ allow(Clusters::Aws::FetchCredentialsService).to receive(:new)
+ .and_raise(Aws::Errors::MissingCredentialsError)
+ end
+
+ include_examples 'provision error', 'Amazon credentials are not configured'
+ end
+
+ context 'Authentication failure' do
+ before do
+ allow(Clusters::Aws::FetchCredentialsService).to receive(:new)
+ .and_raise(Aws::STS::Errors::ServiceError.new(double, 'Error message'))
+ end
+
+ include_examples 'provision error', 'Amazon authentication failed'
+ end
+
+ context 'CloudFormation failure' do
+ before do
+ allow(client).to receive(:create_stack)
+ .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, 'Error message'))
+ end
+
+ include_examples 'provision error', 'Amazon CloudFormation request failed'
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/aws/proxy_service_spec.rb b/spec/services/clusters/aws/proxy_service_spec.rb
new file mode 100644
index 00000000000..7b0e0512b95
--- /dev/null
+++ b/spec/services/clusters/aws/proxy_service_spec.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Aws::ProxyService do
+ let(:role) { create(:aws_role) }
+ let(:credentials) { instance_double(Aws::Credentials) }
+ let(:client_instance) { instance_double(client) }
+
+ let(:region) { 'region' }
+ let(:vpc_id) { }
+ let(:params) do
+ ActionController::Parameters.new({
+ resource: resource,
+ region: region,
+ vpc_id: vpc_id
+ })
+ end
+
+ subject { described_class.new(role, params: params).execute }
+
+ context 'external resources' do
+ before do
+ allow(Clusters::Aws::FetchCredentialsService).to receive(:new) do
+ double(execute: credentials)
+ end
+
+ allow(client).to receive(:new)
+ .with(
+ credentials: credentials, region: region,
+ http_open_timeout: 5, http_read_timeout: 10)
+ .and_return(client_instance)
+ end
+
+ shared_examples 'bad request' do
+ it 'returns an empty hash' do
+ expect(subject.status).to eq :bad_request
+ expect(subject.body).to eq({})
+ end
+ end
+
+ describe 'key_pairs' do
+ let(:client) { Aws::EC2::Client }
+ let(:resource) { 'key_pairs' }
+ let(:response) { double(to_hash: :key_pairs) }
+
+ it 'requests a list of key pairs' do
+ expect(client_instance).to receive(:describe_key_pairs).once.and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :key_pairs
+ end
+ end
+
+ describe 'roles' do
+ let(:client) { Aws::IAM::Client }
+ let(:resource) { 'roles' }
+ let(:response) { double(to_hash: :roles) }
+
+ it 'requests a list of roles' do
+ expect(client_instance).to receive(:list_roles).once.and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :roles
+ end
+ end
+
+ describe 'regions' do
+ let(:client) { Aws::EC2::Client }
+ let(:resource) { 'regions' }
+ let(:response) { double(to_hash: :regions) }
+
+ it 'requests a list of regions' do
+ expect(client_instance).to receive(:describe_regions).once.and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :regions
+ end
+ end
+
+ describe 'security_groups' do
+ let(:client) { Aws::EC2::Client }
+ let(:resource) { 'security_groups' }
+ let(:response) { double(to_hash: :security_groups) }
+
+ include_examples 'bad request'
+
+ context 'VPC is specified' do
+ let(:vpc_id) { 'vpc-1' }
+
+ it 'requests a list of security groups for a VPC' do
+ expect(client_instance).to receive(:describe_security_groups).once
+ .with(filters: [{ name: 'vpc-id', values: [vpc_id] }])
+ .and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :security_groups
+ end
+ end
+ end
+
+ describe 'subnets' do
+ let(:client) { Aws::EC2::Client }
+ let(:resource) { 'subnets' }
+ let(:response) { double(to_hash: :subnets) }
+
+ include_examples 'bad request'
+
+ context 'VPC is specified' do
+ let(:vpc_id) { 'vpc-1' }
+
+ it 'requests a list of subnets for a VPC' do
+ expect(client_instance).to receive(:describe_subnets).once
+ .with(filters: [{ name: 'vpc-id', values: [vpc_id] }])
+ .and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :subnets
+ end
+ end
+ end
+
+ describe 'vpcs' do
+ let(:client) { Aws::EC2::Client }
+ let(:resource) { 'vpcs' }
+ let(:response) { double(to_hash: :vpcs) }
+
+ it 'requests a list of VPCs' do
+ expect(client_instance).to receive(:describe_vpcs).once.and_return(response)
+ expect(subject.status).to eq :ok
+ expect(subject.body).to eq :vpcs
+ end
+ end
+
+ context 'errors' do
+ let(:client) { Aws::EC2::Client }
+
+ context 'unknown resource' do
+ let(:resource) { 'instances' }
+
+ include_examples 'bad request'
+ end
+
+ context 'client and configuration errors' do
+ let(:resource) { 'vpcs' }
+
+ before do
+ allow(client_instance).to receive(:describe_vpcs).and_raise(error)
+ end
+
+ context 'error fetching credentials' do
+ let(:error) { Aws::STS::Errors::ServiceError.new(nil, 'error message') }
+
+ include_examples 'bad request'
+ end
+
+ context 'credentials not configured' do
+ let(:error) { Aws::Errors::MissingCredentialsError.new('error message') }
+
+ include_examples 'bad request'
+ end
+
+ context 'role not configured' do
+ let(:error) { Clusters::Aws::FetchCredentialsService::MissingRoleError.new('error message') }
+
+ include_examples 'bad request'
+ end
+
+ context 'EC2 error' do
+ let(:error) { Aws::EC2::Errors::ServiceError.new(nil, 'error message') }
+
+ include_examples 'bad request'
+ end
+
+ context 'IAM error' do
+ let(:error) { Aws::IAM::Errors::ServiceError.new(nil, 'error message') }
+
+ include_examples 'bad request'
+ end
+
+ context 'STS error' do
+ let(:error) { Aws::STS::Errors::ServiceError.new(nil, 'error message') }
+
+ include_examples 'bad request'
+ end
+ end
+ end
+ end
+
+ context 'local resources' do
+ describe 'instance_types' do
+ let(:resource) { 'instance_types' }
+ let(:cloudformation_template) { double }
+ let(:instance_types) { double(dig: %w(t3.small)) }
+
+ before do
+ allow(File).to receive(:read)
+ .with(Rails.root.join('vendor', 'aws', 'cloudformation', 'eks_cluster.yaml'))
+ .and_return(cloudformation_template)
+
+ allow(YAML).to receive(:safe_load)
+ .with(cloudformation_template)
+ .and_return(instance_types)
+ end
+
+ it 'returns a list of instance types' do
+ expect(subject.status).to eq :ok
+ expect(subject.body).to have_key(:instance_types)
+ expect(subject.body[:instance_types]).to match_array([
+ instance_type_name: 't3.small'
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/aws/verify_provision_status_service_spec.rb b/spec/services/clusters/aws/verify_provision_status_service_spec.rb
new file mode 100644
index 00000000000..b62b0875bf3
--- /dev/null
+++ b/spec/services/clusters/aws/verify_provision_status_service_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Aws::VerifyProvisionStatusService do
+ describe '#execute' do
+ let(:provider) { create(:cluster_provider_aws) }
+
+ let(:stack) { double(stack_status: stack_status, creation_time: creation_time) }
+ let(:creation_time) { 1.minute.ago }
+
+ subject { described_class.new.execute(provider) }
+
+ before do
+ allow(provider.api_client).to receive(:describe_stacks)
+ .with(stack_name: provider.cluster.name)
+ .and_return(double(stacks: [stack]))
+ end
+
+ shared_examples 'provision error' do |message|
+ it "sets the status to :errored with an appropriate error message" do
+ subject
+
+ expect(provider).to be_errored
+ expect(provider.status_reason).to include(message)
+ end
+ end
+
+ context 'stack creation is still in progress' do
+ let(:stack_status) { 'CREATE_IN_PROGRESS' }
+ let(:verify_service) { double(execute: true) }
+
+ it 'schedules a worker to check again later' do
+ expect(WaitForClusterCreationWorker).to receive(:perform_in)
+ .with(described_class::POLL_INTERVAL, provider.cluster_id)
+
+ subject
+ end
+
+ context 'stack creation is taking too long' do
+ let(:creation_time) { 1.hour.ago }
+
+ include_examples 'provision error', 'Kubernetes cluster creation time exceeds timeout'
+ end
+ end
+
+ context 'stack creation is complete' do
+ let(:stack_status) { 'CREATE_COMPLETE' }
+ let(:finalize_service) { double(execute: true) }
+
+ it 'finalizes creation' do
+ expect(Clusters::Aws::FinalizeCreationService).to receive(:new).and_return(finalize_service)
+ expect(finalize_service).to receive(:execute).with(provider).once
+
+ subject
+ end
+ end
+
+ context 'stack creation failed' do
+ let(:stack_status) { 'CREATE_FAILED' }
+
+ include_examples 'provision error', 'Unexpected status'
+ end
+
+ context 'error communicating with CloudFormation API' do
+ let(:stack_status) { 'CREATE_IN_PROGRESS' }
+
+ before do
+ allow(provider.api_client).to receive(:describe_stacks)
+ .and_raise(Aws::CloudFormation::Errors::ServiceError.new(double, 'Error message'))
+ end
+
+ include_examples 'provision error', 'Amazon CloudFormation request failed'
+ end
+ end
+end
diff --git a/spec/services/clusters/destroy_service_spec.rb b/spec/services/clusters/destroy_service_spec.rb
new file mode 100644
index 00000000000..c0fcc971500
--- /dev/null
+++ b/spec/services/clusters/destroy_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::DestroyService do
+ describe '#execute' do
+ subject { described_class.new(cluster.user, params).execute(cluster) }
+
+ let!(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ context 'when correct params' do
+ shared_examples 'only removes cluster' do
+ it 'does not start cleanup' do
+ expect(cluster).not_to receive(:start_cleanup)
+ subject
+ end
+
+ it 'destroys the cluster' do
+ subject
+ expect { cluster.reload }.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+
+ context 'when params are empty' do
+ let(:params) { {} }
+
+ it_behaves_like 'only removes cluster'
+ end
+
+ context 'when cleanup param is false' do
+ let(:params) { { cleanup: 'false' } }
+
+ it_behaves_like 'only removes cluster'
+ end
+
+ context 'when cleanup param is true' do
+ let(:params) { { cleanup: 'true' } }
+
+ before do
+ allow(Clusters::Cleanup::AppWorker).to receive(:perform_async)
+ end
+
+ it 'does not destroy cluster' do
+ subject
+ expect(Clusters::Cluster.where(id: cluster.id).exists?).not_to be_falsey
+ end
+
+ it 'transition cluster#cleanup_status from cleanup_not_started to uninstalling_applications' do
+ expect { subject }.to change { cluster.cleanup_status_name }
+ .from(:cleanup_not_started)
+ .to(:cleanup_uninstalling_applications)
+ end
+ end
+ end
+ end
+end
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 5a3b1cd6cfb..291e63bbe4a 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
@@ -37,6 +37,8 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
stub_kubeclient_put_secret(api_url, "#{namespace}-token", 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)
+ stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace)
stub_kubeclient_get_secret(
api_url,
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 10dbfc800ff..4df73fcc2ae 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
@@ -145,6 +145,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
stub_kubeclient_create_role_binding(api_url, 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)
+ stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME, namespace: namespace)
end
it_behaves_like 'creates service account and token'
@@ -172,6 +174,31 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
)
end
+ it 'creates a role binding granting crossplane database permissions to the service account' do
+ subject
+
+ expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME}").with(
+ body: hash_including(
+ metadata: {
+ name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_BINDING_NAME,
+ namespace: namespace
+ },
+ roleRef: {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'Role',
+ name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME
+ },
+ subjects: [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ )
+ )
+ end
+
it 'creates a role and role binding granting knative serving permissions to the service account' do
subject
@@ -189,6 +216,24 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
)
)
end
+
+ it 'creates a role and role binding granting crossplane database permissions to the service account' do
+ subject
+
+ expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/roles/#{Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME}").with(
+ body: hash_including(
+ metadata: {
+ name: Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME,
+ namespace: namespace
+ },
+ rules: [{
+ apiGroups: %w(database.crossplane.io),
+ resources: %w(postgresqlinstances),
+ verbs: %w(get list create watch)
+ }]
+ )
+ )
+ end
end
end
end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index 3ee45375dca..fdbed4fa5d8 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -90,5 +90,132 @@ describe Clusters::UpdateService do
end
end
end
+
+ context 'when params includes :management_project_id' do
+ context 'management_project is non-existent' do
+ let(:params) do
+ { management_project_id: 0 }
+ end
+
+ it 'does not update management_project_id' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+
+ cluster.reload
+ expect(cluster.management_project_id).to be_nil
+ end
+ end
+
+ shared_examples 'setting a management project' do
+ context 'user is authorized to adminster manangement_project' do
+ before do
+ management_project.add_maintainer(cluster.user)
+ end
+
+ let(:params) do
+ { management_project_id: management_project.id }
+ end
+
+ it 'updates management_project_id' do
+ is_expected.to eq(true)
+
+ expect(cluster.management_project).to eq(management_project)
+ end
+ end
+
+ context 'user is not authorized to adminster manangement_project' do
+ let(:params) do
+ { management_project_id: management_project.id }
+ end
+
+ it 'does not update management_project_id' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+
+ cluster.reload
+ expect(cluster.management_project_id).to be_nil
+ end
+ end
+
+ context 'cluster already has a management project set' do
+ before do
+ cluster.update!(management_project: create(:project))
+ end
+
+ let(:params) do
+ { management_project_id: '' }
+ end
+
+ it 'unsets management_project_id' do
+ is_expected.to eq(true)
+
+ cluster.reload
+ expect(cluster.management_project_id).to be_nil
+ end
+ end
+ end
+
+ context 'project cluster' do
+ include_examples 'setting a management project' do
+ let(:management_project) { create(:project, namespace: cluster.first_project.namespace) }
+ end
+
+ context 'manangement_project is outside of the namespace scope' do
+ before do
+ management_project.update(group: create(:group))
+ end
+
+ let(:params) do
+ { management_project_id: management_project.id }
+ end
+
+ it 'does not update management_project_id' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+
+ cluster.reload
+ expect(cluster.management_project_id).to be_nil
+ end
+ end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group) }
+
+ include_examples 'setting a management project' do
+ let(:management_project) { create(:project, group: cluster.first_group) }
+ end
+
+ context 'manangement_project is outside of the namespace scope' do
+ before do
+ management_project.update(group: create(:group))
+ end
+
+ let(:params) do
+ { management_project_id: management_project.id }
+ end
+
+ it 'does not update management_project_id' do
+ is_expected.to eq(false)
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+
+ cluster.reload
+ expect(cluster.management_project_id).to be_nil
+ end
+ end
+ end
+
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, :instance) }
+
+ include_examples 'setting a management project' do
+ let(:management_project) { create(:project) }
+ end
+ end
+ end
end
end
diff --git a/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb b/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
index 5b653aa331c..9cf7f354191 100644
--- a/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
+++ b/spec/services/concerns/merge_requests/assigns_merge_params_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::AssignsMergeParams do
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb
index 0d34c7f9a82..9661173c9e7 100644
--- a/spec/services/create_branch_service_spec.rb
+++ b/spec/services/create_branch_service_spec.rb
@@ -22,5 +22,20 @@ describe CreateBranchService do
expect(project.repository.branch_exists?('my-feature')).to be_truthy
end
end
+
+ context 'when creating a branch fails' do
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ allow(project.repository).to receive(:add_branch).and_return(false)
+ end
+
+ it 'retruns an error with the branch name' do
+ result = service.execute('my-feature', 'master')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Invalid reference name: my-feature")
+ end
+ end
end
end
diff --git a/spec/services/deployments/after_create_service_spec.rb b/spec/services/deployments/after_create_service_spec.rb
index b34483ea85b..94532ed81ae 100644
--- a/spec/services/deployments/after_create_service_spec.rb
+++ b/spec/services/deployments/after_create_service_spec.rb
@@ -53,6 +53,14 @@ describe Deployments::AfterCreateService do
service.execute
end
+ it 'links merge requests to deployment' do
+ expect_next_instance_of(Deployments::LinkMergeRequestsService, deployment) do |link_mr_service|
+ expect(link_mr_service).to receive(:execute)
+ end
+
+ service.execute
+ end
+
it 'returns the deployment' do
expect(subject.execute).to eq(deployment)
end
@@ -237,4 +245,30 @@ describe Deployments::AfterCreateService do
end
end
end
+
+ describe '#update_environment' do
+ it 'links the merge requests' do
+ double = instance_double(Deployments::LinkMergeRequestsService)
+
+ allow(Deployments::LinkMergeRequestsService)
+ .to receive(:new)
+ .with(deployment)
+ .and_return(double)
+
+ expect(double).to receive(:execute)
+
+ service.update_environment(deployment)
+ end
+
+ context 'when the tracking of merge requests is disabled' do
+ it 'does nothing' do
+ stub_feature_flags(deployment_merge_requests: false)
+
+ expect(Deployments::LinkMergeRequestsService)
+ .not_to receive(:new)
+
+ service.update_environment(deployment)
+ end
+ end
+ end
end
diff --git a/spec/services/deployments/link_merge_requests_service_spec.rb b/spec/services/deployments/link_merge_requests_service_spec.rb
new file mode 100644
index 00000000000..ba069658dfd
--- /dev/null
+++ b/spec/services/deployments/link_merge_requests_service_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::LinkMergeRequestsService do
+ describe '#execute' do
+ context 'when the deployment did not succeed' do
+ it 'does nothing' do
+ deploy = create(:deployment, :failed)
+
+ expect(deploy).not_to receive(:link_merge_requests)
+
+ described_class.new(deploy).execute
+ end
+ end
+
+ context 'when there is a previous deployment' do
+ it 'links all merge requests merged since the previous deployment' do
+ deploy1 = create(:deployment, :success, sha: 'foo')
+ deploy2 = create(
+ :deployment,
+ :success,
+ sha: 'bar',
+ project: deploy1.project,
+ environment: deploy1.environment
+ )
+
+ service = described_class.new(deploy2)
+
+ expect(service)
+ .to receive(:link_merge_requests_for_range)
+ .with('foo', 'bar')
+
+ service.execute
+ end
+ end
+
+ context 'when there are no previous deployments' do
+ it 'links all merged merge requests' do
+ deploy = create(:deployment, :success)
+ service = described_class.new(deploy)
+
+ expect(service).to receive(:link_all_merged_merge_requests)
+
+ service.execute
+ end
+ end
+ end
+
+ describe '#link_merge_requests_for_range' do
+ it 'links merge requests' do
+ project = create(:project, :repository)
+ environment = create(:environment, project: project)
+ deploy =
+ create(:deployment, :success, project: project, environment: environment)
+
+ mr1 = create(
+ :merge_request,
+ :merged,
+ merge_commit_sha: '1e292f8fedd741b75372e19097c76d327140c312',
+ source_project: project,
+ target_project: project
+ )
+
+ mr2 = create(
+ :merge_request,
+ :merged,
+ merge_commit_sha: '2d1db523e11e777e49377cfb22d368deec3f0793',
+ source_project: project,
+ target_project: project
+ )
+
+ described_class.new(deploy).link_merge_requests_for_range(
+ '7975be0116940bf2ad4321f79d02a55c5f7779aa',
+ 'ddd0f15ae83993f5cb66a927a28673882e99100b'
+ )
+
+ expect(deploy.merge_requests).to include(mr1, mr2)
+ end
+ end
+
+ describe '#link_all_merged_merge_requests' do
+ it 'links all merged merge requests targeting the deployed branch' do
+ project = create(:project, :repository)
+ environment = create(:environment, project: project)
+ deploy =
+ create(:deployment, :success, project: project, environment: environment)
+
+ mr1 = create(
+ :merge_request,
+ :merged,
+ source_project: project,
+ target_project: project,
+ source_branch: 'source1',
+ target_branch: deploy.ref
+ )
+
+ mr2 = create(
+ :merge_request,
+ :merged,
+ source_project: project,
+ target_project: project,
+ source_branch: 'source2',
+ target_branch: deploy.ref
+ )
+
+ mr3 = create(
+ :merge_request,
+ :merged,
+ source_project: project,
+ target_project: project,
+ target_branch: 'foo'
+ )
+
+ described_class.new(deploy).link_all_merged_merge_requests
+
+ expect(deploy.merge_requests).to include(mr1, mr2)
+ expect(deploy.merge_requests).not_to include(mr3)
+ end
+ end
+end
diff --git a/spec/services/deployments/update_service_spec.rb b/spec/services/deployments/update_service_spec.rb
index a923099b82c..8a918d28ffd 100644
--- a/spec/services/deployments/update_service_spec.rb
+++ b/spec/services/deployments/update_service_spec.rb
@@ -3,13 +3,55 @@
require 'spec_helper'
describe Deployments::UpdateService do
- let(:deploy) { create(:deployment, :running) }
- let(:service) { described_class.new(deploy, status: 'success') }
+ let(:deploy) { create(:deployment) }
describe '#execute' do
- it 'updates the status of a deployment' do
- expect(service.execute).to eq(true)
- expect(deploy.status).to eq('success')
+ it 'can update the status to running' do
+ expect(described_class.new(deploy, status: 'running').execute)
+ .to be_truthy
+
+ expect(deploy).to be_running
+ end
+
+ it 'can update the status to success' do
+ expect(described_class.new(deploy, status: 'success').execute)
+ .to be_truthy
+
+ expect(deploy).to be_success
+ end
+
+ it 'can update the status to failed' do
+ expect(described_class.new(deploy, status: 'failed').execute)
+ .to be_truthy
+
+ expect(deploy).to be_failed
+ end
+
+ it 'can update the status to canceled' do
+ expect(described_class.new(deploy, status: 'canceled').execute)
+ .to be_truthy
+
+ expect(deploy).to be_canceled
+ end
+
+ it 'returns false when the status is not supported' do
+ expect(described_class.new(deploy, status: 'kittens').execute)
+ .to be_falsey
+ end
+
+ it 'links merge requests when changing the status to success', :sidekiq_inline do
+ mr = create(
+ :merge_request,
+ :merged,
+ target_project: deploy.project,
+ source_project: deploy.project,
+ target_branch: 'master',
+ source_branch: 'foo'
+ )
+
+ described_class.new(deploy, status: 'success').execute
+
+ expect(deploy.merge_requests).to eq([mr])
end
end
end
diff --git a/spec/services/error_tracking/issue_details_service_spec.rb b/spec/services/error_tracking/issue_details_service_spec.rb
new file mode 100644
index 00000000000..4d5505bb5a9
--- /dev/null
+++ b/spec/services/error_tracking/issue_details_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ErrorTracking::IssueDetailsService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:result) { subject.execute }
+
+ let(:error_tracking_setting) do
+ create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
+ end
+
+ subject { described_class.new(project, user) }
+
+ before do
+ expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
+
+ project.add_reporter(user)
+ end
+
+ describe '#execute' do
+ context 'with authorized user' do
+ context 'when issue_details returns a detailed error' do
+ let(:detailed_error) { build(:detailed_error_tracking_error) }
+
+ before do
+ expect(error_tracking_setting)
+ .to receive(:issue_details).and_return(issue: detailed_error)
+ end
+
+ it 'returns the detailed error' do
+ expect(result).to eq(status: :success, issue: detailed_error)
+ end
+ end
+
+ include_examples 'error tracking service data not ready', :issue_details
+ include_examples 'error tracking service sentry error handling', :issue_details
+ include_examples 'error tracking service http status handling', :issue_details
+ end
+
+ include_examples 'error tracking service unauthorized user'
+ include_examples 'error tracking service disabled'
+ end
+end
diff --git a/spec/services/error_tracking/issue_latest_event_service_spec.rb b/spec/services/error_tracking/issue_latest_event_service_spec.rb
new file mode 100644
index 00000000000..cda15042814
--- /dev/null
+++ b/spec/services/error_tracking/issue_latest_event_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ErrorTracking::IssueLatestEventService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
+ let(:token) { 'test-token' }
+ let(:result) { subject.execute }
+
+ let(:error_tracking_setting) do
+ create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
+ end
+
+ subject { described_class.new(project, user) }
+
+ before do
+ expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
+
+ project.add_reporter(user)
+ end
+
+ describe '#execute' do
+ context 'with authorized user' do
+ context 'when issue_latest_event returns an error event' do
+ let(:error_event) { build(:error_tracking_error_event) }
+
+ before do
+ expect(error_tracking_setting)
+ .to receive(:issue_latest_event).and_return(latest_event: error_event)
+ end
+
+ it 'returns the error event' do
+ expect(result).to eq(status: :success, latest_event: error_event)
+ end
+ end
+
+ include_examples 'error tracking service data not ready', :issue_latest_event
+ include_examples 'error tracking service sentry error handling', :issue_latest_event
+ include_examples 'error tracking service http status handling', :issue_latest_event
+ end
+
+ include_examples 'error tracking service unauthorized user'
+ include_examples 'error tracking service disabled'
+ end
+end
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index 3a8f3069911..5b73bc91478 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -37,93 +37,20 @@ describe ErrorTracking::ListIssuesService do
end
end
- context 'when list_sentry_issues returns nil' do
- before do
- expect(error_tracking_setting)
- .to receive(:list_sentry_issues).and_return(nil)
- end
-
- it 'result is not ready' do
- expect(result).to eq(
- status: :error, http_status: :no_content, message: 'Not ready. Try again later')
- end
- end
-
- context 'when list_sentry_issues returns error' do
- before do
- allow(error_tracking_setting)
- .to receive(:list_sentry_issues)
- .and_return(
- error: 'Sentry response status code: 401',
- error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
- )
- end
-
- it 'returns the error' do
- expect(result).to eq(
- status: :error,
- http_status: :bad_request,
- message: 'Sentry response status code: 401'
- )
- end
- end
-
- context 'when list_sentry_issues returns error with http_status' do
- before do
- allow(error_tracking_setting)
- .to receive(:list_sentry_issues)
- .and_return(
- error: 'Sentry API response is missing keys. key not found: "id"',
- error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
- )
- end
-
- it 'returns the error with correct http_status' do
- expect(result).to eq(
- status: :error,
- http_status: :internal_server_error,
- message: 'Sentry API response is missing keys. key not found: "id"'
- )
- end
- end
+ include_examples 'error tracking service data not ready', :list_sentry_issues
+ include_examples 'error tracking service sentry error handling', :list_sentry_issues
+ include_examples 'error tracking service http status handling', :list_sentry_issues
end
- context 'with unauthorized user' do
- let(:unauthorized_user) { create(:user) }
-
- subject { described_class.new(project, unauthorized_user) }
-
- it 'returns error' do
- result = subject.execute
-
- expect(result).to include(
- status: :error,
- message: 'Access denied',
- http_status: :unauthorized
- )
- end
- end
-
- context 'with error tracking disabled' do
- before do
- error_tracking_setting.enabled = false
- end
-
- it 'raises error' do
- result = subject.execute
-
- expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
- end
- end
+ include_examples 'error tracking service unauthorized user'
+ include_examples 'error tracking service disabled'
end
- describe '#sentry_external_url' do
- let(:external_url) { 'https://sentrytest.gitlab.com/sentry-org/sentry-project' }
-
- it 'calls ErrorTracking::ProjectErrorTrackingSetting' do
- expect(error_tracking_setting).to receive(:sentry_external_url).and_call_original
+ describe '#external_url' do
+ it 'calls the project setting sentry_external_url' do
+ expect(error_tracking_setting).to receive(:sentry_external_url).and_return(sentry_url)
- subject.external_url
+ expect(subject.external_url).to eql sentry_url
end
end
end
diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb
index a272a604184..cd4b835e097 100644
--- a/spec/services/error_tracking/list_projects_service_spec.rb
+++ b/spec/services/error_tracking/list_projects_service_spec.rb
@@ -127,7 +127,7 @@ describe ErrorTracking::ListProjectsService do
end
it 'returns error' do
- expect(result).to include(status: :error, message: 'access denied')
+ expect(result).to include(status: :error, message: 'Access denied', http_status: :unauthorized)
end
end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 085b49f31ab..b1c64bc3c0a 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -345,7 +345,7 @@ describe Git::BranchHooksService do
end
end
- context 'when the project is forked' do
+ context 'when the project is forked', :sidekiq_might_not_need_inline do
let(:upstream_project) { project }
let(:forked_project) { fork_project(upstream_project, user, repository: true) }
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index bf68eb0af20..febd4992682 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -246,7 +246,7 @@ describe Git::BranchPushService, services: true do
allow(project.repository).to receive(:commits_between).and_return([commit])
end
- it "creates a note if a pushed commit mentions an issue" do
+ it "creates a note if a pushed commit mentions an issue", :sidekiq_might_not_need_inline do
expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author)
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
@@ -260,7 +260,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" do
+ it "defaults to the pushing user if the commit's author is not known", :sidekiq_might_not_need_inline do
allow(commit).to receive_messages(
author_name: 'unknown name',
author_email: 'unknown@email.com'
@@ -270,7 +270,7 @@ describe Git::BranchPushService, services: true do
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
- it "finds references in the first push to a non-default branch" do
+ it "finds references in the first push to a non-default branch", :sidekiq_might_not_need_inline do
allow(project.repository).to receive(:commits_between).with(blankrev, newrev).and_return([])
allow(project.repository).to receive(:commits_between).with("master", newrev).and_return([commit])
@@ -305,7 +305,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' do
+ it 'sets the metric for referenced issues', :sidekiq_might_not_need_inline 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)
@@ -344,12 +344,12 @@ describe Git::BranchPushService, services: true do
end
context "to default branches" do
- it "closes issues" do
+ it "closes issues", :sidekiq_might_not_need_inline do
execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
expect(Issue.find(issue.id)).to be_closed
end
- it "adds a note indicating that the issue is now closed" do
+ it "adds a note indicating that the issue is now closed", :sidekiq_might_not_need_inline do
expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
end
@@ -366,7 +366,7 @@ describe Git::BranchPushService, services: true do
allow(project).to receive(:default_branch).and_return('not-master')
end
- it "creates cross-reference notes" do
+ it "creates cross-reference notes", :sidekiq_might_not_need_inline do
expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
@@ -407,7 +407,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" do
+ it "initiates one api call to jira server to mention the issue", :sidekiq_might_not_need_inline do
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
@@ -434,7 +434,7 @@ describe Git::BranchPushService, services: true do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1")
end
- context "using right markdown" do
+ context "using right markdown", :sidekiq_might_not_need_inline do
it "initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, oldrev: oldrev, newrev: newrev, ref: ref)
@@ -473,7 +473,7 @@ describe Git::BranchPushService, services: true do
end
end
- context 'when internal issues are enabled' do
+ context 'when internal issues are enabled', :sidekiq_might_not_need_inline do
let(:issue) { create(:issue, project: project) }
let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" }
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index d13739cefd9..055d0243d4b 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -26,7 +26,7 @@ describe Groups::DestroyService do
end
shared_examples 'group destruction' do |async|
- context 'database records' do
+ context 'database records', :sidekiq_might_not_need_inline do
before do
destroy_group(group, user, async)
end
@@ -37,7 +37,7 @@ describe Groups::DestroyService do
it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) }
end
- context 'mattermost team' do
+ context 'mattermost team', :sidekiq_might_not_need_inline do
let!(:chat_team) { create(:chat_team, namespace: group) }
it 'destroys the team too' do
@@ -47,7 +47,7 @@ describe Groups::DestroyService do
end
end
- context 'file system' do
+ context 'file system', :sidekiq_might_not_need_inline do
context 'Sidekiq inline' do
before do
# Run sidekiq immediately to check that renamed dir will be removed
@@ -55,8 +55,8 @@ describe Groups::DestroyService do
end
it 'verifies that paths have been deleted' do
- expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_falsey
- expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, group.path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
end
end
end
@@ -73,13 +73,13 @@ describe Groups::DestroyService do
after do
# Clean up stale directories
- gitlab_shell.rm_namespace(project.repository_storage, group.path)
- gitlab_shell.rm_namespace(project.repository_storage, remove_path)
+ TestEnv.rm_storage_dir(project.repository_storage, group.path)
+ TestEnv.rm_storage_dir(project.repository_storage, remove_path)
end
it 'verifies original paths and projects still exist' do
- expect(gitlab_shell.exists?(project.repository_storage, group.path)).to be_truthy
- expect(gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, group.path)).to be_truthy
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
expect(Project.unscoped.count).to eq(1)
expect(Group.unscoped.count).to eq(2)
end
diff --git a/spec/services/groups/group_links/create_service_spec.rb b/spec/services/groups/group_links/create_service_spec.rb
new file mode 100644
index 00000000000..36faa69577e
--- /dev/null
+++ b/spec/services/groups/group_links/create_service_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::GroupLinks::CreateService, '#execute' do
+ let(:parent_group_user) { create(:user) }
+ let(:group_user) { create(:user) }
+ let(:child_group_user) { create(:user) }
+
+ 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(:shared_group_parent) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private, parent: shared_group_parent) }
+ let_it_be(:shared_group_child) { create(:group, :private, parent: shared_group) }
+
+ let_it_be(:project_parent) { create(:project, group: shared_group_parent) }
+ let_it_be(:project) { create(:project, group: shared_group) }
+ let_it_be(:project_child) { create(:project, group: shared_group_child) }
+
+ let(:opts) do
+ {
+ shared_group_access: Gitlab::Access::DEVELOPER,
+ expires_at: nil
+ }
+ end
+ let(:user) { group_user }
+
+ subject { described_class.new(group, user, opts) }
+
+ before do
+ group.add_guest(group_user)
+ shared_group.add_owner(group_user)
+ end
+
+ it 'adds group to another group' do
+ expect { subject.execute(shared_group) }.to change { group.shared_group_links.count }.from(0).to(1)
+ end
+
+ it 'returns false if shared group is blank' do
+ expect { subject.execute(nil) }.not_to change { group.shared_group_links.count }
+ end
+
+ context 'user does not have access to group' do
+ let(:user) { create(:user) }
+
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'returns error' do
+ result = subject.execute(shared_group)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(404)
+ end
+ end
+
+ context 'user does not have admin access to shared group' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_guest(user)
+ shared_group.add_developer(user)
+ end
+
+ it 'returns error' do
+ result = subject.execute(shared_group)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(404)
+ end
+ end
+
+ context 'group hierarchies' do
+ before do
+ group_parent.add_owner(parent_group_user)
+ group.add_owner(group_user)
+ group_child.add_owner(child_group_user)
+ end
+
+ context 'group user' do
+ let(:user) { group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_truthy
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_truthy
+ end
+ end
+
+ context 'parent group user' do
+ let(:user) { parent_group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_falsey
+ end
+ end
+
+ context 'child group user' do
+ let(:user) { child_group_user }
+
+ it 'create proper authorizations' do
+ subject.execute(shared_group)
+
+ expect(Ability.allowed?(user, :read_project, project_parent)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project)).to be_falsey
+ expect(Ability.allowed?(user, :read_project, project_child)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb
new file mode 100644
index 00000000000..6f49b6eda94
--- /dev/null
+++ b/spec/services/groups/group_links/destroy_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::GroupLinks::DestroyService, '#execute' do
+ let(:user) { create(:user) }
+
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:shared_group) { create(:group, :private) }
+ let_it_be(:project) { create(:project, group: shared_group) }
+
+ subject { described_class.new(nil, nil) }
+
+ context 'single link' do
+ let!(:link) { create(:group_group_link, shared_group: shared_group, shared_with_group: group) }
+
+ it 'destroys link' do
+ expect { subject.execute(link) }.to change { GroupGroupLink.count }.from(1).to(0)
+ end
+
+ it 'revokes project authorization' do
+ group.add_developer(user)
+
+ expect { subject.execute(link) }.to(
+ change { Ability.allowed?(user, :read_project, project) }.from(true).to(false))
+ end
+ end
+
+ context 'multiple links' do
+ let_it_be(:another_group) { create(:group, :private) }
+ let_it_be(:another_shared_group) { create(:group, :private) }
+
+ let!(:links) do
+ [
+ create(:group_group_link, shared_group: shared_group, shared_with_group: group),
+ create(:group_group_link, shared_group: shared_group, shared_with_group: another_group),
+ create(:group_group_link, shared_group: another_shared_group, shared_with_group: group),
+ create(:group_group_link, shared_group: another_shared_group, shared_with_group: another_group)
+ ]
+ end
+
+ it 'updates project authorization once per group' do
+ expect(GroupGroupLink).to receive(:delete)
+ expect(group).to receive(:refresh_members_authorized_projects).once
+ expect(another_group).to receive(:refresh_members_authorized_projects).once
+
+ subject.execute(links)
+ end
+
+ it 'rolls back changes when error happens' do
+ group.add_developer(user)
+
+ expect(group).to receive(:refresh_members_authorized_projects).once.and_call_original
+ expect(another_group).to(
+ receive(:refresh_members_authorized_projects).and_raise('boom'))
+
+ expect { subject.execute(links) }.to raise_error('boom')
+
+ expect(GroupGroupLink.count).to eq(links.length)
+ expect(Ability.allowed?(user, :read_project, project)).to be_truthy
+ end
+ end
+end
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
new file mode 100644
index 00000000000..2024e1ed457
--- /dev/null
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Groups::ImportExport::ExportService do
+ describe '#execute' do
+ let!(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:export_path) { shared.export_path }
+ let(:service) { described_class.new(group: group, user: user, params: { shared: shared }) }
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves the models' do
+ expect(Gitlab::ImportExport::GroupTreeSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ context 'when saver succeeds' do
+ it 'saves the group in the file system' do
+ service.execute
+
+ expect(group.import_export_upload.export_file.file).not_to be_nil
+ expect(File.directory?(export_path)).to eq(false)
+ expect(File.exist?(shared.archive_path)).to eq(false)
+ end
+ end
+
+ context 'when saving services fail' do
+ before do
+ allow(service).to receive_message_chain(:tree_exporter, :save).and_return(false)
+ end
+
+ it 'removes the remaining exported data' do
+ allow_any_instance_of(Gitlab::ImportExport::Saver).to receive(:compress_and_save).and_return(false)
+
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+
+ expect(group.import_export_upload).to be_nil
+ expect(File.directory?(export_path)).to eq(false)
+ expect(File.exist?(shared.archive_path)).to eq(false)
+ end
+
+ it 'notifies logger' do
+ expect_any_instance_of(Gitlab::Import::Logger).to receive(:error)
+
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 5ef1fb1932f..9a490dfd779 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -427,20 +427,34 @@ describe Groups::TransferService do
end
end
- context 'when a project in group has container images' do
+ context 'when a project has container images' do
let(:group) { create(:group, :public, :nested) }
- let!(:project) { create(:project, :repository, :public, namespace: group) }
+ let!(:container_repository) { create(:container_repository, project: project) }
+
+ subject { transfer_service.execute(new_parent_group) }
before do
- stub_container_registry_tags(repository: /image/, tags: %w[rc1])
- create(:container_repository, project: project, name: :image)
- create(:group_member, :owner, group: new_parent_group, user: user)
+ group.add_owner(user)
+ new_parent_group.add_owner(user)
end
- it 'does not allow group to be transferred' do
- transfer_service.execute(new_parent_group)
+ context 'within group' do
+ let(:project) { create(:project, :repository, :public, namespace: group) }
+
+ it 'does not transfer' do
+ expect(subject).to be false
+ expect(transfer_service.error).to match(/Docker images in their Container Registry/)
+ end
+ end
- expect(transfer_service.error).to match(/Docker images in their Container Registry/)
+ context 'within subgroup' do
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project) { create(:project, :repository, :public, namespace: subgroup) }
+
+ it 'does not transfer' do
+ expect(subject).to be false
+ expect(transfer_service.error).to match(/Docker images in their Container Registry/)
+ end
end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index ca8eaf4c970..1aa7e06182b 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -32,6 +32,43 @@ describe Groups::UpdateService do
expect(service.execute).to be_falsey
end
+
+ context 'when a project has container images' do
+ let(:params) { { path: SecureRandom.hex } }
+ let!(:container_repository) { create(:container_repository, project: project) }
+
+ subject { described_class.new(public_group, user, params).execute }
+
+ context 'within group' do
+ let(:project) { create(:project, group: public_group) }
+
+ context 'with path updates' do
+ it 'does not allow the update' do
+ expect(subject).to be false
+ expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/)
+ end
+ end
+
+ context 'with name updates' do
+ let(:params) { { name: 'new-name' } }
+
+ it 'allows the update' do
+ expect(subject).to be true
+ expect(public_group.reload.name).to eq('new-name')
+ end
+ end
+ end
+
+ context 'within subgroup' do
+ let(:subgroup) { create(:group, parent: public_group) }
+ let(:project) { create(:project, group: subgroup) }
+
+ it 'does not allow path updates' do
+ expect(subject).to be false
+ expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/)
+ end
+ end
+ end
end
context "internal group with internal project" do
@@ -148,30 +185,6 @@ describe Groups::UpdateService do
end
end
- context 'projects in group have container images' do
- let(:service) { described_class.new(public_group, user, path: SecureRandom.hex) }
- let(:project) { create(:project, :internal, group: public_group) }
-
- before do
- stub_container_registry_tags(repository: /image/, tags: %w[rc1])
- create(:container_repository, project: project, name: :image)
- end
-
- it 'does not allow path to be changed' do
- result = described_class.new(public_group, user, path: 'new-path').execute
-
- expect(result).to eq false
- expect(public_group.errors[:base].first).to match(/Docker images in their Container Registry/)
- end
-
- it 'allows other settings to be changed' do
- result = described_class.new(public_group, user, name: 'new-name').execute
-
- expect(result).to eq true
- expect(public_group.reload.name).to eq('new-name')
- end
- end
-
context 'for a subgroup' do
let(:subgroup) { create(:group, :private, parent: private_group) }
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index 51720e786dc..9f811f56f50 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -6,7 +6,7 @@ describe ImportExportCleanUpService do
describe '#execute' do
let(:service) { described_class.new }
- let(:tmp_import_export_folder) { 'tmp/project_exports' }
+ let(:tmp_import_export_folder) { 'tmp/gitlab_exports' }
context 'when the import/export directory does not exist' do
it 'does not remove any archives' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 1f7d564b6ec..dce62d1d20e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -70,7 +70,7 @@ describe Issues::CloseService do
end
describe '#close_issue' do
- context "closed by a merge request" do
+ context "closed by a merge request", :sidekiq_might_not_need_inline do
it 'mentions closure via a merge request' do
perform_enqueued_jobs do
described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request)
@@ -100,7 +100,7 @@ describe Issues::CloseService do
end
end
- context "closed by a commit" do
+ context "closed by a commit", :sidekiq_might_not_need_inline do
it 'mentions closure via a commit' do
perform_enqueued_jobs do
described_class.new(project, user).close_issue(issue, closed_via: closing_commit)
@@ -146,7 +146,7 @@ describe Issues::CloseService do
expect(issue.closed_by_id).to be(user.id)
end
- it 'sends email to user2 about assign of new issue' do
+ it 'sends email to user2 about assign of new issue', :sidekiq_might_not_need_inline do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(issue.title)
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 154bfec0da2..604befd7225 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -169,7 +169,7 @@ describe Issues::UpdateService, :mailer do
end
end
- context 'with background jobs processed' do
+ context 'with background jobs processed', :sidekiq_might_not_need_inline do
before do
perform_enqueued_jobs do
update_issue(opts)
@@ -187,7 +187,6 @@ describe Issues::UpdateService, :mailer do
it 'creates system note about issue reassign' do
note = find_note('assigned to')
- expect(note).not_to be_nil
expect(note.note).to include "assigned to #{user2.to_reference}"
end
@@ -202,14 +201,12 @@ describe Issues::UpdateService, :mailer do
it 'creates system note about title change' do
note = find_note('changed title')
- expect(note).not_to be_nil
expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**'
end
it 'creates system note about discussion lock' do
note = find_note('locked this issue')
- expect(note).not_to be_nil
expect(note.note).to eq 'locked this issue'
end
end
@@ -221,20 +218,10 @@ describe Issues::UpdateService, :mailer do
note = find_note('changed the description')
- expect(note).not_to be_nil
expect(note.note).to eq('changed the description')
end
end
- it 'creates zoom_link_added system note when a zoom link is added to the description' do
- update_issue(description: 'Changed description https://zoom.us/j/5873603787')
-
- note = find_note('added a Zoom call')
-
- expect(note).not_to be_nil
- expect(note.note).to eq('added a Zoom call to this issue')
- end
-
context 'when issue turns confidential' do
let(:opts) do
{
@@ -252,7 +239,6 @@ describe Issues::UpdateService, :mailer do
note = find_note('made the issue confidential')
- expect(note).not_to be_nil
expect(note.note).to eq 'made the issue confidential'
end
@@ -366,7 +352,7 @@ describe Issues::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
- it 'sends notifications for subscribers of changed milestone' do
+ it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
issue.milestone = create(:milestone, project: project)
issue.save
@@ -398,7 +384,7 @@ describe Issues::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
- it 'sends notifications for subscribers of changed milestone' do
+ it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
update_issue(milestone: create(:milestone, project: project))
end
@@ -435,7 +421,7 @@ describe Issues::UpdateService, :mailer do
end
end
- it 'sends notifications for subscribers of newly added labels' do
+ it 'sends notifications for subscribers of newly added labels', :sidekiq_might_not_need_inline do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
@@ -620,6 +606,24 @@ describe Issues::UpdateService, :mailer do
end
end
+ context 'when same id is passed as add_label_ids and remove_label_ids' do
+ let(:params) { { add_label_ids: [label.id], remove_label_ids: [label.id] } }
+
+ context 'for a label assigned to an issue' do
+ it 'removes the label' do
+ issue.update(labels: [label])
+
+ expect(result.label_ids).to be_empty
+ end
+ end
+
+ context 'for a label not assigned to an issue' do
+ it 'does not add the label' do
+ expect(result.label_ids).to be_empty
+ end
+ end
+ end
+
context 'when duplicate label titles are given' do
let(:params) do
{ labels: [label3.title, label3.title] }
diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb
index ba3f007c917..ecca9467965 100644
--- a/spec/services/issues/zoom_link_service_spec.rb
+++ b/spec/services/issues/zoom_link_service_spec.rb
@@ -14,27 +14,16 @@ describe Issues::ZoomLinkService do
project.add_reporter(user)
end
- shared_context 'with Zoom link' do
+ shared_context '"added" Zoom meeting' do
before do
- issue.update!(description: "Description\n\n#{zoom_link}")
+ create(:zoom_meeting, issue: issue)
end
end
- shared_context 'with Zoom link not at the end' do
+ shared_context '"removed" zoom meetings' do
before do
- issue.update!(description: "Description with #{zoom_link} some where")
- end
- end
-
- shared_context 'without Zoom link' do
- before do
- issue.update!(description: "Description\n\nhttp://example.com")
- end
- end
-
- shared_context 'without issue description' do
- before do
- issue.update!(description: nil)
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
+ create(:zoom_meeting, issue: issue, issue_status: :removed)
end
end
@@ -45,11 +34,10 @@ describe Issues::ZoomLinkService do
end
describe '#add_link' do
- shared_examples 'can add link' do
- it 'appends the link to issue description' do
+ shared_examples 'can add meeting' do
+ it 'appends the new meeting to zoom_meetings' do
expect(result).to be_success
- expect(result.payload[:description])
- .to eq("#{issue.description}\n\n#{zoom_link}")
+ expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(zoom_link)
end
it 'tracks the add event' do
@@ -57,55 +45,63 @@ describe Issues::ZoomLinkService do
.with('IncidentManagement::ZoomIntegration', 'add_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
+
+ it 'creates a zoom_link_added notification' do
+ expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
+ expect(SystemNoteService).not_to receive(:zoom_link_removed)
+ result
+ end
end
- shared_examples 'cannot add link' do
- it 'cannot add the link' do
+ shared_examples 'cannot add meeting' do
+ it 'cannot add the meeting' do
expect(result).to be_error
expect(result.message).to eq('Failed to add a Zoom meeting')
end
+
+ it 'creates no notification' do
+ expect(SystemNoteService).not_to receive(:zoom_link_added)
+ expect(SystemNoteService).not_to receive(:zoom_link_removed)
+ result
+ end
end
subject(:result) { service.add_link(zoom_link) }
- context 'without Zoom link in the issue description' do
- include_context 'without Zoom link'
- include_examples 'can add link'
+ context 'without existing Zoom meeting' do
+ include_examples 'can add meeting'
- context 'with invalid Zoom link' do
+ context 'with invalid Zoom url' do
let(:zoom_link) { 'https://not-zoom.link' }
- include_examples 'cannot add link'
+ include_examples 'cannot add meeting'
end
context 'with insufficient permissions' do
include_context 'insufficient permissions'
- include_examples 'cannot add link'
+ include_examples 'cannot add meeting'
end
end
- context 'with Zoom link in the issue description' do
- include_context 'with Zoom link'
- include_examples 'cannot add link'
+ context 'with "added" Zoom meeting' do
+ include_context '"added" Zoom meeting'
+ include_examples 'cannot add meeting'
+ end
- context 'but not at the end' do
- include_context 'with Zoom link not at the end'
- include_examples 'can add link'
+ context 'with "added" Zoom meeting and race condition' do
+ include_context '"added" Zoom meeting'
+ before do
+ allow(service).to receive(:can_add_link?).and_return(true)
end
- end
- context 'without issue description' do
- include_context 'without issue description'
- include_examples 'can add link'
+ include_examples 'cannot add meeting'
end
end
describe '#can_add_link?' do
subject { service.can_add_link? }
- context 'without Zoom link in the issue description' do
- include_context 'without Zoom link'
-
+ context 'without "added" zoom meeting' do
it { is_expected.to eq(true) }
context 'with insufficient permissions' do
@@ -115,81 +111,93 @@ describe Issues::ZoomLinkService do
end
end
- context 'with Zoom link in the issue description' do
- include_context 'with Zoom link'
+ context 'with Zoom meeting in the issue description' do
+ include_context '"added" Zoom meeting'
it { is_expected.to eq(false) }
end
end
describe '#remove_link' do
- shared_examples 'cannot remove link' do
- it 'cannot remove the link' do
+ shared_examples 'cannot remove meeting' do
+ it 'cannot remove the meeting' do
expect(result).to be_error
expect(result.message).to eq('Failed to remove a Zoom meeting')
end
- end
- subject(:result) { service.remove_link }
+ it 'creates no notification' do
+ expect(SystemNoteService).not_to receive(:zoom_link_added)
+ expect(SystemNoteService).not_to receive(:zoom_link_removed)
+ result
+ end
+ end
- context 'with Zoom link in the issue description' do
- include_context 'with Zoom link'
+ shared_examples 'can remove meeting' do
+ it 'creates no notification' do
+ expect(SystemNoteService).not_to receive(:zoom_link_added).with(issue, project, user)
+ expect(SystemNoteService).to receive(:zoom_link_removed)
+ result
+ end
- it 'removes the link from the issue description' do
+ it 'can remove the meeting' do
expect(result).to be_success
- expect(result.payload[:description])
- .to eq(issue.description.delete_suffix("\n\n#{zoom_link}"))
+ expect(ZoomMeeting.canonical_meeting_url(issue)).to eq(nil)
end
it 'tracks the remove event' do
expect(Gitlab::Tracking).to receive(:event)
- .with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
-
+ .with('IncidentManagement::ZoomIntegration', 'remove_zoom_meeting', label: 'Issue ID', value: issue.id)
result
end
+ end
- context 'with insufficient permissions' do
- include_context 'insufficient permissions'
- include_examples 'cannot remove link'
- end
+ subject(:result) { service.remove_link }
- context 'but not at the end' do
- include_context 'with Zoom link not at the end'
- include_examples 'cannot remove link'
+ context 'with Zoom meeting' do
+ include_context '"added" Zoom meeting'
+
+ context 'removes the link' do
+ include_examples 'can remove meeting'
end
- end
- context 'without Zoom link in the issue description' do
- include_context 'without Zoom link'
- include_examples 'cannot remove link'
+ context 'with insufficient permissions' do
+ include_context 'insufficient permissions'
+ include_examples 'cannot remove meeting'
+ end
end
- context 'without issue description' do
- include_context 'without issue description'
- include_examples 'cannot remove link'
+ context 'without "added" Zoom meeting' do
+ include_context '"removed" zoom meetings'
+ include_examples 'cannot remove meeting'
end
end
describe '#can_remove_link?' do
subject { service.can_remove_link? }
- context 'with Zoom link in the issue description' do
- include_context 'with Zoom link'
+ context 'without Zoom meeting' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with only "removed" zoom meetings' do
+ include_context '"removed" zoom meetings'
+ it { is_expected.to eq(false) }
+ end
+ context 'with "added" Zoom meeting' do
+ include_context '"added" Zoom meeting'
it { is_expected.to eq(true) }
+ context 'with "removed" zoom meetings' do
+ include_context '"removed" zoom meetings'
+ it { is_expected.to eq(true) }
+ end
+
context 'with insufficient permissions' do
include_context 'insufficient permissions'
-
it { is_expected.to eq(false) }
end
end
-
- context 'without Zoom link in the issue description' do
- include_context 'without Zoom link'
-
- it { is_expected.to eq(false) }
- end
end
describe '#parse_link' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index effcaf53535..73ac0bd7716 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -45,7 +45,7 @@ describe Members::DestroyService do
shared_examples 'a service destroying a member with access' do
it_behaves_like 'a service destroying a member'
- it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do
+ it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures, :sidekiq_might_not_need_inline do
create(:issue, project: group_project, assignees: [member_user])
create(:merge_request, source_project: group_project, assignees: [member_user])
create(:todo, :pending, project: group_project, user: member_user)
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index f26b67f902d..203048984a1 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -10,9 +10,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
let(:ref) { merge_request.source_branch }
let(:pipeline) do
- create(:ci_pipeline_with_one_job, ref: ref,
- project: project,
- sha: sha)
+ create(:ci_pipeline, ref: ref, project: project, sha: sha)
end
let(:service) do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 68e53553043..9b358839c06 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -80,7 +80,7 @@ describe MergeRequests::BuildService do
end
it 'does not assign force_remove_source_branch' do
- expect(merge_request.force_remove_source_branch?).to be_falsey
+ expect(merge_request.force_remove_source_branch?).to be_truthy
end
context 'with force_remove_source_branch parameter when the user is authorized' do
@@ -91,6 +91,36 @@ describe MergeRequests::BuildService do
it 'assigns force_remove_source_branch' do
expect(merge_request.force_remove_source_branch?).to be_truthy
end
+
+ context 'with project setting remove_source_branch_after_merge false' do
+ before do
+ project.remove_source_branch_after_merge = false
+ end
+
+ it 'assigns force_remove_source_branch' do
+ expect(merge_request.force_remove_source_branch?).to be_truthy
+ end
+ end
+ end
+
+ context 'with project setting remove_source_branch_after_merge true' do
+ before do
+ project.remove_source_branch_after_merge = true
+ end
+
+ it 'assigns force_remove_source_branch' do
+ expect(merge_request.force_remove_source_branch?).to be_truthy
+ end
+
+ context 'with force_remove_source_branch parameter false' do
+ before do
+ params[:force_remove_source_branch] = '0'
+ end
+
+ it 'does not assign force_remove_source_branch' do
+ expect(merge_request.force_remove_source_branch?).to be(false)
+ end
+ end
end
context 'missing source branch' do
@@ -131,7 +161,7 @@ describe MergeRequests::BuildService do
let!(:project) { fork_project(target_project, user, namespace: user.namespace, repository: true) }
let(:source_project) { project }
- it 'creates compare object with target branch as default branch' do
+ it 'creates compare object with target branch as default branch', :sidekiq_might_not_need_inline do
expect(merge_request.compare).to be_present
expect(merge_request.target_branch).to eq(project.default_branch)
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 29b7e0f17e2..b037b73752e 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -38,7 +38,7 @@ describe MergeRequests::CloseService do
.with(@merge_request, 'close')
end
- it 'sends email to user2 about assign of new merge_request' do
+ it 'sends email to user2 about assign of new merge_request', :sidekiq_might_not_need_inline do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 51a5c51f6c3..7145cfe7897 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -36,25 +36,25 @@ describe MergeRequests::CreateFromIssueService do
expect(result[:message]).to eq('Invalid issue iid')
end
- it 'creates a branch based on issue title' do
+ it 'creates a branch based on issue title', :sidekiq_might_not_need_inline do
service.execute
expect(target_project.repository.branch_exists?(issue.to_branch_name)).to be_truthy
end
- it 'creates a branch using passed name' do
+ it 'creates a branch using passed name', :sidekiq_might_not_need_inline do
service_with_custom_source_branch.execute
expect(target_project.repository.branch_exists?(custom_source_branch)).to be_truthy
end
- it 'creates the new_merge_request system note' do
+ it 'creates the new_merge_request system note', :sidekiq_might_not_need_inline do
expect(SystemNoteService).to receive(:new_merge_request).with(issue, project, user, instance_of(MergeRequest))
service.execute
end
- it 'creates the new_issue_branch system note when the branch could be created but the merge_request cannot be created' do
+ it 'creates the new_issue_branch system note when the branch could be created but the merge_request cannot be created', :sidekiq_might_not_need_inline do
expect_any_instance_of(MergeRequest).to receive(:valid?).at_least(:once).and_return(false)
expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name, branch_project: target_project)
@@ -62,35 +62,35 @@ describe MergeRequests::CreateFromIssueService do
service.execute
end
- it 'creates a merge request' do
+ it 'creates a merge request', :sidekiq_might_not_need_inline do
expect { service.execute }.to change(target_project.merge_requests, :count).by(1)
end
- it 'sets the merge request author to current user' do
+ it 'sets the merge request author to current user', :sidekiq_might_not_need_inline do
result = service.execute
expect(result[:merge_request].author).to eq(user)
end
- it 'sets the merge request source branch to the new issue branch' do
+ it 'sets the merge request source branch to the new issue branch', :sidekiq_might_not_need_inline do
result = service.execute
expect(result[:merge_request].source_branch).to eq(issue.to_branch_name)
end
- it 'sets the merge request source branch to the passed branch name' do
+ it 'sets the merge request source branch to the passed branch name', :sidekiq_might_not_need_inline do
result = service_with_custom_source_branch.execute
expect(result[:merge_request].source_branch).to eq(custom_source_branch)
end
- it 'sets the merge request target branch to the project default branch' do
+ it 'sets the merge request target branch to the project default branch', :sidekiq_might_not_need_inline do
result = service.execute
expect(result[:merge_request].target_branch).to eq(target_project.default_branch)
end
- it 'executes quick actions if the build service sets them in the description' do
+ it 'executes quick actions if the build service sets them in the description', :sidekiq_might_not_need_inline do
allow(service).to receive(:merge_request).and_wrap_original do |m, *args|
m.call(*args).tap do |merge_request|
merge_request.description = "/assign #{user.to_reference}"
@@ -102,7 +102,7 @@ describe MergeRequests::CreateFromIssueService do
expect(result[:merge_request].assignees).to eq([user])
end
- context 'when ref branch is set' do
+ context 'when ref branch is set', :sidekiq_might_not_need_inline do
subject { described_class.new(project, user, ref: 'feature', **service_params).execute }
it 'sets the merge request source branch to the new issue branch' do
@@ -193,7 +193,7 @@ describe MergeRequests::CreateFromIssueService do
it_behaves_like 'a service that creates a merge request from an issue'
- it 'sets the merge request title to: "WIP: $issue-branch-name' do
+ it 'sets the merge request title to: "WIP: $issue-branch-name', :sidekiq_might_not_need_inline do
result = service.execute
expect(result[:merge_request].title).to eq("WIP: #{issue.to_branch_name.titleize.humanize}")
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 699f2a98088..3db1471bf3c 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -57,7 +57,7 @@ describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
expect(Todo.where(attributes).count).to be_zero
end
- it 'creates exactly 1 create MR event' do
+ it 'creates exactly 1 create MR event', :sidekiq_might_not_need_inline do
attributes = {
action: Event::CREATED,
target_id: merge_request.id,
@@ -216,7 +216,7 @@ describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
target_project.add_maintainer(user)
end
- it 'create legacy detached merge request pipeline for fork merge request' do
+ it 'create legacy detached merge request pipeline for fork merge request', :sidekiq_might_not_need_inline do
expect(merge_request.actual_head_pipeline)
.to be_legacy_detached_merge_request_pipeline
end
@@ -477,7 +477,7 @@ describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
project.add_developer(user)
end
- it 'creates the merge request' do
+ it 'creates the merge request', :sidekiq_might_not_need_inline do
merge_request = described_class.new(project, user, opts).execute
expect(merge_request).to be_persisted
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 3b1096c51cb..87fcd70a298 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -13,6 +13,7 @@ describe MergeRequests::FfMergeService do
author: create(:user))
end
let(:project) { merge_request.project }
+ let(:valid_merge_params) { { sha: merge_request.diff_head_sha } }
before do
project.add_maintainer(user)
@@ -21,39 +22,69 @@ describe MergeRequests::FfMergeService do
describe '#execute' do
context 'valid params' do
- let(:service) { described_class.new(project, user, {}) }
-
- before do
- allow(service).to receive(:execute_hooks)
+ let(:service) { described_class.new(project, user, valid_merge_params) }
+ def execute_ff_merge
perform_enqueued_jobs do
service.execute(merge_request)
end
end
+ before do
+ allow(service).to receive(:execute_hooks)
+ end
+
it "does not create merge commit" do
+ execute_ff_merge
+
source_branch_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
+
expect(source_branch_sha).to eq(target_branch_sha)
end
- it { expect(merge_request).to be_valid }
- it { expect(merge_request).to be_merged }
+ it 'keeps the merge request valid' do
+ expect { execute_ff_merge }
+ .not_to change { merge_request.valid? }
+ end
+
+ it 'updates the merge request to merged' do
+ expect { execute_ff_merge }
+ .to change { merge_request.merged? }
+ .from(false)
+ .to(true)
+ end
it 'sends email to user2 about merge of new merge_request' do
+ execute_ff_merge
+
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
it 'creates system note about merge_request merge' do
+ execute_ff_merge
+
note = merge_request.notes.last
expect(note.note).to include 'merged'
end
+
+ it 'does not update squash_commit_sha if it is not a squash' do
+ expect { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
+ end
+
+ it 'updates squash_commit_sha if it is a squash' do
+ merge_request.update!(squash: true)
+
+ expect { execute_ff_merge }
+ .to change { merge_request.squash_commit_sha }
+ .from(nil)
+ end
end
- context "error handling" do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+ context 'error handling' do
+ let(:service) { described_class.new(project, user, valid_merge_params.merge(commit_message: 'Awesome message')) }
before do
allow(Rails.logger).to receive(:error)
@@ -82,6 +113,16 @@ describe MergeRequests::FfMergeService do
expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+
+ it 'does not update squash_commit_sha if squash merge is not successful' do
+ merge_request.update!(squash: true)
+
+ expect(project.repository.raw).to receive(:ff_merge) do
+ raise 'Merge error'
+ end
+
+ expect { service.execute(merge_request) }.not_to change { merge_request.squash_commit_sha }
+ end
end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 22578436c18..c938dd1cb0b 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -14,9 +14,12 @@ describe MergeRequests::MergeService do
end
describe '#execute' do
- context 'valid params' do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+ let(:service) { described_class.new(project, user, merge_params) }
+ let(:merge_params) do
+ { commit_message: 'Awesome message', sha: merge_request.diff_head_sha }
+ end
+ context 'valid params' do
before do
allow(service).to receive(:execute_hooks)
@@ -38,11 +41,80 @@ describe MergeRequests::MergeService do
note = merge_request.notes.last
expect(note.note).to include 'merged'
end
+
+ context 'when squashing' do
+ let(:merge_params) do
+ { commit_message: 'Merge commit message',
+ squash_commit_message: 'Squash commit message',
+ sha: merge_request.diff_head_sha }
+ end
+
+ let(:merge_request) do
+ # A merge reqeust with 5 commits
+ create(:merge_request, :simple,
+ author: user2,
+ assignees: [user2],
+ squash: true,
+ source_branch: 'improve/awesome',
+ target_branch: 'fix')
+ end
+
+ it 'merges the merge request with squashed commits' do
+ expect(merge_request).to be_merged
+
+ merge_commit = merge_request.merge_commit
+ squash_commit = merge_request.merge_commit.parents.last
+
+ expect(merge_commit.message).to eq('Merge commit message')
+ expect(squash_commit.message).to eq("Squash commit message\n")
+ end
+ end
end
- context 'closes related issues' do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+ context 'when an invalid sha is passed' do
+ let(:merge_request) do
+ create(:merge_request, :simple,
+ author: user2,
+ assignees: [user2],
+ squash: true,
+ source_branch: 'improve/awesome',
+ target_branch: 'fix')
+ end
+
+ let(:merge_params) do
+ { sha: merge_request.commits.second.sha }
+ end
+
+ it 'does not merge the MR' do
+ service.execute(merge_request)
+
+ expect(merge_request).not_to be_merged
+ expect(merge_request.merge_error).to match(/Branch has been updated/)
+ end
+ end
+
+ context 'when the `sha` param is missing' do
+ let(:merge_params) { {} }
+
+ it 'returns the error' do
+ merge_error = 'Branch has been updated since the merge was requested. '\
+ 'Please review the changes.'
+
+ expect { service.execute(merge_request) }
+ .to change { merge_request.merge_error }
+ .from(nil).to(merge_error)
+ end
+
+ it 'merges the MR when the feature is disabled' do
+ stub_feature_flags(validate_merge_sha: false)
+ service.execute(merge_request)
+
+ expect(merge_request).to be_merged
+ end
+ end
+
+ context 'closes related issues' do
before do
allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
end
@@ -83,12 +155,12 @@ describe MergeRequests::MergeService do
service.execute(merge_request)
end
- context "when jira_issue_transition_id is not present" do
+ context 'when jira_issue_transition_id is not present' do
before do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil)
end
- it "does not close issue" do
+ it 'does not close issue' do
allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil)
expect_any_instance_of(JiraService).not_to receive(:transition_issue)
@@ -97,7 +169,7 @@ describe MergeRequests::MergeService do
end
end
- context "wrong issue markdown" do
+ context 'wrong issue markdown' do
it 'does not close issues on Jira issue tracker' do
jira_issue = ExternalIssue.new('#JIRA-123', project)
stub_jira_urls(jira_issue)
@@ -115,7 +187,7 @@ describe MergeRequests::MergeService do
context 'closes related todos' do
let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
let!(:todo) do
create(:todo, :assigned,
project: project,
@@ -139,7 +211,7 @@ describe MergeRequests::MergeService do
context 'source branch removal' do
context 'when the source branch is protected' do
let(:service) do
- described_class.new(project, user, 'should_remove_source_branch' => true)
+ described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true))
end
before do
@@ -154,7 +226,7 @@ describe MergeRequests::MergeService do
context 'when the source branch is the default branch' do
let(:service) do
- described_class.new(project, user, 'should_remove_source_branch' => true)
+ described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true))
end
before do
@@ -169,8 +241,6 @@ describe MergeRequests::MergeService do
context 'when the source branch can be removed' do
context 'when MR author set the source branch to be removed' do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
-
before do
merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' })
end
@@ -183,7 +253,7 @@ describe MergeRequests::MergeService do
end
context 'when the merger set the source branch not to be removed' do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => false) }
+ let(:service) { described_class.new(project, user, merge_params.merge('should_remove_source_branch' => false)) }
it 'does not delete the source branch' do
expect(DeleteBranchService).not_to receive(:new)
@@ -194,7 +264,7 @@ describe MergeRequests::MergeService do
context 'when MR merger set the source branch to be removed' do
let(:service) do
- described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => true)
+ described_class.new(project, user, merge_params.merge('should_remove_source_branch' => true))
end
it 'removes the source branch using the current user' do
@@ -207,9 +277,7 @@ describe MergeRequests::MergeService do
end
end
- context "error handling" do
- let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
-
+ context 'error handling' do
before do
allow(Rails.logger).to receive(:error)
end
@@ -230,7 +298,7 @@ describe MergeRequests::MergeService do
it 'logs and saves error if there is an exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise("error message")
+ allow(service).to receive(:repository).and_raise('error message')
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
@@ -310,7 +378,7 @@ describe MergeRequests::MergeService do
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
- context "when fast-forward merge is not allowed" do
+ context 'when fast-forward merge is not allowed' do
before do
allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
end
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index 758679edc45..cccafddc450 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -76,7 +76,7 @@ describe MergeRequests::MergeToRefService do
described_class.new(project, user, **params)
end
- let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true } }
+ let(:params) { { commit_message: 'Awesome message', should_remove_source_branch: true, sha: merge_request.diff_head_sha } }
def process_merge_to_ref
perform_enqueued_jobs do
@@ -103,7 +103,7 @@ describe MergeRequests::MergeToRefService do
end
let(:merge_service) do
- MergeRequests::MergeService.new(project, user, {})
+ MergeRequests::MergeService.new(project, user, { sha: merge_request.diff_head_sha })
end
context 'when merge commit' do
@@ -205,7 +205,7 @@ describe MergeRequests::MergeToRefService do
end
context 'when target ref is passed as a parameter' do
- let(:params) { { commit_message: 'merge train', target_ref: target_ref } }
+ let(:params) { { commit_message: 'merge train', target_ref: target_ref, sha: merge_request.diff_head_sha } }
it_behaves_like 'successfully merges to ref with merge method' do
let(:first_parent_ref) { 'refs/heads/master' }
@@ -215,7 +215,7 @@ describe MergeRequests::MergeToRefService do
describe 'cascading merge refs' do
set(:project) { create(:project, :repository) }
- let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref } }
+ let(:params) { { commit_message: 'Cascading merge', first_parent_ref: first_parent_ref, target_ref: target_ref, sha: merge_request.diff_head_sha } }
context 'when first merge happens' do
let(:merge_request) do
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
index ff4cdd3e7e2..75b9c2304a6 100644
--- a/spec/services/merge_requests/push_options_handler_service_spec.rb
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -46,7 +46,7 @@ describe MergeRequests::PushOptionsHandlerService do
expect(last_mr.assignees).to contain_exactly(user)
end
- context 'when project has been forked' do
+ context 'when project has been forked', :sidekiq_might_not_need_inline do
let(:forked_project) { fork_project(project, user, repository: true) }
let(:service) { described_class.new(forked_project, user, changes, push_options) }
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 7b8c94c86fe..9c535664c26 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -174,7 +174,7 @@ describe MergeRequests::RebaseService do
target_branch: 'master', target_project: project)
end
- it 'rebases source branch' do
+ it 'rebases source branch', :sidekiq_might_not_need_inline do
parent_sha = forked_project.repository.commit(merge_request_from_fork.source_branch).parents.first.sha
target_branch_sha = project.repository.commit(merge_request_from_fork.target_branch).sha
expect(parent_sha).to eq(target_branch_sha)
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2dc932c9f2c..9d0ad60a624 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -137,7 +137,7 @@ describe MergeRequests::RefreshService do
subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
- it 'updates the head_pipeline_id for @merge_request' do
+ it 'updates the head_pipeline_id for @merge_request', :sidekiq_might_not_need_inline do
expect { subject }.to change { @merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id)
end
@@ -200,7 +200,7 @@ describe MergeRequests::RefreshService do
context 'when service runs on forked project' do
let(:project) { @fork_project }
- it 'creates legacy detached merge request pipeline for fork merge request' do
+ it 'creates legacy detached merge request pipeline for fork merge request', :sidekiq_might_not_need_inline do
expect { subject }
.to change { @fork_merge_request.pipelines_for_merge_request.count }.by(1)
@@ -232,7 +232,7 @@ describe MergeRequests::RefreshService do
subject
end
- it 'sets the latest detached merge request pipeline as a head pipeline' do
+ it 'sets the latest detached merge request pipeline as a head pipeline', :sidekiq_might_not_need_inline do
@merge_request.reload
expect(@merge_request.actual_head_pipeline).to be_merge_request_event
end
@@ -304,7 +304,7 @@ describe MergeRequests::RefreshService do
end
end
- context 'push to origin repo target branch' do
+ context 'push to origin repo target branch', :sidekiq_might_not_need_inline do
context 'when all MRs to the target branch had diffs' do
before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
@@ -354,7 +354,7 @@ describe MergeRequests::RefreshService do
end
end
- context 'manual merge of source branch' do
+ context 'manual merge of source branch', :sidekiq_might_not_need_inline do
before do
# Merge master -> feature branch
@project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
@@ -374,7 +374,7 @@ describe MergeRequests::RefreshService do
end
end
- context 'push to fork repo source branch' do
+ context 'push to fork repo source branch', :sidekiq_might_not_need_inline do
let(:refresh_service) { service.new(@fork_project, @user) }
def refresh
@@ -431,7 +431,7 @@ describe MergeRequests::RefreshService do
end
end
- context 'push to fork repo target branch' do
+ context 'push to fork repo target branch', :sidekiq_might_not_need_inline do
describe 'changes to merge requests' do
before do
service.new(@fork_project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
@@ -457,7 +457,7 @@ describe MergeRequests::RefreshService do
end
end
- context 'forked projects with the same source branch name as target branch' do
+ context 'forked projects with the same source branch name as target branch', :sidekiq_might_not_need_inline do
let!(:first_commit) do
@fork_project.repository.create_file(@user, 'test1.txt', 'Test data',
message: 'Test commit',
@@ -537,7 +537,7 @@ describe MergeRequests::RefreshService do
context 'push new branch that exists in a merge request' do
let(:refresh_service) { service.new(@fork_project, @user) }
- it 'refreshes the merge request' do
+ it 'refreshes the merge request', :sidekiq_might_not_need_inline do
expect(refresh_service).to receive(:execute_hooks)
.with(@fork_merge_request, 'update', old_rev: Gitlab::Git::BLANK_SHA)
allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
@@ -769,7 +769,7 @@ describe MergeRequests::RefreshService do
fork_project(target_project, author, repository: true)
end
- let_it_be(:merge_request) do
+ let_it_be(:merge_request, refind: true) do
create(:merge_request,
author: author,
source_project: source_project,
@@ -795,88 +795,58 @@ describe MergeRequests::RefreshService do
.parent_id
end
+ let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
let(:refresh_service) { service.new(project, user) }
before do
target_project.merge_method = merge_method
target_project.save!
+ merge_request.auto_merge_strategy = auto_merge_strategy
+ merge_request.save!
refresh_service.execute(oldrev, newrev, 'refs/heads/master')
merge_request.reload
end
- let(:aborted_message) do
- /aborted the automatic merge because target branch was updated/
- end
-
- shared_examples 'aborted MWPS' do
- it 'aborts auto_merge' do
- expect(merge_request.auto_merge_enabled?).to be_falsey
- expect(merge_request.notes.last.note).to match(aborted_message)
- end
-
- it 'removes merge_user' do
- expect(merge_request.merge_user).to be_nil
- end
-
- it 'does not add todos for merge user' do
- expect(user.todos.for_target(merge_request)).to be_empty
- end
-
- it 'adds todos for merge author' do
- expect(author.todos.for_target(merge_request)).to be_present.and be_all(&:pending?)
- end
- end
-
context 'when Project#merge_method is set to FF' do
let(:merge_method) { :ff }
- it_behaves_like 'aborted MWPS'
+ it_behaves_like 'aborted merge requests for MWPS'
context 'with forked project' do
let(:source_project) { forked_project }
- it_behaves_like 'aborted MWPS'
+ it_behaves_like 'aborted merge requests for MWPS'
+ end
+
+ context 'with bogus auto merge strategy' do
+ let(:auto_merge_strategy) { 'bogus' }
+
+ it_behaves_like 'maintained merge requests for MWPS'
end
end
context 'when Project#merge_method is set to rebase_merge' do
let(:merge_method) { :rebase_merge }
- it_behaves_like 'aborted MWPS'
+ it_behaves_like 'aborted merge requests for MWPS'
context 'with forked project' do
let(:source_project) { forked_project }
- it_behaves_like 'aborted MWPS'
+ it_behaves_like 'aborted merge requests for MWPS'
end
end
context 'when Project#merge_method is set to merge' do
let(:merge_method) { :merge }
- shared_examples 'maintained MWPS' do
- it 'does not cancel auto merge' do
- expect(merge_request.auto_merge_enabled?).to be_truthy
- expect(merge_request.notes).to be_empty
- end
-
- it 'does not change merge_user' do
- expect(merge_request.merge_user).to eq(user)
- end
-
- it 'does not add todos' do
- expect(author.todos.for_target(merge_request)).to be_empty
- expect(user.todos.for_target(merge_request)).to be_empty
- end
- end
-
- it_behaves_like 'maintained MWPS'
+ it_behaves_like 'maintained merge requests for MWPS'
context 'with forked project' do
let(:source_project) { forked_project }
- it_behaves_like 'maintained MWPS'
+ it_behaves_like 'maintained merge requests for MWPS'
end
end
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 7a98437f724..25ab79d70c3 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -37,7 +37,7 @@ describe MergeRequests::ReopenService do
.with(merge_request, 'reopen')
end
- it 'sends email to user2 about reopen of merge_request' do
+ it 'sends email to user2 about reopen of merge_request', :sidekiq_might_not_need_inline do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
index 0a10a9ee13b..dc2bd5bf3d0 100644
--- a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
+++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
@@ -38,7 +38,7 @@ describe MergeRequests::ResolvedDiscussionNotificationService do
subject.execute(merge_request)
end
- it "sends a notification email" do
+ it "sends a notification email", :sidekiq_might_not_need_inline do
expect_any_instance_of(NotificationService).to receive(:resolve_all_discussions).with(merge_request, user)
subject.execute(merge_request)
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index d31f5dc0176..baa0ecf27e3 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -98,7 +98,7 @@ describe MergeRequests::UpdateService, :mailer do
)
end
- it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+ it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment', :sidekiq_might_not_need_inline do
deliveries = ActionMailer::Base.deliveries
email = deliveries.last
recipients = deliveries.last(2).flat_map(&:to)
@@ -181,7 +181,7 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- it 'merges the MR' do
+ it 'merges the MR', :sidekiq_might_not_need_inline do
expect(@merge_request).to be_valid
expect(@merge_request.state).to eq('merged')
expect(@merge_request.merge_error).to be_nil
@@ -190,7 +190,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'with finished pipeline' do
before do
- create(:ci_pipeline_with_one_job,
+ create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
@@ -202,7 +202,7 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- it 'merges the MR' do
+ it 'merges the MR', :sidekiq_might_not_need_inline do
expect(@merge_request).to be_valid
expect(@merge_request.state).to eq('merged')
end
@@ -212,14 +212,14 @@ describe MergeRequests::UpdateService, :mailer do
before do
service_mock = double
create(
- :ci_pipeline_with_one_job,
+ :ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
head_pipeline_of: merge_request
)
- expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, {})
+ expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, { sha: merge_request.diff_head_sha })
.and_return(service_mock)
allow(service_mock).to receive(:available_for?) { true }
expect(service_mock).to receive(:execute).with(merge_request)
@@ -332,7 +332,7 @@ describe MergeRequests::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
- it 'sends notifications for subscribers of changed milestone' do
+ it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
merge_request.milestone = create(:milestone, project: project)
merge_request.save
@@ -364,7 +364,7 @@ describe MergeRequests::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
- it 'sends notifications for subscribers of changed milestone' do
+ it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
update_merge_request(milestone: create(:milestone, project: project))
end
@@ -411,7 +411,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'when auto merge is enabled and target branch changed' do
before do
- AutoMergeService.new(project, user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ AutoMergeService.new(project, user, { sha: merge_request.diff_head_sha }).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
update_merge_request({ target_branch: 'target' })
end
@@ -431,7 +431,7 @@ describe MergeRequests::UpdateService, :mailer do
project.add_developer(subscriber)
end
- it 'sends notifications for subscribers of newly added labels' do
+ it 'sends notifications for subscribers of newly added labels', :sidekiq_might_not_need_inline do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
new file mode 100644
index 00000000000..f200c636aac
--- /dev/null
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::Dashboard::GrafanaMetricEmbedService do
+ include MetricsDashboardHelpers
+ include ReactiveCachingHelpers
+ include GrafanaApiHelpers
+
+ let_it_be(:project) { build(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ let(:grafana_url) do
+ valid_grafana_dashboard_link(grafana_integration.grafana_url)
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe '.valid_params?' do
+ let(:valid_params) { { embedded: true, grafana_url: grafana_url } }
+
+ subject { described_class.valid_params?(params) }
+
+ let(:params) { valid_params }
+
+ it { is_expected.to be_truthy }
+
+ context 'not embedded' do
+ let(:params) { valid_params.except(:embedded) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'undefined grafana_url' do
+ let(:params) { valid_params.except(:grafana_url) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.from_cache' do
+ let(:params) { [project.id, user.id, grafana_url] }
+
+ subject { described_class.from_cache(*params) }
+
+ it 'initializes an instance of GrafanaMetricEmbedService' do
+ expect(subject).to be_an_instance_of(described_class)
+ expect(subject.project).to eq(project)
+ expect(subject.current_user).to eq(user)
+ expect(subject.params[:grafana_url]).to eq(grafana_url)
+ end
+ end
+
+ describe '#get_dashboard', :use_clean_rails_memory_store_caching do
+ let(:service_params) do
+ [
+ project,
+ user,
+ {
+ embedded: true,
+ grafana_url: grafana_url
+ }
+ ]
+ end
+
+ let(:service) { described_class.new(*service_params) }
+ let(:service_call) { service.get_dashboard }
+
+ context 'without caching' do
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ it_behaves_like 'raises error for users with insufficient permissions'
+
+ context 'without a grafana integration' do
+ before do
+ allow(project).to receive(:grafana_integration).and_return(nil)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request
+ end
+
+ context 'when grafana cannot be reached' do
+ before do
+ allow(grafana_integration.client).to receive(:get_dashboard).and_raise(::Grafana::Client::Error)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :service_unavailable
+ end
+
+ context 'when panelId is missing' do
+ let(:grafana_url) do
+ grafana_integration.grafana_url +
+ '/d/XDaNK6amz/gitlab-omnibus-redis' \
+ '?from=1570397739557&to=1570484139557'
+ end
+
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when uid is missing' do
+ let(:grafana_url) { grafana_integration.grafana_url + '/d/' }
+
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the dashboard response contains misconfigured json' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url, body: '')
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the datasource response contains misconfigured json' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ stub_datasource_request(grafana_integration.grafana_url, body: '')
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the embed was created successfully' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ stub_datasource_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'valid embedded dashboard service response'
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ let(:cache_params) { [project.id, user.id, grafana_url] }
+
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(service_call).to eq(nil)
+ end
+ end
+
+ context 'when value present in cache' do
+ let(:return_value) { { 'http_status' => :ok, 'dashboard' => '{}' } }
+
+ before do
+ stub_reactive_cache(service, return_value, cache_params)
+ end
+
+ it 'returns cached value' do
+ expect(ReactiveCachingWorker)
+ .not_to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(service_call[:http_status]).to eq(return_value[:http_status])
+ expect(service_call[:dashboard]).to eq(return_value[:dashboard])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/metrics/dashboard/project_dashboard_service_spec.rb b/spec/services/metrics/dashboard/project_dashboard_service_spec.rb
index e76db868425..ab7a7b97861 100644
--- a/spec/services/metrics/dashboard/project_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/project_dashboard_service_spec.rb
@@ -80,7 +80,8 @@ describe Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_st
[{
path: dashboard_path,
display_name: 'test.yml',
- default: false
+ default: false,
+ system_dashboard: false
}]
)
end
diff --git a/spec/services/metrics/dashboard/system_dashboard_service_spec.rb b/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
index 8be3e7f6064..ec861465662 100644
--- a/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/system_dashboard_service_spec.rb
@@ -44,7 +44,8 @@ describe Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_sto
[{
path: described_class::SYSTEM_DASHBOARD_PATH,
display_name: described_class::SYSTEM_DASHBOARD_NAME,
- default: true
+ default: true,
+ system_dashboard: true
}]
)
end
diff --git a/spec/services/namespaces/statistics_refresher_service_spec.rb b/spec/services/namespaces/statistics_refresher_service_spec.rb
index f4d9c96f7f4..9d42e917efe 100644
--- a/spec/services/namespaces/statistics_refresher_service_spec.rb
+++ b/spec/services/namespaces/statistics_refresher_service_spec.rb
@@ -23,7 +23,7 @@ describe Namespaces::StatisticsRefresherService, '#execute' do
end
end
- context 'with a root storage statistics relation' do
+ context 'with a root storage statistics relation', :sidekiq_might_not_need_inline do
before do
Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: group.id)
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index aa67b87a645..25900043f11 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -682,7 +682,7 @@ describe NotificationService, :mailer do
context 'when recipients for a new release exist' do
let(:release) { create(:release) }
- it 'calls new_release_email for each relevant recipient' do
+ it 'calls new_release_email for each relevant recipient', :sidekiq_might_not_need_inline do
user_1 = create(:user)
user_2 = create(:user)
user_3 = create(:user)
@@ -869,6 +869,18 @@ describe NotificationService, :mailer do
should_email(user_4)
end
+ it 'adds "subscribed" reason to subscriber emails' do
+ user_1 = create(:user)
+ label = create(:label, project: project, issues: [issue])
+ issue.reload
+ label.subscribe(user_1)
+
+ notification.new_issue(issue, @u_disabled)
+
+ email = find_email_for(user_1)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::SUBSCRIBED)
+ end
+
it_behaves_like 'project emails are disabled' do
let(:notification_target) { issue }
let(:notification_trigger) { notification.new_issue(issue, @u_disabled) }
@@ -1272,6 +1284,17 @@ describe NotificationService, :mailer do
let(:notification_target) { issue }
let(:notification_trigger) { notification.close_issue(issue, @u_disabled) }
end
+
+ it 'adds "subscribed" reason to subscriber emails' do
+ user_1 = create(:user)
+ issue.subscribe(user_1)
+ issue.reload
+
+ notification.close_issue(issue, @u_disabled)
+
+ email = find_email_for(user_1)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::SUBSCRIBED)
+ end
end
describe '#reopen_issue' do
diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb
index 8585d495ffb..bf637b70aaf 100644
--- a/spec/services/projects/after_rename_service_spec.rb
+++ b/spec/services/projects/after_rename_service_spec.rb
@@ -222,7 +222,7 @@ describe Projects::AfterRenameService do
def expect_repository_exist(full_path_with_extension)
expect(
- gitlab_shell.exists?(
+ TestEnv.storage_dir_exists?(
project.repository_storage,
full_path_with_extension
)
diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb
index f296ef3a776..1cfe3582e56 100644
--- a/spec/services/projects/container_repository/delete_tags_service_spec.rb
+++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb
@@ -57,21 +57,7 @@ describe Projects::ContainerRepository::DeleteTagsService do
end
end
- context 'with dummy tags disabled' do
- let(:tags) { %w[A Ba] }
-
- before do
- stub_feature_flags(container_registry_smart_delete: false)
- end
-
- it 'deletes tags one by one' do
- expect_delete_tag('sha256:configA')
- expect_delete_tag('sha256:configB')
- is_expected.to include(status: :success)
- end
- end
-
- context 'with dummy tags enabled' do
+ context 'with tags to delete' do
let(:tags) { %w[A Ba] }
it 'deletes the tags using a dummy image' do
@@ -102,6 +88,33 @@ describe Projects::ContainerRepository::DeleteTagsService do
is_expected.to include(status: :success)
end
+
+ context 'with failures' do
+ context 'when the dummy manifest generation fails' do
+ before do
+ stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false)
+ end
+
+ it { is_expected.to include(status: :error) }
+ end
+
+ context 'when updating the tags fails' do
+ before do
+ stub_upload("{\n \"config\": {\n }\n}", 'sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/A")
+ .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/Ba")
+ .to_return(status: 500, body: "", headers: { 'docker-content-digest' => 'sha256:dummy' })
+
+ stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3")
+ .to_return(status: 200, body: "", headers: {})
+ end
+
+ it { is_expected.to include(status: :error) }
+ end
+ end
end
end
end
@@ -121,10 +134,10 @@ describe Projects::ContainerRepository::DeleteTagsService do
end
end
- def stub_upload(content, digest)
+ def stub_upload(content, digest, success: true)
expect_any_instance_of(ContainerRegistry::Client)
.to receive(:upload_blob)
- .with(repository.path, content, digest) { double(success?: true ) }
+ .with(repository.path, content, digest) { double(success?: success ) }
end
def expect_delete_tag(digest)
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 2331281bd8e..642986bb176 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -81,7 +81,7 @@ describe Projects::DestroyService do
end
let!(:async) { true }
- it 'destroys them' do
+ it 'destroys them', :sidekiq_might_not_need_inline do
expect(RemoteMirror.count).to eq(0)
end
end
@@ -102,7 +102,7 @@ describe Projects::DestroyService do
end
let!(:async) { true }
- it 'destroys project and export' do
+ it 'destroys project and export', :sidekiq_might_not_need_inline do
expect { destroy_project(project_with_export, user) }.to change(ImportExportUpload, :count).by(-1)
expect(Project.all).not_to include(project_with_export)
@@ -153,7 +153,7 @@ describe Projects::DestroyService do
end
end
- context 'with async_execute' do
+ context 'with async_execute', :sidekiq_might_not_need_inline do
let(:async) { true }
context 'async delete of project with private issue visibility' do
@@ -346,21 +346,21 @@ describe Projects::DestroyService do
let(:path) { project.disk_path + '.git' }
before do
- expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, path)).to be_truthy
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
# Dont run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
- expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_falsey
- expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_truthy
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_truthy
end
it 'restores the repositories' do
Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback }
- expect(project.gitlab_shell.exists?(project.repository_storage, path)).to be_truthy
- expect(project.gitlab_shell.exists?(project.repository_storage, remove_path)).to be_falsey
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, path)).to be_truthy
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, remove_path)).to be_falsey
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 7e7e80ca240..5a3796fec3d 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -79,7 +79,7 @@ describe Projects::ForkService do
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
- it 'imports the repository of the forked project' do
+ it 'imports the repository of the forked project', :sidekiq_might_not_need_inline do
to_project = fork_project(@from_project, @to_user, repository: true)
expect(to_project.empty_repo?).to be_falsy
diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
new file mode 100644
index 00000000000..34c37be6703
--- /dev/null
+++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::HashedStorage::BaseAttachmentService do
+ let(:project) { create(:project, :repository, storage_version: 0, skip_disk_validation: true) }
+
+ subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }
+
+ describe '#old_disk_path' do
+ it { is_expected.to respond_to :old_disk_path }
+ end
+
+ describe '#new_disk_path' do
+ it { is_expected.to respond_to :new_disk_path }
+ end
+
+ describe '#skipped?' do
+ it { is_expected.to respond_to :skipped? }
+ end
+
+ describe '#target_path_discardable?' do
+ it 'returns false' do
+ expect(subject.target_path_discardable?('something/something')).to be_falsey
+ end
+ end
+
+ describe '#discard_path!' do
+ it 'renames target path adding a timestamp at the end' do
+ target_path = Dir.mktmpdir
+ expect(Dir.exist?(target_path)).to be_truthy
+
+ Timecop.freeze do
+ suffix = Time.now.utc.to_i
+ subject.send(:discard_path!, target_path)
+
+ expected_renamed_path = "#{target_path}-#{suffix}"
+
+ expect(Dir.exist?(target_path)).to be_falsey
+ expect(Dir.exist?(expected_renamed_path)).to be_truthy
+ end
+ end
+ end
+
+ describe '#move_folder!' do
+ context 'when old_path is not a directory' do
+ it 'adds information to the logger and returns true' do
+ Tempfile.create do |old_path|
+ new_path = "#{old_path}-new"
+
+ expect(subject.send(:move_folder!, old_path, new_path)).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
index 32ebec318f2..ab9d2bdba8f 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Projects::HashedStorage::MigrateAttachmentsService do
- subject(:service) { described_class.new(project, project.full_path, logger: nil) }
+ subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }
let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
@@ -72,7 +72,23 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
FileUtils.mkdir_p(base_path(hashed_storage))
end
- it 'raises AttachmentCannotMoveError' do
+ it 'succeed when target is empty' do
+ expect { service.execute }.not_to raise_error
+ end
+
+ it 'succeed when target include only discardable items' do
+ Projects::HashedStorage::MigrateAttachmentsService::DISCARDABLE_PATHS.each do |path_fragment|
+ discardable_path = File.join(base_path(hashed_storage), path_fragment)
+ FileUtils.mkdir_p(discardable_path)
+ end
+
+ expect { service.execute }.not_to raise_error
+ end
+
+ it 'raises AttachmentCannotMoveError when there are non discardable items on target path' do
+ not_discardable_path = File.join(base_path(hashed_storage), 'something')
+ FileUtils.mkdir_p(not_discardable_path)
+
expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))
expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentCannotMoveError)
@@ -100,6 +116,18 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
end
end
+ context '#target_path_discardable?' do
+ it 'returns true when it include only items on the discardable list' do
+ hashed_attachments_path = File.join(base_path(hashed_storage))
+ Projects::HashedStorage::MigrateAttachmentsService::DISCARDABLE_PATHS.each do |path_fragment|
+ discardable_path = File.join(hashed_attachments_path, path_fragment)
+ FileUtils.mkdir_p(discardable_path)
+ end
+
+ expect(service.target_path_discardable?(hashed_attachments_path)).to be_truthy
+ end
+ end
+
def base_path(storage)
File.join(FileUploader.root, storage.disk_path)
end
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 70785c606a5..132b895fc35 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -10,7 +10,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
- subject(:service) { described_class.new(project, project.disk_path) }
+ subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path) }
describe '#execute' do
let(:old_disk_path) { legacy_storage.disk_path }
diff --git a/spec/services/projects/hashed_storage/migration_service_spec.rb b/spec/services/projects/hashed_storage/migration_service_spec.rb
index e3191cd7ebc..f3ac26e7761 100644
--- a/spec/services/projects/hashed_storage/migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migration_service_spec.rb
@@ -10,13 +10,14 @@ describe Projects::HashedStorage::MigrationService do
describe '#execute' do
context 'repository migration' do
- let(:repository_service) { Projects::HashedStorage::MigrateRepositoryService.new(project, project.full_path, logger: logger) }
+ let(:repository_service) do
+ Projects::HashedStorage::MigrateRepositoryService.new(project: project,
+ old_disk_path: project.full_path,
+ logger: logger)
+ end
it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do
- expect(Projects::HashedStorage::MigrateRepositoryService)
- .to receive(:new)
- .with(project, project.full_path, logger: logger)
- .and_return(repository_service)
+ expect(service).to receive(:migrate_repository_service).and_return(repository_service)
expect(repository_service).to receive(:execute)
service.execute
@@ -31,13 +32,14 @@ describe Projects::HashedStorage::MigrationService do
end
context 'attachments migration' do
- let(:attachments_service) { Projects::HashedStorage::MigrateAttachmentsService.new(project, project.full_path, logger: logger) }
+ let(:attachments_service) do
+ Projects::HashedStorage::MigrateAttachmentsService.new(project: project,
+ old_disk_path: project.full_path,
+ logger: logger)
+ end
it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do
- expect(Projects::HashedStorage::MigrateAttachmentsService)
- .to receive(:new)
- .with(project, project.full_path, logger: logger)
- .and_return(attachments_service)
+ expect(service).to receive(:migrate_attachments_service).and_return(attachments_service)
expect(attachments_service).to receive(:execute)
service.execute
diff --git a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
index 815c85e0866..c2ba9626f41 100644
--- a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Projects::HashedStorage::RollbackAttachmentsService do
- subject(:service) { described_class.new(project, logger: nil) }
+ subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path, logger: nil) }
let(:project) { create(:project, :repository, skip_disk_validation: true) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
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 3ca9ee5bee5..97c7c0af946 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -10,7 +10,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
- subject(:service) { described_class.new(project, project.disk_path) }
+ subject(:service) { described_class.new(project: project, old_disk_path: project.disk_path) }
describe '#execute' do
let(:old_disk_path) { hashed_storage.disk_path }
diff --git a/spec/services/projects/hashed_storage/rollback_service_spec.rb b/spec/services/projects/hashed_storage/rollback_service_spec.rb
index 427d1535559..48d4eac9eb7 100644
--- a/spec/services/projects/hashed_storage/rollback_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_service_spec.rb
@@ -6,17 +6,15 @@ describe Projects::HashedStorage::RollbackService do
let(:project) { create(:project, :empty_repo, :wiki_repo) }
let(:logger) { double }
- subject(:service) { described_class.new(project, project.full_path, logger: logger) }
+ subject(:service) { described_class.new(project, project.disk_path, logger: logger) }
describe '#execute' do
context 'attachments rollback' do
let(:attachments_service_class) { Projects::HashedStorage::RollbackAttachmentsService }
- let(:attachments_service) { attachments_service_class.new(project, logger: logger) }
+ let(:attachments_service) { attachments_service_class.new(project: project, old_disk_path: project.disk_path, logger: logger) }
it 'delegates rollback to Projects::HashedStorage::RollbackAttachmentsService' do
- expect(attachments_service_class).to receive(:new)
- .with(project, logger: logger)
- .and_return(attachments_service)
+ expect(service).to receive(:rollback_attachments_service).and_return(attachments_service)
expect(attachments_service).to receive(:execute)
service.execute
@@ -31,15 +29,12 @@ describe Projects::HashedStorage::RollbackService do
end
context 'repository rollback' do
+ let(:project) { create(:project, :empty_repo, :wiki_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
let(:repository_service_class) { Projects::HashedStorage::RollbackRepositoryService }
- let(:repository_service) { repository_service_class.new(project, project.full_path, logger: logger) }
+ let(:repository_service) { repository_service_class.new(project: project, old_disk_path: project.disk_path, logger: logger) }
it 'delegates rollback to RollbackRepositoryService' do
- project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
-
- expect(repository_service_class).to receive(:new)
- .with(project, project.full_path, logger: logger)
- .and_return(repository_service)
+ expect(service).to receive(:rollback_repository_service).and_return(repository_service)
expect(repository_service).to receive(:execute)
service.execute
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 146d656c909..a557e61da78 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -66,7 +66,7 @@ describe Projects::ImportExport::ExportService do
end
it 'saves the project in the file system' do
- expect(Gitlab::ImportExport::Saver).to receive(:save).with(project: project, shared: shared)
+ expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared)
service.execute
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index 66233787d3a..aca59079b3c 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -16,6 +16,13 @@ describe Projects::LfsPointers::LfsLinkService do
end
describe '#execute' do
+ it 'raises an error when trying to link too many objects at once' do
+ oids = Array.new(described_class::MAX_OIDS) { |i| "oid-#{i}" }
+ oids << 'the straw'
+
+ expect { subject.execute(oids) }.to raise_error(described_class::TooManyOidsError)
+ end
+
it 'links existing lfs objects to the project' do
expect(project.all_lfs_objects.count).to eq 2
@@ -28,7 +35,7 @@ describe Projects::LfsPointers::LfsLinkService do
it 'returns linked oids' do
linked = lfs_objects_project.map(&:lfs_object).map(&:oid) << new_lfs_object.oid
- expect(subject.execute(new_oid_list.keys)).to eq linked
+ expect(subject.execute(new_oid_list.keys)).to contain_exactly(*linked)
end
it 'links in batches' do
@@ -48,5 +55,26 @@ describe Projects::LfsPointers::LfsLinkService do
expect(project.all_lfs_objects.count).to eq 9
expect(linked.size).to eq 7
end
+
+ it 'only queries for the batch that will be processed', :aggregate_failures do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ oids = %w(one two)
+
+ expect(LfsObject).to receive(:where).with(oid: %w(one)).once.and_call_original
+ expect(LfsObject).to receive(:where).with(oid: %w(two)).once.and_call_original
+
+ subject.execute(oids)
+ end
+
+ it 'only queries 3 times' do
+ # make sure that we don't count the queries in the setup
+ new_oid_list
+
+ # These are repeated for each batch of oids: maximum (MAX_OIDS / BATCH_SIZE) times
+ # 1. Load the batch of lfs object ids that we might know already
+ # 2. Load the objects that have not been linked to the project yet
+ # 3. Insert the lfs_objects_projects for that batch
+ expect { subject.execute(new_oid_list.keys) }.not_to exceed_query_limit(3)
+ end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 31bd0f0f836..c848a5397e1 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -151,7 +151,7 @@ describe Projects::UpdateService do
context 'when we update project but not enabling a wiki' do
it 'does not try to create an empty wiki' do
- Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
+ TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path)
result = update_project(project, user, { name: 'test1' })
@@ -172,7 +172,7 @@ describe Projects::UpdateService do
context 'when enabling a wiki' do
it 'creates a wiki' do
project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
- Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
+ TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path)
result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 83101add724..e2ed7581ad4 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -115,51 +115,36 @@ describe SystemNoteService do
end
describe '.merge_when_pipeline_succeeds' do
- let(:pipeline) { build(:ci_pipeline_without_jobs )}
- let(:noteable) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, pipeline.sha) }
+ it 'calls MergeRequestsService' do
+ sha = double
- it_behaves_like 'a system note' do
- let(:action) { 'merge' }
- end
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:merge_when_pipeline_succeeds).with(sha)
+ end
- it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds})
+ described_class.merge_when_pipeline_succeeds(noteable, project, author, sha)
end
end
describe '.cancel_merge_when_pipeline_succeeds' do
- let(:noteable) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'merge' }
- end
+ it 'calls MergeRequestsService' do
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:cancel_merge_when_pipeline_succeeds)
+ end
- it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to eq "canceled the automatic merge"
+ described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author)
end
end
describe '.abort_merge_when_pipeline_succeeds' do
- let(:noteable) do
- create(:merge_request, source_project: project, target_project: project)
- end
+ it 'calls MergeRequestsService' do
+ reason = double
- subject { described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, 'merge request was closed') }
-
- it_behaves_like 'a system note' do
- let(:action) { 'merge' }
- end
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:abort_merge_when_pipeline_succeeds).with(reason)
+ end
- it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to eq "aborted the automatic merge because merge request was closed"
+ described_class.abort_merge_when_pipeline_succeeds(noteable, project, author, reason)
end
end
@@ -196,77 +181,55 @@ describe SystemNoteService do
end
describe '.change_branch' do
- subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) }
-
- let(:old_branch) { 'old_branch'}
- let(:new_branch) { 'new_branch'}
-
- it_behaves_like 'a system note' do
- let(:action) { 'branch' }
- end
+ it 'calls MergeRequestsService' do
+ old_branch = double
+ new_branch = double
+ branch_type = double
- context 'when target branch name changed' do
- it 'sets the note text' do
- expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:change_branch).with(branch_type, old_branch, new_branch)
end
+
+ described_class.change_branch(noteable, project, author, branch_type, old_branch, new_branch)
end
end
describe '.change_branch_presence' do
- subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'branch' }
- end
+ it 'calls MergeRequestsService' do
+ presence = double
+ branch = double
+ branch_type = double
- context 'when source branch deleted' do
- it 'sets the note text' do
- expect(subject.note).to eq "deleted source branch `feature`"
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:change_branch_presence).with(branch_type, branch, presence)
end
+
+ described_class.change_branch_presence(noteable, project, author, branch_type, branch, presence)
end
end
describe '.new_issue_branch' do
- let(:branch) { '1-mepmep' }
+ it 'calls MergeRequestsService' do
+ branch = double
+ branch_project = double
- subject { described_class.new_issue_branch(noteable, project, author, branch, branch_project: branch_project) }
-
- shared_examples_for 'a system note for new issue branch' do
- it_behaves_like 'a system note' do
- let(:action) { 'branch' }
- end
-
- context 'when a branch is created from the new branch button' do
- it 'sets the note text' do
- expect(subject.note).to start_with("created branch [`#{branch}`]")
- end
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:new_issue_branch).with(branch, branch_project: branch_project)
end
- end
- context 'branch_project is set' do
- let(:branch_project) { create(:project, :repository) }
-
- it_behaves_like 'a system note for new issue branch'
- end
-
- context 'branch_project is not set' do
- let(:branch_project) { nil }
-
- it_behaves_like 'a system note for new issue branch'
+ described_class.new_issue_branch(noteable, project, author, branch, branch_project: branch_project)
end
end
describe '.new_merge_request' do
- subject { described_class.new_merge_request(noteable, project, author, merge_request) }
-
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ it 'calls MergeRequestsService' do
+ merge_request = double
- it_behaves_like 'a system note' do
- let(:action) { 'merge' }
- end
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:new_merge_request).with(merge_request)
+ end
- it 'sets the new merge request note text' do
- expect(subject.note).to eq("created merge request #{merge_request.to_reference(project)} to address this issue")
+ described_class.new_merge_request(noteable, project, author, merge_request)
end
end
@@ -642,57 +605,24 @@ describe SystemNoteService do
end
describe '.handle_merge_request_wip' do
- context 'adding wip note' do
- let(:noteable) { create(:merge_request, source_project: project, title: 'WIP Lorem ipsum') }
-
- subject { described_class.handle_merge_request_wip(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
+ it 'calls MergeRequestsService' do
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:handle_merge_request_wip)
end
- it 'sets the note text' do
- expect(subject.note).to eq 'marked as a **Work In Progress**'
- end
- end
-
- context 'removing wip note' do
- let(:noteable) { create(:merge_request, source_project: project, title: 'Lorem ipsum') }
-
- subject { described_class.handle_merge_request_wip(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
- end
-
- it 'sets the note text' do
- expect(subject.note).to eq 'unmarked as a **Work In Progress**'
- end
+ described_class.handle_merge_request_wip(noteable, project, author)
end
end
describe '.add_merge_request_wip_from_commit' do
- let(:noteable) do
- create(:merge_request, source_project: project, target_project: project)
- end
-
- subject do
- described_class.add_merge_request_wip_from_commit(
- noteable,
- project,
- author,
- noteable.diff_head_commit
- )
- end
+ it 'calls MergeRequestsService' do
+ commit = double
- it_behaves_like 'a system note' do
- let(:action) { 'title' }
- end
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:add_merge_request_wip_from_commit).with(commit)
+ end
- it "posts the 'marked as a Work In Progress from commit' system note" do
- expect(subject.note).to match(
- /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
- )
+ described_class.add_merge_request_wip_from_commit(noteable, project, author, commit)
end
end
@@ -709,75 +639,25 @@ describe SystemNoteService do
end
describe '.resolve_all_discussions' do
- let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
-
- subject { described_class.resolve_all_discussions(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'discussion' }
- end
+ it 'calls MergeRequestsService' do
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:resolve_all_discussions)
+ end
- it 'sets the note text' do
- expect(subject.note).to eq 'resolved all threads'
+ described_class.resolve_all_discussions(noteable, project, author)
end
end
describe '.diff_discussion_outdated' do
- let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
- let(:merge_request) { discussion.noteable }
- let(:change_position) { discussion.position }
+ it 'calls MergeRequestsService' do
+ discussion = double
+ change_position = double
- def reloaded_merge_request
- MergeRequest.find(merge_request.id)
- end
-
- subject { described_class.diff_discussion_outdated(discussion, project, author, change_position) }
-
- it_behaves_like 'a system note' do
- let(:expected_noteable) { discussion.first_note.noteable }
- let(:action) { 'outdated' }
- end
-
- context 'when the change_position is valid for the discussion' do
- it 'creates a new note in the discussion' do
- # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
- expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
- end
-
- it 'links to the diff in the system note' do
- diff_id = merge_request.merge_request_diff.id
- line_code = change_position.line_code(project.repository)
- link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)
-
- expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})")
+ expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
+ expect(service).to receive(:diff_discussion_outdated).with(discussion, change_position)
end
- context 'discussion is on an image' do
- let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion }
-
- it 'links to the diff in the system note' do
- diff_id = merge_request.merge_request_diff.id
- file_hash = change_position.file_hash
- link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash)
-
- expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})")
- end
- end
- end
-
- context 'when the change_position does not point to a valid version' do
- before do
- allow(merge_request).to receive(:version_params_for).and_return(nil)
- end
-
- it 'creates a new note in the discussion' do
- # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
- expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
- end
-
- it 'does not create a link' do
- expect(subject.note).to eq('changed this line in version 1 of the diff')
- end
+ described_class.diff_discussion_outdated(discussion, project, author, change_position)
end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 5023abad4cd..ba484d95c9c 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -395,7 +395,7 @@ describe ::SystemNotes::IssuablesService do
end
end
- context 'commit with cross-reference from fork' do
+ context 'commit with cross-reference from fork', :sidekiq_might_not_need_inline do
let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) { fork_project(project, author2, repository: true) }
let(:commit2) { forked_project.commit }
diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb
new file mode 100644
index 00000000000..6d2473e8c03
--- /dev/null
+++ b/spec/services/system_notes/merge_requests_service_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::SystemNotes::MergeRequestsService do
+ include Gitlab::Routing
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:author) { create(:user) }
+
+ let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
+
+ let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
+
+ describe '.merge_when_pipeline_succeeds' do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ subject { service.merge_when_pipeline_succeeds(pipeline.sha) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds})
+ end
+ end
+
+ describe '.cancel_merge_when_pipeline_succeeds' do
+ subject { service.cancel_merge_when_pipeline_succeeds }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to eq "canceled the automatic merge"
+ end
+ end
+
+ describe '.abort_merge_when_pipeline_succeeds' do
+ subject { service.abort_merge_when_pipeline_succeeds('merge request was closed') }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it "posts the 'merge when pipeline succeeds' system note" do
+ expect(subject.note).to eq "aborted the automatic merge because merge request was closed"
+ end
+ end
+
+ describe '.handle_merge_request_wip' do
+ context 'adding wip note' do
+ let(:noteable) { create(:merge_request, source_project: project, title: 'WIP Lorem ipsum') }
+
+ subject { service.handle_merge_request_wip }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'marked as a **Work In Progress**'
+ end
+ end
+
+ context 'removing wip note' do
+ subject { service.handle_merge_request_wip }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'unmarked as a **Work In Progress**'
+ end
+ end
+ end
+
+ describe '.add_merge_request_wip_from_commit' do
+ subject { service.add_merge_request_wip_from_commit(noteable.diff_head_commit) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it "posts the 'marked as a Work In Progress from commit' system note" do
+ expect(subject.note).to match(
+ /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ )
+ end
+ end
+
+ describe '.resolve_all_discussions' do
+ subject { service.resolve_all_discussions }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'discussion' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'resolved all threads'
+ end
+ end
+
+ describe '.diff_discussion_outdated' do
+ let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
+ let(:merge_request) { discussion.noteable }
+ let(:change_position) { discussion.position }
+
+ def reloaded_merge_request
+ MergeRequest.find(merge_request.id)
+ end
+
+ let(:service) { described_class.new(project: project, author: author) }
+
+ subject { service.diff_discussion_outdated(discussion, change_position) }
+
+ it_behaves_like 'a system note' do
+ let(:expected_noteable) { discussion.first_note.noteable }
+ let(:action) { 'outdated' }
+ end
+
+ context 'when the change_position is valid for the discussion' do
+ it 'creates a new note in the discussion' do
+ # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
+ expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ end
+
+ it 'links to the diff in the system note' do
+ diff_id = merge_request.merge_request_diff.id
+ line_code = change_position.line_code(project.repository)
+ link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code)
+
+ expect(subject.note).to eq("changed this line in [version 1 of the diff](#{link})")
+ end
+
+ context 'discussion is on an image' do
+ let(:discussion) { create(:image_diff_note_on_merge_request, project: project).to_discussion }
+
+ it 'links to the diff in the system note' do
+ diff_id = merge_request.merge_request_diff.id
+ file_hash = change_position.file_hash
+ link = diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: file_hash)
+
+ expect(subject.note).to eq("changed this file in [version 1 of the diff](#{link})")
+ end
+ end
+ end
+
+ context 'when the change_position does not point to a valid version' do
+ before do
+ allow(merge_request).to receive(:version_params_for).and_return(nil)
+ end
+
+ it 'creates a new note in the discussion' do
+ # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
+ expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ end
+
+ it 'does not create a link' do
+ expect(subject.note).to eq('changed this line in version 1 of the diff')
+ end
+ end
+ end
+
+ describe '.change_branch' do
+ subject { service.change_branch('target', old_branch, new_branch) }
+
+ let(:old_branch) { 'old_branch'}
+ let(:new_branch) { 'new_branch'}
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
+
+ context 'when target branch name changed' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`"
+ end
+ end
+ end
+
+ describe '.change_branch_presence' do
+ subject { service.change_branch_presence(:source, 'feature', :delete) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
+
+ context 'when source branch deleted' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "deleted source branch `feature`"
+ end
+ end
+ end
+
+ describe '.new_issue_branch' do
+ let(:branch) { '1-mepmep' }
+
+ subject { service.new_issue_branch(branch, branch_project: branch_project) }
+
+ shared_examples_for 'a system note for new issue branch' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
+
+ context 'when a branch is created from the new branch button' do
+ it 'sets the note text' do
+ expect(subject.note).to start_with("created branch [`#{branch}`]")
+ end
+ end
+ end
+
+ context 'branch_project is set' do
+ let(:branch_project) { create(:project, :repository) }
+
+ it_behaves_like 'a system note for new issue branch'
+ end
+
+ context 'branch_project is not set' do
+ let(:branch_project) { nil }
+
+ it_behaves_like 'a system note for new issue branch'
+ end
+ end
+
+ describe '.new_merge_request' do
+ subject { service.new_merge_request(merge_request) }
+
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: generate(:branch), target_project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
+
+ it 'sets the new merge request note text' do
+ expect(subject.note).to eq("created merge request #{merge_request.to_reference(project)} to address this issue")
+ end
+ end
+end
diff --git a/spec/services/users/signup_service_spec.rb b/spec/services/users/signup_service_spec.rb
new file mode 100644
index 00000000000..7d3cd614142
--- /dev/null
+++ b/spec/services/users/signup_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Users::SignupService do
+ let(:user) { create(:user, setup_for_company: true) }
+
+ describe '#execute' do
+ context 'when updating name' do
+ it 'updates the name attribute' do
+ result = update_user(user, name: 'New Name')
+
+ expect(result).to eq(status: :success)
+ expect(user.reload.name).to eq('New Name')
+ end
+
+ it 'returns an error result when name is missing' do
+ result = update_user(user, name: '')
+
+ expect(user.reload.name).not_to be_blank
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to include("Name can't be blank")
+ end
+ end
+
+ context 'when updating role' do
+ it 'updates the role attribute' do
+ result = update_user(user, role: 'development_team_lead')
+
+ expect(result).to eq(status: :success)
+ expect(user.reload.role).to eq('development_team_lead')
+ end
+
+ it 'returns an error result when role is missing' do
+ result = update_user(user, role: '')
+
+ expect(user.reload.role).not_to be_blank
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Role can't be blank")
+ end
+ end
+
+ context 'when updating setup_for_company' do
+ it 'updates the setup_for_company attribute' do
+ result = update_user(user, setup_for_company: 'false')
+
+ expect(result).to eq(status: :success)
+ expect(user.reload.setup_for_company).to be(false)
+ end
+
+ it 'returns an error result when setup_for_company is missing' do
+ result = update_user(user, setup_for_company: '')
+
+ expect(user.reload.setup_for_company).not_to be_blank
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Setup for company can't be blank")
+ end
+ end
+
+ def update_user(user, opts)
+ described_class.new(user, opts).execute
+ end
+ end
+end
diff --git a/spec/services/zoom_notes_service_spec.rb b/spec/services/zoom_notes_service_spec.rb
deleted file mode 100644
index 419ecf3f374..00000000000
--- a/spec/services/zoom_notes_service_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe ZoomNotesService do
- describe '#execute' do
- let(:issue) { OpenStruct.new(description: description) }
- let(:project) { Object.new }
- let(:user) { Object.new }
- let(:description) { 'an issue description' }
- let(:old_description) { nil }
-
- subject { described_class.new(issue, project, user, old_description: old_description) }
-
- shared_examples 'no notifications' do
- it "doesn't create notifications" do
- expect(SystemNoteService).not_to receive(:zoom_link_added)
- expect(SystemNoteService).not_to receive(:zoom_link_removed)
-
- subject.execute
- end
- end
-
- it_behaves_like 'no notifications'
-
- context 'when the zoom link exists in both description and old_description' do
- let(:description) { 'a changed issue description https://zoom.us/j/123' }
- let(:old_description) { 'an issue description https://zoom.us/j/123' }
-
- it_behaves_like 'no notifications'
- end
-
- context "when the zoom link doesn't exist in both description and old_description" do
- let(:description) { 'a changed issue description' }
- let(:old_description) { 'an issue description' }
-
- it_behaves_like 'no notifications'
- end
-
- context 'when description == old_description' do
- let(:old_description) { 'an issue description' }
-
- it_behaves_like 'no notifications'
- end
-
- context 'when the description contains a zoom link and old_description is nil' do
- let(:description) { 'a changed issue description https://zoom.us/j/123' }
-
- it 'creates a zoom_link_added notification' do
- expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
- expect(SystemNoteService).not_to receive(:zoom_link_removed)
-
- subject.execute
- end
- end
-
- context 'when the zoom link has been added to the description' do
- let(:description) { 'a changed issue description https://zoom.us/j/123' }
- let(:old_description) { 'an issue description' }
-
- it 'creates a zoom_link_added notification' do
- expect(SystemNoteService).to receive(:zoom_link_added).with(issue, project, user)
- expect(SystemNoteService).not_to receive(:zoom_link_removed)
-
- subject.execute
- end
- end
-
- context 'when the zoom link has been removed from the description' do
- let(:description) { 'a changed issue description' }
- let(:old_description) { 'an issue description https://zoom.us/j/123' }
-
- it 'creates a zoom_link_removed notification' do
- expect(SystemNoteService).not_to receive(:zoom_link_added).with(issue, project, user)
- expect(SystemNoteService).to receive(:zoom_link_removed)
-
- subject.execute
- end
- end
- end
-end
diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb
index 2e7de75fd08..20347b4d306 100644
--- a/spec/sidekiq/cron/job_gem_dependency_spec.rb
+++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Sidekiq::Cron::Job do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 7a5e570558e..d7533f99683 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -66,6 +66,11 @@ RSpec.configure do |config|
config.infer_spec_type_from_file_location!
config.full_backtrace = !!ENV['CI']
+ unless ENV['CI']
+ # Re-run failures locally with `--only-failures`
+ config.example_status_persistence_file_path = './spec/examples.txt'
+ end
+
config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location]
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 7b3b966bd50..2bd4750dffa 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -101,8 +101,12 @@ RSpec.configure do |config|
config.after(:example, :js) do |example|
# when a test fails, display any messages in the browser's console
- if example.exception
+ # but fail don't add the message if the failure is a pending test that got
+ # fixed. If we raised the `JSException` the fixed test would be marked as
+ # failed again.
+ if example.exception && !example.exception.is_a?(RSpec::Core::Pending::PendingExampleFixedError)
console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER }
+
if console.present?
message = "Unexpected browser console output:\n" + console.map(&:message).join("\n")
raise JSConsoleError, message
diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
index d636c1cf6cd..8a8a2f714bc 100644
--- a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -10,6 +10,8 @@ shared_context 'Ldap::OmniauthCallbacksController' do
let(:provider) { 'ldapmain' }
let(:valid_login?) { true }
let(:user) { create(:omniauth_user, extern_uid: uid, provider: provider) }
+ let(:ldap_setting_defaults) { { enabled: true, servers: ldap_server_config } }
+ let(:ldap_settings) { ldap_setting_defaults }
let(:ldap_server_config) do
{ main: ldap_config_defaults(:main) }
end
@@ -23,7 +25,7 @@ shared_context 'Ldap::OmniauthCallbacksController' do
end
before do
- stub_ldap_setting(enabled: true, servers: ldap_server_config)
+ stub_ldap_setting(ldap_settings)
described_class.define_providers!
Rails.application.reload_routes!
@@ -36,4 +38,8 @@ shared_context 'Ldap::OmniauthCallbacksController' do
after do
Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
end
+
+ after(:all) do
+ Rails.application.reload_routes!
+ end
end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index c57abbd96c6..2096ec90c5b 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -29,7 +29,7 @@ module CycleAnalyticsHelpers
scenarios.each do |start_time_conditions, end_time_conditions|
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" do
+ it "finds the median of available durations between the two conditions", :sidekiq_might_not_need_inline do
time_differences = Array.new(5) do |index|
data = data_fn[self]
start_time = (index * 10).days.from_now
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index bee9d419376..b63ff7147ec 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
#
# # generate-seed-repo-rb
#
@@ -15,9 +16,9 @@
require 'erb'
require 'tempfile'
-SOURCE = File.expand_path('gitlab-git-test.git', __dir__).freeze
-SCRIPT_NAME = 'generate-seed-repo-rb'.freeze
-REPO_NAME = 'gitlab-git-test.git'.freeze
+SOURCE = File.expand_path('gitlab-git-test.git', __dir__)
+SCRIPT_NAME = 'generate-seed-repo-rb'
+REPO_NAME = 'gitlab-git-test.git'
def main
Dir.mktmpdir do |dir|
diff --git a/spec/support/helpers/access_matchers_helpers.rb b/spec/support/helpers/access_matchers_helpers.rb
new file mode 100644
index 00000000000..9100f245d36
--- /dev/null
+++ b/spec/support/helpers/access_matchers_helpers.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module AccessMatchersHelpers
+ USER_ACCESSOR_METHOD_NAME = 'user'
+
+ def provide_user(role, membership = nil)
+ case role
+ when :admin
+ create(:admin)
+ when :auditor
+ create(:user, :auditor)
+ when :user
+ create(:user)
+ when :external
+ create(:user, :external)
+ when :visitor, :anonymous
+ nil
+ when User
+ role
+ when *Gitlab::Access.sym_options_with_owner.keys # owner, maintainer, developer, reporter, guest
+ raise ArgumentError, "cannot provide #{role} when membership reference is blank" unless membership
+
+ provide_user_by_membership(role, membership)
+ else
+ raise ArgumentError, "cannot provide user of an unknown role #{role}"
+ end
+ end
+
+ def provide_user_by_membership(role, membership)
+ if role == :owner && membership.owner
+ membership.owner
+ else
+ create(:user).tap do |user|
+ membership.public_send(:"add_#{role}", user)
+ end
+ end
+ end
+
+ def raise_if_non_block_expectation!(actual)
+ raise ArgumentError, 'This matcher supports block expectations only.' unless actual.is_a?(Proc)
+ end
+
+ def update_owner(objects, user)
+ return unless objects
+
+ objects.each do |object|
+ if object.respond_to?(:owner)
+ object.update_attribute(:owner, user)
+ elsif object.respond_to?(:user)
+ object.update_attribute(:user, user)
+ else
+ raise ArgumentError, "cannot own this object #{object}"
+ end
+ end
+ end
+
+ def patch_example_group(user)
+ return if user.nil? # for anonymous users
+
+ # This call is evaluated in context of ExampleGroup instance in which the matcher is called. Overrides the `user`
+ # (or defined by `method_name`) method generated by `let` definition in example group before it's used by `subject`.
+ # This override is per concrete example only because the example group class gets re-created for each example.
+ instance_eval(<<~CODE, __FILE__, __LINE__ + 1)
+ if instance_variable_get(:@__#{USER_ACCESSOR_METHOD_NAME}_patched)
+ raise ArgumentError, 'An access matcher be_allowed_for/be_denied_for can be used only once per example (`it` block)'
+ end
+ instance_variable_set(:@__#{USER_ACCESSOR_METHOD_NAME}_patched, true)
+
+ def #{USER_ACCESSOR_METHOD_NAME}
+ @#{USER_ACCESSOR_METHOD_NAME} ||= User.find(#{user.id})
+ end
+ CODE
+ end
+
+ def prepare_matcher_environment(role, membership, owned_objects)
+ user = provide_user(role, membership)
+
+ if user
+ update_owner(owned_objects, user)
+ patch_example_group(user)
+ end
+ end
+
+ def run_matcher(action, role, membership, owned_objects)
+ raise_if_non_block_expectation!(action)
+
+ prepare_matcher_environment(role, membership, owned_objects)
+
+ if block_given?
+ yield action
+ else
+ action.call
+ end
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index a604359942f..d101b092e7d 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -77,7 +77,7 @@ module CycleAnalyticsHelpers
.new(project, user)
.closed_by_merge_requests(issue)
- merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user, sha: merge_request.diff_head_sha).execute(merge_request) }
end
def deploy_master(user, project, environment: 'production')
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 39c818b1763..5dc87c36931 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -114,6 +114,10 @@ module FilteredSearchHelpers
create_token('Milestone', milestone_name, symbol)
end
+ def release_token(release_tag = nil)
+ create_token('Release', release_tag)
+ end
+
def label_token(label_name = nil, has_symbol = true)
symbol = has_symbol ? '~' : nil
create_token('Label', label_name, symbol)
diff --git a/spec/support/helpers/grafana_api_helpers.rb b/spec/support/helpers/grafana_api_helpers.rb
new file mode 100644
index 00000000000..e47b1a808f2
--- /dev/null
+++ b/spec/support/helpers/grafana_api_helpers.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module GrafanaApiHelpers
+ def valid_grafana_dashboard_link(base_url)
+ base_url +
+ '/d/XDaNK6amz/gitlab-omnibus-redis' \
+ '?from=1570397739557&to=1570484139557' \
+ '&var-instance=localhost:9121&panelId=8'
+ end
+
+ def stub_dashboard_request(base_url, path: '/api/dashboards/uid/XDaNK6amz', body: nil)
+ body ||= fixture_file('grafana/dashboard_response.json')
+
+ stub_request(:get, "#{base_url}#{path}")
+ .to_return(
+ status: 200,
+ body: body,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def stub_datasource_request(base_url, path: '/api/datasources/name/GitLab%20Omnibus', body: nil)
+ body ||= fixture_file('grafana/datasource_response.json')
+
+ stub_request(:get, "#{base_url}#{path}")
+ .to_return(
+ status: 200,
+ body: body,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def stub_all_grafana_proxy_requests(base_url)
+ stub_request(:any, /#{base_url}\/api\/datasources\/proxy/)
+ .to_return(
+ status: 200,
+ body: fixture_file('grafana/proxy_response.json'),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 6fb1d279456..80a3f7df05f 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -37,9 +37,12 @@ module GraphqlHelpers
# BatchLoader::GraphQL returns a wrapper, so we need to :sync in order
# to get the actual values
def batch_sync(max_queries: nil, &blk)
- result = batch(max_queries: nil, &blk)
+ wrapper = proc do
+ lazy_vals = yield
+ lazy_vals.is_a?(Array) ? lazy_vals.map(&:sync) : lazy_vals&.sync
+ end
- result.is_a?(Array) ? result.map(&:sync) : result&.sync
+ batch(max_queries: max_queries, &wrapper)
end
def graphql_query_for(name, attributes = {}, fields = nil)
@@ -157,7 +160,13 @@ module GraphqlHelpers
def attributes_to_graphql(attributes)
attributes.map do |name, value|
- "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\""
+ value_str = if value.is_a?(Array)
+ '["' + value.join('","') + '"]'
+ else
+ "\"#{value}\""
+ end
+
+ "#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
end.join(", ")
end
@@ -282,6 +291,12 @@ module GraphqlHelpers
def allow_high_graphql_recursion
allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
end
+
+ def node_array(data, extract_attribute = nil)
+ data.map do |item|
+ extract_attribute ? item['node'][extract_attribute] : item['node']
+ end
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index e74dbca4f93..677aef57661 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -16,7 +16,7 @@ module KubernetesHelpers
end
def kube_logs_response
- kube_response(kube_logs_body)
+ { body: kube_logs_body }
end
def kube_deployments_response
@@ -319,10 +319,10 @@ module KubernetesHelpers
}
end
- def kube_knative_services_body(legacy_knative: false, **options)
+ def kube_knative_services_body(**options)
{
"kind" => "List",
- "items" => [legacy_knative ? knative_05_service(options) : kube_service(options)]
+ "items" => [knative_07_service(options)]
}
end
@@ -398,77 +398,171 @@ module KubernetesHelpers
}
end
- def kube_service(name: "kubetest", namespace: "default", domain: "example.com")
- {
- "metadata" => {
- "creationTimestamp" => "2018-11-21T06:16:33Z",
- "name" => name,
- "namespace" => namespace,
- "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
- },
+ # noinspection RubyStringKeysInHashInspection
+ def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production')
+ { "apiVersion" => "serving.knative.dev/v1alpha1",
+ "kind" => "Service",
+ "metadata" =>
+ { "annotations" =>
+ { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account",
+ "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" },
+ "creationTimestamp" => "2019-10-22T21:19:20Z",
+ "generation" => 1,
+ "labels" => { "service" => name },
+ "name" => name,
+ "namespace" => namespace,
+ "resourceVersion" => "6042",
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
+ "uid" => "9c7f63d0-f511-11e9-8815-42010a80002f" },
"spec" => {
- "generation" => 2
+ "runLatest" => {
+ "configuration" => {
+ "revisionTemplate" => {
+ "metadata" => {
+ "annotations" => { "Description" => description },
+ "creationTimestamp" => "2019-10-22T21:19:20Z",
+ "labels" => { "service" => name }
+ },
+ "spec" => {
+ "container" => {
+ "env" => [{ "name" => "timestamp", "value" => "2019-10-22 21:19:20" }],
+ "image" => "image_name",
+ "name" => "",
+ "resources" => {}
+ },
+ "timeoutSeconds" => 300
+ }
+ }
+ }
+ }
},
"status" => {
- "url" => "http://#{name}.#{namespace}.#{domain}",
"address" => {
- "url" => "#{name}.#{namespace}.svc.cluster.local"
+ "hostname" => "#{name}.#{namespace}.svc.cluster.local",
+ "url" => "http://#{name}.#{namespace}.svc.cluster.local"
},
- "latestCreatedRevisionName" => "#{name}-00002",
- "latestReadyRevisionName" => "#{name}-00002",
- "observedGeneration" => 2
- }
- }
- end
-
- def knative_05_service(name: "kubetest", namespace: "default", domain: "example.com")
- {
- "metadata" => {
- "creationTimestamp" => "2018-11-21T06:16:33Z",
- "name" => name,
- "namespace" => namespace,
- "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}"
- },
- "spec" => {
- "generation" => 2
- },
- "status" => {
+ "conditions" =>
+ [{ "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "ConfigurationsReady" },
+ { "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "Ready" },
+ { "lastTransitionTime" => "2019-10-22T21:20:25Z", "status" => "True", "type" => "RoutesReady" }],
"domain" => "#{name}.#{namespace}.#{domain}",
"domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
- "latestCreatedRevisionName" => "#{name}-00002",
- "latestReadyRevisionName" => "#{name}-00002",
- "observedGeneration" => 2
- }
- }
- end
-
- def kube_service_full(name: "kubetest", namespace: "kube-ns", domain: "example.com")
- {
- "metadata" => {
- "creationTimestamp" => "2018-11-21T06:16:33Z",
- "name" => name,
- "namespace" => namespace,
- "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
- "annotation" => {
- "description" => "This is a test description"
- }
+ "latestCreatedRevisionName" => "#{name}-bskx6",
+ "latestReadyRevisionName" => "#{name}-bskx6",
+ "observedGeneration" => 1,
+ "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-bskx6" }],
+ "url" => "http://#{name}.#{namespace}.#{domain}"
},
+ "environment_scope" => environment,
+ "cluster_id" => 9,
+ "podcount" => 0 }
+ end
+
+ # noinspection RubyStringKeysInHashInspection
+ def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production')
+ { "apiVersion" => "serving.knative.dev/v1alpha1",
+ "kind" => "Service",
+ "metadata" =>
+ { "annotations" =>
+ { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account",
+ "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" },
+ "creationTimestamp" => "2019-10-22T21:19:13Z",
+ "generation" => 1,
+ "labels" => { "service" => name },
+ "name" => name,
+ "namespace" => namespace,
+ "resourceVersion" => "289726",
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
+ "uid" => "988349fa-f511-11e9-9ea1-42010a80005e" },
"spec" => {
- "generation" => 2,
- "build" => {
- "template" => "go-1.10.3"
+ "template" => {
+ "metadata" => {
+ "annotations" => { "Description" => description },
+ "creationTimestamp" => "2019-10-22T21:19:12Z",
+ "labels" => { "service" => name }
+ },
+ "spec" => {
+ "containers" => [{
+ "env" =>
+ [{ "name" => "timestamp", "value" => "2019-10-22 21:19:12" }],
+ "image" => "image_name",
+ "name" => "user-container",
+ "resources" => {}
+ }],
+ "timeoutSeconds" => 300
+ }
+ },
+ "traffic" => [{ "latestRevision" => true, "percent" => 100 }]
+ },
+ "status" =>
+ { "address" => { "url" => "http://#{name}.#{namespace}.svc.cluster.local" },
+ "conditions" =>
+ [{ "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "ConfigurationsReady" },
+ { "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "Ready" },
+ { "lastTransitionTime" => "2019-10-22T21:20:15Z", "status" => "True", "type" => "RoutesReady" }],
+ "latestCreatedRevisionName" => "#{name}-92tsj",
+ "latestReadyRevisionName" => "#{name}-92tsj",
+ "observedGeneration" => 1,
+ "traffic" => [{ "latestRevision" => true, "percent" => 100, "revisionName" => "#{name}-92tsj" }],
+ "url" => "http://#{name}.#{namespace}.#{domain}" },
+ "environment_scope" => environment,
+ "cluster_id" => 5,
+ "podcount" => 0 }
+ end
+
+ # noinspection RubyStringKeysInHashInspection
+ def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production')
+ { "apiVersion" => "serving.knative.dev/v1alpha1",
+ "kind" => "Service",
+ "metadata" =>
+ { "annotations" =>
+ { "serving.knative.dev/creator" => "system:serviceaccount:#{namespace}:#{namespace}-service-account",
+ "serving.knative.dev/lastModifier" => "system:serviceaccount:#{namespace}:#{namespace}-service-account" },
+ "creationTimestamp" => "2019-10-22T21:19:19Z",
+ "generation" => 1,
+ "labels" => { "service" => name },
+ "name" => name,
+ "namespace" => namespace,
+ "resourceVersion" => "330390",
+ "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}",
+ "uid" => "9c710da6-f511-11e9-9ba0-42010a800161" },
+ "spec" => {
+ "runLatest" => {
+ "configuration" => {
+ "revisionTemplate" => {
+ "metadata" => {
+ "annotations" => { "Description" => description },
+ "creationTimestamp" => "2019-10-22T21:19:19Z",
+ "labels" => { "service" => name }
+ },
+ "spec" => {
+ "container" => {
+ "env" => [{ "name" => "timestamp", "value" => "2019-10-22 21:19:19" }],
+ "image" => "image_name",
+ "name" => "",
+ "resources" => { "requests" => { "cpu" => "400m" } }
+ },
+ "timeoutSeconds" => 300
+ }
+ }
+ }
}
},
- "status" => {
- "url" => "http://#{name}.#{namespace}.#{domain}",
- "address" => {
- "url" => "#{name}.#{namespace}.svc.cluster.local"
- },
- "latestCreatedRevisionName" => "#{name}-00002",
- "latestReadyRevisionName" => "#{name}-00002",
- "observedGeneration" => 2
- }
- }
+ "status" =>
+ { "address" => { "hostname" => "#{name}.#{namespace}.svc.cluster.local" },
+ "conditions" =>
+ [{ "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "ConfigurationsReady" },
+ { "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "Ready" },
+ { "lastTransitionTime" => "2019-10-22T21:20:24Z", "status" => "True", "type" => "RoutesReady" }],
+ "domain" => "#{name}.#{namespace}.#{domain}",
+ "domainInternal" => "#{name}.#{namespace}.svc.cluster.local",
+ "latestCreatedRevisionName" => "#{name}-58qgr",
+ "latestReadyRevisionName" => "#{name}-58qgr",
+ "observedGeneration" => 1,
+ "traffic" => [{ "percent" => 100, "revisionName" => "#{name}-58qgr" }] },
+ "environment_scope" => environment,
+ "cluster_id" => 8,
+ "podcount" => 0 }
end
def kube_terminals(service, pod)
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 7d5896e4eeb..1d42f26ad3e 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -53,7 +53,7 @@ module LoginHelpers
fill_in 'password', with: user.password
- click_button 'Enter admin mode'
+ click_button 'Enter Admin Mode'
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
index 656b3e196ba..3ad19cd3da0 100644
--- a/spec/support/helpers/smime_helper.rb
+++ b/spec/support/helpers/smime_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SmimeHelper
include OpenSSL
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
index ed868e22c6e..7a5a188ab4d 100644
--- a/spec/support/helpers/stub_experiments.rb
+++ b/spec/support/helpers/stub_experiments.rb
@@ -9,7 +9,19 @@ module StubExperiments
# - `stub_experiment(signup_flow: false)` ... Disable `signup_flow` experiment globally.
def stub_experiment(experiments)
experiments.each do |experiment_key, enabled|
- allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key, any_args) { enabled }
+ allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key) { enabled }
+ end
+ end
+
+ # Stub Experiment for user with `key: true/false`
+ #
+ # @param [Hash] experiment where key is feature name and value is boolean whether enabled or not.
+ #
+ # Examples
+ # - `stub_experiment_for_user(signup_flow: false)` ... Disable `signup_flow` experiment for user.
+ def stub_experiment_for_user(experiments)
+ experiments.each do |experiment_key, enabled|
+ allow(Gitlab::Experimentation).to receive(:enabled_for_user?).with(experiment_key, anything) { enabled }
end
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index e3dde888277..fe343da7838 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -18,8 +18,13 @@ module StubGitlabCalls
stub_ci_pipeline_yaml_file(gitlab_ci_yaml)
end
- def stub_ci_pipeline_yaml_file(ci_yaml)
- allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
+ def stub_ci_pipeline_yaml_file(ci_yaml_content)
+ allow_any_instance_of(Repository).to receive(:gitlab_ci_yml_for).and_return(ci_yaml_content)
+
+ # Ensure we don't hit auto-devops when config not found in repository
+ unless ci_yaml_content
+ allow_any_instance_of(Project).to receive(:auto_devops_enabled?).and_return(false)
+ end
end
def stub_pipeline_modified_paths(pipeline, modified_paths)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index a409dd2ef26..6a23875f103 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -148,8 +148,6 @@ module TestEnv
end
def setup_gitaly
- socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
- gitaly_dir = File.dirname(socket_path)
install_gitaly_args = [gitaly_dir, repos_path, gitaly_url].compact.join(',')
component_timed_setup('Gitaly',
@@ -162,8 +160,16 @@ module TestEnv
end
end
+ def gitaly_socket_path
+ Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
+ end
+
+ def gitaly_dir
+ File.dirname(gitaly_socket_path)
+ end
+
def start_gitaly(gitaly_dir)
- if ENV['CI'].present?
+ if ci?
# Gitaly has been spawned outside this process already
return
end
@@ -172,8 +178,13 @@ module TestEnv
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
Bundler.with_original_env do
- raise "gitaly spawn failed" unless system(spawn_script)
+ unless system(spawn_script)
+ message = 'gitaly spawn failed'
+ message += " (try `rm -rf #{gitaly_dir}` ?)" unless ci?
+ raise message
+ end
end
+
@gitaly_pid = Integer(File.read('tmp/tests/gitaly.pid'))
Kernel.at_exit { stop_gitaly }
@@ -243,6 +254,22 @@ module TestEnv
FileUtils.chmod_R 0755, target_repo_path
end
+ def rm_storage_dir(storage, dir)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
+ target_repo_refs_path = File.join(repos_path, dir)
+ FileUtils.remove_dir(target_repo_refs_path)
+ end
+ rescue Errno::ENOENT
+ end
+
+ def storage_dir_exists?(storage, dir)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path
+ File.exist?(File.join(repos_path, dir))
+ end
+ end
+
def create_bare_repository(path)
FileUtils.mkdir_p(path)
@@ -370,7 +397,7 @@ module TestEnv
ensure_component_dir_name_is_correct!(component, install_dir)
# On CI, once installed, components never need update
- return if File.exist?(install_dir) && ENV['CI']
+ return if File.exist?(install_dir) && ci?
if component_needs_update?(install_dir, version)
# Cleanup the component entirely to ensure we start fresh
@@ -391,6 +418,10 @@ module TestEnv
puts " #{component} set up in #{Time.now - start} seconds...\n"
end
+ def ci?
+ ENV['CI'].present?
+ end
+
def ensure_component_dir_name_is_correct!(component, path)
actual_component_dir_name = File.basename(path)
expected_component_dir_name = component.parameterize
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index ac6840dbcfc..4e149c9fa54 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -8,5 +8,12 @@ module ImportExport
File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
end
+
+ def setup_import_export_config(name, prefix = nil)
+ export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
+ export_path = File.join(*export_path)
+
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path }
+ end
end
end
diff --git a/spec/support/matchers/access_matchers_for_request.rb b/spec/support/matchers/access_matchers_for_request.rb
new file mode 100644
index 00000000000..9b80bf8562c
--- /dev/null
+++ b/spec/support/matchers/access_matchers_for_request.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+# AccessMatchersForRequest
+#
+# Matchers to test the access permissions for requests specs (most useful for API tests).
+module AccessMatchersForRequest
+ extend RSpec::Matchers::DSL
+ include AccessMatchersHelpers
+
+ EXPECTED_STATUS_CODES_ALLOWED = [200, 201, 204, 302, 304].freeze
+ EXPECTED_STATUS_CODES_DENIED = [401, 403, 404].freeze
+
+ def description_for(role, type, expected, result)
+ "be #{type} for #{role} role. Expected status code: any of #{expected.join(', ')} Got: #{result}"
+ end
+
+ matcher :be_allowed_for do |role|
+ match do |action|
+ # methods called in this and negated block are being run in context of ExampleGroup
+ # (not matcher) instance so we have to pass data via local vars
+
+ run_matcher(action, role, @membership, @owned_objects)
+
+ EXPECTED_STATUS_CODES_ALLOWED.include?(response.status)
+ end
+
+ match_when_negated do |action|
+ run_matcher(action, role, @membership, @owned_objects)
+
+ EXPECTED_STATUS_CODES_DENIED.include?(response.status)
+ end
+
+ chain :of do |membership|
+ @membership = membership
+ end
+
+ chain :own do |*owned_objects|
+ @owned_objects = owned_objects
+ end
+
+ failure_message do
+ "expected this action to #{description_for(role, 'allowed', EXPECTED_STATUS_CODES_ALLOWED, response.status)}"
+ end
+
+ failure_message_when_negated do
+ "expected this action to #{description_for(role, 'denied', EXPECTED_STATUS_CODES_DENIED, response.status)}"
+ end
+
+ supports_block_expectations
+ end
+
+ RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
+end
diff --git a/spec/support/matchers/access_matchers_generic.rb b/spec/support/matchers/access_matchers_generic.rb
new file mode 100644
index 00000000000..13955750f4f
--- /dev/null
+++ b/spec/support/matchers/access_matchers_generic.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# AccessMatchersGeneric
+#
+# Matchers to test the access permissions for service classes or other generic pieces of business logic.
+module AccessMatchersGeneric
+ extend RSpec::Matchers::DSL
+ include AccessMatchersHelpers
+
+ ERROR_CLASS = Gitlab::Access::AccessDeniedError
+
+ def error_message(error)
+ str = error.class.name
+ str += ": #{error.message}" if error.message != error.class.name
+ str
+ end
+
+ def error_expectation_message(allowed, error)
+ if allowed
+ "Expected to raise nothing but #{error_message(error)} was raised."
+ else
+ "Expected to raise #{ERROR_CLASS} but nothing was raised."
+ end
+ end
+
+ def description_for(role, type, error)
+ allowed = type == 'allowed'
+ "be #{type} for #{role} role. #{error_expectation_message(allowed, error)}"
+ end
+
+ matcher :be_allowed_for do |role|
+ match do |action|
+ # methods called in this and negated block are being run in context of ExampleGroup
+ # (not matcher) instance so we have to pass data via local vars
+
+ run_matcher(action, role, @membership, @owned_objects) do |action|
+ action.call
+ rescue => e
+ @error = e
+ raise unless e.is_a?(ERROR_CLASS)
+ end
+
+ @error.nil?
+ end
+
+ chain :of do |membership|
+ @membership = membership
+ end
+
+ chain :own do |*owned_objects|
+ @owned_objects = owned_objects
+ end
+
+ failure_message do
+ "expected this action to #{description_for(role, 'allowed', @error)}"
+ end
+
+ failure_message_when_negated do
+ "expected this action to #{description_for(role, 'denied', @error)}"
+ end
+
+ supports_block_expectations
+ end
+
+ RSpec::Matchers.define_negated_matcher :be_denied_for, :be_allowed_for
+end
diff --git a/spec/support/matchers/db_schema_matchers.rb b/spec/support/matchers/db_schema_matchers.rb
new file mode 100644
index 00000000000..55843b7bb49
--- /dev/null
+++ b/spec/support/matchers/db_schema_matchers.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+EXPECTED_SMALLINT_LIMIT = 2
+
+RSpec::Matchers.define :use_smallint_for_enums do |enums|
+ match do |actual|
+ @failing_enums = enums.select do |enum|
+ enum_type = actual.type_for_attribute(enum)
+ actual_limit = enum_type.send(:subtype).limit
+ actual_limit != EXPECTED_SMALLINT_LIMIT
+ end
+ @failing_enums.empty?
+ end
+
+ failure_message do
+ <<~FAILURE_MESSAGE
+ Expected #{actual.name} enums: #{failing_enums.join(', ')} to use the smallint type.
+
+ The smallint type is 2 bytes which is more than sufficient for an enum.
+ Using the smallint type would help us save space in the database.
+ To fix this, please add `limit: 2` in the migration file, for example:
+
+ def change
+ add_column :ci_job_artifacts, :file_format, :integer, limit: 2
+ end
+ FAILURE_MESSAGE
+ end
+
+ def failing_enums
+ @failing_enums ||= []
+ end
+end
diff --git a/spec/support/prepare-gitlab-git-test-for-commit b/spec/support/prepare-gitlab-git-test-for-commit
index d08e3ba5481..77c7f309312 100755
--- a/spec/support/prepare-gitlab-git-test-for-commit
+++ b/spec/support/prepare-gitlab-git-test-for-commit
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
abort unless [
system('spec/support/generate-seed-repo-rb', out: 'spec/support/helpers/seed_repo.rb'),
diff --git a/spec/support/shared_examples/ci/auto_merge_merge_requests_examples.rb b/spec/support/shared_examples/ci/auto_merge_merge_requests_examples.rb
new file mode 100644
index 00000000000..c11448ffe0f
--- /dev/null
+++ b/spec/support/shared_examples/ci/auto_merge_merge_requests_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+shared_examples 'aborted merge requests for MWPS' do
+ let(:aborted_message) do
+ /aborted the automatic merge because target branch was updated/
+ end
+
+ it 'aborts auto_merge' do
+ expect(merge_request.auto_merge_enabled?).to be_falsey
+ expect(merge_request.notes.last.note).to match(aborted_message)
+ end
+
+ it 'removes merge_user' do
+ expect(merge_request.merge_user).to be_nil
+ end
+
+ it 'does not add todos for merge user' do
+ expect(user.todos.for_target(merge_request)).to be_empty
+ end
+
+ it 'adds todos for merge author' do
+ expect(author.todos.for_target(merge_request)).to be_present.and be_all(&:pending?)
+ end
+end
+
+shared_examples 'maintained merge requests for MWPS' do
+ it 'does not cancel auto merge' do
+ expect(merge_request.auto_merge_enabled?).to be_truthy
+ expect(merge_request.notes).to be_empty
+ end
+
+ it 'does not change merge_user' do
+ expect(merge_request.merge_user).to eq(user)
+ end
+
+ it 'does not add todos' do
+ expect(author.todos.for_target(merge_request)).to be_empty
+ expect(user.todos.for_target(merge_request)).to be_empty
+ end
+end
diff --git a/spec/support/shared_examples/container_repositories_shared_examples.rb b/spec/support/shared_examples/container_repositories_shared_examples.rb
index 946b130fca2..b4f45ba9a00 100644
--- a/spec/support/shared_examples/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/container_repositories_shared_examples.rb
@@ -56,3 +56,11 @@ shared_examples 'returns repositories for allowed users' do |user_type, scope|
end
end
end
+
+shared_examples 'a gitlab tracking event' do |category, action|
+ it "creates a gitlab tracking event #{action}" do
+ expect(Gitlab::Tracking).to receive(:event).with(category, action, {})
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
index dce1dbe1cd1..028b8da94a6 100644
--- a/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
+++ b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
shared_examples_for 'cycle analytics event' do
- let(:instance) { described_class.new({}) }
+ let(:params) { {} }
+ let(:instance) { described_class.new(params) }
it { expect(described_class.name).to be_a_kind_of(String) }
it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
diff --git a/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb
index afa035d039a..c781f72ff11 100644
--- a/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb
+++ b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb
@@ -10,6 +10,11 @@ shared_examples_for 'cycle analytics stage' do
}
end
+ describe 'associations' do
+ it { is_expected.to belong_to(:end_event_label) }
+ it { is_expected.to belong_to(:start_event_label) }
+ end
+
describe 'validation' do
it 'is valid' do
expect(described_class.new(valid_params)).to be_valid
@@ -18,22 +23,22 @@ shared_examples_for 'cycle analytics stage' do
it 'validates presence of parent' do
stage = described_class.new(valid_params.except(:parent))
- expect(stage).not_to be_valid
- expect(stage.errors.details[parent_name]).to eq([{ error: :blank }])
+ expect(stage).to be_invalid
+ expect(stage.errors[parent_name]).to include("can't be blank")
end
it 'validates presence of start_event_identifier' do
stage = described_class.new(valid_params.except(:start_event_identifier))
- expect(stage).not_to be_valid
- expect(stage.errors.details[:start_event_identifier]).to eq([{ error: :blank }])
+ expect(stage).to be_invalid
+ expect(stage.errors[:start_event_identifier]).to include("can't be blank")
end
it 'validates presence of end_event_identifier' do
stage = described_class.new(valid_params.except(:end_event_identifier))
- expect(stage).not_to be_valid
- expect(stage.errors.details[:end_event_identifier]).to eq([{ error: :blank }])
+ expect(stage).to be_invalid
+ expect(stage.errors[:end_event_identifier]).to include("can't be blank")
end
it 'is invalid when end_event is not allowed for the given start_event' do
@@ -43,8 +48,8 @@ shared_examples_for 'cycle analytics stage' do
)
stage = described_class.new(invalid_params)
- expect(stage).not_to be_valid
- expect(stage.errors.details[:end_event]).to eq([{ error: :not_allowed_for_the_given_start_event }])
+ expect(stage).to be_invalid
+ expect(stage.errors[:end_event]).to include(s_('CycleAnalytics|not allowed for the given start event'))
end
context 'disallows default stage names when creating custom stage' do
@@ -105,3 +110,119 @@ shared_examples_for 'cycle analytics stage' do
end
end
end
+
+shared_examples_for 'cycle analytics label based stage' do
+ context 'when creating label based event' do
+ context 'when the label id is not passed' do
+ it 'returns validation error when `start_event_label_id` is missing' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent,
+ start_event_identifier: :issue_label_added,
+ end_event_identifier: :issue_closed
+ })
+
+ expect(stage).to be_invalid
+ expect(stage.errors[:start_event_label]).to include("can't be blank")
+ end
+
+ it 'returns validation error when `end_event_label_id` is missing' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent,
+ start_event_identifier: :issue_closed,
+ end_event_identifier: :issue_label_added
+ })
+
+ expect(stage).to be_invalid
+ expect(stage.errors[:end_event_label]).to include("can't be blank")
+ end
+ end
+
+ context 'when group label is defined on the root group' do
+ it 'succeeds' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent,
+ start_event_identifier: :issue_label_added,
+ start_event_label: group_label,
+ end_event_identifier: :issue_closed
+ })
+
+ expect(stage).to be_valid
+ end
+ end
+
+ context 'when subgroup is given' do
+ it 'succeeds' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent_in_subgroup,
+ start_event_identifier: :issue_label_added,
+ start_event_label: group_label,
+ end_event_identifier: :issue_closed
+ })
+
+ expect(stage).to be_valid
+ end
+ end
+
+ context 'when label is defined for a different group' do
+ let(:error_message) { s_('CycleAnalyticsStage|is not available for the selected group') }
+
+ it 'returns validation for `start_event_label`' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent_outside_of_group_label_scope,
+ start_event_identifier: :issue_label_added,
+ start_event_label: group_label,
+ end_event_identifier: :issue_closed
+ })
+
+ expect(stage).to be_invalid
+ expect(stage.errors[:start_event_label]).to include(error_message)
+ end
+
+ it 'returns validation for `end_event_label`' do
+ stage = described_class.new({
+ name: 'My Stage',
+ parent: parent_outside_of_group_label_scope,
+ start_event_identifier: :issue_closed,
+ end_event_identifier: :issue_label_added,
+ end_event_label: group_label
+ })
+
+ expect(stage).to be_invalid
+ expect(stage.errors[:end_event_label]).to include(error_message)
+ end
+ end
+
+ context 'when `ProjectLabel is given' do
+ let_it_be(:label) { create(:label) }
+
+ it 'raises error when `ProjectLabel` is given for `start_event_label`' do
+ params = {
+ name: 'My Stage',
+ parent: parent,
+ start_event_identifier: :issue_label_added,
+ start_event_label: label,
+ end_event_identifier: :issue_closed
+ }
+
+ expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ end
+
+ it 'raises error when `ProjectLabel` is given for `end_event_label`' do
+ params = {
+ name: 'My Stage',
+ parent: parent,
+ start_event_identifier: :issue_closed,
+ end_event_identifier: :issue_label_added,
+ end_event_label: label
+ }
+
+ expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb b/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb
index 920fcbde483..21c32c9c04a 100644
--- a/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb
+++ b/spec/support/shared_examples/features/archive_download_buttons_shared_examples.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
shared_examples 'archive download buttons' do
- let(:formats) { %w(zip tar.gz tar.bz2 tar) }
let(:path_to_visit) { project_path(project) }
let(:ref) { project.default_branch }
@@ -13,7 +12,7 @@ shared_examples 'archive download buttons' do
context 'private project' do
it 'shows archive download buttons with external storage URL prepended and user token appended to their href' do
- formats.each do |format|
+ Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
@@ -28,7 +27,7 @@ shared_examples 'archive download buttons' do
let(:project) { create(:project, :repository, :public) }
it 'shows archive download buttons with external storage URL prepended to their href' do
- formats.each do |format|
+ Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
uri = URI('https://cdn.gitlab.com')
uri.path = path
@@ -45,7 +44,7 @@ shared_examples 'archive download buttons' do
end
it 'shows default archive download buttons' do
- formats.each do |format|
+ Gitlab::Workhorse::ARCHIVE_FORMATS.each do |format|
path = archive_path(project, ref, format)
expect(page).to have_link format, href: path
diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb
index 984a06ccd1a..f4b28b94090 100644
--- a/spec/support/shared_examples/file_finder.rb
+++ b/spec/support/shared_examples/file_finder.rb
@@ -4,19 +4,19 @@ shared_examples 'file finder' do
let(:query) { 'files' }
let(:search_results) { subject.find(query) }
- it 'finds by name' do
- blob = search_results.find { |blob| blob.filename == expected_file_by_name }
+ it 'finds by path' do
+ blob = search_results.find { |blob| blob.path == expected_file_by_path }
- expect(blob.filename).to eq(expected_file_by_name)
+ expect(blob.path).to eq(expected_file_by_path)
expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
it 'finds by content' do
- blob = search_results.find { |blob| blob.filename == expected_file_by_content }
+ blob = search_results.find { |blob| blob.path == expected_file_by_content }
- expect(blob.filename).to eq(expected_file_by_content)
+ expect(blob.path).to eq(expected_file_by_content)
expect(blob).to be_a(Gitlab::Search::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
diff --git a/spec/support/shared_examples/graphql/connection_paged_nodes.rb b/spec/support/shared_examples/graphql/connection_paged_nodes.rb
new file mode 100644
index 00000000000..830d2d2d4b1
--- /dev/null
+++ b/spec/support/shared_examples/graphql/connection_paged_nodes.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'connection with paged nodes' do
+ it 'returns the collection limited to max page size' do
+ expect(paged_nodes.size).to eq(3)
+ end
+
+ it 'is a loaded memoized array' do
+ expect(paged_nodes).to be_an(Array)
+ expect(paged_nodes.object_id).to eq(paged_nodes.object_id)
+ end
+
+ context 'when `first` is passed' do
+ let(:arguments) { { first: 2 } }
+
+ it 'returns only the first elements' do
+ expect(paged_nodes).to contain_exactly(all_nodes.first, all_nodes.second)
+ end
+ end
+
+ context 'when `last` is passed' do
+ let(:arguments) { { last: 2 } }
+
+ it 'returns only the last elements' do
+ expect(paged_nodes).to contain_exactly(all_nodes[3], all_nodes[4])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/sort_enum_shared_examples.rb b/spec/support/shared_examples/graphql/sort_enum_shared_examples.rb
new file mode 100644
index 00000000000..becea9bcae1
--- /dev/null
+++ b/spec/support/shared_examples/graphql/sort_enum_shared_examples.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'common sort values' do
+ it 'exposes all the existing common sort values' do
+ expect(described_class.values.keys).to include(*%w[updated_desc updated_asc created_desc created_asc])
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb
new file mode 100644
index 00000000000..b0b3e46332d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/ci/config/entry/key_validations_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'key entry validations' do |config_name|
+ shared_examples 'key with slash' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+
+ it 'reports errors with config value' do
+ expect(entry.errors).to include "#{config_name} config cannot contain the \"/\" character"
+ end
+ end
+
+ shared_examples 'key with only dots' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+
+ it 'reports errors with config value' do
+ expect(entry.errors).to include "#{config_name} config cannot be \".\" or \"..\""
+ end
+ end
+
+ context 'when entry value contains slash' do
+ let(:config) { 'key/with/some/slashes' }
+
+ it_behaves_like 'key with slash'
+ end
+
+ context 'when entry value contains URI encoded slash (%2F)' do
+ let(:config) { 'key%2Fwith%2Fsome%2Fslashes' }
+
+ it_behaves_like 'key with slash'
+ end
+
+ context 'when entry value is a dot' do
+ let(:config) { '.' }
+
+ it_behaves_like 'key with only dots'
+ end
+
+ context 'when entry value is two dots' do
+ let(:config) { '..' }
+
+ it_behaves_like 'key with only dots'
+ end
+
+ context 'when entry value is a URI encoded dot (%2E)' do
+ let(:config) { '%2e' }
+
+ it_behaves_like 'key with only dots'
+ end
+
+ context 'when entry value is two URI encoded dots (%2E)' do
+ let(:config) { '%2E%2e' }
+
+ it_behaves_like 'key with only dots'
+ end
+
+ context 'when entry value is one dot and one URI encoded dot' do
+ let(:config) { '.%2e' }
+
+ it_behaves_like 'key with only dots'
+ end
+
+ context 'when key is a string' do
+ let(:config) { 'test' }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq 'test'
+ end
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb
new file mode 100644
index 00000000000..556d81133bc
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'with inheritable CI config' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:ignored_inheritable_columns) { [] }
+
+ it 'does prepend an Inheritable mixin' do
+ expect(described_class).to include_module(Gitlab::Config::Entry::Inheritable)
+ end
+
+ it 'all inheritable entries are covered' do
+ inheritable_entries = inheritable_class.nodes.keys
+ entries = described_class.nodes.keys
+
+ expect(entries + ignored_inheritable_columns).to include(
+ *inheritable_entries)
+ end
+
+ it 'all entries do have inherit flag' do
+ without_inherit_flag = described_class.nodes.map do |key, factory|
+ key if factory.inherit.nil?
+ end.compact
+
+ expect(without_inherit_flag).to be_empty
+ end
+
+ context 'for non-inheritable entries' do
+ where(:entry_key) do
+ described_class.nodes.map do |key, factory|
+ [key] unless factory.inherit
+ end.compact
+ end
+
+ with_them do
+ it 'inheritable_class does not define entry' do
+ expect(inheritable_class.nodes).not_to include(entry_key)
+ end
+ end
+ end
+
+ context 'for inheritable entries' do
+ where(:entry_key, :entry_class) do
+ described_class.nodes.map do |key, factory|
+ [key, factory.entry_class] if factory.inherit
+ end.compact
+ end
+
+ with_them do
+ let(:specified) { double('deps_specified', 'specified?' => true, value: 'specified') }
+ let(:unspecified) { double('unspecified', 'specified?' => false) }
+ let(:inheritable) { double(inheritable_key, '[]' => unspecified) }
+
+ let(:deps) do
+ if inheritable_key
+ double('deps', inheritable_key => inheritable, '[]' => unspecified)
+ else
+ inheritable
+ end
+ end
+
+ it 'inheritable_class does define entry' do
+ expect(inheritable_class.nodes).to include(entry_key)
+ expect(inheritable_class.nodes[entry_key].entry_class).to eq(entry_class)
+ end
+
+ context 'when is specified' do
+ it 'does inherit value' do
+ expect(inheritable).to receive('[]').with(entry_key).and_return(specified)
+
+ entry.compose!(deps)
+
+ expect(entry[entry_key]).to eq(specified)
+ end
+
+ context 'when entry is specified' do
+ let(:entry_specified) do
+ double('entry_specified', 'specified?' => true, value: 'specified', errors: [])
+ end
+
+ it 'does not inherit value' do
+ entry.send(:entries)[entry_key] = entry_specified
+
+ allow(inheritable).to receive('[]').with(entry_key).and_return(specified)
+
+ expect do
+ # we ignore exceptions as `#overwrite_entry`
+ # can raise exception on duplicates
+ entry.compose!(deps) rescue described_class::InheritError
+ end.not_to change { entry[entry_key] }
+ end
+ end
+ end
+
+ context 'when inheritable does not specify' do
+ it 'does not inherit value' do
+ entry.compose!(deps)
+
+ expect(entry[entry_key]).to be_a(
+ Gitlab::Config::Entry::Undefined)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb b/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb
new file mode 100644
index 00000000000..80120629a32
--- /dev/null
+++ b/spec/support/shared_examples/merge_requests_rendering_a_single_diff_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# This pending test can be removed when `single_mr_diff_view` is enabled by default
+# disabling the feature flag above is then not needed anymore.
+RSpec.shared_examples 'rendering a single diff version' do |attribute|
+ pending 'allows editing diff settings single_mr_diff_view is enabled' do
+ project = create(:project, :repository)
+ user = project.creator
+ merge_request = create(:merge_request, source_project: project)
+ stub_feature_flags(single_mr_diff_view: true)
+ sign_in(user)
+
+ visit(diffs_project_merge_request_path(project, merge_request))
+
+ expect(page).to have_selector('.js-show-diff-settings')
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
index 7ddb3b11c85..1c8c19acc74 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -8,10 +8,6 @@ shared_examples 'cluster application helm specs' do |application_name|
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
- it 'has the application name' do
- expect(subject.name).to eq(application.name)
- end
-
it 'has files' do
expect(subject.files).to eq(application.files)
end
diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb
index 4978a403324..4978a403324 100644
--- a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb
diff --git a/spec/support/shared_examples/models/concerns/redactable_shared_examples.rb b/spec/support/shared_examples/models/concerns/redactable_shared_examples.rb
new file mode 100644
index 00000000000..c5c14901268
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/redactable_shared_examples.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+shared_examples 'model with redactable field' do
+ it 'redacts unsubscribe token' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expect(model[field]).to eq 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ end
+
+ it 'ignores not hexadecimal tokens' do
+ text = 'some text /sent_notifications/token/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'ignores not matching texts' do
+ text = 'some text /sent_notifications/.*/unsubscribe more text'
+ model[field] = text
+
+ model.save!
+
+ expect(model[field]).to eq text
+ end
+
+ it 'redacts the field when saving the model before creating markdown cache' do
+ model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text'
+
+ model.save!
+
+ expected = 'some text /sent_notifications/REDACTED/unsubscribe more text'
+ expect(model[field]).to eq expected
+ expect(model["#{field}_html"]).to eq "<p dir=\"auto\">#{expected}</p>"
+ end
+end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index 822836c771e..3d622ba8195 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -18,7 +18,7 @@ shared_examples_for 'model with uploads' do |supports_fileuploads|
end
end
- context 'with not mounted uploads', :sidekiq, skip: !supports_fileuploads do
+ context 'with not mounted uploads', :sidekiq_might_not_need_inline, skip: !supports_fileuploads do
context 'with local files' do
let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: model_object) }
diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
index b4a8e3fca4d..92bbc4abe77 100644
--- a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
@@ -2,22 +2,19 @@
shared_examples 'zoom quick actions' do
let(:zoom_link) { 'https://zoom.us/j/123456789' }
+ let(:existing_zoom_link) { 'https://zoom.us/j/123456780' }
let(:invalid_zoom_link) { 'https://invalid-zoom' }
- before do
- issue.update!(description: description)
- end
-
describe '/zoom' do
shared_examples 'skip silently' do
- it 'skip addition silently' do
+ it 'skips addition silently' do
add_note("/zoom #{zoom_link}")
wait_for_requests
expect(page).not_to have_content('Zoom meeting added')
expect(page).not_to have_content('Failed to add a Zoom meeting')
- expect(issue.reload.description).to eq(description)
+ expect(ZoomMeeting.canonical_meeting_url(issue.reload)).not_to eq(zoom_link)
end
end
@@ -28,13 +25,11 @@ shared_examples 'zoom quick actions' do
wait_for_requests
expect(page).to have_content('Zoom meeting added')
- expect(issue.reload.description).to end_with(zoom_link)
+ expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to eq(zoom_link)
end
end
- context 'without issue description' do
- let(:description) { nil }
-
+ context 'without zoom_meetings' do
include_examples 'success'
it 'cannot add invalid zoom link' do
@@ -47,14 +42,18 @@ shared_examples 'zoom quick actions' do
end
end
- context 'with Zoom link not at the end of the issue description' do
- let(:description) { "A link #{zoom_link} not at the end" }
+ context 'with "removed" zoom meeting' do
+ before do
+ create(:zoom_meeting, issue_status: :removed, url: existing_zoom_link, issue: issue)
+ end
include_examples 'success'
end
- context 'with Zoom link at end of the issue description' do
- let(:description) { "Text\n#{zoom_link}" }
+ context 'with "added" zoom meeting' do
+ before do
+ create(:zoom_meeting, issue_status: :added, url: existing_zoom_link, issue: issue)
+ end
include_examples 'skip silently'
end
@@ -62,19 +61,19 @@ shared_examples 'zoom quick actions' do
describe '/remove_zoom' do
shared_examples 'skip silently' do
- it 'skip removal silently' do
+ it 'skips removal silently' do
add_note('/remove_zoom')
wait_for_requests
expect(page).not_to have_content('Zoom meeting removed')
expect(page).not_to have_content('Failed to remove a Zoom meeting')
- expect(issue.reload.description).to eq(description)
+ expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
end
end
- context 'with Zoom link in the description' do
- let(:description) { "Text with #{zoom_link}\n\n\n#{zoom_link}" }
+ context 'with added zoom meeting' do
+ let!(:added_zoom_meeting) { create(:zoom_meeting, url: zoom_link, issue: issue, issue_status: :added) }
it 'removes last Zoom link' do
add_note('/remove_zoom')
@@ -82,14 +81,8 @@ shared_examples 'zoom quick actions' do
wait_for_requests
expect(page).to have_content('Zoom meeting removed')
- expect(issue.reload.description).to eq("Text with #{zoom_link}")
+ expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
end
end
-
- context 'with a Zoom link not at the end of the description' do
- let(:description) { "A link #{zoom_link} not at the end" }
-
- include_examples 'skip silently'
- end
end
end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
index ac7c17915de..a77d729aa2c 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -7,7 +7,7 @@ shared_examples 'merge quick action' do
visit project_merge_request_path(project, merge_request)
end
- it 'merges the MR' do
+ it 'merges the MR', :sidekiq_might_not_need_inline do
add_note("/merge")
expect(page).to have_content 'Scheduled to merge this merge request when the pipeline succeeds.'
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
index a36bc2dc9b5..2a5a48f3054 100644
--- a/spec/support/shared_examples/requests/api/discussions.rb
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -117,6 +117,29 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_r
expect(response).to have_gitlab_http_status(401)
end
+ it 'tracks a Notes::CreateService event' do
+ expect(Gitlab::Tracking).to receive(:event) do |category, action, data|
+ expect(category).to eq('Notes::CreateService')
+ expect(action).to eq('execute')
+ expect(data[:label]).to eq('note')
+ expect(data[:value]).to be_an(Integer)
+ end
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), params: { body: 'hi!' }
+ end
+
+ context 'with notes_create_service_tracking feature flag disabled' do
+ before do
+ stub_feature_flags(notes_create_service_tracking: false)
+ end
+
+ it 'does not track any events' do
+ expect(Gitlab::Tracking).not_to receive(:event)
+
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' }
+ end
+ end
+
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb
index 354ae7288b1..4ce78d885bc 100644
--- a/spec/support/shared_examples/requests/api/notes.rb
+++ b/spec/support/shared_examples/requests/api/notes.rb
@@ -139,7 +139,7 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(401)
end
- it "creates an activity event when a note is created" do
+ it "creates an activity event when a note is created", :sidekiq_might_not_need_inline do
expect(Event).to receive(:create!)
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index a2e38cfc60b..c078e982e87 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -2,8 +2,9 @@
#
# Requires let variables:
# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths"
-# * get_args
-# * other_user_get_args
+# * request_method
+# * request_args
+# * other_user_request_args
# * requests_per_period
# * period_in_seconds
# * period
@@ -31,66 +32,66 @@ shared_examples_for 'rate-limited token-authenticated requests' do
it 'rejects requests over the rate limit' do
# At first, allow requests under the rate limit.
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
# the last straw
- expect_rejection { get(*get_args) }
+ expect_rejection { make_request(request_args) }
end
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
- expect_rejection { get(*get_args) }
+ expect_rejection { make_request(request_args) }
Timecop.travel(period.from_now) do
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
- expect_rejection { get(*get_args) }
+ expect_rejection { make_request(request_args) }
end
end
it 'counts requests from different users separately, even from the same IP' do
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
# would be over the limit if this wasn't a different user
- get(*other_user_get_args)
- expect(response).to have_http_status 200
+ make_request(other_user_request_args)
+ expect(response).not_to have_http_status 429
end
it 'counts all requests from the same user, even via different IPs' do
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).at_least(:once).and_return('1.2.3.4')
- expect_rejection { get(*get_args) }
+ expect_rejection { make_request(request_args) }
end
it 'logs RackAttack info into structured logs' do
requests_per_period.times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
arguments = {
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: get_args.first,
+ request_method: request_method,
+ path: request_args.first,
user_id: user.id,
username: user.username,
throttle_type: throttle_types[throttle_setting_prefix]
@@ -98,7 +99,7 @@ shared_examples_for 'rate-limited token-authenticated requests' do
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
- expect_rejection { get(*get_args) }
+ expect_rejection { make_request(request_args) }
end
end
@@ -110,17 +111,26 @@ shared_examples_for 'rate-limited token-authenticated requests' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get(*get_args)
- expect(response).to have_http_status 200
+ make_request(request_args)
+ expect(response).not_to have_http_status 429
end
end
end
+
+ def make_request(args)
+ if request_method == 'POST'
+ post(*args)
+ else
+ get(*args)
+ end
+ end
end
# Requires let variables:
# * throttle_setting_prefix: "throttle_authenticated_web" or "throttle_protected_paths"
# * user
# * url_that_requires_authentication
+# * request_method
# * requests_per_period
# * period_in_seconds
# * period
@@ -149,68 +159,68 @@ shared_examples_for 'rate-limited web authenticated requests' do
it 'rejects requests over the rate limit' do
# At first, allow requests under the rate limit.
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
# the last straw
- expect_rejection { get url_that_requires_authentication }
+ expect_rejection { request_authenticated_web_url }
end
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
- expect_rejection { get url_that_requires_authentication }
+ expect_rejection { request_authenticated_web_url }
Timecop.travel(period.from_now) do
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
- expect_rejection { get url_that_requires_authentication }
+ expect_rejection { request_authenticated_web_url }
end
end
it 'counts requests from different users separately, even from the same IP' do
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
# would be over the limit if this wasn't a different user
login_as(create(:user))
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
it 'counts all requests from the same user, even via different IPs' do
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
- expect_any_instance_of(Rack::Attack::Request).to receive(:ip).and_return('1.2.3.4')
+ expect_any_instance_of(Rack::Attack::Request).to receive(:ip).at_least(:once).and_return('1.2.3.4')
- expect_rejection { get url_that_requires_authentication }
+ expect_rejection { request_authenticated_web_url }
end
it 'logs RackAttack info into structured logs' do
requests_per_period.times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
arguments = {
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
- request_method: 'GET',
- path: '/dashboard/snippets',
+ request_method: request_method,
+ path: url_that_requires_authentication,
user_id: user.id,
username: user.username,
throttle_type: throttle_types[throttle_setting_prefix]
@@ -218,7 +228,7 @@ shared_examples_for 'rate-limited web authenticated requests' do
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
- get url_that_requires_authentication
+ request_authenticated_web_url
end
end
@@ -230,9 +240,17 @@ shared_examples_for 'rate-limited web authenticated requests' do
it 'allows requests over the rate limit' do
(1 + requests_per_period).times do
- get url_that_requires_authentication
- expect(response).to have_http_status 200
+ request_authenticated_web_url
+ expect(response).not_to have_http_status 429
end
end
end
+
+ def request_authenticated_web_url
+ if request_method == 'POST'
+ post url_that_requires_authentication
+ else
+ get url_that_requires_authentication
+ end
+ end
end
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
index 96cb71be737..d2c269c597c 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
@@ -31,14 +31,43 @@ shared_examples 'diff file entity' do
it 'exposes correct attributes' do
expect(subject).to include(:added_lines, :removed_lines,
- :context_lines_path, :highlighted_diff_lines,
- :parallel_diff_lines)
+ :context_lines_path)
end
it 'includes viewer' do
expect(subject[:viewer].with_indifferent_access)
.to match_schema('entities/diff_viewer')
end
+
+ context 'diff files' do
+ context 'when diff_view is parallel' do
+ let(:options) { { diff_view: :parallel } }
+
+ it 'contains only the parallel diff lines', :aggregate_failures do
+ expect(subject).to include(:parallel_diff_lines)
+ expect(subject).not_to include(:highlighted_diff_lines)
+ end
+ end
+
+ context 'when diff_view is parallel' do
+ let(:options) { { diff_view: :inline } }
+
+ it 'contains only the inline diff lines', :aggregate_failures do
+ expect(subject).not_to include(:parallel_diff_lines)
+ expect(subject).to include(:highlighted_diff_lines)
+ end
+ end
+
+ context 'when the `single_mr_diff_view` feature is disabled' do
+ before do
+ stub_feature_flags(single_mr_diff_view: false)
+ end
+
+ it 'contains both kinds of diffs' do
+ expect(subject).to include(:highlighted_diff_lines, :parallel_diff_lines)
+ end
+ end
+ end
end
shared_examples 'diff file discussion entity' do
diff --git a/spec/support/shared_examples/services/error_tracking_service_shared_examples.rb b/spec/support/shared_examples/services/error_tracking_service_shared_examples.rb
new file mode 100644
index 00000000000..83c6d89e560
--- /dev/null
+++ b/spec/support/shared_examples/services/error_tracking_service_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+shared_examples 'error tracking service data not ready' do |service_call|
+ context "when #{service_call} returns nil" do
+ before do
+ expect(error_tracking_setting)
+ .to receive(service_call).and_return(nil)
+ end
+
+ it 'result is not ready' do
+ expect(result).to eq(
+ status: :error, http_status: :no_content, message: 'Not ready. Try again later')
+ end
+ end
+end
+
+shared_examples 'error tracking service sentry error handling' do |service_call|
+ context "when #{service_call} returns error" do
+ before do
+ allow(error_tracking_setting)
+ .to receive(service_call)
+ .and_return(
+ error: 'Sentry response status code: 401',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
+ )
+ end
+
+ it 'returns the error' do
+ expect(result).to eq(
+ status: :error,
+ http_status: :bad_request,
+ message: 'Sentry response status code: 401'
+ )
+ end
+ end
+end
+
+shared_examples 'error tracking service http status handling' do |service_call|
+ context "when #{service_call} returns error with http_status" do
+ before do
+ allow(error_tracking_setting)
+ .to receive(service_call)
+ .and_return(
+ error: 'Sentry API response is missing keys. key not found: "id"',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
+ )
+ end
+
+ it 'returns the error with correct http_status' do
+ expect(result).to eq(
+ status: :error,
+ http_status: :internal_server_error,
+ message: 'Sentry API response is missing keys. key not found: "id"'
+ )
+ end
+ end
+end
+
+shared_examples 'error tracking service unauthorized user' do
+ context 'with unauthorized user' do
+ let(:unauthorized_user) { create(:user) }
+
+ subject { described_class.new(project, unauthorized_user) }
+
+ it 'returns error' do
+ result = subject.execute
+
+ expect(result).to include(
+ status: :error,
+ message: 'Access denied',
+ http_status: :unauthorized
+ )
+ end
+ end
+end
+
+shared_examples 'error tracking service disabled' do
+ context 'with error tracking disabled' do
+ before do
+ error_tracking_setting.enabled = false
+ end
+
+ it 'raises error' do
+ result = subject.execute
+
+ expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb
index 9a8f8012762..84f6c4d136a 100644
--- a/spec/support/shared_examples/updating_mentions_shared_examples.rb
+++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb
@@ -27,7 +27,7 @@ RSpec.shared_examples 'updating mentions' do |service_class|
update_mentionable(title: "For #{mentioned_user.to_reference}")
end
- it 'emails only the newly-mentioned user' do
+ it 'emails only the newly-mentioned user', :sidekiq_might_not_need_inline do
should_only_email(mentioned_user)
end
end
@@ -37,7 +37,7 @@ RSpec.shared_examples 'updating mentions' do |service_class|
update_mentionable(description: "For #{mentioned_user.to_reference}")
end
- it 'emails only the newly-mentioned user' do
+ it 'emails only the newly-mentioned user', :sidekiq_might_not_need_inline do
should_only_email(mentioned_user)
end
end
@@ -51,16 +51,32 @@ RSpec.shared_examples 'updating mentions' do |service_class|
)
end
- it 'emails group members' do
+ it 'emails group members', :sidekiq_might_not_need_inline do
should_email(mentioned_user)
should_email(group_member1)
should_email(group_member2)
end
end
+ shared_examples 'updating attribute with existing group mention' do |attribute|
+ before do
+ mentionable.update!({ attribute => "FYI: #{group.to_reference}" })
+ end
+
+ it 'creates todos for only newly mentioned users' do
+ expect do
+ update_mentionable(
+ { attribute => "For #{group.to_reference}, cc: #{mentioned_user.to_reference}" }
+ )
+ end.to change { Todo.count }.by(1)
+ end
+ end
+
context 'when group is public' do
it_behaves_like 'updating attribute with allowed mentions', :title
it_behaves_like 'updating attribute with allowed mentions', :description
+ it_behaves_like 'updating attribute with existing group mention', :title
+ it_behaves_like 'updating attribute with existing group mention', :description
end
context 'when the group is private' do
@@ -70,6 +86,8 @@ RSpec.shared_examples 'updating mentions' do |service_class|
it_behaves_like 'updating attribute with allowed mentions', :title
it_behaves_like 'updating attribute with allowed mentions', :description
+ it_behaves_like 'updating attribute with existing group mention', :title
+ it_behaves_like 'updating attribute with existing group mention', :description
end
end
@@ -81,7 +99,7 @@ RSpec.shared_examples 'updating mentions' do |service_class|
)
end
- it 'emails mentioned user' do
+ it 'emails mentioned user', :sidekiq_might_not_need_inline do
should_only_email(mentioned_user)
end
end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
index 585c458a64e..246efedc7e5 100644
--- a/spec/support/sidekiq.rb
+++ b/spec/support/sidekiq.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'sidekiq/testing/inline'
+require 'sidekiq/testing'
# If Sidekiq::Testing.inline! is used, SQL transactions done inside
# Sidekiq worker are included in the SQL query limit (in a real
@@ -27,7 +27,9 @@ Sidekiq::Testing.server_middleware do |chain|
end
RSpec.configure do |config|
- config.after(:each, :sidekiq) do
+ config.around(:each, :sidekiq) do |example|
+ Sidekiq::Worker.clear_all
+ example.run
Sidekiq::Worker.clear_all
end
@@ -36,4 +38,19 @@ RSpec.configure do |config|
connection.redis.flushdb
end
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::Worker.clear_all
+ Sidekiq::Testing.inline! { example.run }
+ Sidekiq::Worker.clear_all
+ end
+
+ config.around(:example, :sidekiq_inline) do |example|
+ Sidekiq::Worker.clear_all
+ Sidekiq::Testing.inline! { example.run }
+ Sidekiq::Worker.clear_all
+ end
end
diff --git a/spec/support/unpack-gitlab-git-test b/spec/support/unpack-gitlab-git-test
index d5b4912457d..5d5f1b7d082 100755
--- a/spec/support/unpack-gitlab-git-test
+++ b/spec/support/unpack-gitlab-git-test
@@ -1,10 +1,12 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
require 'fileutils'
-REPO = 'spec/support/gitlab-git-test.git'.freeze
+REPO = 'spec/support/gitlab-git-test.git'
PACK_DIR = REPO + '/objects/pack'
GIT = %W[git --git-dir=#{REPO}].freeze
-BASE_PACK = 'pack-691247af2a6acb0b63b73ac0cb90540e93614043'.freeze
+BASE_PACK = 'pack-691247af2a6acb0b63b73ac0cb90540e93614043'
def main
unpack
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index abad16be580..08b3fea0c80 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -17,7 +17,7 @@ describe 'gitlab:shell rake tasks' do
expect_any_instance_of(Gitlab::TaskHelpers).to receive(:checkout_or_clone_version)
allow(Kernel).to receive(:system).with('bin/install', *storages).and_return(true)
- allow(Kernel).to receive(:system).with('bin/compile').and_return(true)
+ allow(Kernel).to receive(:system).with('make', 'build').and_return(true)
run_rake_task('gitlab:shell:install')
end
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index 4b4f7d7c956..4546d3bdfaf 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -20,22 +20,12 @@ describe Gitlab::TaskHelpers do
end
it 'checkout the version and reset to it' do
+ expect(subject).to receive(:get_version).with(version).and_call_original
expect(subject).to receive(:checkout_version).with(tag, clone_path)
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
end
- context 'with a branch version' do
- let(:version) { '=branch_name' }
- let(:branch) { 'branch_name' }
-
- it 'checkout the version and reset to it with a branch name' do
- expect(subject).to receive(:checkout_version).with(branch, clone_path)
-
- subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
- end
- end
-
context "target_dir doesn't exist" do
it 'clones the repo' do
expect(subject).to receive(:clone_repo).with(repo, clone_path)
@@ -96,4 +86,19 @@ describe Gitlab::TaskHelpers do
expect { subject.run_command!(['bash', '-c', 'exit 1']) }.to raise_error Gitlab::TaskFailedError
end
end
+
+ describe '#get_version' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:version, :result) do
+ '1.1.1' | 'v1.1.1'
+ 'master' | 'master'
+ '12.4.0-rc7' | 'v12.4.0-rc7'
+ '594c3ea3e0e5540e5915bd1c49713a0381459dd6' | '594c3ea3e0e5540e5915bd1c49713a0381459dd6'
+ end
+
+ with_them do
+ it { expect(subject.get_version(version)).to eq(result) }
+ end
+ end
end
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
index cf4872d6904..38b70d33993 100644
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -22,7 +22,7 @@ describe ObjectStorage::BackgroundMoveWorker do
stub_lfs_object_storage(background_upload: true)
end
- it 'uploads object to storage' do
+ it 'uploads object to storage', :sidekiq_might_not_need_inline do
expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
end
@@ -65,7 +65,7 @@ describe ObjectStorage::BackgroundMoveWorker do
stub_artifacts_object_storage(background_upload: true)
end
- it "migrates file to remote storage" do
+ it "migrates file to remote storage", :sidekiq_might_not_need_inline do
perform
expect(artifact.reload.file_store).to eq(remote)
@@ -91,7 +91,7 @@ describe ObjectStorage::BackgroundMoveWorker do
let(:subject_class) { project.class }
let(:subject_id) { project.id }
- it "migrates file to remote storage" do
+ it "migrates file to remote storage", :sidekiq_might_not_need_inline do
perform
project.reload
BatchLoader::Executor.clear_current
@@ -104,7 +104,7 @@ describe ObjectStorage::BackgroundMoveWorker do
let(:subject_class) { Upload }
let(:subject_id) { project.avatar.upload.id }
- it "migrates file to remote storage" do
+ it "migrates file to remote storage", :sidekiq_might_not_need_inline do
perform
expect(project.reload.avatar).not_to be_file_storage
diff --git a/spec/views/admin/application_settings/integrations.html.haml_spec.rb b/spec/views/admin/application_settings/integrations.html.haml_spec.rb
new file mode 100644
index 00000000000..392d43ef2d4
--- /dev/null
+++ b/spec/views/admin/application_settings/integrations.html.haml_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'admin/application_settings/integrations.html.haml' do
+ let(:app_settings) { build(:application_setting) }
+
+ describe 'sourcegraph integration' do
+ let(:sourcegraph_flag) { true }
+
+ before do
+ assign(:application_setting, app_settings)
+ allow(Gitlab::Sourcegraph).to receive(:feature_available?).and_return(sourcegraph_flag)
+ end
+
+ context 'when sourcegraph feature is enabled' do
+ it 'show the form' do
+ render
+
+ expect(rendered).to have_field('application_setting_sourcegraph_enabled')
+ end
+ end
+
+ context 'when sourcegraph feature is disabled' do
+ let(:sourcegraph_flag) { false }
+
+ it 'show the form' do
+ render
+
+ expect(rendered).not_to have_field('application_setting_sourcegraph_enabled')
+ end
+ end
+ end
+end
diff --git a/spec/views/devise/sessions/new.html.haml_spec.rb b/spec/views/devise/sessions/new.html.haml_spec.rb
new file mode 100644
index 00000000000..66afc2af7ce
--- /dev/null
+++ b/spec/views/devise/sessions/new.html.haml_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'devise/sessions/new' do
+ describe 'ldap' do
+ include LdapHelpers
+
+ let(:server) { { provider_name: 'ldapmain', label: 'LDAP' }.with_indifferent_access }
+
+ before do
+ enable_ldap
+ stub_devise
+ disable_captcha
+ disable_sign_up
+ disable_other_signin_methods
+
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ end
+
+ it 'is shown when enabled' do
+ render
+
+ expect(rendered).to have_selector('.new-session-tabs')
+ expect(rendered).to have_selector('[data-qa-selector="ldap_tab"]')
+ expect(rendered).to have_field('LDAP Username')
+ end
+
+ it 'is not shown when LDAP sign in is disabled' do
+ disable_ldap_sign_in
+
+ render
+
+ expect(rendered).to have_content('No authentication methods configured')
+ expect(rendered).not_to have_selector('[data-qa-selector="ldap_tab"]')
+ expect(rendered).not_to have_field('LDAP Username')
+ end
+ end
+
+ def disable_other_signin_methods
+ allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
+ allow(view).to receive(:omniauth_enabled?).and_return(false)
+ end
+
+ def disable_sign_up
+ allow(view).to receive(:allow_signup?).and_return(false)
+ end
+
+ def stub_devise
+ allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user])
+ allow(view).to receive(:resource).and_return(spy)
+ allow(view).to receive(:resource_name).and_return(:user)
+ end
+
+ def enable_ldap
+ stub_ldap_setting(enabled: true)
+ assign(:ldap_servers, [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, [])
+ end
+
+ def disable_captcha
+ allow(view).to receive(:captcha_enabled?).and_return(false)
+ allow(view).to receive(:captcha_on_login_required?).and_return(false)
+ end
+end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index e9b3334fffc..f181e18e53d 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -84,7 +84,7 @@ describe 'layouts/_head' do
allow(Gitlab::CurrentSettings).to receive(:snowplow_collector_hostname).and_return('www.snow.plow')
end
- it 'add a snowplow script tag with asset host' do
+ it 'adds a snowplow script tag with asset host' do
render
expect(rendered).to match('http://test.host/assets/snowplow/')
expect(rendered).to match('window.snowplow')
diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb
new file mode 100644
index 00000000000..52933c42621
--- /dev/null
+++ b/spec/views/profiles/preferences/show.html.haml_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'profiles/preferences/show' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { build(:user) }
+
+ before do
+ assign(:user, user)
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ context 'sourcegraph' do
+ def have_sourcegraph_field(*args)
+ have_field('user_sourcegraph_enabled', *args)
+ end
+
+ def have_integrations_section
+ have_css('.profile-settings-sidebar', { text: 'Integrations' })
+ end
+
+ before do
+ # Can't use stub_feature_flags because we use Feature.get to check if conditinally applied
+ Feature.get(:sourcegraph).enable sourcegraph_feature
+ stub_application_setting(sourcegraph_enabled: sourcegraph_enabled)
+ end
+
+ context 'when not fully enabled' do
+ where(:feature, :admin_enabled) do
+ false | false
+ false | true
+ true | false
+ end
+
+ with_them do
+ let(:sourcegraph_feature) { feature }
+ let(:sourcegraph_enabled) { admin_enabled }
+
+ before do
+ render
+ end
+
+ it 'does not display sourcegraph field' do
+ expect(rendered).not_to have_sourcegraph_field
+ end
+
+ it 'does not display integrations settings' do
+ expect(rendered).not_to have_integrations_section
+ end
+ end
+ end
+
+ context 'when fully enabled' do
+ let(:sourcegraph_feature) { true }
+ let(:sourcegraph_enabled) { true }
+
+ before do
+ render
+ end
+
+ it 'displays the sourcegraph field' do
+ expect(rendered).to have_sourcegraph_field
+ end
+
+ it 'displays the integrations section' do
+ expect(rendered).to have_integrations_section
+ 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 592b3a56ba3..14e6feed3ab 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -8,6 +8,7 @@ describe 'profiles/show' do
before do
assign(:user, user)
allow(controller).to receive(:current_user).and_return(user)
+ allow(view).to receive(:experiment_enabled?)
end
context 'when the profile page is opened' do
diff --git a/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb b/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb
new file mode 100644
index 00000000000..1cb2f9a4301
--- /dev/null
+++ b/spec/views/projects/clusters/clusters/gcp/_form.html.haml_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'clusters/clusters/gcp/_form' do
+ let(:admin) { create(:admin) }
+ let(:environment) { create(:environment) }
+ let(:gcp_cluster) { create(:cluster, :provided_by_gcp) }
+ let(:clusterable) { ClusterablePresenter.fabricate(environment.project, current_user: admin) }
+
+ before do
+ assign(:environment, environment)
+ assign(:gcp_cluster, gcp_cluster)
+ allow(view).to receive(:clusterable).and_return(clusterable)
+ allow(view).to receive(:url_for).and_return('#')
+ allow(view).to receive(:token_in_session).and_return('')
+ end
+
+ context 'with all feature flags enabled' do
+ it 'has a cloud run checkbox' do
+ render
+
+ expect(rendered).to have_selector("input[id='cluster_provider_gcp_attributes_cloud_run']")
+ end
+ end
+
+ context 'with cloud run feature flag disabled' do
+ before do
+ stub_feature_flags(create_cloud_run_clusters: false)
+ end
+
+ it 'does not have a cloud run checkbox' do
+ render
+
+ expect(rendered).not_to have_selector("input[id='cluster_provider_gcp_attributes_cloud_run']")
+ end
+ end
+end
diff --git a/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
index 54ec4f32856..9168bc8e833 100644
--- a/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
+++ b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
@@ -48,7 +48,7 @@ describe 'projects/deployments/_confirm_rollback_modal' do
render
expect(rendered).to have_selector('h4', text: "Rollback environment #{environment.name}?")
- expect(rendered).to have_selector('p', text: "This action will run the job defined by staging for commit #{deployment.short_sha}, 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?")
+ expect(rendered).to have_selector('p', text: "This action will run the job defined by #{environment.name} for commit #{deployment.short_sha}, 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?")
expect(rendered).to have_selector('a.btn-danger', text: 'Rollback')
end
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index 71d74b06f85..755a40a7e4c 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'projects/merge_requests/_commits.html.haml' do
+describe 'projects/merge_requests/_commits.html.haml', :sidekiq_might_not_need_inline do
include Devise::Test::ControllerHelpers
include ProjectForksHelper
diff --git a/spec/views/projects/pages_domains/show.html.haml_spec.rb b/spec/views/projects/pages_domains/show.html.haml_spec.rb
index ba0544a49b0..331bfe63f28 100644
--- a/spec/views/projects/pages_domains/show.html.haml_spec.rb
+++ b/spec/views/projects/pages_domains/show.html.haml_spec.rb
@@ -30,39 +30,5 @@ describe 'projects/pages_domains/show' do
expect(rendered).to have_content("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.")
end
end
-
- context 'when certificate is present' do
- let(:domain) { create(:pages_domain, :letsencrypt, project: project) }
-
- it 'shows certificate info' do
- render
-
- # test just a random part of cert represenations(X509v3 Subject Key Identifier:)
- expect(rendered).to have_content("C6:5F:56:4B:10:69:AC:1D:33:D2:26:C9:B3:7A:D7:12:4D:3E:F7:90")
- end
- end
- end
-
- context 'when auto_ssl is disabled' do
- context 'when certificate is present' do
- let(:domain) { create(:pages_domain, project: project) }
-
- it 'shows certificate info' do
- render
-
- # test just a random part of cert represenations(X509v3 Subject Key Identifier:)
- expect(rendered).to have_content("C6:5F:56:4B:10:69:AC:1D:33:D2:26:C9:B3:7A:D7:12:4D:3E:F7:90")
- end
- end
-
- context 'when certificate is absent' do
- let(:domain) { create(:pages_domain, :without_certificate, :without_key, project: project) }
-
- it 'shows missing certificate' do
- render
-
- expect(rendered).to have_content("missing")
- end
- end
end
end
diff --git a/spec/views/projects/show.html.haml_spec.rb b/spec/views/projects/show.html.haml_spec.rb
new file mode 100644
index 00000000000..4f5f0f0285c
--- /dev/null
+++ b/spec/views/projects/show.html.haml_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/show' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:admin) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ presented_project = project.present(current_user: user)
+
+ allow(presented_project).to receive(:default_view).and_return('customize_workflow')
+ allow(controller).to receive(:current_user).and_return(user)
+
+ assign(:project, presented_project)
+ end
+
+ context 'commit signatures' do
+ context 'with vue tree view enabled' do
+ it 'are not rendered via js-signature-container' do
+ render
+
+ expect(rendered).not_to have_css('.js-signature-container')
+ end
+ end
+
+ context 'with vue tree view disabled' do
+ before do
+ stub_feature_flags(vue_file_list: false)
+ end
+
+ it 'rendered via js-signature-container' do
+ render
+
+ expect(rendered).to have_css('.js-signature-container')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/tree/_tree_header.html.haml_spec.rb b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
index 4b71ea9ffe3..caf8c4d1969 100644
--- a/spec/views/projects/tree/_tree_header.html.haml_spec.rb
+++ b/spec/views/projects/tree/_tree_header.html.haml_spec.rb
@@ -8,6 +8,8 @@ describe 'projects/tree/_tree_header' do
let(:repository) { project.repository }
before do
+ stub_feature_flags(vue_file_list: false)
+
assign(:project, project)
assign(:repository, repository)
assign(:id, File.join('master', ''))
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 960cf42a793..8c6b229247d 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -7,10 +7,12 @@ describe 'projects/tree/show' do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
+ let(:ref) { 'master' }
+ let(:commit) { repository.commit(ref) }
+ let(:path) { '' }
+ let(:tree) { repository.tree(commit.id, path) }
before do
- stub_feature_flags(vue_file_list: false)
-
assign(:project, project)
assign(:repository, repository)
assign(:lfs_blob_ids, [])
@@ -19,26 +21,44 @@ describe 'projects/tree/show' do
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
allow(view).to receive_message_chain('user_access.can_push_to_branch?').and_return(true)
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ allow(view).to receive(:current_user).and_return(project.creator)
+
+ assign(:id, File.join(ref, path))
+ assign(:ref, ref)
+ assign(:path, path)
+ assign(:last_commit, commit)
+ assign(:tree, tree)
end
context 'for branch names ending on .json' do
let(:ref) { 'ends-with.json' }
- let(:commit) { repository.commit(ref) }
- let(:path) { '' }
- let(:tree) { repository.tree(commit.id, path) }
-
- before do
- assign(:id, File.join(ref, path))
- assign(:ref, ref)
- assign(:path, path)
- assign(:last_commit, commit)
- assign(:tree, tree)
- end
it 'displays correctly' do
render
+
expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
- expect(rendered).to have_css('.readme-holder')
+ end
+ end
+
+ context 'commit signatures' do
+ context 'with vue tree view disabled' do
+ before do
+ stub_feature_flags(vue_file_list: false)
+ end
+
+ it 'rendered via js-signature-container' do
+ render
+
+ expect(rendered).to have_css('.js-signature-container')
+ end
+ end
+
+ context 'with vue tree view enabled' do
+ it 'are not rendered via js-signature-container' do
+ render
+
+ expect(rendered).not_to have_css('.js-signature-container')
+ end
end
end
end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index 3f69962f25d..608639331fd 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -9,7 +9,18 @@ describe ClusterProvisionWorker do
let(:provider) { create(:cluster_provider_gcp, :scheduled) }
it 'provision a cluster' do
- expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute)
+ expect_any_instance_of(Clusters::Gcp::ProvisionService).to receive(:execute).with(provider)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is aws' do
+ let(:cluster) { create(:cluster, provider_type: :aws, provider_aws: provider) }
+ let(:provider) { create(:cluster_provider_aws, :scheduled) }
+
+ it 'provision a cluster' do
+ expect_any_instance_of(Clusters::Aws::ProvisionService).to receive(:execute).with(provider)
described_class.new.perform(cluster.id)
end
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index b7ba4d61723..5ceb54eb2d5 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -21,8 +21,8 @@ describe 'Every Sidekiq worker' do
missing_from_file = worker_queues - file_worker_queues
expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
- unncessarily_in_file = file_worker_queues - worker_queues
- expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
+ unnecessarily_in_file = file_worker_queues - worker_queues
+ expect(unnecessarily_in_file).to be_empty, "expected #{unnecessarily_in_file.to_a.inspect} not to be in Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS"
end
it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do
@@ -42,7 +42,7 @@ describe 'Every Sidekiq worker' do
end
# All Sidekiq worker classes should declare a valid `feature_category`
- # or explicitely be excluded with the `feature_category_not_owned!` annotation.
+ # or explicitly be excluded with the `feature_category_not_owned!` annotation.
# Please see doc/development/sidekiq_style_guide.md#Feature-Categorization for more details.
it 'has a feature_category or feature_category_not_owned! attribute', :aggregate_failures do
Gitlab::SidekiqConfig.workers.each do |worker|
@@ -62,5 +62,36 @@ describe 'Every Sidekiq worker' do
expect(feature_categories).to include(worker.get_feature_category), "expected #{worker.inspect} to declare a valid feature_category, but got #{worker.get_feature_category}"
end
end
+
+ # Memory-bound workers are very expensive to run, since they need to run on nodes with very low
+ # concurrency, so that each job can consume a large amounts of memory. For this reason, on
+ # GitLab.com, when a large number of memory-bound jobs arrive at once, we let them queue up
+ # rather than scaling the hardware to meet the SLO. For this reason, memory-bound,
+ # latency-sensitive jobs are explicitly discouraged and disabled.
+ it 'is (exclusively) memory-bound or latency-sentitive, not both', :aggregate_failures do
+ latency_sensitive_workers = Gitlab::SidekiqConfig.workers
+ .select(&:latency_sensitive_worker?)
+
+ latency_sensitive_workers.each do |worker|
+ expect(worker.get_worker_resource_boundary).not_to eq(:memory), "#{worker.inspect} cannot be both memory-bound and latency sensitive"
+ end
+ end
+
+ # In high traffic installations, such as GitLab.com, `latency_sensitive` workers run in a
+ # dedicated fleet. In order to ensure short queue times, `latency_sensitive` jobs have strict
+ # SLOs in order to ensure throughput. However, when a worker depends on an external service,
+ # such as a user's k8s cluster or a third-party internet service, we cannot guarantee latency,
+ # and therefore throughput. An outage to an 3rd party service could therefore impact throughput
+ # on other latency_sensitive jobs, leading to degradation through the GitLab application.
+ # Please see doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for more
+ # details.
+ it 'has (exclusively) external dependencies or is latency-sentitive, not both', :aggregate_failures do
+ latency_sensitive_workers = Gitlab::SidekiqConfig.workers
+ .select(&:latency_sensitive_worker?)
+
+ latency_sensitive_workers.each do |worker|
+ expect(worker.worker_has_external_dependencies?).to be_falsey, "#{worker.inspect} cannot have both external dependencies and be latency sensitive"
+ end
+ end
end
end
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 74d6b5605d1..0a0aea838d2 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -3,62 +3,11 @@
require 'spec_helper'
describe ExpireBuildArtifactsWorker do
- include RepoHelpers
-
let(:worker) { described_class.new }
- before do
- Sidekiq::Worker.clear_all
- end
-
describe '#perform' do
- before do
- stub_feature_flags(ci_new_expire_job_artifacts_service: false)
- build
- end
-
- subject! do
- Sidekiq::Testing.fake! { worker.perform }
- end
-
- context 'with expired artifacts' do
- let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
-
- it 'enqueues that build' do
- expect(jobs_enqueued.size).to eq(1)
- expect(jobs_enqueued[0]["args"]).to eq([build.id])
- end
- end
-
- context 'with not yet expired artifacts' do
- let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
-
- it 'does not enqueue that build' do
- expect(jobs_enqueued.size).to eq(0)
- end
- end
-
- context 'without expire date' do
- let(:build) { create(:ci_build, :artifacts) }
-
- it 'does not enqueue that build' do
- expect(jobs_enqueued.size).to eq(0)
- end
- end
-
- def jobs_enqueued
- Sidekiq::Queues.jobs_by_worker['ExpireBuildInstanceArtifactsWorker']
- end
- end
-
- describe '#perform with ci_new_expire_job_artifacts_service feature flag' do
- before do
- stub_feature_flags(ci_new_expire_job_artifacts_service: true)
- end
-
it 'executes a service' do
expect_any_instance_of(Ci::DestroyExpiredJobArtifactsService).to receive(:execute)
- expect(ExpireBuildInstanceArtifactsWorker).not_to receive(:bulk_perform_async)
worker.perform
end
diff --git a/spec/workers/group_export_worker_spec.rb b/spec/workers/group_export_worker_spec.rb
new file mode 100644
index 00000000000..4aa85d2b381
--- /dev/null
+++ b/spec/workers/group_export_worker_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupExportWorker do
+ let!(:user) { create(:user) }
+ let!(:group) { create(:group) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ context 'when it succeeds' do
+ it 'calls the ExportService' do
+ expect_any_instance_of(::Groups::ImportExport::ExportService).to receive(:execute)
+
+ subject.perform(user.id, group.id, {})
+ end
+ end
+
+ context 'when it fails' do
+ it 'raises an exception when params are invalid' do
+ expect_any_instance_of(::Groups::ImportExport::ExportService).not_to receive(:execute)
+
+ expect { subject.perform(1234, group.id, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+ expect { subject.perform(user.id, 1234, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/workers/hashed_storage/migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index 12c1a26104e..9180da87058 100644
--- a/spec/workers/hashed_storage/migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -15,7 +15,7 @@ describe HashedStorage::MigratorWorker do
worker.perform(5, 10)
end
- it 'migrates projects in the specified range' do
+ it 'migrates projects in the specified range', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
worker.perform(ids.min, ids.max)
end
diff --git a/spec/workers/hashed_storage/rollbacker_worker_spec.rb b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
index 5fcb1adf9ae..3ca2601df0f 100644
--- a/spec/workers/hashed_storage/rollbacker_worker_spec.rb
+++ b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
@@ -15,7 +15,7 @@ describe HashedStorage::RollbackerWorker do
worker.perform(5, 10)
end
- it 'rollsback projects in the specified range' do
+ it 'rollsback projects in the specified range', :sidekiq_might_not_need_inline do
perform_enqueued_jobs do
worker.perform(ids.min, ids.max)
end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 138a99abde6..dc98c9836fa 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -20,6 +20,7 @@ describe MergeWorker do
described_class.new.perform(
merge_request.id, merge_request.author_id,
commit_message: 'wow such merge',
+ sha: merge_request.diff_head_sha,
should_remove_source_branch: true)
merge_request.reload
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
index 2966a201a62..ae62237960a 100644
--- a/spec/workers/new_note_worker_spec.rb
+++ b/spec/workers/new_note_worker_spec.rb
@@ -7,16 +7,17 @@ describe NewNoteWorker do
let(:note) { create(:note) }
it "calls NotificationService#new_note" do
- expect_any_instance_of(NotificationService).to receive(:new_note).with(note)
+ expect_next_instance_of(NotificationService) do |service|
+ expect(service).to receive(:new_note).with(note)
+ end
described_class.new.perform(note.id)
end
it "calls Notes::PostProcessService#execute" do
- notes_post_process_service = double(Notes::PostProcessService)
- allow(Notes::PostProcessService).to receive(:new).with(note) { notes_post_process_service }
-
- expect(notes_post_process_service).to receive(:execute)
+ expect_next_instance_of(Notes::PostProcessService) do |service|
+ expect(service).to receive(:execute)
+ end
described_class.new.perform(note.id)
end
@@ -36,14 +37,14 @@ describe NewNoteWorker do
expect { described_class.new.perform(unexistent_note_id) }.not_to raise_error
end
- it "does not call NotificationService#new_note" do
- expect_any_instance_of(NotificationService).not_to receive(:new_note)
+ it "does not call NotificationService" do
+ expect(NotificationService).not_to receive(:new)
described_class.new.perform(unexistent_note_id)
end
- it "does not call Notes::PostProcessService#execute" do
- expect_any_instance_of(Notes::PostProcessService).not_to receive(:execute)
+ it "does not call Notes::PostProcessService" do
+ expect(Notes::PostProcessService).not_to receive(:new)
described_class.new.perform(unexistent_note_id)
end
diff --git a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
index 08a3511f70b..10c23cbb6d4 100644
--- a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
@@ -13,7 +13,7 @@ describe PagesDomainSslRenewalCronWorker do
describe '#perform' do
let(:project) { create :project }
- let!(:domain) { create(:pages_domain, project: project) }
+ let!(:domain) { create(:pages_domain, project: project, auto_ssl_enabled: false) }
let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) }
let!(:domain_with_obtained_letsencrypt) do
create(:pages_domain, :letsencrypt, project: project, auto_ssl_enabled: true)
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 9326db34209..4926c14a6ab 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -28,7 +28,7 @@ describe PipelineScheduleWorker do
context 'when there is a scheduled pipeline within next_run_at' do
shared_examples 'successful scheduling' do
- it 'creates a new pipeline' do
+ it 'creates a new pipeline', :sidekiq_might_not_need_inline do
expect { subject }.to change { project.ci_pipelines.count }.by(1)
expect(Ci::Pipeline.last).to be_schedule
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index eb1d3c364ac..99800135075 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -81,9 +81,10 @@ describe ProcessCommitWorker do
let(:commit) do
project.repository.create_branch('feature-merged', 'feature')
+ project.repository.after_create_branch
MergeRequests::MergeService
- .new(project, merge_request.author)
+ .new(project, merge_request.author, { sha: merge_request.diff_head_sha })
.execute(merge_request)
merge_request.reload.merge_commit
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 7f3c4881b89..fa02762d716 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -105,7 +105,7 @@ describe ProjectCacheWorker do
end
context 'when a lease could be obtained' do
- it 'updates the project statistics twice' do
+ it 'updates the project statistics twice', :sidekiq_might_not_need_inline do
stub_exclusive_lease(lease_key, timeout: lease_timeout)
expect(Projects::UpdateStatisticsService).to receive(:new)
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
index 10d9aa37dee..9557aa3086c 100644
--- a/spec/workers/remove_expired_group_links_worker_spec.rb
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -4,23 +4,54 @@ require 'spec_helper'
describe RemoveExpiredGroupLinksWorker do
describe '#perform' do
- let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
- let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
- let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+ context 'ProjectGroupLinks' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
- it 'removes expired group links' do
- expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
- expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
- end
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
- it 'leaves group links that expire in the future' do
- subject.perform
- expect(project_group_link_expiring_in_future.reload).to be_present
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
end
- it 'leaves group links that do not expire at all' do
- subject.perform
- expect(non_expiring_project_group_link.reload).to be_present
+ context 'GroupGroupLinks' do
+ let(:mock_destroy_service) { instance_double(Groups::GroupLinks::DestroyService) }
+
+ before do
+ allow(Groups::GroupLinks::DestroyService).to(
+ receive(:new).and_return(mock_destroy_service))
+ end
+
+ context 'expired GroupGroupLink exists' do
+ before do
+ create(:group_group_link, expires_at: 1.hour.ago)
+ end
+
+ it 'calls Groups::GroupLinks::DestroyService' do
+ expect(mock_destroy_service).to receive(:execute).once
+
+ subject.perform
+ end
+ end
+
+ context 'expired GroupGroupLink does not exist' do
+ it 'does not call Groups::GroupLinks::DestroyService' do
+ expect(mock_destroy_service).not_to receive(:execute)
+
+ subject.perform
+ end
+ end
end
end
end
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 65e1c5e9d5d..6870e15424f 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -68,7 +68,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
it 'creates missing wikis' do
project = create(:project, :wiki_enabled)
- Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
+ TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path)
subject.perform(project.id)
@@ -77,12 +77,12 @@ describe RepositoryCheck::SingleRepositoryWorker do
it 'does not create a wiki if the main repo does not exist at all' do
project = create(:project, :repository)
- Gitlab::Shell.new.rm_directory(project.repository_storage, project.path)
- Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path)
+ TestEnv.rm_storage_dir(project.repository_storage, project.path)
+ TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path)
subject.perform(project.id)
- expect(Gitlab::Shell.new.exists?(project.repository_storage, project.wiki.path)).to eq(false)
+ expect(TestEnv.storage_dir_exists?(project.repository_storage, project.wiki.path)).to eq(false)
end
def create_push_event(project)
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index c3d577e2dae..59707409b5a 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -18,15 +18,30 @@ describe StuckCiJobsWorker do
end
shared_examples 'job is dropped' do
- before do
+ it "changes status" do
worker.perform
job.reload
- end
- it "changes status" do
expect(job).to be_failed
expect(job).to be_stuck_or_timeout_failure
end
+
+ context 'when job have data integrity problem' do
+ it "does drop the job and logs the reason" do
+ job.update_columns(yaml_variables: '[{"key" => "value"}]')
+
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
+ .with(anything, a_hash_including(extra: a_hash_including(build_id: job.id)))
+ .once
+ .and_call_original
+
+ worker.perform
+ job.reload
+
+ expect(job).to be_failed
+ expect(job).to be_data_integrity_failure
+ end
+ end
end
shared_examples 'job is unchanged' do
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index 09efed6d2cf..8ceaf1fc555 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -22,7 +22,7 @@ describe StuckMergeJobsWorker do
expect(mr_without_sha.merge_jid).to be_nil
end
- it 'updates merge request to opened when locked but has not been merged' do
+ it 'updates merge request to opened when locked but has not been merged', :sidekiq_might_not_need_inline do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha)
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index 850eba263a7..b21a9b612af 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -8,8 +8,19 @@ describe WaitForClusterCreationWorker do
let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
let(:provider) { create(:cluster_provider_gcp, :creating) }
- it 'provision a cluster' do
- expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute)
+ it 'provisions a cluster' do
+ expect_any_instance_of(Clusters::Gcp::VerifyProvisionStatusService).to receive(:execute).with(provider)
+
+ described_class.new.perform(cluster.id)
+ end
+ end
+
+ context 'when provider type is aws' do
+ let(:cluster) { create(:cluster, provider_type: :aws, provider_aws: provider) }
+ let(:provider) { create(:cluster_provider_aws, :creating) }
+
+ it 'provisions a cluster' do
+ expect_any_instance_of(Clusters::Aws::VerifyProvisionStatusService).to receive(:execute).with(provider)
described_class.new.perform(cluster.id)
end
diff --git a/vendor/aws/cloudformation/eks_cluster.yaml b/vendor/aws/cloudformation/eks_cluster.yaml
new file mode 100644
index 00000000000..c32f54d66dc
--- /dev/null
+++ b/vendor/aws/cloudformation/eks_cluster.yaml
@@ -0,0 +1,340 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Description: GitLab EKS Cluster
+
+Parameters:
+
+ KubernetesVersion:
+ Description: The Kubernetes version to install
+ Type: String
+ Default: 1.14
+ AllowedValues:
+ - 1.12
+ - 1.13
+ - 1.14
+
+ KeyName:
+ Description: The EC2 Key Pair to allow SSH access to the node instances
+ Type: AWS::EC2::KeyPair::KeyName
+
+ NodeImageIdSSMParam:
+ Type: "AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>"
+ Default: /aws/service/eks/optimized-ami/1.14/amazon-linux-2/recommended/image_id
+ Description: AWS Systems Manager Parameter Store parameter of the AMI ID for the worker node instances.
+
+ NodeInstanceType:
+ Description: EC2 instance type for the node instances
+ Type: String
+ Default: t3.medium
+ ConstraintDescription: Must be a valid EC2 instance type
+ AllowedValues:
+ - t2.small
+ - t2.medium
+ - t2.large
+ - t2.xlarge
+ - t2.2xlarge
+ - t3.nano
+ - t3.micro
+ - t3.small
+ - t3.medium
+ - t3.large
+ - t3.xlarge
+ - t3.2xlarge
+ - m3.medium
+ - m3.large
+ - m3.xlarge
+ - m3.2xlarge
+ - m4.large
+ - m4.xlarge
+ - m4.2xlarge
+ - m4.4xlarge
+ - m4.10xlarge
+ - m5.large
+ - m5.xlarge
+ - m5.2xlarge
+ - m5.4xlarge
+ - m5.12xlarge
+ - m5.24xlarge
+ - c4.large
+ - c4.xlarge
+ - c4.2xlarge
+ - c4.4xlarge
+ - c4.8xlarge
+ - c5.large
+ - c5.xlarge
+ - c5.2xlarge
+ - c5.4xlarge
+ - c5.9xlarge
+ - c5.18xlarge
+ - i3.large
+ - i3.xlarge
+ - i3.2xlarge
+ - i3.4xlarge
+ - i3.8xlarge
+ - i3.16xlarge
+ - r3.xlarge
+ - r3.2xlarge
+ - r3.4xlarge
+ - r3.8xlarge
+ - r4.large
+ - r4.xlarge
+ - r4.2xlarge
+ - r4.4xlarge
+ - r4.8xlarge
+ - r4.16xlarge
+ - x1.16xlarge
+ - x1.32xlarge
+ - p2.xlarge
+ - p2.8xlarge
+ - p2.16xlarge
+ - p3.2xlarge
+ - p3.8xlarge
+ - p3.16xlarge
+ - p3dn.24xlarge
+ - r5.large
+ - r5.xlarge
+ - r5.2xlarge
+ - r5.4xlarge
+ - r5.12xlarge
+ - r5.24xlarge
+ - r5d.large
+ - r5d.xlarge
+ - r5d.2xlarge
+ - r5d.4xlarge
+ - r5d.12xlarge
+ - r5d.24xlarge
+ - z1d.large
+ - z1d.xlarge
+ - z1d.2xlarge
+ - z1d.3xlarge
+ - z1d.6xlarge
+ - z1d.12xlarge
+
+ NodeAutoScalingGroupDesiredCapacity:
+ Description: Desired capacity of Node Group ASG.
+ Type: Number
+ Default: 3
+
+ NodeVolumeSize:
+ Description: Node volume size
+ Type: Number
+ Default: 20
+
+ ClusterName:
+ Description: Unique name for your Amazon EKS cluster.
+ Type: String
+
+ ClusterRole:
+ Description: The IAM Role to allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf.
+ Type: String
+
+ ClusterControlPlaneSecurityGroup:
+ Description: The security groups to apply to the EKS-managed Elastic Network Interfaces that are created in your worker node subnets.
+ Type: AWS::EC2::SecurityGroup::Id
+
+ VpcId:
+ Description: The VPC to use for your EKS Cluster resources.
+ Type: AWS::EC2::VPC::Id
+
+ Subnets:
+ Description: The subnets in your VPC where your worker nodes will run.
+ Type: List<AWS::EC2::Subnet::Id>
+
+Metadata:
+
+ AWS::CloudFormation::Interface:
+ ParameterGroups:
+ - Label:
+ default: EKS Cluster
+ Parameters:
+ - ClusterName
+ - ClusterRole
+ - KubernetesVersion
+ - ClusterControlPlaneSecurityGroup
+ - Label:
+ default: Worker Node Configuration
+ Parameters:
+ - NodeAutoScalingGroupDesiredCapacity
+ - NodeInstanceType
+ - NodeImageIdSSMParam
+ - NodeVolumeSize
+ - KeyName
+ - Label:
+ default: Worker Network Configuration
+ Parameters:
+ - VpcId
+ - Subnets
+
+Resources:
+
+ Cluster:
+ Type: AWS::EKS::Cluster
+ Properties:
+ Name: !Sub ${ClusterName}
+ Version: !Sub ${KubernetesVersion}
+ RoleArn: !Sub ${ClusterRole}
+ ResourcesVpcConfig:
+ SecurityGroupIds:
+ - !Ref ClusterControlPlaneSecurityGroup
+ SubnetIds: !Ref Subnets
+
+ NodeInstanceProfile:
+ Type: AWS::IAM::InstanceProfile
+ Properties:
+ Path: "/"
+ Roles:
+ - !Ref NodeInstanceRole
+
+ NodeInstanceRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service: ec2.amazonaws.com
+ Action: sts:AssumeRole
+ Path: "/"
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
+ - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
+ - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
+
+ NodeSecurityGroup:
+ Type: AWS::EC2::SecurityGroup
+ Properties:
+ GroupDescription: Security group for all nodes in the cluster
+ VpcId: !Ref VpcId
+ Tags:
+ - Key: !Sub kubernetes.io/cluster/${ClusterName}
+ Value: owned
+
+ NodeSecurityGroupIngress:
+ Type: AWS::EC2::SecurityGroupIngress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow nodes to communicate with each other
+ GroupId: !Ref NodeSecurityGroup
+ SourceSecurityGroupId: !Ref NodeSecurityGroup
+ IpProtocol: -1
+ FromPort: 0
+ ToPort: 65535
+
+ NodeSecurityGroupFromControlPlaneIngress:
+ Type: AWS::EC2::SecurityGroupIngress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow worker Kubelets and pods to receive communication from the cluster control plane
+ GroupId: !Ref NodeSecurityGroup
+ SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup
+ IpProtocol: tcp
+ FromPort: 1025
+ ToPort: 65535
+
+ ControlPlaneEgressToNodeSecurityGroup:
+ Type: AWS::EC2::SecurityGroupEgress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow the cluster control plane to communicate with worker Kubelet and pods
+ GroupId: !Ref ClusterControlPlaneSecurityGroup
+ DestinationSecurityGroupId: !Ref NodeSecurityGroup
+ IpProtocol: tcp
+ FromPort: 1025
+ ToPort: 65535
+
+ NodeSecurityGroupFromControlPlaneOn443Ingress:
+ Type: AWS::EC2::SecurityGroupIngress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow pods running extension API servers on port 443 to receive communication from cluster control plane
+ GroupId: !Ref NodeSecurityGroup
+ SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup
+ IpProtocol: tcp
+ FromPort: 443
+ ToPort: 443
+
+ ControlPlaneEgressToNodeSecurityGroupOn443:
+ Type: AWS::EC2::SecurityGroupEgress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow the cluster control plane to communicate with pods running extension API servers on port 443
+ GroupId: !Ref ClusterControlPlaneSecurityGroup
+ DestinationSecurityGroupId: !Ref NodeSecurityGroup
+ IpProtocol: tcp
+ FromPort: 443
+ ToPort: 443
+
+ ClusterControlPlaneSecurityGroupIngress:
+ Type: AWS::EC2::SecurityGroupIngress
+ DependsOn: NodeSecurityGroup
+ Properties:
+ Description: Allow pods to communicate with the cluster API Server
+ GroupId: !Ref ClusterControlPlaneSecurityGroup
+ SourceSecurityGroupId: !Ref NodeSecurityGroup
+ IpProtocol: tcp
+ ToPort: 443
+ FromPort: 443
+
+ NodeGroup:
+ Type: AWS::AutoScaling::AutoScalingGroup
+ DependsOn: Cluster
+ Properties:
+ DesiredCapacity: !Ref NodeAutoScalingGroupDesiredCapacity
+ LaunchConfigurationName: !Ref NodeLaunchConfig
+ MinSize: !Ref NodeAutoScalingGroupDesiredCapacity
+ MaxSize: !Ref NodeAutoScalingGroupDesiredCapacity
+ VPCZoneIdentifier: !Ref Subnets
+ Tags:
+ - Key: Name
+ Value: !Sub ${ClusterName}-node
+ PropagateAtLaunch: true
+ - Key: !Sub kubernetes.io/cluster/${ClusterName}
+ Value: owned
+ PropagateAtLaunch: true
+ UpdatePolicy:
+ AutoScalingRollingUpdate:
+ MaxBatchSize: 1
+ MinInstancesInService: !Ref NodeAutoScalingGroupDesiredCapacity
+ PauseTime: PT5M
+
+ NodeLaunchConfig:
+ Type: AWS::AutoScaling::LaunchConfiguration
+ Properties:
+ AssociatePublicIpAddress: true
+ IamInstanceProfile: !Ref NodeInstanceProfile
+ ImageId: !Ref NodeImageIdSSMParam
+ InstanceType: !Ref NodeInstanceType
+ KeyName: !Ref KeyName
+ SecurityGroups:
+ - !Ref NodeSecurityGroup
+ BlockDeviceMappings:
+ - DeviceName: /dev/xvda
+ Ebs:
+ VolumeSize: !Ref NodeVolumeSize
+ VolumeType: gp2
+ DeleteOnTermination: true
+ UserData:
+ Fn::Base64:
+ !Sub |
+ #!/bin/bash
+ set -o xtrace
+ /etc/eks/bootstrap.sh "${ClusterName}"
+ /opt/aws/bin/cfn-signal --exit-code $? \
+ --stack ${AWS::StackName} \
+ --resource NodeGroup \
+ --region ${AWS::Region}
+
+Outputs:
+
+ NodeInstanceRole:
+ Description: The node instance role
+ Value: !GetAtt NodeInstanceRole.Arn
+
+ ClusterCertificate:
+ Description: The cluster certificate
+ Value: !GetAtt Cluster.CertificateAuthorityData
+
+ ClusterEndpoint:
+ Description: The cluster endpoint
+ Value: !GetAtt Cluster.Endpoint
diff --git a/vendor/crossplane/values.yaml b/vendor/crossplane/values.yaml
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/vendor/crossplane/values.yaml
diff --git a/vendor/elastic_stack/values.yaml b/vendor/elastic_stack/values.yaml
new file mode 100644
index 00000000000..9346c0e25e6
--- /dev/null
+++ b/vendor/elastic_stack/values.yaml
@@ -0,0 +1,47 @@
+elasticsearch:
+ enabled: true
+ cluster:
+ env:
+ MINIMUM_MASTER_NODES: "1"
+ master:
+ replicas: 2
+ client:
+ replicas: 1
+ data:
+ replicas: 1
+
+kibana:
+ enabled: true
+ env:
+ ELASTICSEARCH_HOSTS: http://elastic-stack-elasticsearch-client:9200
+ ingress:
+ enabled: true
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ kubernetes.io/tls-acme: "true"
+
+logstash:
+ enabled: false
+
+filebeat:
+ enabled: true
+ config:
+ output.file.enabled: false
+ output.elasticsearch:
+ enabled: true
+ hosts: ["http://elastic-stack-elasticsearch-client:9200"]
+
+fluentd:
+ enabled: false
+
+fluent-bit:
+ enabled: false
+
+nginx-ldapauth-proxy:
+ enabled: false
+
+elasticsearch-curator:
+ enabled: false
+
+elasticsearch-exporter:
+ enabled: false
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index 259148fa18f..259148fa18f 100755..100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index a1c2a238a96..a1c2a238a96 100755..100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
diff --git a/vendor/ingress/modsecurity.conf b/vendor/ingress/modsecurity.conf
new file mode 100644
index 00000000000..3a6b5cee2e5
--- /dev/null
+++ b/vendor/ingress/modsecurity.conf
@@ -0,0 +1,274 @@
+# -- GitLab Customization ----------------------------------------------
+# Based on https://github.com/SpiderLabs/ModSecurity/blob/v3.0.3/modsecurity.conf-recommended
+# Our base modsecurity.conf includes some minor customization:
+# - `SecRuleEngine` is disabled, defaulting to `DetectionOnly`. Overridable at project-level
+# - `SecAuditLogType` is disabled, defaulting to `Serial`. Overridable at project-level
+# - `SecStatusEngine` is disabled, to disallow usage reporting
+#
+# ----------------------------------------------------------------------------
+
+# -- Rule engine initialization ----------------------------------------------
+
+# Enable ModSecurity, attaching it to every transaction. Use detection
+# only to start with, because that minimises the chances of post-installation
+# disruption.
+#
+# SecRuleEngine DetectionOnly
+
+
+# -- Request body handling ---------------------------------------------------
+
+# Allow ModSecurity to access request bodies. If you don't, ModSecurity
+# won't be able to see any POST parameters, which opens a large security
+# hole for attackers to exploit.
+#
+SecRequestBodyAccess On
+
+
+# Enable XML request body parser.
+# Initiate XML Processor in case of xml content-type
+#
+SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \
+ "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
+
+# Enable JSON request body parser.
+# Initiate JSON Processor in case of JSON content-type; change accordingly
+# if your application does not use 'application/json'
+#
+SecRule REQUEST_HEADERS:Content-Type "application/json" \
+ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
+
+# Maximum request body size we will accept for buffering. If you support
+# file uploads then the value given on the first line has to be as large
+# as the largest file you are willing to accept. The second value refers
+# to the size of data, with files excluded. You want to keep that value as
+# low as practical.
+#
+SecRequestBodyLimit 13107200
+SecRequestBodyNoFilesLimit 131072
+
+# What do do if the request body size is above our configured limit.
+# Keep in mind that this setting will automatically be set to ProcessPartial
+# when SecRuleEngine is set to DetectionOnly mode in order to minimize
+# disruptions when initially deploying ModSecurity.
+#
+SecRequestBodyLimitAction Reject
+
+# Verify that we've correctly processed the request body.
+# As a rule of thumb, when failing to process a request body
+# you should reject the request (when deployed in blocking mode)
+# or log a high-severity alert (when deployed in detection-only mode).
+#
+SecRule REQBODY_ERROR "!@eq 0" \
+"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
+
+# By default be strict with what we accept in the multipart/form-data
+# request body. If the rule below proves to be too strict for your
+# environment consider changing it to detection-only. You are encouraged
+# _not_ to remove it altogether.
+#
+SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
+"id:'200003',phase:2,t:none,log,deny,status:400, \
+msg:'Multipart request body failed strict validation: \
+PE %{REQBODY_PROCESSOR_ERROR}, \
+BQ %{MULTIPART_BOUNDARY_QUOTED}, \
+BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
+DB %{MULTIPART_DATA_BEFORE}, \
+DA %{MULTIPART_DATA_AFTER}, \
+HF %{MULTIPART_HEADER_FOLDING}, \
+LF %{MULTIPART_LF_LINE}, \
+SM %{MULTIPART_MISSING_SEMICOLON}, \
+IQ %{MULTIPART_INVALID_QUOTING}, \
+IP %{MULTIPART_INVALID_PART}, \
+IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
+FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
+
+# Did we see anything that might be a boundary?
+#
+# Here is a short description about the ModSecurity Multipart parser: the
+# parser returns with value 0, if all "boundary-like" line matches with
+# the boundary string which given in MIME header. In any other cases it returns
+# with different value, eg. 1 or 2.
+#
+# The RFC 1341 descript the multipart content-type and its syntax must contains
+# only three mandatory lines (above the content):
+# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING
+# * --BOUNDARY_STRING
+# * --BOUNDARY_STRING--
+#
+# First line indicates, that this is a multipart content, second shows that
+# here starts a part of the multipart content, third shows the end of content.
+#
+# If there are any other lines, which starts with "--", then it should be
+# another boundary id - or not.
+#
+# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive.
+#
+# If multipart content contains the three necessary lines with correct order, but
+# there are one or more lines with "--", then parser returns with value 2 (non-zero).
+#
+# If some of the necessary lines (usually the start or end) misses, or the order
+# is wrong, then parser returns with value 1 (also a non-zero).
+#
+# You can choose, which one is what you need. The example below contains the
+# 'strict' mode, which means if there are any lines with start of "--", then
+# ModSecurity blocked the content. But the next, commented example contains
+# the 'permissive' mode, then you check only if the necessary lines exists in
+# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."),
+# or other text files, which contains eg. HTTP headers.
+#
+# The difference is only the operator - in strict mode (first) the content blocked
+# in case of any non-zero value. In permissive mode (second, commented) the
+# content blocked only if the value is explicit 1. If it 0 or 2, the content will
+# allowed.
+#
+
+#
+# See #1747 and #1924 for further information on the possible values for
+# MULTIPART_UNMATCHED_BOUNDARY.
+#
+SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
+ "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
+
+
+# PCRE Tuning
+# We want to avoid a potential RegEx DoS condition
+#
+SecPcreMatchLimit 1000
+SecPcreMatchLimitRecursion 1000
+
+# Some internal errors will set flags in TX and we will need to look for these.
+# All of these are prefixed with "MSC_". The following flags currently exist:
+#
+# MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
+#
+SecRule TX:/^MSC_/ "!@streq 0" \
+ "id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'"
+
+
+# -- Response body handling --------------------------------------------------
+
+# Allow ModSecurity to access response bodies.
+# You should have this directive enabled in order to identify errors
+# and data leakage issues.
+#
+# Do keep in mind that enabling this directive does increases both
+# memory consumption and response latency.
+#
+SecResponseBodyAccess On
+
+# Which response MIME types do you want to inspect? You should adjust the
+# configuration below to catch documents but avoid static files
+# (e.g., images and archives).
+#
+SecResponseBodyMimeType text/plain text/html text/xml
+
+# Buffer response bodies of up to 512 KB in length.
+SecResponseBodyLimit 524288
+
+# What happens when we encounter a response body larger than the configured
+# limit? By default, we process what we have and let the rest through.
+# That's somewhat less secure, but does not break any legitimate pages.
+#
+SecResponseBodyLimitAction ProcessPartial
+
+
+# -- Filesystem configuration ------------------------------------------------
+
+# The location where ModSecurity stores temporary files (for example, when
+# it needs to handle a file upload that is larger than the configured limit).
+#
+# This default setting is chosen due to all systems have /tmp available however,
+# this is less than ideal. It is recommended that you specify a location that's private.
+#
+SecTmpDir /tmp/
+
+# The location where ModSecurity will keep its persistent data. This default setting
+# is chosen due to all systems have /tmp available however, it
+# too should be updated to a place that other users can't access.
+#
+SecDataDir /tmp/
+
+
+# -- File uploads handling configuration -------------------------------------
+
+# The location where ModSecurity stores intercepted uploaded files. This
+# location must be private to ModSecurity. You don't want other users on
+# the server to access the files, do you?
+#
+#SecUploadDir /opt/modsecurity/var/upload/
+
+# By default, only keep the files that were determined to be unusual
+# in some way (by an external inspection script). For this to work you
+# will also need at least one file inspection rule.
+#
+#SecUploadKeepFiles RelevantOnly
+
+# Uploaded files are by default created with permissions that do not allow
+# any other user to access them. You may need to relax that if you want to
+# interface ModSecurity to an external program (e.g., an anti-virus).
+#
+#SecUploadFileMode 0600
+
+
+# -- Debug log configuration -------------------------------------------------
+
+# The default debug log configuration is to duplicate the error, warning
+# and notice messages from the error log.
+#
+#SecDebugLog /opt/modsecurity/var/log/debug.log
+#SecDebugLogLevel 3
+
+
+# -- Audit log configuration -------------------------------------------------
+
+# Log the transactions that are marked by a rule, as well as those that
+# trigger a server error (determined by a 5xx or 4xx, excluding 404,
+# level response status codes).
+#
+SecAuditEngine RelevantOnly
+SecAuditLogRelevantStatus "^(?:5|4(?!04))"
+
+# Log everything we know about a transaction.
+SecAuditLogParts ABIJDEFHZ
+
+# Use a single file for logging. This is much easier to look at, but
+# assumes that you will use the audit log only ocassionally.
+#
+# SecAuditLogType Serial
+SecAuditLogFormat JSON
+SecAuditLog /var/log/modsec/audit.log
+
+# Specify the path for concurrent audit logging.
+#SecAuditLogStorageDir /opt/modsecurity/var/audit/
+
+
+# -- Miscellaneous -----------------------------------------------------------
+
+# Use the most commonly used application/x-www-form-urlencoded parameter
+# separator. There's probably only one application somewhere that uses
+# something else so don't expect to change this value.
+#
+SecArgumentSeparator &
+
+# Settle on version 0 (zero) cookies, as that is what most applications
+# use. Using an incorrect cookie version may open your installation to
+# evasion attacks (against the rules that examine named cookies).
+#
+SecCookieFormat 0
+
+# Specify your Unicode Code Point.
+# This mapping is used by the t:urlDecodeUni transformation function
+# to properly map encoded data to your language. Properly setting
+# these directives helps to reduce false positives and negatives.
+#
+SecUnicodeMapFile unicode.mapping 20127
+
+# Improve the quality of ModSecurity by sharing information about your
+# current ModSecurity version and dependencies versions.
+# The following information will be shared: ModSecurity version,
+# Web Server version, APR version, PCRE version, Lua version, Libxml2
+# version, Anonymous unique id for host.
+# SecStatusEngine On
+
+
diff --git a/vendor/project_templates/serverless_framework.tar.gz b/vendor/project_templates/serverless_framework.tar.gz
new file mode 100644
index 00000000000..b09de0ec3a2
--- /dev/null
+++ b/vendor/project_templates/serverless_framework.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 3abd18d1111..3b359ee98e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,7 +9,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
-"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.6.2":
+"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.2.tgz#069a776e8d5e9eefff76236bc8845566bd31dd91"
integrity sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ==
@@ -29,7 +29,7 @@
semver "^5.4.1"
source-map "^0.5.0"
-"@babel/generator@^7.1.3", "@babel/generator@^7.4.0", "@babel/generator@^7.6.2":
+"@babel/generator@^7.4.0", "@babel/generator@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03"
integrity sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==
@@ -54,14 +54,6 @@
"@babel/helper-explode-assignable-expression" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-builder-react-jsx@^7.3.0":
- version "7.3.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz#a1ac95a5d2b3e88ae5e54846bf462eeb81b318a4"
- integrity sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==
- dependencies:
- "@babel/types" "^7.3.0"
- esutils "^2.0.0"
-
"@babel/helper-call-delegate@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz#87c1f8ca19ad552a736a7a27b1c1fcf8b1ff1f43"
@@ -71,7 +63,7 @@
"@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.4"
-"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.5", "@babel/helper-create-class-features-plugin@^7.6.0":
+"@babel/helper-create-class-features-plugin@^7.5.5", "@babel/helper-create-class-features-plugin@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.6.0.tgz#769711acca889be371e9bc2eb68641d55218021f"
integrity sha512-O1QWBko4fzGju6VoVvrZg0RROCVifcLxiApnGP3OWfWzvxRZFCoBD81K5ur5e3bVY2Vf/5rIJm8cqPKn8HUJng==
@@ -204,7 +196,7 @@
dependencies:
"@babel/types" "^7.4.4"
-"@babel/helper-wrap-function@^7.1.0", "@babel/helper-wrap-function@^7.2.0":
+"@babel/helper-wrap-function@^7.1.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
@@ -232,11 +224,6 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
-"@babel/parser@7.1.3":
- version "7.1.3"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.3.tgz#2c92469bac2b7fbff810b67fca07bd138b48af77"
- integrity sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==
-
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1"
@@ -251,7 +238,7 @@
"@babel/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
-"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.5.5":
+"@babel/plugin-proposal-class-properties@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4"
integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==
@@ -259,23 +246,6 @@
"@babel/helper-create-class-features-plugin" "^7.5.5"
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-proposal-decorators@^7.1.2":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0"
- integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw==
- dependencies:
- "@babel/helper-create-class-features-plugin" "^7.4.4"
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-decorators" "^7.2.0"
-
-"@babel/plugin-proposal-do-expressions@^7.0.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-do-expressions/-/plugin-proposal-do-expressions-7.5.0.tgz#ceb594d4a618545b00aa0b5cd61cad4aaaeb7a5a"
- integrity sha512-xe0QQrhm+DGj6H23a6XtwkJNimy1fo71O/YVBfrfvfSl0fsq9T9dfoQBIY4QceEIdUo7u9s7OPEdsWEuizfGeg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-do-expressions" "^7.2.0"
-
"@babel/plugin-proposal-dynamic-import@^7.5.0":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506"
@@ -284,40 +254,7 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-dynamic-import" "^7.2.0"
-"@babel/plugin-proposal-export-default-from@^7.0.0":
- version "7.5.2"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.5.2.tgz#2c0ac2dcc36e3b2443fead2c3c5fc796fb1b5145"
- integrity sha512-wr9Itk05L1/wyyZKVEmXWCdcsp/e185WUNl6AfYZeEKYaUPPvHXRDqO5K1VH7/UamYqGJowFRuCv30aDYZawsg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-export-default-from" "^7.2.0"
-
-"@babel/plugin-proposal-export-namespace-from@^7.0.0":
- version "7.5.2"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.5.2.tgz#ccd5ed05b06d700688ff1db01a9dd27155e0d2a0"
- integrity sha512-TKUdOL07anjZEbR1iSxb5WFh810KyObdd29XLFLGo1IDsSuGrjH3ouWSbAxHNmrVKzr9X71UYl2dQ7oGGcRp0g==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-export-namespace-from" "^7.2.0"
-
-"@babel/plugin-proposal-function-bind@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz#94dc2cdc505cafc4e225c0014335a01648056bf7"
- integrity sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-function-bind" "^7.2.0"
-
-"@babel/plugin-proposal-function-sent@^7.1.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-function-sent/-/plugin-proposal-function-sent-7.5.0.tgz#39233aa801145e7d8072077cdb2d25f781c1ffd7"
- integrity sha512-JXdfiQpKoC6UgQliZkp3NX7K3MVec1o1nfTWiCCIORE5ag/QZXhL0aSD8/Y2K+hIHonSTxuJF9rh9zsB6hBi2A==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-wrap-function" "^7.2.0"
- "@babel/plugin-syntax-function-sent" "^7.2.0"
-
-"@babel/plugin-proposal-json-strings@^7.0.0", "@babel/plugin-proposal-json-strings@^7.2.0":
+"@babel/plugin-proposal-json-strings@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
@@ -325,30 +262,6 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-json-strings" "^7.2.0"
-"@babel/plugin-proposal-logical-assignment-operators@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.2.0.tgz#8a5cea6c42a7c87446959e02fff5fad012c56f57"
- integrity sha512-0w797xwdPXKk0m3Js74hDi0mCTZplIu93MOSfb1ZLd/XFe3abWypx1QknVk0J+ohnsjYpvjH4Gwfo2i3RicB6Q==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-logical-assignment-operators" "^7.2.0"
-
-"@babel/plugin-proposal-nullish-coalescing-operator@^7.0.0":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.4.4.tgz#41c360d59481d88e0ce3a3f837df10121a769b39"
- integrity sha512-Amph7Epui1Dh/xxUxS2+K22/MUi6+6JVTvy3P58tja3B6yKTSjwwx0/d83rF7551D6PVSSoplQb8GCwqec7HRw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-nullish-coalescing-operator" "^7.2.0"
-
-"@babel/plugin-proposal-numeric-separator@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.2.0.tgz#646854daf4cd22fd6733f6076013a936310443ac"
- integrity sha512-DohMOGDrZiMKS7LthjUZNNcWl8TAf5BZDwZAH4wpm55FuJTHgfqPGdibg7rZDmont/8Yg0zA03IgT6XLeP+4sg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-numeric-separator" "^7.2.0"
-
"@babel/plugin-proposal-object-rest-spread@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz#8ffccc8f3a6545e9f78988b6bf4fe881b88e8096"
@@ -365,22 +278,6 @@
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-"@babel/plugin-proposal-optional-chaining@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.2.0.tgz#ae454f4c21c6c2ce8cb2397dc332ae8b420c5441"
- integrity sha512-ea3Q6edZC/55wEBVZAEz42v528VulyO0eir+7uky/sT4XRcdkWJcFi1aPtitTlwUzGnECWJNExWww1SStt+yWw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-optional-chaining" "^7.2.0"
-
-"@babel/plugin-proposal-pipeline-operator@^7.0.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-pipeline-operator/-/plugin-proposal-pipeline-operator-7.5.0.tgz#4100ec55ef4f6a4c2490b5f5a4f2a22dfa272c06"
- integrity sha512-HFYuu/yGnkn69ligXxU0ohOVvQDsMNOUJs/c4PYLUVS6ntCYOyGmRQQaSYJARJ9rvc7/ulZKIzxd4wk91hN63A==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-pipeline-operator" "^7.5.0"
-
"@babel/plugin-proposal-private-methods@^7.6.0":
version "7.6.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.6.0.tgz#19ddc493c7b5d47afdd4291e740c609a83c9fae4"
@@ -389,14 +286,6 @@
"@babel/helper-create-class-features-plugin" "^7.6.0"
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-proposal-throw-expressions@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-throw-expressions/-/plugin-proposal-throw-expressions-7.2.0.tgz#2d9e452d370f139000e51db65d0a85dc60c64739"
- integrity sha512-adsydM8DQF4i5DLNO4ySAU5VtHTPewOtNBV3u7F4lNMPADFF9bWQ+iDtUUe8+033cYCUz+bFlQdXQJmJOwoLpw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-throw-expressions" "^7.2.0"
-
"@babel/plugin-proposal-unicode-property-regex@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz#05413762894f41bfe42b9a5e80919bd575dcc802"
@@ -413,63 +302,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-syntax-decorators@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz#c50b1b957dcc69e4b1127b65e1c33eef61570c1b"
- integrity sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-do-expressions@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-do-expressions/-/plugin-syntax-do-expressions-7.2.0.tgz#f3d4b01be05ecde2892086d7cfd5f1fa1ead5a2a"
- integrity sha512-/u4rJ+XEmZkIhspVuKRS+7WLvm7Dky9j9TvGK5IgId8B3FKir9MG+nQxDZ9xLn10QMBvW58dZ6ABe2juSmARjg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.2.0":
+"@babel/plugin-syntax-dynamic-import@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-syntax-export-default-from@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.2.0.tgz#edd83b7adc2e0d059e2467ca96c650ab6d2f3820"
- integrity sha512-c7nqUnNST97BWPtoe+Ssi+fJukc9P9/JMZ71IOMNQWza2E+Psrd46N6AEvtw6pqK+gt7ChjXyrw4SPDO79f3Lw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-export-namespace-from@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.2.0.tgz#8d257838c6b3b779db52c0224443459bd27fb039"
- integrity sha512-1zGA3UNch6A+A11nIzBVEaE3DDJbjfB+eLIcf0GGOh/BJr/8NxL3546MGhV/r0RhH4xADFIEso39TKCfEMlsGA==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-flow@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.2.0.tgz#a765f061f803bc48f240c26f8747faf97c26bf7c"
- integrity sha512-r6YMuZDWLtLlu0kqIim5o/3TNRAlWb073HwT3e2nKf9I8IIvOggPrnILYPsrrKilmn/mYEMCf/Z07w3yQJF6dg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-function-bind@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz#68fe85b0c0da67125f87bf239c68051b06c66309"
- integrity sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-function-sent@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-function-sent/-/plugin-syntax-function-sent-7.2.0.tgz#91474d4d400604e4c6cbd4d77cd6cb3b8565576c"
- integrity sha512-2MOVuJ6IMAifp2cf0RFkHQaOvHpbBYyWCvgtF/WVqXhTd7Bgtov8iXVCadLXp2FN1BrI2EFl+JXuwXy0qr3KoQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-import-meta@^7.0.0", "@babel/plugin-syntax-import-meta@^7.2.0":
+"@babel/plugin-syntax-import-meta@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40"
integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA==
@@ -483,34 +323,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-syntax-jsx@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz#0b85a3b4bc7cdf4cc4b8bf236335b907ca22e7c7"
- integrity sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-logical-assignment-operators@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.2.0.tgz#fcab7388530e96c6f277ce494c55caa6c141fcfb"
- integrity sha512-l/NKSlrnvd73/EL540t9hZhcSo4TULBrIPs9Palju8Oc/A8DXDO+xQf04whfeuZLpi8AuIvCAdpKmmubLN4EfQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-nullish-coalescing-operator@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.2.0.tgz#f75083dfd5ade73e783db729bbd87e7b9efb7624"
- integrity sha512-lRCEaKE+LTxDQtgbYajI04ddt6WW0WJq57xqkAZ+s11h4YgfRHhVA/Y2VhfPzzFD4qeLHWg32DMp9HooY4Kqlg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-numeric-separator@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.2.0.tgz#7470fe070c2944469a756752a69a6963135018be"
- integrity sha512-DroeVNkO/BnGpL2R7+ZNZqW+E24aR/4YWxP3Qb15d6lPU8KDzF8HlIUIRCOJRn4X77/oyW4mJY+7FHfY82NLtQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
@@ -525,27 +337,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-syntax-optional-chaining@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.2.0.tgz#a59d6ae8c167e7608eaa443fda9fa8fa6bf21dff"
- integrity sha512-HtGCtvp5Uq/jH/WNUPkK6b7rufnCPLLlDAFN7cmACoIjaOOiXxUt3SswU5loHqrhtqTsa/WoLQ1OQ1AGuZqaWA==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-pipeline-operator@^7.5.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-pipeline-operator/-/plugin-syntax-pipeline-operator-7.5.0.tgz#8ea7c2c22847c797748bf07752722a317079dc1e"
- integrity sha512-5FVxPiMTMXWk4R7Kq9pt272nDu8VImJdaIzvXFSTcXFbgKWWaOdbic12TvUvl6cK+AE5EgnhwvxuWik4ZYYdzg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-throw-expressions@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-throw-expressions/-/plugin-syntax-throw-expressions-7.2.0.tgz#79001ee2afe1b174b1733cdc2fc69c9a46a0f1f8"
- integrity sha512-ngwynuqu1Rx0JUS9zxSDuPgW1K8TyVZCi2hHehrL4vyjqE7RGoNHWlZsS7KQT2vw9Yjk4YLa0+KldBXTRdPLRg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
"@babel/plugin-transform-arrow-functions@^7.2.0":
version "7.2.0"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
@@ -629,14 +420,6 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-flow-strip-types@^7.0.0":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.4.4.tgz#d267a081f49a8705fc9146de0768c6b58dccd8f7"
- integrity sha512-WyVedfeEIILYEaWGAUWzVNyqG4sfsNooMhXWsu/YzOvVGcsnPb5PguysjJqI3t3qiaYj0BR8T2f5njdjTGe44Q==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-flow" "^7.2.0"
-
"@babel/plugin-transform-for-of@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz#0267fc735e24c808ba173866c6c4d1440fc3c556"
@@ -740,38 +523,6 @@
dependencies:
"@babel/helper-plugin-utils" "^7.0.0"
-"@babel/plugin-transform-react-display-name@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz#ebfaed87834ce8dc4279609a4f0c324c156e3eb0"
- integrity sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-transform-react-jsx-self@^7.0.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz#461e21ad9478f1031dd5e276108d027f1b5240ba"
- integrity sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-jsx" "^7.2.0"
-
-"@babel/plugin-transform-react-jsx-source@^7.0.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz#583b10c49cf057e237085bcbd8cc960bd83bd96b"
- integrity sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-jsx" "^7.2.0"
-
-"@babel/plugin-transform-react-jsx@^7.0.0":
- version "7.3.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz#f2cab99026631c767e2745a5368b331cfe8f5290"
- integrity sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==
- dependencies:
- "@babel/helper-builder-react-jsx" "^7.3.0"
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-jsx" "^7.2.0"
-
"@babel/plugin-transform-regenerator@^7.4.5":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz#629dc82512c55cee01341fb27bdfcb210354680f"
@@ -832,7 +583,7 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.6.0"
-"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.6.2":
+"@babel/preset-env@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.6.2.tgz#abbb3ed785c7fe4220d4c82a53621d71fc0c75d3"
integrity sha512-Ru7+mfzy9M1/YTEtlDS8CD45jd22ngb9tXnn64DvQK3ooyqSw9K4K9DUWmYknTTVk4TqygL9dqCrZgm1HMea/Q==
@@ -888,30 +639,6 @@
js-levenshtein "^1.1.3"
semver "^5.5.0"
-"@babel/preset-flow@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.0.0.tgz#afd764835d9535ec63d8c7d4caf1c06457263da2"
- integrity sha512-bJOHrYOPqJZCkPVbG1Lot2r5OSsB+iUOaxiHdlOeB1yPWS6evswVHwvkDLZ54WTaTRIk89ds0iHmGZSnxlPejQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-transform-flow-strip-types" "^7.0.0"
-
-"@babel/preset-react@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.0.0.tgz#e86b4b3d99433c7b3e9e91747e2653958bc6b3c0"
- integrity sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-transform-react-display-name" "^7.0.0"
- "@babel/plugin-transform-react-jsx" "^7.0.0"
- "@babel/plugin-transform-react-jsx-self" "^7.0.0"
- "@babel/plugin-transform-react-jsx-source" "^7.0.0"
-
-"@babel/preset-stage-0@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/preset-stage-0/-/preset-stage-0-7.0.0.tgz#999aaec79ee8f0a763042c68c06539c97c6e0646"
- integrity sha512-FBMd0IiARPtH5aaOFUVki6evHiJQiY0pFy7fizyRF7dtwc+el3nwpzvhb9qBNzceG1OIJModG1xpE0DDFjPXwA==
-
"@babel/standalone@^7.0.0":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.5.5.tgz#9d3143f6078ff408db694a4254bd6f03c5c33962"
@@ -926,7 +653,7 @@
"@babel/parser" "^7.6.0"
"@babel/types" "^7.6.0"
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.4", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.2":
+"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.5.5", "@babel/traverse@^7.6.2":
version "7.6.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c"
integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==
@@ -941,7 +668,7 @@
globals "^11.1.0"
lodash "^4.17.13"
-"@babel/types@^7.0.0", "@babel/types@^7.1.3", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0":
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5", "@babel/types@^7.6.0":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648"
integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==
@@ -990,15 +717,15 @@
dependencies:
vue-eslint-parser "^6.0.4"
-"@gitlab/svgs@^1.78.0":
- version "1.78.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.78.0.tgz#469493bd6cdd254eb5d1271edeab22bbbee2f4c4"
- integrity sha512-dBgEB/Q4FRD0NapmNrD86DF1FsV0uSgTx0UOJloHnGE2DNR2P1HQrCmLW2fX+QgN4P9CDAzdi2buVHuholofWw==
+"@gitlab/svgs@^1.82.0":
+ version "1.82.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.82.0.tgz#c059c460afc13ebfe9df370521ca8963fa5afb80"
+ integrity sha512-9L4Brys2LCk44lHvFsCFDKN768lYjoMVYDb4PD7FSjqUEruQQ1SRj0rvb1RWKLhiTCDKrtDOXkH6I1TTEms24w==
-"@gitlab/ui@5.36.0":
- version "5.36.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.36.0.tgz#3087b23c138ad1c222f6b047e533f253371bc618"
- integrity sha512-XXWUYZbRItKh9N92Vxql04BJ05uW5HlOuTCkD+lMbUgneqYTgVoKGH8d9kD++Jy7q8l5+AfzjboUn2n9sbQMZA==
+"@gitlab/ui@7.11.0":
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-7.11.0.tgz#b5c981f3b1edbf0ad75bcca8fa1cd81017676b3b"
+ integrity sha512-PxZkgdY2j/XdriTdp3jsnsif9cgcxd1wUF8PVOho2HIyJqU244E8ELewIXkDozQq3p3ZXzWnjR/GvYcNMZtGmA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -1013,10 +740,10 @@
vue "^2.6.10"
vue-loader "^15.4.2"
-"@gitlab/visual-review-tools@1.0.3":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.0.3.tgz#b49c4a6fd8af3a1517d7e7d04096562f8bcb5d14"
- integrity sha512-96j+0+Ivon5nYvT2doDCLQoBzU/GZYfQGLBmZZE3FZVMsIPAEsqDcSV/6+XCikUzU3B8VnH6er6l9OxE5x1RVw==
+"@gitlab/visual-review-tools@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.2.0.tgz#8d6757917193c1023012bb4a316dc1a97309a27a"
+ integrity sha512-GaV/lYLmOF0hWtv8K8MLWGaCZ7PL1LF4D0/gargXYf9HO0Cw4wtz4oWyaLS15wFposJIYdPIHSNfrLVk4Dk9sQ==
"@gitlab/vue-toasted@^1.2.1":
version "1.2.1"
@@ -1191,6 +918,63 @@
consola "^2.3.0"
node-fetch "^2.3.0"
+"@sentry/browser@^5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.7.1.tgz#1f8435e2a325d7a09f830065ebce40a2b3c708a4"
+ integrity sha512-K0x1XhsHS8PPdtlVOLrKZyYvi5Vexs9WApdd214bO6KaGF296gJvH1mG8XOY0+7aA5i2A7T3ttcaJNDYS49lzw==
+ dependencies:
+ "@sentry/core" "5.7.1"
+ "@sentry/types" "5.7.1"
+ "@sentry/utils" "5.7.1"
+ tslib "^1.9.3"
+
+"@sentry/core@5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.7.1.tgz#3eb2b7662cac68245931ee939ec809bf7a639d0e"
+ integrity sha512-AOn3k3uVWh2VyajcHbV9Ta4ieDIeLckfo7UMLM+CTk2kt7C89SayDGayJMSsIrsZlL4qxBoLB9QY4W2FgAGJrg==
+ dependencies:
+ "@sentry/hub" "5.7.1"
+ "@sentry/minimal" "5.7.1"
+ "@sentry/types" "5.7.1"
+ "@sentry/utils" "5.7.1"
+ tslib "^1.9.3"
+
+"@sentry/hub@5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.7.1.tgz#a52acd9fead7f3779d96e9965c6978aecc8b9cad"
+ integrity sha512-evGh323WR073WSBCg/RkhlUmCQyzU0xzBzCZPscvcoy5hd4SsLE6t9Zin+WACHB9JFsRQIDwNDn+D+pj3yKsig==
+ dependencies:
+ "@sentry/types" "5.7.1"
+ "@sentry/utils" "5.7.1"
+ tslib "^1.9.3"
+
+"@sentry/minimal@5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.7.1.tgz#56afc537737586929e25349765e37a367958c1e1"
+ integrity sha512-nS/Dg+jWAZtcxQW8wKbkkw4dYvF6uyY/vDiz/jFCaux0LX0uhgXAC9gMOJmgJ/tYBLJ64l0ca5LzpZa7BMJQ0g==
+ dependencies:
+ "@sentry/hub" "5.7.1"
+ "@sentry/types" "5.7.1"
+ tslib "^1.9.3"
+
+"@sentry/types@5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.7.1.tgz#4c4c1d4d891b6b8c2c3c7b367d306a8b1350f090"
+ integrity sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==
+
+"@sentry/utils@5.7.1":
+ version "5.7.1"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.7.1.tgz#cf37ad55f78e317665cd8680f202d307fa77f1d0"
+ integrity sha512-nhirUKj/qFLsR1i9kJ5BRvNyzdx/E2vorIsukuDrbo8e3iZ11JMgCOVrmC8Eq9YkHBqgwX4UnrPumjFyvGMZ2Q==
+ dependencies:
+ "@sentry/types" "5.7.1"
+ tslib "^1.9.3"
+
+"@sourcegraph/code-host-integration@^0.0.13":
+ version "0.0.13"
+ resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.13.tgz#4fd5fe1e0088c63b2a26be231c5a2a4ca79b1596"
+ integrity sha512-IjF9gb9e8dG8p12DKg5Z7UMOVQO/ClH3AyMCPfX/qH7DH/0b55WH6stYVqZu6y776quFonO4Z9gWYM8pQZjzKw==
+
"@types/anymatch@*":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff"
@@ -1273,7 +1057,7 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
-"@types/node@*", "@types/node@^10.11.7":
+"@types/node@*", "@types/node@>=6", "@types/node@^10.11.7":
version "10.12.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.9.tgz#a07bfa74331471e1dc22a47eb72026843f7b95c8"
integrity sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==
@@ -1305,7 +1089,7 @@
dependencies:
source-map "^0.6.1"
-"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
+"@types/unist@*", "@types/unist@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
@@ -1531,6 +1315,21 @@
"@webassemblyjs/wast-parser" "1.8.5"
"@xtuc/long" "4.2.2"
+"@wry/context@^0.4.0":
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.4.4.tgz#e50f5fa1d6cfaabf2977d1fda5ae91717f8815f8"
+ integrity sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==
+ dependencies:
+ "@types/node" ">=6"
+ tslib "^1.9.3"
+
+"@wry/equality@^0.1.2":
+ version "0.1.9"
+ resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909"
+ integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==
+ dependencies:
+ tslib "^1.9.3"
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -1546,14 +1345,6 @@
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
-JSONStream@^1.0.3:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
- integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
- dependencies:
- jsonparse "^1.2.0"
- through ">=2.2.7 <3"
-
abab@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f"
@@ -1590,7 +1381,7 @@ acorn-walk@^6.0.1, acorn-walk@^6.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
-acorn@^5.2.1, acorn@^5.5.3:
+acorn@^5.5.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
@@ -1647,7 +1438,7 @@ ansi-escapes@^3.0.0:
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
-ansi-html@0.0.7, ansi-html@^0.0.7:
+ansi-html@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
@@ -1695,37 +1486,36 @@ anymatch@^3.0.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
-apollo-cache-inmemory@^1.5.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.5.1.tgz#265d1ee67b0bf0aca9c37629d410bfae44e62953"
- integrity sha512-D3bdpPmWfaKQkWy8lfwUg+K8OBITo3sx0BHLs1B/9vIdOIZ7JNCKq3EUcAgAfInomJUdN0QG1yOfi8M8hxkN1g==
+apollo-cache-inmemory@^1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz#826861d20baca4abc45f7ca7a874105905b8525d"
+ integrity sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg==
dependencies:
- apollo-cache "^1.2.1"
- apollo-utilities "^1.2.1"
- optimism "^0.6.9"
- ts-invariant "^0.2.1"
+ apollo-cache "^1.3.2"
+ apollo-utilities "^1.3.2"
+ optimism "^0.10.0"
+ ts-invariant "^0.4.0"
tslib "^1.9.3"
-apollo-cache@1.2.1, apollo-cache@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.2.1.tgz#aae71eb4a11f1f7322adc343f84b1a39b0693644"
- integrity sha512-nzFmep/oKlbzUuDyz6fS6aYhRmfpcHWqNkkA9Bbxwk18RD6LXC4eZkuE0gXRX0IibVBHNjYVK+Szi0Yied4SpQ==
+apollo-cache@1.3.2, apollo-cache@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-cache/-/apollo-cache-1.3.2.tgz#df4dce56240d6c95c613510d7e409f7214e6d26a"
+ integrity sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==
dependencies:
- apollo-utilities "^1.2.1"
+ apollo-utilities "^1.3.2"
tslib "^1.9.3"
-apollo-client@^2.5.1:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.5.1.tgz#36126ed1d32edd79c3713c6684546a3bea80e6d1"
- integrity sha512-MNcQKiqLHdGmNJ0rZ0NXaHrToXapJgS/5kPk0FygXt+/FmDCdzqcujI7OPxEC6e9Yw5S/8dIvOXcRNuOMElHkA==
+apollo-client@^2.6.4:
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/apollo-client/-/apollo-client-2.6.4.tgz#872c32927263a0d34655c5ef8a8949fbb20b6140"
+ integrity sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ==
dependencies:
"@types/zen-observable" "^0.8.0"
- apollo-cache "1.2.1"
+ apollo-cache "1.3.2"
apollo-link "^1.0.0"
- apollo-link-dedup "^1.0.0"
- apollo-utilities "1.2.1"
+ apollo-utilities "1.3.2"
symbol-observable "^1.0.2"
- ts-invariant "^0.2.1"
+ ts-invariant "^0.4.0"
tslib "^1.9.3"
zen-observable "^0.8.0"
@@ -1747,13 +1537,6 @@ apollo-link-batch@^1.1.12:
apollo-link "^1.2.11"
tslib "^1.9.3"
-apollo-link-dedup@^1.0.0:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae"
- integrity sha512-tpUI9lMZsidxdNygSY1FxflXEkUZnvKRkMUsXXuQUNoSLeNtEvUX7QtKRAl4k9ubLl8JKKc9X3L3onAFeGTK8w==
- dependencies:
- apollo-link "^1.2.3"
-
apollo-link-http-common@^0.2.13, apollo-link-http-common@^0.2.8:
version "0.2.13"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350"
@@ -1763,7 +1546,7 @@ apollo-link-http-common@^0.2.13, apollo-link-http-common@^0.2.8:
ts-invariant "^0.3.2"
tslib "^1.9.3"
-apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.3, apollo-link@^1.2.6:
+apollo-link@^1.0.0, apollo-link@^1.2.11, apollo-link@^1.2.6:
version "1.2.11"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.11.tgz#493293b747ad3237114ccd22e9f559e5e24a194d"
integrity sha512-PQvRCg13VduLy3X/0L79M6uOpTh5iHdxnxYuo8yL7sJlWybKRJwsv4IcRBJpMFbChOOaHY7Og9wgPo6DLKDKDA==
@@ -1782,22 +1565,16 @@ apollo-upload-client@^10.0.0:
apollo-link-http-common "^0.2.8"
extract-files "^5.0.0"
-apollo-utilities@1.2.1, apollo-utilities@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.2.1.tgz#1c3a1ebf5607d7c8efe7636daaf58e7463b41b3c"
- integrity sha512-Zv8Udp9XTSFiN8oyXOjf6PMHepD4yxxReLsl6dPUy5Ths7jti3nmlBzZUOxuTWRwZn0MoclqL7RQ5UEJN8MAxg==
+apollo-utilities@1.3.2, apollo-utilities@^1.2.1, apollo-utilities@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.2.tgz#8cbdcf8b012f664cd6cb5767f6130f5aed9115c9"
+ integrity sha512-JWNHj8XChz7S4OZghV6yc9FNnzEXj285QYp/nLNh943iObycI5GTDO3NGR9Dth12LRrSFMeDOConPfPln+WGfg==
dependencies:
+ "@wry/equality" "^0.1.2"
fast-json-stable-stringify "^2.0.0"
- ts-invariant "^0.2.1"
+ ts-invariant "^0.4.0"
tslib "^1.9.3"
-append-buffer@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1"
- integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=
- dependencies:
- buffer-equal "^1.0.0"
-
append-transform@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab"
@@ -2110,11 +1887,6 @@ babel-preset-jest@^24.6.0:
"@babel/plugin-syntax-object-rest-spread" "^7.0.0"
babel-plugin-jest-hoist "^24.6.0"
-babelify@^10.0.0:
- version "10.0.0"
- resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5"
- integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg==
-
babylon@7.0.0-beta.19:
version "7.0.0-beta.19"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503"
@@ -2250,16 +2022,6 @@ body-parser@1.19.0, body-parser@^1.16.1:
raw-body "2.4.0"
type-is "~1.6.17"
-body@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069"
- integrity sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=
- dependencies:
- continuable-cache "^0.3.1"
- error "^7.0.0"
- raw-body "~1.1.0"
- safe-json-parse "~1.0.1"
-
bonjour@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@@ -2342,7 +2104,7 @@ browser-process-hrtime@^0.1.2:
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4"
integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==
-browser-resolve@^1.11.3, browser-resolve@^1.7.0:
+browser-resolve@^1.11.3:
version "1.11.3"
resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6"
integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==
@@ -2430,11 +2192,6 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
-buffer-equal@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
- integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74=
-
buffer-from@1.x, buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -2450,11 +2207,6 @@ buffer-json@^2.0.0:
resolved "https://registry.yarnpkg.com/buffer-json/-/buffer-json-2.0.0.tgz#f73e13b1e42f196fe2fd67d001c7d7107edd7c23"
integrity sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==
-buffer-shims@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
- integrity sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=
-
buffer-xor@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
@@ -2474,11 +2226,6 @@ builtin-status-codes@^3.0.0:
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
-bytes@1:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
- integrity sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=
-
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@@ -2557,11 +2304,6 @@ cache-loader@^4.1.0:
neo-async "^2.6.1"
schema-utils "^2.0.0"
-cached-path-relative@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db"
- integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==
-
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
@@ -2687,7 +2429,7 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.3.tgz#f1cec43f332e2ea5a569fd46f9f5bde4e6102aff"
integrity sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw==
-chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
+chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2909,11 +2651,6 @@ cliui@^5.0.0:
strip-ansi "^5.2.0"
wrap-ansi "^5.1.0"
-clone-buffer@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
- integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
-
clone-deep@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
@@ -2930,25 +2667,6 @@ clone-regexp@^2.1.0:
dependencies:
is-regexp "^2.0.0"
-clone-stats@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
- integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=
-
-clone@^2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
- integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
-
-cloneable-readable@^1.0.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec"
- integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==
- dependencies:
- inherits "^2.0.1"
- process-nextick-args "^2.0.0"
- readable-stream "^2.3.5"
-
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -2978,7 +2696,7 @@ codesandbox-import-utils@^1.2.3:
istextorbinary "^2.2.1"
lz-string "^1.4.4"
-collapse-white-space@^1.0.0, collapse-white-space@^1.0.2:
+collapse-white-space@^1.0.2:
version "1.0.5"
resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.5.tgz#c2495b699ab1ed380d29a1091e01063e75dbbe3a"
integrity sha512-703bOOmytCYAX9cXYqoikYIx6twmFCXsnzRQheBcTG3nzKYBR4P/+wkYeH+Mvj7qUz8zZDtdyzbxfnEi/kYzRQ==
@@ -2991,7 +2709,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
-color-convert@^0.5.3:
+color-convert@^0.5.3, color-convert@~0.5.0:
version "0.5.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
integrity sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=
@@ -3020,11 +2738,6 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
-comma-separated-tokens@^1.0.1:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.7.tgz#419cd7fb3258b1ed838dc0953167a25e152f5b59"
- integrity sha512-Jrx3xsP4pPv4AwJUDWY9wOXGtwPXARej6Xd99h4TUGotmf8APuquKMpK+dnD3UgyxK7OEWaisjZz+3b5jtL6xQ==
-
commander@2, commander@^2.10.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0:
version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
@@ -3099,7 +2812,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-stream@^1.5.0, concat-stream@^1.6.0:
+concat-stream@^1.5.0:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -3109,15 +2822,6 @@ concat-stream@^1.5.0, concat-stream@^1.6.0:
readable-stream "^2.2.2"
typedarray "^0.0.6"
-concat-stream@~1.5.0:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
- integrity sha1-cIl4Yk2FavQaWnQd790mHadSwmY=
- dependencies:
- inherits "~2.0.1"
- readable-stream "~2.0.0"
- typedarray "~0.0.5"
-
config-chain@^1.1.12:
version "1.1.12"
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa"
@@ -3199,12 +2903,7 @@ content-type@~1.0.4:
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-continuable-cache@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f"
- integrity sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=
-
-convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
+convert-source-map@^1.1.0, convert-source-map@^1.4.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
@@ -3406,6 +3105,13 @@ crypto-random-string@^1.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
+crypto-random-string@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-3.0.1.tgz#29d7dc759d577a768afb3b7b2765dd9bd7ffe36a"
+ integrity sha512-dUL0cJ4PBLanJGJQBHQUkvZ3C4q13MXzl54oRqAIiJGiNkOZ4JDwkg/SBo7daGghzlJv16yW1p/4lIQukmbedA==
+ dependencies:
+ type-fest "^0.5.2"
+
css-b64-images@~0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/css-b64-images/-/css-b64-images-0.2.5.tgz#42005d83204b2b4a5d93b6b1a5644133b5927a02"
@@ -3468,6 +3174,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+cssfontparser@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3"
+ integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=
+
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
version "0.3.4"
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"
@@ -3777,10 +3488,10 @@ d3@^4.13.0:
d3-voronoi "1.1.2"
d3-zoom "1.7.1"
-d3@^5.7.0:
- version "5.9.2"
- resolved "https://registry.yarnpkg.com/d3/-/d3-5.9.2.tgz#64e8a7e9c3d96d9e6e4999d2c8a2c829767e67f5"
- integrity sha512-ydrPot6Lm3nTWH+gJ/Cxf3FcwuvesYQ5uk+j/kXEH/xbuYWYWTMAHTJQkyeuG8Y5WM5RSEYB41EctUrXQQytRQ==
+d3@^5.12, d3@^5.7.0:
+ version "5.12.0"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-5.12.0.tgz#0ddeac879c28c882317cd439b495290acd59ab61"
+ integrity sha512-flYVMoVuhPFHd9zVCe2BxIszUWqBcd5fvQGMNRmSiBrgdnh6Vlruh60RJQTouAK9xPbOB0plxMvBm4MoyODXNg==
dependencies:
d3-array "1"
d3-axis "1"
@@ -3814,21 +3525,22 @@ d3@^5.7.0:
d3-voronoi "1"
d3-zoom "1"
-dagre-d3-renderer@^0.5.8:
- version "0.5.8"
- resolved "https://registry.yarnpkg.com/dagre-d3-renderer/-/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3"
- integrity sha512-XH2a86isUHRxzIYbjQVEuZtJnWEufb64H5DuXIUmn8esuB40jgLEbUUclulWOW62/ZoXlj2ZDyL8SJ+YRxs+jQ==
+dagre-d3@dagrejs/dagre-d3:
+ version "0.6.4-pre"
+ resolved "https://codeload.github.com/dagrejs/dagre-d3/tar.gz/e1a00e5cb518f5d2304a35647e024f31d178e55b"
dependencies:
- dagre-layout "^0.8.8"
- lodash "^4.17.5"
+ d3 "^5.12"
+ dagre "^0.8.4"
+ graphlib "^2.1.7"
+ lodash "^4.17.15"
-dagre-layout@^0.8.8:
- version "0.8.8"
- resolved "https://registry.yarnpkg.com/dagre-layout/-/dagre-layout-0.8.8.tgz#9b6792f24229f402441c14162c1049e3f261f6d9"
- integrity sha512-ZNV15T9za7X+fV8Z07IZquUKugCxm5owoiPPxfEx6OJRD331nkiIaF3vSt0JEY5FkrY0KfRQxcpQ3SpXB7pLPQ==
+dagre@^0.8.4:
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.4.tgz#26b9fb8f7bdc60c6110a0458c375261836786061"
+ integrity sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==
dependencies:
- graphlibrary "^2.2.0"
- lodash "^4.17.5"
+ graphlib "^2.1.7"
+ lodash "^4.17.4"
dashdash@^1.12.0:
version "1.14.1"
@@ -3907,10 +3619,10 @@ decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-deckar01-task_list@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.2.0.tgz#5cc3ea06f01d3d786b1a667064a462eb5d069bd3"
- integrity sha512-NUfu5ARoD9SC2k+fBT5cBer59iKfEdawPrmfqp5+zAahTECb8z9dsuS1Xnx7jzFAmCCLnEs3z/aYucYXzNrKkQ==
+deckar01-task_list@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.2.1.tgz#e1e8a16c4fd6e153e51fd9258fdbee067ebcd86b"
+ integrity sha512-aNAVYAYwONXezSQy2p5M67wjZE+U7JpPotdegbyy1Wq35V6jDhF3qndJYA1rYnY3aI9ifCep6EMGPav/UQaBZw==
decode-uri-component@^0.2.0:
version "0.2.0"
@@ -3981,11 +3693,6 @@ define-property@^2.0.2:
is-descriptor "^1.0.2"
isobject "^3.0.1"
-defined@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
- integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
-
del@^2.0.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
@@ -4050,13 +3757,6 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
-detab@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.2.tgz#074970d1a807b045d0258a4235df5928dd683561"
- integrity sha512-Q57yPrxScy816TTE1P/uLRXLDKjXhvYTbfxS/e6lPD+YrqghbsMlGB9nQzj/zVtSPaF0DFPSdO916EWO4sQUyQ==
- dependencies:
- repeat-string "^1.5.4"
-
detect-file@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
@@ -4077,14 +3777,6 @@ detect-node@^2.0.4:
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
-detective@^4.0.0:
- version "4.7.1"
- resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.1.tgz#0eca7314338442febb6d65da54c10bb1c82b246e"
- integrity sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==
- dependencies:
- acorn "^5.2.1"
- defined "^1.0.0"
-
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
@@ -4100,11 +3792,6 @@ diff@^3.2.0, diff@^3.4.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
-diff@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff"
- integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==
-
diffie-hellman@^5.0.0:
version "5.0.2"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
@@ -4146,13 +3833,6 @@ docdash@^1.0.2:
resolved "https://registry.yarnpkg.com/docdash/-/docdash-1.0.2.tgz#0449a8f6bb247f563020b78a5485dea95ae2e094"
integrity sha512-IEM57bWPLtVXpUeCKbiGvHsHtW9O9ZiiBPfeQDAZ7JdQiAF3aNWQoJ3e/+uJ63lHO009yn0tbJjtKpXJ2AURCQ==
-doctrine-temporary-fork@2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/doctrine-temporary-fork/-/doctrine-temporary-fork-2.1.0.tgz#36f2154f556ee4f1e60311d391cd23de5187ed57"
- integrity sha512-nliqOv5NkE4zMON4UA6AMJE6As35afs8aYXATpU4pTUdIKiARZwrJVEP1boA3Rx1ZXHVkwxkhcq4VkqvsuRLsA==
- dependencies:
- esutils "^2.0.2"
-
doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -4175,77 +3855,6 @@ document-register-element@1.13.1:
dependencies:
lightercollective "^0.1.0"
-documentation@^12.0.1:
- version "12.0.3"
- resolved "https://registry.yarnpkg.com/documentation/-/documentation-12.0.3.tgz#32f91da8e5cb4104f69db9fd32c87773a1ad6240"
- integrity sha512-RoqkH+mQ4Vi/nFMxG0BaqPAnjKfsJ9lbLWB8KqoKVAZy+urSpk1K1zBzaFesdDkKeaR3aBgeR3RjtHp8Ut/1Wg==
- dependencies:
- "@babel/core" "^7.1.2"
- "@babel/generator" "^7.1.3"
- "@babel/parser" "7.1.3"
- "@babel/plugin-proposal-class-properties" "^7.1.0"
- "@babel/plugin-proposal-decorators" "^7.1.2"
- "@babel/plugin-proposal-do-expressions" "^7.0.0"
- "@babel/plugin-proposal-export-default-from" "^7.0.0"
- "@babel/plugin-proposal-export-namespace-from" "^7.0.0"
- "@babel/plugin-proposal-function-bind" "^7.0.0"
- "@babel/plugin-proposal-function-sent" "^7.1.0"
- "@babel/plugin-proposal-json-strings" "^7.0.0"
- "@babel/plugin-proposal-logical-assignment-operators" "^7.0.0"
- "@babel/plugin-proposal-nullish-coalescing-operator" "^7.0.0"
- "@babel/plugin-proposal-numeric-separator" "^7.0.0"
- "@babel/plugin-proposal-optional-chaining" "^7.0.0"
- "@babel/plugin-proposal-pipeline-operator" "^7.0.0"
- "@babel/plugin-proposal-throw-expressions" "^7.0.0"
- "@babel/plugin-syntax-dynamic-import" "^7.0.0"
- "@babel/plugin-syntax-import-meta" "^7.0.0"
- "@babel/preset-env" "^7.1.0"
- "@babel/preset-flow" "^7.0.0"
- "@babel/preset-react" "^7.0.0"
- "@babel/preset-stage-0" "^7.0.0"
- "@babel/traverse" "^7.1.4"
- "@babel/types" "^7.1.3"
- ansi-html "^0.0.7"
- babelify "^10.0.0"
- chalk "^2.3.0"
- chokidar "^2.0.4"
- concat-stream "^1.6.0"
- diff "^4.0.1"
- doctrine-temporary-fork "2.1.0"
- get-port "^4.0.0"
- git-url-parse "^10.0.1"
- github-slugger "1.2.0"
- glob "^7.1.2"
- globals-docs "^2.4.0"
- highlight.js "^9.15.5"
- js-yaml "^3.10.0"
- lodash "^4.17.10"
- mdast-util-inject "^1.1.0"
- micromatch "^3.1.5"
- mime "^2.2.0"
- module-deps-sortable "5.0.0"
- parse-filepath "^1.0.2"
- pify "^4.0.0"
- read-pkg-up "^4.0.0"
- remark "^9.0.0"
- remark-html "^8.0.0"
- remark-reference-links "^4.0.1"
- remark-toc "^5.0.0"
- remote-origin-url "0.4.0"
- resolve "^1.8.1"
- stream-array "^1.1.2"
- strip-json-comments "^2.0.1"
- tiny-lr "^1.1.0"
- unist-builder "^1.0.2"
- unist-util-visit "^1.3.0"
- vfile "^4.0.0"
- vfile-reporter "^6.0.0"
- vfile-sort "^2.1.0"
- vinyl "^2.1.0"
- vinyl-fs "^3.0.2"
- vue-template-compiler "^2.5.16"
- yargs "^12.0.2"
-
dom-serialize@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
@@ -4313,13 +3922,6 @@ dropzone@^4.2.0:
resolved "https://registry.yarnpkg.com/dropzone/-/dropzone-4.2.0.tgz#fbe7acbb9918e0706489072ef663effeef8a79f3"
integrity sha1-++esu5kY4HBkiQcu9mPv/u+KefM=
-duplexer2@^0.1.2, duplexer2@~0.1.0:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
- integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
- dependencies:
- readable-stream "^2.0.2"
-
duplexer3@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
@@ -4400,11 +4002,6 @@ elliptic@^6.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
-"emoji-regex@>=6.0.0 <=6.1.1":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"
- integrity sha1-xs0OwbBkLio8Z6ETfvxeeW2k+I4=
-
emoji-regex@^7.0.1, emoji-regex@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -4519,14 +4116,6 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-error@^7.0.0:
- version "7.0.2"
- resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02"
- integrity sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=
- dependencies:
- string-template "~0.2.1"
- xtend "~4.0.0"
-
es-abstract@^1.5.1, es-abstract@^1.6.1:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@@ -4814,7 +4403,7 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
-esutils@^2.0.0, esutils@^2.0.2:
+esutils@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
@@ -5077,7 +4666,7 @@ fault@^1.0.2:
dependencies:
format "^0.2.2"
-faye-websocket@^0.10.0, faye-websocket@~0.10.0:
+faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
@@ -5276,7 +4865,7 @@ flatted@^2.0.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
-flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
+flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
@@ -5361,14 +4950,6 @@ fs-minipass@^1.2.5:
dependencies:
minipass "^2.2.1"
-fs-mkdirp-stream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb"
- integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=
- dependencies:
- graceful-fs "^4.1.11"
- through2 "^2.0.3"
-
fs-write-stream-atomic@^1.0.8:
version "1.0.10"
resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -5453,11 +5034,6 @@ get-caller-file@^2.0.1:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-port@^4.0.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"
- integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==
-
get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -5524,35 +5100,6 @@ gettext-extractor@^3.4.3:
pofile "^1"
typescript "2 - 3"
-git-up@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/git-up/-/git-up-2.1.0.tgz#2f14cfe78327e7c4a2b92fcac7bfc674fdfad40c"
- integrity sha512-MJgwfcSd9qxgDyEYpRU/CDxNpUadrK80JHuEQDG4Urn0m7tpSOgCBrtiSIa9S9KH8Tbuo/TN8SSQmJBvsw1HkA==
- dependencies:
- is-ssh "^1.3.0"
- parse-url "^3.0.2"
-
-git-url-parse@^10.0.1:
- version "10.1.0"
- resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-10.1.0.tgz#a27813218f8777e91d15f1c121b83bf14721b67e"
- integrity sha512-goZOORAtFjU1iG+4zZgWq+N7It09PqS3Xsy43ZwhP5unDD0tTSmXTpqULHodMdJXGejm3COwXIhIRT6Z8DYVZQ==
- dependencies:
- git-up "^2.0.0"
-
-github-slugger@1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.0.tgz#8ada3286fd046d8951c3c952a8d7854cfd90fd9a"
- integrity sha512-wIaa75k1vZhyPm9yWrD08A5Xnx/V+RmzGrpjQuLemGKSb77Qukiaei58Bogrl/LZSADDfPzKJX8jhLs4CRTl7Q==
- dependencies:
- emoji-regex ">=6.0.0 <=6.1.1"
-
-github-slugger@^1.0.0, github-slugger@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.2.1.tgz#47e904e70bf2dccd0014748142d31126cfd49508"
- integrity sha512-SsZUjg/P03KPzQBt7OxJPasGw6NRO5uOgiZ5RGXVud5iSIZ0eNZeNp5rTwCxtavrRUa/A77j8mePVc5lEvk0KQ==
- dependencies:
- emoji-regex ">=6.0.0 <=6.1.1"
-
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -5568,22 +5115,6 @@ glob-parent@^5.0.0:
dependencies:
is-glob "^4.0.1"
-glob-stream@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
- integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=
- dependencies:
- extend "^3.0.0"
- glob "^7.1.1"
- glob-parent "^3.1.0"
- is-negated-glob "^1.0.0"
- ordered-read-streams "^1.0.0"
- pumpify "^1.3.5"
- readable-stream "^2.1.5"
- remove-trailing-separator "^1.0.1"
- to-absolute-glob "^2.0.0"
- unique-stream "^2.0.2"
-
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
@@ -5649,11 +5180,6 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
-globals-docs@^2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/globals-docs/-/globals-docs-2.4.0.tgz#f2c647544eb6161c7c38452808e16e693c2dafbb"
- integrity sha512-B69mWcqCmT3jNYmSxRxxOXWfzu3Go8NQXPfl2o0qPd1EEFhwW0dFUg9ztTu915zPQzqwIhWAlw6hmfIcCK4kkQ==
-
globals@^11.1.0, globals@^11.7.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -5753,7 +5279,7 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.2.0"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
@@ -5763,10 +5289,10 @@ graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
-graphlibrary@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/graphlibrary/-/graphlibrary-2.2.0.tgz#017a14899775228dec4497a39babfdd6bf56eac6"
- integrity sha512-XTcvT55L8u4MBZrM37zXoUxsgxs/7sow7YSygd9CIwfWTVO8RVu7AYXhhCiTuFEf+APKgx6Jk4SuQbYR0vYKmQ==
+graphlib@^2.1.7:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.7.tgz#b6a69f9f44bd9de3963ce6804a2fc9e73d86aecc"
+ integrity sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==
dependencies:
lodash "^4.17.5"
@@ -5924,50 +5450,12 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-hast-util-is-element@^1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-1.0.3.tgz#423b4b26fe8bf1f25950fe052e9ce8f83fd5f6a4"
- integrity sha512-C62CVn7jbjp89yOhhy7vrkSaB7Vk906Gtcw/Ihd+Iufnq+2pwOZjdPmpzpKLWJXPJBMDX3wXg4FqmdOayPcewA==
-
-hast-util-sanitize@^1.0.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-1.3.1.tgz#4e60d66336bd67e52354d581967467029a933f2e"
- integrity sha512-AIeKHuHx0Wk45nSkGVa2/ujQYTksnDl8gmmKo/mwQi7ag7IBZ8cM3nJ2G86SajbjGP/HRpud6kMkPtcM2i0Tlw==
- dependencies:
- xtend "^4.0.1"
-
-hast-util-to-html@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-4.0.1.tgz#3666b05afb62bd69f8f5e6c94db04dea19438e2a"
- integrity sha512-2emzwyf0xEsc4TBIPmDJmBttIw8R4SXAJiJZoiRR/s47ODYWgOqNoDbf2SJAbMbfNdFWMiCSOrI3OVnX6Qq2Mg==
- dependencies:
- ccount "^1.0.0"
- comma-separated-tokens "^1.0.1"
- hast-util-is-element "^1.0.0"
- hast-util-whitespace "^1.0.0"
- html-void-elements "^1.0.0"
- property-information "^4.0.0"
- space-separated-tokens "^1.0.0"
- stringify-entities "^1.0.1"
- unist-util-is "^2.0.0"
- xtend "^4.0.1"
-
-hast-util-whitespace@^1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-1.0.3.tgz#6d161b307bd0693b5ec000c7c7e8b5445109ee34"
- integrity sha512-AlkYiLTTwPOyxZ8axq2/bCwRUPjIPBfrHkXuCR92B38b3lSdU22R5F/Z4DL6a2kxWpekWq1w6Nj48tWat6GeRA==
-
he@^1.1.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-highlight.js@^9.13.1, highlight.js@^9.15.5:
- version "9.15.8"
- resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.8.tgz#f344fda123f36f1a65490e932cf90569e4999971"
- integrity sha512-RrapkKQWwE+wKdF73VsOa2RQdIoO3mxwJ4P8mhbI6KYJUraUHRKM5w5zQQKXNk0xNL4UVRdulV9SBJcmzJNzVA==
-
-highlight.js@~9.13.0:
+highlight.js@^9.13.1, highlight.js@~9.13.0:
version "9.13.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==
@@ -6038,12 +5526,7 @@ html-tags@^3.0.0:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.0.0.tgz#41f57708c9e6b7b46a00a22317d614c4a2bab166"
integrity sha512-xiXEBjihaNI+VZ2mKEoI5ZPxqUsevTKM+aeeJ/W4KAg2deGE35minmCJMn51BvwJZmiHaeAxrb2LAS0yZJxuuA==
-html-void-elements@^1.0.0:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.4.tgz#95e8bb5ecd6b88766569c2645f2b5f1591db9ba5"
- integrity sha512-yMk3naGPLrfvUV9TdDbuYXngh/TpHbA6TrOw3HL9kS8yhwx7i309BReNg7CbAJXGE+UMJ6je5OqJ7lC63o6YuQ==
-
-htmlparser2@^3.10.0, htmlparser2@^3.9.0:
+htmlparser2@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==
@@ -6175,11 +5658,6 @@ immediate@~3.0.5:
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
-immutable-tuple@^0.4.9:
- version "0.4.9"
- resolved "https://registry.yarnpkg.com/immutable-tuple/-/immutable-tuple-0.4.9.tgz#473ebdd6c169c461913a454bf87ef8f601a20ff0"
- integrity sha512-LWbJPZnidF8eczu7XmcnLBsumuyRBkpwIRPCZxlojouhBo5jEBO4toj6n7hMy6IxHU/c+MqDSWkvaTpPlMQcyA==
-
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@@ -6269,7 +5747,7 @@ inherits@2.0.1:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
-ini@^1.3.3, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
+ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@@ -6343,14 +5821,6 @@ is-absolute-url@^3.0.2:
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.2.tgz#554f2933e7385cc46e94351977ca2081170a206e"
integrity sha512-+5g/wLlcm1AcxSP7014m6GvbPHswDx980vD/3bZaap8aGV9Yfs7Q6y6tfaupgZ5O74Byzc8dGrSCJ+bFXx0KdA==
-is-absolute@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
- integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==
- dependencies:
- is-relative "^1.0.0"
- is-windows "^1.0.1"
-
is-accessor-descriptor@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
@@ -6402,7 +5872,7 @@ is-binary-path@^2.1.0:
dependencies:
binary-extensions "^2.0.0"
-is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
+is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@@ -6551,11 +6021,6 @@ is-installed-globally@^0.1.0:
global-dirs "^0.1.0"
is-path-inside "^1.0.0"
-is-negated-glob@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2"
- integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=
-
is-npm@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -6616,7 +6081,7 @@ is-path-inside@^2.1.0:
dependencies:
path-is-inside "^1.0.2"
-is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
+is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -6655,13 +6120,6 @@ is-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
-is-relative@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
- integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==
- dependencies:
- is-unc-path "^1.0.0"
-
is-resolvable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
@@ -6672,13 +6130,6 @@ is-retry-allowed@^1.0.0:
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
-is-ssh@^1.3.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3"
- integrity sha512-0eRIASHZt1E68/ixClI8bp2YK2wmBPVWEismTs6M+M099jKgrzl/3E976zIbImSIob48N2/XGe9y7ZiYdImSlg==
- dependencies:
- protocols "^1.1.0"
-
is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -6696,23 +6147,11 @@ is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
-is-unc-path@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d"
- integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==
- dependencies:
- unc-path-regex "^0.1.2"
-
-is-utf8@^0.2.0, is-utf8@^0.2.1:
+is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
-is-valid-glob@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
- integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=
-
is-whitespace-character@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed"
@@ -6877,6 +6316,14 @@ jed@^1.1.1:
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
integrity sha1-elSbvZ/+FYWwzQoZHiAwVb7ldLQ=
+jest-canvas-mock@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.1.2.tgz#0d16c9f91534f773fd132fc289f2e6b6db8faa28"
+ integrity sha512-1VI4PK4/X70yrSjYScYVkYJYbXYlZLKJkUrAlyHjQsfolv64aoFyIrmMDtqCjpYrpVvWYEcAGUaYv5DVJj00oQ==
+ dependencies:
+ cssfontparser "^1.2.1"
+ parse-color "^1.0.0"
+
jest-changed-files@^24.8.0:
version "24.8.0"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.8.0.tgz#7e7eb21cf687587a85e50f3d249d1327e15b157b"
@@ -7307,7 +6754,7 @@ js-tokens@^3.0.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-js-yaml@^3.10.0, js-yaml@^3.12.0, js-yaml@^3.13.1:
+js-yaml@^3.12.0, js-yaml@^3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -7443,11 +6890,6 @@ jsonfile@^4.0.0:
optionalDependencies:
graceful-fs "^4.1.6"
-jsonparse@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
- integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
-
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -7623,13 +7065,6 @@ latest-version@^3.0.0:
dependencies:
package-json "^4.0.0"
-lazystream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
- integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
- dependencies:
- readable-stream "^2.0.5"
-
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@@ -7644,13 +7079,6 @@ lcid@^2.0.0:
dependencies:
invert-kv "^2.0.0"
-lead@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42"
- integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=
- dependencies:
- flush-write-stream "^1.0.2"
-
left-pad@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
@@ -7693,11 +7121,6 @@ linkify-it@^2.0.0:
dependencies:
uc.micro "^1.0.1"
-livereload-js@^2.3.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.4.0.tgz#447c31cf1ea9ab52fc20db615c5ddf678f78009c"
- integrity sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==
-
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -7796,12 +7219,22 @@ lodash.isequal@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+lodash.isplainobject@^4.0.6:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
+ integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
+
+lodash.isstring@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
+ integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
+
lodash.kebabcase@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
integrity sha1-hImxyw0p/4gZXM7KRI/21swpXDY=
-lodash.mergewith@^4.6.0:
+lodash.mergewith@^4.6.1:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
@@ -7960,7 +7393,7 @@ map-age-cleaner@^0.1.1:
dependencies:
p-defer "^1.0.0"
-map-cache@^0.2.0, map-cache@^0.2.2:
+map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
@@ -8071,52 +7504,6 @@ mdast-util-compact@^1.0.0:
dependencies:
unist-util-visit "^1.1.0"
-mdast-util-definitions@^1.2.0:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-1.2.4.tgz#2b54ad4eecaff9d9fcb6bf6f9f6b68b232d77ca7"
- integrity sha512-HfUArPog1j4Z78Xlzy9Q4aHLnrF/7fb57cooTHypyGoe2XFNbcx/kWZDoOz+ra8CkUzvg3+VHV434yqEd1DRmA==
- dependencies:
- unist-util-visit "^1.0.0"
-
-mdast-util-inject@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz#db06b8b585be959a2dcd2f87f472ba9b756f3675"
- integrity sha1-2wa4tYW+lZotzS+H9HK6m3VvNnU=
- dependencies:
- mdast-util-to-string "^1.0.0"
-
-mdast-util-to-hast@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-3.0.4.tgz#132001b266031192348d3366a6b011f28e54dc40"
- integrity sha512-/eIbly2YmyVgpJNo+bFLLMCI1XgolO/Ffowhf+pHDq3X4/V6FntC9sGQCDLM147eTS+uSXv5dRzJyFn+o0tazA==
- dependencies:
- collapse-white-space "^1.0.0"
- detab "^2.0.0"
- mdast-util-definitions "^1.2.0"
- mdurl "^1.0.1"
- trim "0.0.1"
- trim-lines "^1.0.0"
- unist-builder "^1.0.1"
- unist-util-generated "^1.1.0"
- unist-util-position "^3.0.0"
- unist-util-visit "^1.1.0"
- xtend "^4.0.1"
-
-mdast-util-to-string@^1.0.0, mdast-util-to-string@^1.0.5:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.0.6.tgz#7d85421021343b33de1552fc71cb8e5b4ae7536d"
- integrity sha512-868pp48gUPmZIhfKrLbaDneuzGiw3OTDjHc5M1kAepR2CWBJ+HpEsm252K4aXdiP5coVZaJPOqGtVU6Po8xnXg==
-
-mdast-util-toc@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/mdast-util-toc/-/mdast-util-toc-3.1.0.tgz#395eeb877f067f9d2165d990d77c7eea6f740934"
- integrity sha512-Za0hqL1PqWrvxGtA/3NH9D5nhGAUS9grMM4obEAz5+zsk1RIw/vWUchkaoDLNdrwk05A0CSC5eEXng36/1qE5w==
- dependencies:
- github-slugger "^1.2.1"
- mdast-util-to-string "^1.0.5"
- unist-util-is "^2.1.2"
- unist-util-visit "^1.1.0"
-
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@@ -8204,21 +7591,22 @@ merge2@^1.2.3:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5"
integrity sha512-gdUU1Fwj5ep4kplwcmftruWofEFt6lfpkkr3h860CXbAB9c3hGb55EOL2ali0Td5oebvW0E1+3Sr+Ur7XfKpRA==
-mermaid@^8.2.6:
- version "8.2.6"
- resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.2.6.tgz#e73f396461a435c39a998819171c2114f59e46e1"
- integrity sha512-A8y4zW2aXPj8Yw+BkrCkV6fvzhsFWVESV1IkzRjqQ6T/+tzhkz946+bdebCmHqicEJGTncu/U6h8dgjo5pWo6Q==
+mermaid@^8.4.2:
+ version "8.4.2"
+ resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.4.2.tgz#91d3d8e9541e72eed7a78d0e882db11564fab3bb"
+ integrity sha512-vYSCP2u4XkOnjliWz/QIYwvzF/znQAq22vWJJ3YV40SnwV2JQyHblnwwNYXCprkXw7XfwBKDpSNaJ3HP4WfnZw==
dependencies:
"@braintree/sanitize-url" "^3.1.0"
+ crypto-random-string "^3.0.1"
d3 "^5.7.0"
- dagre-d3-renderer "^0.5.8"
- dagre-layout "^0.8.8"
- documentation "^12.0.1"
- graphlibrary "^2.2.0"
+ dagre "^0.8.4"
+ dagre-d3 dagrejs/dagre-d3
+ graphlib "^2.1.7"
he "^1.2.0"
lodash "^4.17.11"
minify "^4.1.1"
moment-mini "^2.22.1"
+ prettier "^1.18.2"
scope-css "^1.2.1"
methods@~1.1.2:
@@ -8226,7 +7614,7 @@ methods@~1.1.2:
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
-micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.5, micromatch@^3.1.6:
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.6:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
@@ -8278,7 +7666,7 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-mime@^2.2.0, mime@^2.3.1, mime@^2.4.4:
+mime@^2.3.1, mime@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
@@ -8341,7 +7729,7 @@ minimist@1.1.x:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8"
integrity sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=
-minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
@@ -8392,26 +7780,6 @@ mkdirp@0.5.x, mkdirp@0.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp
dependencies:
minimist "0.0.8"
-module-deps-sortable@5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/module-deps-sortable/-/module-deps-sortable-5.0.0.tgz#99db5bb08f7eab55e4c31f6b7c722c6a2144ba74"
- integrity sha512-bnGGeghQmz/t/6771/KC4FmxpVm126iR6AAzzq4N6hVZQVl4+ZZBv+VF3PJmDyxXtVtgcgTSSP7NL+jq1QAHrg==
- dependencies:
- JSONStream "^1.0.3"
- browser-resolve "^1.7.0"
- cached-path-relative "^1.0.0"
- concat-stream "~1.5.0"
- defined "^1.0.0"
- detective "^4.0.0"
- duplexer2 "^0.1.2"
- inherits "^2.0.1"
- readable-stream "^2.0.2"
- resolve "^1.1.3"
- stream-combiner2 "^1.1.1"
- subarg "^1.0.0"
- through2 "^2.0.0"
- xtend "^4.0.0"
-
moment-mini@^2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.22.1.tgz#bc32d73e43a4505070be6b53494b17623183420d"
@@ -8736,23 +8104,6 @@ normalize-selector@^0.2.0:
resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
-normalize-url@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c"
- integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=
- dependencies:
- object-assign "^4.0.1"
- prepend-http "^1.0.0"
- query-string "^4.1.0"
- sort-keys "^1.0.0"
-
-now-and-later@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c"
- integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==
- dependencies:
- once "^1.3.2"
-
npm-bundled@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
@@ -8834,7 +8185,7 @@ object-visit@^1.0.0:
dependencies:
isobject "^3.0.0"
-object.assign@^4.0.4, object.assign@^4.1.0:
+object.assign@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
@@ -8886,7 +8237,7 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
-once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@@ -8912,12 +8263,12 @@ opn@^5.5.0:
dependencies:
is-wsl "^1.1.0"
-optimism@^0.6.9:
- version "0.6.9"
- resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.6.9.tgz#19258ff8b3be0cea29ac35f06bff818e026e30bb"
- integrity sha512-xoQm2lvXbCA9Kd7SCx6y713Y7sZ6fUc5R6VYpoL5M6svKJbTuvtNopexK8sO8K4s0EOUYHuPN2+yAEsNyRggkQ==
+optimism@^0.10.0:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.10.3.tgz#163268fdc741dea2fb50f300bedda80356445fd7"
+ integrity sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==
dependencies:
- immutable-tuple "^0.4.9"
+ "@wry/context" "^0.4.0"
optimist@^0.6.1:
version "0.6.1"
@@ -8939,13 +8290,6 @@ optionator@^0.8.1, optionator@^0.8.2:
type-check "~0.3.2"
wordwrap "~1.0.0"
-ordered-read-streams@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
- integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=
- dependencies:
- readable-stream "^2.0.1"
-
orderedmap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba"
@@ -9123,6 +8467,13 @@ parse-asn1@^5.0.0:
evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3"
+parse-color@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-color/-/parse-color-1.0.0.tgz#7b748b95a83f03f16a94f535e52d7f3d94658619"
+ integrity sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=
+ dependencies:
+ color-convert "~0.5.0"
+
parse-entities@^1.0.2, parse-entities@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.0.tgz#9deac087661b2e36814153cb78d7e54a4c5fd6f4"
@@ -9135,22 +8486,6 @@ parse-entities@^1.0.2, parse-entities@^1.1.0:
is-decimal "^1.0.0"
is-hexadecimal "^1.0.0"
-parse-filepath@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
- integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=
- dependencies:
- is-absolute "^1.0.0"
- map-cache "^0.2.0"
- path-root "^0.1.1"
-
-parse-git-config@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/parse-git-config/-/parse-git-config-0.2.0.tgz#272833fdd15fea146fb75d336d236b963b6ff706"
- integrity sha1-Jygz/dFf6hRvt10zbSNrljtv9wY=
- dependencies:
- ini "^1.3.3"
-
parse-json@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
@@ -9171,24 +8506,6 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
-parse-path@^3.0.1:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-3.0.4.tgz#a48b7b529da41f34d9d1428602a39b29fc7180e4"
- integrity sha512-wP70vtwv2DyrM2YoA7ZHVv4zIXa4P7dGgHlj+VwyXNDduLLVJ7NMY1zsFxjUUJ3DAwJLupGb1H5gMDDiNlJaxw==
- dependencies:
- is-ssh "^1.3.0"
- protocols "^1.4.0"
-
-parse-url@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-3.0.2.tgz#602787a7063a795d72b8673197505e72f60610be"
- integrity sha1-YCeHpwY6eV1yuGcxl1BecvYGEL4=
- dependencies:
- is-ssh "^1.3.0"
- normalize-url "^1.9.1"
- parse-path "^3.0.1"
- protocols "^1.4.0"
-
parse5@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@@ -9270,18 +8587,6 @@ path-parse@^1.0.6:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
-path-root-regex@^0.1.0:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d"
- integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=
-
-path-root@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7"
- integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=
- dependencies:
- path-root-regex "^0.1.0"
-
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
@@ -9349,7 +8654,7 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
-pify@^4.0.0, pify@^4.0.1:
+pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
@@ -9594,7 +8899,7 @@ 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.14, postcss@^6.0.23:
+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==
@@ -9603,10 +8908,10 @@ postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.23:
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.7:
- version "7.0.18"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233"
- integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g==
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.7:
+ version "7.0.21"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.21.tgz#06bb07824c19c2021c5d056d5b10c35b989f7e17"
+ integrity sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
@@ -9617,7 +8922,7 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
-prepend-http@^1.0.0, prepend-http@^1.0.1:
+prepend-http@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
@@ -9627,7 +8932,7 @@ prettier@1.16.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==
-prettier@1.18.2:
+prettier@1.18.2, prettier@^1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
@@ -9654,16 +8959,16 @@ private@^0.1.6:
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
-process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
- integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
process-nextick-args@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
@@ -9687,13 +8992,6 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
-property-information@^4.0.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/property-information/-/property-information-4.2.0.tgz#f0e66e07cbd6fed31d96844d958d153ad3eb486e"
- integrity sha512-TlgDPagHh+eBKOnH2VYvk8qbwsCG/TAJdmTL7f1PROUcSO8qt/KSmShEQ/OKvock8X9tFjtqjCScyOkkkvIKVQ==
- dependencies:
- xtend "^4.0.1"
-
prosemirror-commands@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz#e5a2ba821e29ea7065c88277fe2c3d7f6b0b9d37"
@@ -9815,11 +9113,6 @@ proto-list@~1.2.1:
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
-protocols@^1.1.0, protocols@^1.4.0:
- version "1.4.7"
- resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32"
- integrity sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg==
-
proxy-addr@~2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34"
@@ -9875,7 +9168,7 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
-pumpify@^1.3.3, pumpify@^1.3.5:
+pumpify@^1.3.3:
version "1.5.1"
resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
@@ -9904,7 +9197,7 @@ qjobs@^1.1.4:
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
-qs@6.7.0, qs@^6.4.0:
+qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
@@ -9914,14 +9207,6 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
-query-string@^4.1.0:
- version "4.3.4"
- resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
- integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s=
- dependencies:
- object-assign "^4.1.0"
- strict-uri-encode "^1.0.0"
-
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -9969,11 +9254,6 @@ raphael@^2.2.7:
dependencies:
eve-raphael "0.5.0"
-raven-js@^3.22.1:
- version "3.22.1"
- resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.1.tgz#1117f00dfefaa427ef6e1a7d50bbb1fb998a24da"
- integrity sha512-2Y8czUl5a9usbvXbpV8a+GPAiDXjxQjaHImZL0TyJWI5k5jV/6o+wceaBAg9g6RpO9OOJp0/w2mMs6pBoqOyDA==
-
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
@@ -9984,14 +9264,6 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
-raw-body@~1.1.0:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425"
- integrity sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=
- dependencies:
- bytes "1"
- string_decoder "0.10"
-
raw-loader@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-3.1.0.tgz#5e9d399a5a222cc0de18f42c3bc5e49677532b3f"
@@ -10074,7 +9346,7 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
path-type "^3.0.0"
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
@@ -10096,7 +9368,7 @@ readable-stream@^3.0.6:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
-readable-stream@~2.0.0, readable-stream@~2.0.6:
+readable-stream@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
integrity sha1-j5A0HmilPMySh4jaz80Rs265t44=
@@ -10108,19 +9380,6 @@ readable-stream@~2.0.0, readable-stream@~2.0.6:
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@~2.1.0:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
- integrity sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=
- dependencies:
- buffer-shims "^1.0.0"
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
- string_decoder "~0.10.x"
- util-deprecate "~1.0.1"
-
readdir-enhanced@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/readdir-enhanced/-/readdir-enhanced-2.2.4.tgz#773fb8a8de5f645fb13d9403746d490d4facb3e6"
@@ -10265,37 +9524,6 @@ relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
-remark-html@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/remark-html/-/remark-html-8.0.0.tgz#9fcb859a6f3cb40f3ef15402950f1a62ec301b3a"
- integrity sha512-3V2391GL3hxKhrkzYOyfPpxJ6taIKLCfuLVqumeWQOk3H9nTtSQ8St8kMYkBVIEAquXN1chT83qJ/2lAW+dpEg==
- dependencies:
- hast-util-sanitize "^1.0.0"
- hast-util-to-html "^4.0.0"
- mdast-util-to-hast "^3.0.0"
- xtend "^4.0.1"
-
-remark-parse@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95"
- integrity sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==
- dependencies:
- collapse-white-space "^1.0.2"
- is-alphabetical "^1.0.0"
- is-decimal "^1.0.0"
- is-whitespace-character "^1.0.0"
- is-word-character "^1.0.0"
- markdown-escapes "^1.0.0"
- parse-entities "^1.1.0"
- repeat-string "^1.5.4"
- state-toggle "^1.0.0"
- trim "0.0.1"
- trim-trailing-lines "^1.0.0"
- unherit "^1.0.4"
- unist-util-remove-position "^1.0.0"
- vfile-location "^2.0.0"
- xtend "^4.0.1"
-
remark-parse@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-6.0.3.tgz#c99131052809da482108413f87b0ee7f52180a3a"
@@ -10317,42 +9545,6 @@ remark-parse@^6.0.0:
vfile-location "^2.0.0"
xtend "^4.0.1"
-remark-reference-links@^4.0.1:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/remark-reference-links/-/remark-reference-links-4.0.4.tgz#190579a0d6b002859d6cdbdc5aeb8bbdae4e06ab"
- integrity sha512-+2X8hwSQqxG4tvjYZNrTcEC+bXp8shQvwRGG6J/rnFTvBoU4G0BBviZoqKGZizLh/DG+0gSYhiDDWCqyxXW1iQ==
- dependencies:
- unist-util-visit "^1.0.0"
-
-remark-slug@^5.0.0:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-5.1.2.tgz#715ecdef8df1226786204b1887d31ab16aa24609"
- integrity sha512-DWX+Kd9iKycqyD+/B+gEFO3jjnt7Yg1O05lygYSNTe5i5PIxxxPjp5qPBDxPIzp5wreF7+1ROCwRgjEcqmzr3A==
- dependencies:
- github-slugger "^1.0.0"
- mdast-util-to-string "^1.0.0"
- unist-util-visit "^1.0.0"
-
-remark-stringify@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-5.0.0.tgz#336d3a4d4a6a3390d933eeba62e8de4bd280afba"
- integrity sha512-Ws5MdA69ftqQ/yhRF9XhVV29mhxbfGhbz0Rx5bQH+oJcNhhSM6nCu1EpLod+DjrFGrU0BMPs+czVmJZU7xiS7w==
- dependencies:
- ccount "^1.0.0"
- is-alphanumeric "^1.0.0"
- is-decimal "^1.0.0"
- is-whitespace-character "^1.0.0"
- longest-streak "^2.0.1"
- markdown-escapes "^1.0.0"
- markdown-table "^1.1.0"
- mdast-util-compact "^1.0.0"
- parse-entities "^1.0.2"
- repeat-string "^1.5.4"
- state-toggle "^1.0.0"
- stringify-entities "^1.0.1"
- unherit "^1.0.4"
- xtend "^4.0.1"
-
remark-stringify@^6.0.0:
version "6.0.4"
resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088"
@@ -10373,14 +9565,6 @@ remark-stringify@^6.0.0:
unherit "^1.0.4"
xtend "^4.0.1"
-remark-toc@^5.0.0:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/remark-toc/-/remark-toc-5.1.1.tgz#8c229d6f834cdb43fde6685e2d43248d3fc82d78"
- integrity sha512-vCPW4YOsm2CfyuScdktM9KDnJXVHJsd/ZeRtst+dnBU3B3KKvt8bc+bs5syJjyptAHfqo7H+5Uhz+2blWBfwow==
- dependencies:
- mdast-util-toc "^3.0.0"
- remark-slug "^5.0.0"
-
remark@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/remark/-/remark-10.0.1.tgz#3058076dc41781bf505d8978c291485fe47667df"
@@ -10390,39 +9574,6 @@ remark@^10.0.1:
remark-stringify "^6.0.0"
unified "^7.0.0"
-remark@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/remark/-/remark-9.0.0.tgz#c5cfa8ec535c73a67c4b0f12bfdbd3a67d8b2f60"
- integrity sha512-amw8rGdD5lHbMEakiEsllmkdBP+/KpjW/PRK6NSGPZKCQowh0BT4IWXDAkRMyG3SB9dKPXWMviFjNusXzXNn3A==
- dependencies:
- remark-parse "^5.0.0"
- remark-stringify "^5.0.0"
- unified "^6.0.0"
-
-remote-origin-url@0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/remote-origin-url/-/remote-origin-url-0.4.0.tgz#4d3e2902f34e2d37d1c263d87710b77eb4086a30"
- integrity sha1-TT4pAvNOLTfRwmPYdxC3frQIajA=
- dependencies:
- parse-git-config "^0.2.0"
-
-remove-bom-buffer@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53"
- integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==
- dependencies:
- is-buffer "^1.1.5"
- is-utf8 "^0.2.1"
-
-remove-bom-stream@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523"
- integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=
- dependencies:
- remove-bom-buffer "^3.0.0"
- safe-buffer "^5.1.0"
- through2 "^2.0.3"
-
remove-trailing-separator@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -10433,7 +9584,7 @@ repeat-element@^1.1.2:
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
-repeat-string@^1.5.0, repeat-string@^1.5.4, repeat-string@^1.6.1:
+repeat-string@^1.5.4, repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
@@ -10445,7 +9596,7 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
-replace-ext@1.0.0, replace-ext@^1.0.0:
+replace-ext@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
@@ -10567,13 +9718,6 @@ resolve-from@^5.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-resolve-options@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131"
- integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=
- dependencies:
- value-or-function "^3.0.0"
-
resolve-url@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -10584,7 +9728,7 @@ resolve@1.1.7:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
-resolve@1.x, resolve@^1.1.3, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.8.1, resolve@^1.9.0:
+resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.9.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e"
integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==
@@ -10670,11 +9814,6 @@ safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, s
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-json-parse@~1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57"
- integrity sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=
-
safe-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -10702,18 +9841,21 @@ sane@^4.0.3:
minimist "^1.1.1"
walker "~1.0.5"
-sanitize-html@^1.16.1:
- version "1.16.3"
- resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56"
- integrity sha512-XpAJGnkMfNM7AzXLRw225blBB/pE4dM4jzRn98g4r88cfxwN6g+5IsRmCAh/gbhYGm6u6i97zsatMOM7Lr8wyw==
+sanitize-html@^1.20.0:
+ version "1.20.1"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.20.1.tgz#f6effdf55dd398807171215a62bfc21811bacf85"
+ integrity sha512-txnH8TQjaQvg2Q0HY06G6CDJLVYCpbnxrdO0WN8gjCKaU5J0KbyGYhZxx5QJg3WLZ1lB7XU9kDkfrCXUozqptA==
dependencies:
- htmlparser2 "^3.9.0"
+ chalk "^2.4.1"
+ htmlparser2 "^3.10.0"
lodash.clonedeep "^4.5.0"
lodash.escaperegexp "^4.1.2"
- lodash.mergewith "^4.6.0"
- postcss "^6.0.14"
+ lodash.isplainobject "^4.0.6"
+ lodash.isstring "^4.0.1"
+ lodash.mergewith "^4.6.1"
+ postcss "^7.0.5"
srcset "^1.0.0"
- xtend "^4.0.0"
+ xtend "^4.0.1"
sass-graph@^2.2.4:
version "2.2.4"
@@ -11097,13 +10239,6 @@ sockjs@0.3.19:
faye-websocket "^0.10.0"
uuid "^3.0.1"
-sort-keys@^1.0.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
- integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
- dependencies:
- is-plain-obj "^1.0.0"
-
sortablejs@^1.10.0, sortablejs@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.10.0.tgz#0ebc054acff2486569194a2f975b2b145dd5e7d6"
@@ -11165,11 +10300,6 @@ source-map@^0.7.3:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-space-separated-tokens@^1.0.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.4.tgz#27910835ae00d0adfcdbd0ad7e611fb9544351fa"
- integrity sha512-UyhMSmeIqZrQn2UdjYpxEkwY9JUrn8pP+7L4f91zRzOQuI8MF1FGLfYU9DKCYeLdo7LXMxwrX5zKFy7eeeVHuA==
-
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -11307,13 +10437,6 @@ stickyfilljs@^2.0.5:
resolved "https://registry.yarnpkg.com/stickyfilljs/-/stickyfilljs-2.0.5.tgz#d229e372d2199ddf5d283bbe34ac1f7d2529c2fc"
integrity sha512-KGKdqKbv1jXit54ltFPIWw/XVeuSrJmTUS8viT1Pmdpp1Jyv3SMpFmhvPBdddX9FHDlHbm9s8cPAhPviBaBVpA==
-stream-array@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/stream-array/-/stream-array-1.1.2.tgz#9e5f7345f2137c30ee3b498b9114e80b52bb7eb5"
- integrity sha1-nl9zRfITfDDuO0mLkRToC1K7frU=
- dependencies:
- readable-stream "~2.1.0"
-
stream-browserify@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
@@ -11322,14 +10445,6 @@ stream-browserify@^2.0.1:
inherits "~2.0.1"
readable-stream "^2.0.2"
-stream-combiner2@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe"
- integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4=
- dependencies:
- duplexer2 "~0.1.0"
- readable-stream "^2.0.2"
-
stream-each@^1.1.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
@@ -11365,11 +10480,6 @@ streamroller@^1.0.6:
fs-extra "^7.0.1"
lodash "^4.17.14"
-strict-uri-encode@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
- integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
-
string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@@ -11378,11 +10488,6 @@ string-length@^2.0.0:
astral-regex "^1.0.0"
strip-ansi "^4.0.0"
-string-template@~0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
- integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
-
string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
@@ -11409,7 +10514,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"
-string-width@^4.0.0, string-width@^4.1.0:
+string-width@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff"
integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==
@@ -11418,11 +10523,6 @@ string-width@^4.0.0, string-width@^4.1.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^5.2.0"
-string_decoder@0.10, string_decoder@~0.10.x:
- version "0.10.31"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
- integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -11430,6 +10530,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+ integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
+
stringify-entities@^1.0.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.2.tgz#a98417e5471fd227b3e45d3db1861c11caf668f7"
@@ -11585,13 +10690,6 @@ stylelint@^10.1.0:
svg-tags "^1.0.0"
table "^5.2.3"
-subarg@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
- integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI=
- dependencies:
- minimist "^1.1.0"
-
sugarss@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-2.0.0.tgz#ddd76e0124b297d40bf3cca31c8b22ecb43bc61d"
@@ -11599,7 +10697,7 @@ sugarss@^2.0.0:
dependencies:
postcss "^7.0.2"
-supports-color@6.1.0, supports-color@^6.0.0, supports-color@^6.1.0:
+supports-color@6.1.0, supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
@@ -11761,15 +10859,7 @@ throttle-debounce@^2.0.0:
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.0.1.tgz#7307ddd6cd9acadb349132fbf6c18d78c88a5e62"
integrity sha512-Sr6jZBlWShsAaSXKyNXyNicOrJW/KtkDqIEwHt4wYwWA2wa/q67Luhqoujg48V8hTk60wB56tYrJJn6jc2R7VA==
-through2-filter@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
- integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==
- dependencies:
- through2 "~2.0.0"
- xtend "~4.0.0"
-
-through2@^2.0.0, through2@^2.0.3, through2@~2.0.0:
+through2@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
@@ -11777,7 +10867,7 @@ through2@^2.0.0, through2@^2.0.3, through2@~2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
-"through@>=2.2.7 <3", through@^2.3.6:
+through@^2.3.6:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@@ -11816,18 +10906,6 @@ tiny-emitter@^2.0.0:
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==
-tiny-lr@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab"
- integrity sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==
- dependencies:
- body "^5.1.0"
- debug "^3.1.0"
- faye-websocket "~0.10.0"
- livereload-js "^2.3.0"
- object-assign "^4.1.0"
- qs "^6.4.0"
-
tiptap-commands@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.4.0.tgz#0cfb3ac138ee3099de56114cb119abd841fbcbe7"
@@ -11891,14 +10969,6 @@ tmpl@1.0.x:
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
-to-absolute-glob@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
- integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=
- dependencies:
- is-absolute "^1.0.0"
- is-negated-glob "^1.0.0"
-
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@@ -11946,13 +11016,6 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2"
safe-regex "^1.1.0"
-to-through@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6"
- integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=
- dependencies:
- through2 "^2.0.3"
-
toggle-selection@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
@@ -11985,11 +11048,6 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
-trim-lines@^1.0.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-1.1.2.tgz#c8adbdbdae21bb5c2766240a661f693afe23e59b"
- integrity sha512-3GOuyNeTqk3FAqc3jOJtw7FTjYl94XBR5aD9QnDbK/T4CA9sW/J0l9RoaRPE9wyPP7NF331qnHnvJFBJ+IDkmQ==
-
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -12037,13 +11095,6 @@ tryer@^1.0.0:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.0.tgz#027b69fa823225e551cace3ef03b11f6ab37c1d7"
integrity sha1-Antp+oIyJeVRys4+8DsR9qs3wdc=
-ts-invariant@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f"
- integrity sha512-Z/JSxzVmhTo50I+LKagEISFJW3pvPCqsMWLamCTX8Kr3N5aMrnGOqcflbe5hLUzwjvgPfnLzQtHZv0yWQ+FIHg==
- dependencies:
- tslib "^1.9.3"
-
ts-invariant@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.3.2.tgz#89a2ffeb70879b777258df1df1c59383c35209b0"
@@ -12051,6 +11102,13 @@ ts-invariant@^0.3.2:
dependencies:
tslib "^1.9.3"
+ts-invariant@^0.4.0:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86"
+ integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==
+ dependencies:
+ tslib "^1.9.3"
+
ts-jest@24.0.0, ts-jest@^23.10.5:
version "24.0.0"
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.0.0.tgz#3f26bf2ec1fa584863a5a9c29bd8717d549efbf6"
@@ -12095,6 +11153,11 @@ type-check@~0.3.2:
dependencies:
prelude-ls "~1.1.2"
+type-fest@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2"
+ integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==
+
type-is@~1.6.17, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@@ -12103,7 +11166,7 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
-typedarray@^0.0.6, typedarray@~0.0.5:
+typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
@@ -12131,11 +11194,6 @@ ultron@~1.1.0:
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
-unc-path-regex@^0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
- integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
-
undefsafe@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76"
@@ -12196,18 +11254,6 @@ unicode-property-aliases-ecmascript@^1.0.4:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0"
integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==
-unified@^6.0.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba"
- integrity sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==
- dependencies:
- bail "^1.0.0"
- extend "^3.0.0"
- is-plain-obj "^1.1.0"
- trough "^1.0.0"
- vfile "^2.0.0"
- x-is-string "^0.1.0"
-
unified@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/unified/-/unified-7.1.0.tgz#5032f1c1ee3364bd09da12e27fdd4a7553c7be13"
@@ -12251,14 +11297,6 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
-unique-stream@^2.0.2:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac"
- integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==
- dependencies:
- json-stable-stringify-without-jsonify "^1.0.1"
- through2-filter "^3.0.0"
-
unique-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
@@ -12266,13 +11304,6 @@ unique-string@^1.0.0:
dependencies:
crypto-random-string "^1.0.0"
-unist-builder@^1.0.1, unist-builder@^1.0.2:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-1.0.4.tgz#e1808aed30bd72adc3607f25afecebef4dd59e17"
- integrity sha512-v6xbUPP7ILrT15fHGrNyHc1Xda8H3xVhP7/HAIotHOhVPjH5dCXA097C3Rry1Q2O+HbOLCao4hfPB+EYEjHgVg==
- dependencies:
- object-assign "^4.1.0"
-
unist-util-find-all-after@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unist-util-find-all-after/-/unist-util-find-all-after-1.0.2.tgz#9be49cfbae5ca1566b27536670a92836bf2f8d6d"
@@ -12280,21 +11311,11 @@ unist-util-find-all-after@^1.0.2:
dependencies:
unist-util-is "^2.0.0"
-unist-util-generated@^1.1.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.4.tgz#2261c033d9fc23fae41872cdb7663746e972c1a7"
- integrity sha512-SA7Sys3h3X4AlVnxHdvN/qYdr4R38HzihoEVY2Q2BZu8NHWDnw5OGcC/tXWjQfd4iG+M6qRFNIRGqJmp2ez4Ww==
-
unist-util-is@^2.0.0, unist-util-is@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.2.tgz#1193fa8f2bfbbb82150633f3a8d2eb9a1c1d55db"
integrity sha512-YkXBK/H9raAmG7KXck+UUpnKiNmUdB+aBGrknfQ4EreE1banuzrKABx3jP6Z5Z3fMSPMQQmeXBlKpCbMwBkxVw==
-unist-util-position@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.0.3.tgz#fff942b879538b242096c148153826664b1ca373"
- integrity sha512-28EpCBYFvnMeq9y/4w6pbnFmCUfzlsc41NJui5c51hOFjBA1fejcwc+5W4z2+0ECVbScG3dURS3JTVqwenzqZw==
-
unist-util-remove-position@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz#86b5dad104d0bbfbeb1db5f5c92f3570575c12cb"
@@ -12307,13 +11328,6 @@ unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1:
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6"
integrity sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==
-unist-util-stringify-position@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.1.tgz#de2a2bc8d3febfa606652673a91455b6a36fb9f3"
- integrity sha512-Zqlf6+FRI39Bah8Q6ZnNGrEHUhwJOkHde2MHVk96lLyftfJJckaPslKgzhVcviXj8KcE9UJM9F+a4JEiBUTYgA==
- dependencies:
- "@types/unist" "^2.0.2"
-
unist-util-visit-parents@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.0.1.tgz#63fffc8929027bee04bfef7d2cce474f71cb6217"
@@ -12321,7 +11335,7 @@ unist-util-visit-parents@^2.0.0:
dependencies:
unist-util-is "^2.1.2"
-unist-util-visit@^1.0.0, unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
+unist-util-visit@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.1.tgz#4724aaa8486e6ee6e26d7ff3c8685960d560b1e3"
integrity sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==
@@ -12497,11 +11511,6 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0"
-value-or-function@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
- integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=
-
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@@ -12528,46 +11537,6 @@ vfile-message@^1.0.0:
dependencies:
unist-util-stringify-position "^1.1.1"
-vfile-message@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.1.tgz#951881861c22fc1eb39f873c0b93e336a64e8f6d"
- integrity sha512-KtasSV+uVU7RWhUn4Lw+wW1Zl/nW8JWx7JCPps10Y9JRRIDeDXf8wfBLoOSsJLyo27DqMyAi54C6Jf/d6Kr2Bw==
- dependencies:
- "@types/unist" "^2.0.2"
- unist-util-stringify-position "^2.0.0"
-
-vfile-reporter@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/vfile-reporter/-/vfile-reporter-6.0.0.tgz#753119f51dec9289b7508b457afc0cddf5e07f2e"
- integrity sha512-8Is0XxFxWJUhPJdOg3CyZTqd3ICCWg6r304PuBl818ZG91h4FMS3Q+lrOPS+cs5/DZK3H0+AkJdH0J8JEwKtDA==
- dependencies:
- repeat-string "^1.5.0"
- string-width "^4.0.0"
- supports-color "^6.0.0"
- unist-util-stringify-position "^2.0.0"
- vfile-sort "^2.1.2"
- vfile-statistics "^1.1.0"
-
-vfile-sort@^2.1.0, vfile-sort@^2.1.2:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/vfile-sort/-/vfile-sort-2.2.1.tgz#74e714f9175618cdae96bcaedf1a3dc711d87567"
- integrity sha512-5dt7xEhC44h0uRQKhbM2JAe0z/naHphIZlMOygtMBM9Nn0pZdaX5fshhwWit9wvsuP8t/wp43nTDRRErO1WK8g==
-
-vfile-statistics@^1.1.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/vfile-statistics/-/vfile-statistics-1.1.3.tgz#e9c87071997fbcb4243764d2c3805e0bb0820c60"
- integrity sha512-CstaK/ebTz1W3Qp41Bt9Lj/2DmumFsCwC2sKahDNSPh0mPh7/UyMLCoU8ZBX34CRU0d61B4W41yIFsV0NKMZeA==
-
-vfile@^2.0.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a"
- integrity sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==
- dependencies:
- is-buffer "^1.1.4"
- replace-ext "1.0.0"
- unist-util-stringify-position "^1.0.0"
- vfile-message "^1.0.0"
-
vfile@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-3.0.1.tgz#47331d2abe3282424f4a4bb6acd20a44c4121803"
@@ -12578,65 +11547,6 @@ vfile@^3.0.0:
unist-util-stringify-position "^1.0.0"
vfile-message "^1.0.0"
-vfile@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.0.1.tgz#fc3d43a1c71916034216bf65926d5ee3c64ed60c"
- integrity sha512-lRHFCuC4SQBFr7Uq91oJDJxlnftoTLQ7eKIpMdubhYcVMho4781a8MWXLy3qZrZ0/STD1kRiKc0cQOHm4OkPeA==
- dependencies:
- "@types/unist" "^2.0.0"
- is-buffer "^2.0.0"
- replace-ext "1.0.0"
- unist-util-stringify-position "^2.0.0"
- vfile-message "^2.0.0"
-
-vinyl-fs@^3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7"
- integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==
- dependencies:
- fs-mkdirp-stream "^1.0.0"
- glob-stream "^6.1.0"
- graceful-fs "^4.0.0"
- is-valid-glob "^1.0.0"
- lazystream "^1.0.0"
- lead "^1.0.0"
- object.assign "^4.0.4"
- pumpify "^1.3.5"
- readable-stream "^2.3.3"
- remove-bom-buffer "^3.0.0"
- remove-bom-stream "^1.2.0"
- resolve-options "^1.1.0"
- through2 "^2.0.0"
- to-through "^2.0.0"
- value-or-function "^3.0.0"
- vinyl "^2.0.0"
- vinyl-sourcemap "^1.1.0"
-
-vinyl-sourcemap@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16"
- integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=
- dependencies:
- append-buffer "^1.0.2"
- convert-source-map "^1.5.0"
- graceful-fs "^4.1.6"
- normalize-path "^2.1.1"
- now-and-later "^2.0.0"
- remove-bom-buffer "^3.0.0"
- vinyl "^2.0.0"
-
-vinyl@^2.0.0, vinyl@^2.1.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
- integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
- dependencies:
- clone "^2.1.1"
- clone-buffer "^1.0.0"
- clone-stats "^1.0.0"
- cloneable-readable "^1.0.0"
- remove-trailing-separator "^1.0.1"
- replace-ext "^1.0.0"
-
visibilityjs@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63"
@@ -12730,7 +11640,7 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.5.16, vue-template-compiler@^2.5.20, vue-template-compiler@^2.6.10:
+vue-template-compiler@^2.5.20, vue-template-compiler@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
@@ -13160,7 +12070,7 @@ xmlhttprequest@1:
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
-xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1:
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==