summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--.gitlab/CODEOWNERS.disabled4
-rw-r--r--.gitlab/ci/cng.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml11
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml11
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml62
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml123
-rw-r--r--.gitlab/issue_templates/Database Reviewer.md8
-rw-r--r--.gitlab/issue_templates/Refactoring.md41
-rw-r--r--.haml-lint.yml4
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_todo.yml515
-rw-r--r--.stylelintrc10
-rw-r--r--CHANGELOG.md400
-rw-r--r--Dangerfile2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile41
-rw-r--r--Gemfile.lock225
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/batch_comments/mixins/resolved_status.js13
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js5
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js3
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js5
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js3
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js5
-rw-r--r--app/assets/javascripts/blob/sketch/index.js5
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js3
-rw-r--r--app/assets/javascripts/blob/viewer/index.js14
-rw-r--r--app/assets/javascripts/boards/boards_util.js7
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js3
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue23
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.vue4
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue6
-rw-r--r--app/assets/javascripts/boards/components/modal/index.vue13
-rw-r--r--app/assets/javascripts/boards/components/modal/list.vue4
-rw-r--r--app/assets/javascripts/boards/index.js4
-rw-r--r--app/assets/javascripts/boards/mixins/issue_card_inner.js5
-rw-r--r--app/assets/javascripts/boards/models/list.js1
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js18
-rw-r--r--app/assets/javascripts/boards/stores/boards_store_ee.js5
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/native_form_variable_list.js1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js67
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue211
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue149
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue33
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue74
-rw-r--r--app/assets/javascripts/clusters/constants.js27
-rw-r--r--app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js5
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js175
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js4
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js108
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js9
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js3
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js5
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js12
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js5
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js4
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue24
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue15
-rw-r--r--app/assets/javascripts/diffs/constants.js7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js113
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js64
-rw-r--r--app/assets/javascripts/diffs/store/utils.js33
-rw-r--r--app/assets/javascripts/diffs/workers/tree_worker.js5
-rw-r--r--app/assets/javascripts/due_date_select.js4
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js3
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js4
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js21
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service_error.js4
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js15
-rw-r--r--app/assets/javascripts/gl_form.js4
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue1
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue8
-rw-r--r--app/assets/javascripts/ide/constants.js7
-rw-r--r--app/assets/javascripts/ide/ide_router.js13
-rw-r--r--app/assets/javascripts/ide/lib/files.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js25
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js5
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/mutations.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js4
-rw-r--r--app/assets/javascripts/ide/stores/utils.js14
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue2
-rw-r--r--app/assets/javascripts/import_projects/store/index.js2
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js9
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue20
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue47
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue1
-rw-r--r--app/assets/javascripts/label_manager.js17
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js4
-rw-r--r--app/assets/javascripts/main.js18
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js15
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js3
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js2
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js114
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue59
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js5
-rw-r--r--app/assets/javascripts/monitoring/services/monitoring_service.js4
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js7
-rw-r--r--app/assets/javascripts/mr_popover/constants.js8
-rw-r--r--app/assets/javascripts/mr_popover/index.js15
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js22
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue1
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue58
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue155
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue11
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue7
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue109
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue209
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue3
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/notes/mixins/note_form.js24
-rw-r--r--app/assets/javascripts/notes/stores/actions.js39
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/utils.js7
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue57
-rw-r--r--app/assets/javascripts/operation_settings/index.js26
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js2
-rw-r--r--app/assets/javascripts/pages/admin/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/clusters/edit/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index.js21
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/clusters/show/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js10
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/search/show/search.js7
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js50
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue34
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue25
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_triggerer.vue35
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js1
-rw-r--r--app/assets/javascripts/projects/project_new.js41
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js3
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js7
-rw-r--r--app/assets/javascripts/raven/index.js7
-rw-r--r--app/assets/javascripts/raven/raven_config.js11
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue4
-rw-r--r--app/assets/javascripts/reports/store/utils.js6
-rw-r--r--app/assets/javascripts/serverless/components/url.vue2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue6
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js7
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js3
-rw-r--r--app/assets/javascripts/snippet/snippet_embed.js6
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js3
-rw-r--r--app/assets/javascripts/terminal/terminal.js4
-rw-r--r--app/assets/javascripts/u2f/error.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue46
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue21
-rw-r--r--app/assets/javascripts/vue_shared/directives/tooltip.js1
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss23
-rw-r--r--app/assets/stylesheets/components/popover.scss37
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss406
-rw-r--r--app/assets/stylesheets/components/toast.scss3
-rw-r--r--app/assets/stylesheets/framework/common.scss16
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss41
-rw-r--r--app/assets/stylesheets/framework/icons.scss4
-rw-r--r--app/assets/stylesheets/framework/modal.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss202
-rw-r--r--app/assets/stylesheets/pages/commits.scss50
-rw-r--r--app/assets/stylesheets/pages/diff.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss5
-rw-r--r--app/assets/stylesheets/pages/labels.scss27
-rw-r--r--app/assets/stylesheets/pages/members.scss3
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss83
-rw-r--r--app/assets/stylesheets/pages/search.scss5
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss61
-rw-r--r--app/assets/stylesheets/performance_bar.scss8
-rw-r--r--app/controllers/admin/application_controller.rb7
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/clusters/applications_controller.rb11
-rw-r--r--app/controllers/admin/clusters_controller.rb13
-rw-r--r--app/controllers/application_controller.rb7
-rw-r--r--app/controllers/clusters/applications_controller.rb13
-rw-r--r--app/controllers/clusters/clusters_controller.rb5
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb7
-rw-r--r--app/controllers/concerns/enforces_admin_authentication.rb19
-rw-r--r--app/controllers/concerns/milestone_actions.rb2
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb6
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb8
-rw-r--r--app/controllers/projects/environments_controller.rb44
-rw-r--r--app/controllers/projects/issues_controller.rb20
-rw-r--r--app/controllers/projects/merge_requests_controller.rb30
-rw-r--r--app/controllers/projects/mirrors_controller.rb1
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/projects/settings/operations_controller.rb6
-rw-r--r--app/controllers/projects/stages_controller.rb25
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/controllers/sessions_controller.rb12
-rw-r--r--app/controllers/uploads_controller.rb5
-rw-r--r--app/finders/autocomplete/users_finder.rb10
-rw-r--r--app/finders/issuable_finder.rb27
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/projects_finder.rb8
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb19
-rw-r--r--app/graphql/resolvers/concerns/resolves_pipelines.rb10
-rw-r--r--app/graphql/resolvers/full_path_resolver.rb6
-rw-r--r--app/graphql/resolvers/group_resolver.rb13
-rw-r--r--app/graphql/resolvers/issues_resolver.rb13
-rw-r--r--app/graphql/resolvers/project_resolver.rb4
-rw-r--r--app/graphql/types/base_field.rb34
-rw-r--r--app/graphql/types/group_type.rb21
-rw-r--r--app/graphql/types/issue_type.rb6
-rw-r--r--app/graphql/types/namespace_type.rb19
-rw-r--r--app/graphql/types/permission_types/group.rb11
-rw-r--r--app/graphql/types/project_type.rb3
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/broadcast_messages_helper.rb2
-rw-r--r--app/helpers/ci_status_helper.rb8
-rw-r--r--app/helpers/ci_variables_helper.rb7
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/labels_helper.rb50
-rw-r--r--app/helpers/markup_helper.rb5
-rw-r--r--app/helpers/mirror_helper.rb4
-rw-r--r--app/helpers/notes_helper.rb10
-rw-r--r--app/helpers/notifications_helper.rb7
-rw-r--r--app/helpers/projects_helper.rb13
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sidekiq_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb94
-rw-r--r--app/helpers/storage_helper.rb10
-rw-r--r--app/models/application_record.rb17
-rw-r--r--app/models/application_setting.rb10
-rw-r--r--app/models/application_setting_implementation.rb18
-rw-r--r--app/models/ci/bridge.rb3
-rw-r--r--app/models/ci/build.rb29
-rw-r--r--app/models/ci/job_artifact.rb26
-rw-r--r--app/models/ci/legacy_stage.rb4
-rw-r--r--app/models/ci/pipeline.rb29
-rw-r--r--app/models/ci/pipeline_schedule.rb3
-rw-r--r--app/models/ci/stage.rb4
-rw-r--r--app/models/clusters/applications/cert_manager.rb6
-rw-r--r--app/models/clusters/applications/helm.rb7
-rw-r--r--app/models/clusters/applications/ingress.rb7
-rw-r--r--app/models/clusters/applications/jupyter.rb6
-rw-r--r--app/models/clusters/applications/knative.rb8
-rw-r--r--app/models/clusters/applications/prometheus.rb18
-rw-r--r--app/models/clusters/applications/runner.rb15
-rw-r--r--app/models/clusters/cluster.rb15
-rw-r--r--app/models/clusters/concerns/application_core.rb10
-rw-r--r--app/models/clusters/concerns/application_data.rb8
-rw-r--r--app/models/clusters/concerns/application_status.rb13
-rw-r--r--app/models/clusters/instance.rb17
-rw-r--r--app/models/clusters/platforms/kubernetes.rb17
-rw-r--r--app/models/commit_range.rb6
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/atomic_internal_id.rb14
-rw-r--r--app/models/concerns/avatarable.rb3
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/ci/contextable.rb8
-rw-r--r--app/models/concerns/ci/pipeline_delegator.rb21
-rw-r--r--app/models/concerns/deployment_platform.rb13
-rw-r--r--app/models/concerns/has_ref.rb3
-rw-r--r--app/models/concerns/has_status.rb4
-rw-r--r--app/models/concerns/has_variable.rb7
-rw-r--r--app/models/concerns/issuable.rb10
-rw-r--r--app/models/concerns/maskable.rb2
-rw-r--r--app/models/concerns/noteable.rb2
-rw-r--r--app/models/concerns/redactable.rb2
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb24
-rw-r--r--app/models/concerns/taskable.rb2
-rw-r--r--app/models/concerns/update_project_statistics.rb60
-rw-r--r--app/models/deployment.rb17
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb2
-rw-r--r--app/models/global_label.rb6
-rw-r--r--app/models/global_milestone.rb4
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/internal_id.rb55
-rw-r--r--app/models/label.rb5
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/members/project_member.rb2
-rw-r--r--app/models/merge_request.rb24
-rw-r--r--app/models/merge_request_diff.rb15
-rw-r--r--app/models/namespace.rb42
-rw-r--r--app/models/note_diff_file.rb4
-rw-r--r--app/models/notification_recipient.rb40
-rw-r--r--app/models/pages_domain.rb18
-rw-r--r--app/models/pool_repository.rb5
-rw-r--r--app/models/project.rb56
-rw-r--r--app/models/project_metrics_setting.rb9
-rw-r--r--app/models/project_services/chat_message/deployment_message.rb77
-rw-r--r--app/models/project_services/chat_notification_service.rb4
-rw-r--r--app/models/project_services/discord_service.rb5
-rw-r--r--app/models/project_services/hangouts_chat_service.rb5
-rw-r--r--app/models/project_services/kubernetes_service.rb4
-rw-r--r--app/models/project_services/microsoft_teams_service.rb5
-rw-r--r--app/models/project_services/mock_ci_service.rb2
-rw-r--r--app/models/project_statistics.rb9
-rw-r--r--app/models/project_wiki.rb22
-rw-r--r--app/models/release.rb7
-rw-r--r--app/models/remote_mirror.rb14
-rw-r--r--app/models/repository.rb65
-rw-r--r--app/models/route.rb16
-rw-r--r--app/models/service.rb3
-rw-r--r--app/models/storage/legacy_project.rb2
-rw-r--r--app/models/user.rb10
-rw-r--r--app/policies/clusters/cluster_policy.rb1
-rw-r--r--app/policies/clusters/instance_policy.rb21
-rw-r--r--app/policies/global_policy.rb1
-rw-r--r--app/policies/personal_snippet_policy.rb13
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/ci/pipeline_presenter.rb12
-rw-r--r--app/presenters/clusters/cluster_presenter.rb4
-rw-r--r--app/presenters/instance_clusterable_presenter.rb69
-rw-r--r--app/presenters/label_presenter.rb43
-rw-r--r--app/presenters/merge_request_presenter.rb33
-rw-r--r--app/presenters/project_presenter.rb4
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/concerns/user_status_tooltip.rb2
-rw-r--r--app/serializers/merge_request_widget_entity.rb4
-rw-r--r--app/services/ci/create_pipeline_service.rb12
-rw-r--r--app/services/ci/play_manual_stage_service.rb29
-rw-r--r--app/services/ci/stop_environments_service.rb16
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/clusters/applications/check_uninstall_progress_service.rb62
-rw-r--r--app/services/clusters/applications/create_service.rb4
-rw-r--r--app/services/clusters/applications/destroy_service.rb23
-rw-r--r--app/services/clusters/applications/install_service.rb4
-rw-r--r--app/services/clusters/applications/patch_service.rb4
-rw-r--r--app/services/clusters/applications/uninstall_service.rb29
-rw-r--r--app/services/clusters/applications/update_service.rb2
-rw-r--r--app/services/clusters/applications/upgrade_service.rb4
-rw-r--r--app/services/clusters/build_service.rb2
-rw-r--r--app/services/clusters/create_service.rb2
-rw-r--r--app/services/clusters/refresh_service.rb6
-rw-r--r--app/services/compare_service.rb4
-rw-r--r--app/services/delete_branch_service.rb24
-rw-r--r--app/services/files/update_service.rb2
-rw-r--r--app/services/git/base_hooks_service.rb14
-rw-r--r--app/services/lfs/file_transformer.rb9
-rw-r--r--app/services/merge_requests/base_service.rb7
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/rebase_service.rb12
-rw-r--r--app/services/notification_recipient_service.rb4
-rw-r--r--app/services/projects/cleanup_service.rb47
-rw-r--r--app/services/projects/housekeeping_service.rb7
-rw-r--r--app/services/projects/import_error_filter.rb2
-rw-r--r--app/services/projects/import_service.rb13
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb92
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb96
-rw-r--r--app/services/projects/operations/update_service.rb11
-rw-r--r--app/services/projects/propagate_service_template.rb2
-rw-r--r--app/services/projects/update_service.rb5
-rw-r--r--app/services/service_response.rb31
-rw-r--r--app/services/system_hooks_service.rb6
-rw-r--r--app/services/tags/destroy_service.rb11
-rw-r--r--app/services/test_hooks/project_service.rb2
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb13
-rw-r--r--app/uploaders/file_uploader.rb4
-rw-r--r--app/uploaders/import_export_uploader.rb4
-rw-r--r--app/uploaders/object_storage.rb2
-rw-r--r--app/views/admin/application_settings/_logging.html.haml6
-rw-r--r--app/views/admin/application_settings/_pages.html.haml29
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml4
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml2
-rw-r--r--app/views/admin/applications/show.html.haml4
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml1
-rw-r--r--app/views/admin/groups/show.html.haml10
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml9
-rw-r--r--app/views/ci/variables/_variable_row.html.haml8
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml13
-rw-r--r--app/views/clusters/clusters/show.html.haml4
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml52
-rw-r--r--app/views/clusters/platforms/kubernetes/_form.html.haml7
-rw-r--r--app/views/dashboard/_projects_head.html.haml24
-rw-r--r--app/views/dashboard/projects/_nav.html.haml27
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml2
-rw-r--r--app/views/doorkeeper/applications/show.html.haml4
-rw-r--r--app/views/events/event/_common.html.haml3
-rw-r--r--app/views/events/event/_note.html.haml3
-rw-r--r--app/views/explore/projects/_filter.html.haml10
-rw-r--r--app/views/explore/projects/index.html.haml4
-rw-r--r--app/views/explore/projects/starred.html.haml4
-rw-r--r--app/views/explore/projects/trending.html.haml4
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/labels/index.html.haml7
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/help/index.html.haml3
-rw-r--r--app/views/import/gitea/new.html.haml6
-rw-r--r--app/views/import/gitea/status.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml5
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml15
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml1
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb2
-rw-r--r--app/views/notify/member_access_granted_email.html.haml11
-rw-r--r--app/views/notify/member_access_granted_email.text.erb7
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_import_project_pane.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml19
-rw-r--r--app/views/projects/_merge_request_merge_method_settings.html.haml49
-rw-r--r--app/views/projects/_merge_request_merge_options_settings.html.haml14
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml24
-rw-r--r--app/views/projects/_merge_request_settings.html.haml4
-rw-r--r--app/views/projects/_zen.html.haml1
-rw-r--r--app/views/projects/branches/_commit.html.haml4
-rw-r--r--app/views/projects/buttons/_download.html.haml45
-rw-r--r--app/views/projects/buttons/_download_links.html.haml5
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/cleanup/_show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_inline_commit.html.haml2
-rw-r--r--app/views/projects/default_branch/_show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml2
-rw-r--r--app/views/projects/deployments/_commit.html.haml4
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/forks/error.html.haml14
-rw-r--r--app/views/projects/forks/index.html.haml13
-rw-r--r--app/views/projects/forks/new.html.haml16
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml8
-rw-r--r--app/views/projects/issues/_merge_requests_status.html.haml25
-rw-r--r--app/views/projects/issues/new.html.haml9
-rw-r--r--app/views/projects/issues/show.html.haml10
-rw-r--r--app/views/projects/labels/index.html.haml41
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/projects/merge_requests/show.html.haml8
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml21
-rw-r--r--app/views/projects/mirrors/_disabled_mirror_badge.html.haml1
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml36
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml2
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml2
-rw-r--r--app/views/projects/new.html.haml11
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml2
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/search/_results.html.haml5
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_delete_label_modal.html.haml4
-rw-r--r--app/views/shared/_label.html.haml8
-rw-r--r--app/views/shared/_label_row.html.haml16
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml4
-rw-r--r--app/views/shared/boards/_show.html.haml10
-rw-r--r--app/views/shared/boards/components/_board.html.haml13
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml6
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml6
-rw-r--r--app/views/shared/icons/_gitea_logo.svg.erb1
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml6
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/members/_access_request_links.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/notifications/_new_button.html.haml4
-rw-r--r--app/views/shared/projects/_dropdown.html.haml21
-rw-r--r--app/views/shared/projects/_search_bar.html.haml28
-rw-r--r--app/views/shared/projects/_search_form.html.haml7
-rw-r--r--app/views/shared/projects/_sort_dropdown.html.haml39
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/cluster_configure_worker.rb8
-rw-r--r--app/workers/cluster_project_configure_worker.rb2
-rw-r--r--app/workers/clusters/applications/uninstall_worker.rb17
-rw-r--r--app/workers/clusters/applications/wait_for_uninstall_app_worker.rb20
-rw-r--r--app/workers/deployments/finished_worker.rb13
-rw-r--r--app/workers/git_garbage_collect_worker.rb11
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb16
-rw-r--r--app/workers/pipeline_schedule_worker.rb28
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--babel.config.js5
-rw-r--r--changelogs/unreleased/10012-move-ee-diff-for-boards-issue-card-inner.yml5
-rw-r--r--changelogs/unreleased/10029-env-item.yml5
-rw-r--r--changelogs/unreleased/10081-env-table.yml5
-rw-r--r--changelogs/unreleased/10083-dropdown-ce-ee-difference.yml5
-rw-r--r--changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml5
-rw-r--r--changelogs/unreleased/10735-geo-gitlab-revision-can-return-not-consistent-results.yml5
-rw-r--r--changelogs/unreleased/11124-update-deployment-service-fails-if-project-is-nil.yml5
-rw-r--r--changelogs/unreleased/11254-overflow-ce.yml5
-rw-r--r--changelogs/unreleased/13784-validate-variables-for-masking.yml5
-rw-r--r--changelogs/unreleased/18432-switch-to-sassc-rails.yml5
-rw-r--r--changelogs/unreleased/24704-download-repository-path.yml5
-rw-r--r--changelogs/unreleased/24936-remove-type-from-review-app-name.yml5
-rw-r--r--changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml5
-rw-r--r--changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml5
-rw-r--r--changelogs/unreleased/25942-remove-fake-repository-path-response.yml5
-rw-r--r--changelogs/unreleased/27383-added_omniauth_openid_connect_startegy.yml5
-rw-r--r--changelogs/unreleased/27424-tklk-gitea-logo.yml5
-rw-r--r--changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml5
-rw-r--r--changelogs/unreleased/28741-play-all-manual-jobs.yml5
-rw-r--r--changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml5
-rw-r--r--changelogs/unreleased/30093-apply-bfg-object-map-to-database.yml5
-rw-r--r--changelogs/unreleased/38564-cant-leave-subgroup.yml5
-rw-r--r--changelogs/unreleased/43263-git-push-option-to-create-mr.yml5
-rw-r--r--changelogs/unreleased/43297-authorized-application-count.yml5
-rw-r--r--changelogs/unreleased/46048-canary-next.yml5
-rw-r--r--changelogs/unreleased/46787-create-project-label-window-is-cut-off-at-the-bottom.yml5
-rw-r--r--changelogs/unreleased/46806-typed-ci-variables.yml5
-rw-r--r--changelogs/unreleased/47234-composable-auto-devops.yml5
-rw-r--r--changelogs/unreleased/47584-label-text-color.yml5
-rw-r--r--changelogs/unreleased/47771-highlighting-in-diff.yml5
-rw-r--r--changelogs/unreleased/48090-filter-sensitive-metric-labels.yml5
-rw-r--r--changelogs/unreleased/48132-display-output-from-pre-receive-scripts.yml5
-rw-r--r--changelogs/unreleased/48297-fix-code-selection.yml6
-rw-r--r--changelogs/unreleased/48479-auto-direction-for-issue-title.yml5
-rw-r--r--changelogs/unreleased/49856-upgrade-bootstrap-form-gem.yml5
-rw-r--r--changelogs/unreleased/49863-ingress-ip-loading-state.yml5
-rw-r--r--changelogs/unreleased/49910-reopening-a-closed-milestone-from-the-closed-milestones-page-fails2.yml5
-rw-r--r--changelogs/unreleased/50926-sort-by-due-date-and-popularity.yml5
-rw-r--r--changelogs/unreleased/51963-support-prometheus-for-group-level-clusters.yml5
-rw-r--r--changelogs/unreleased/51988-install-group-runner-on-group-cluster.yml5
-rw-r--r--changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml5
-rw-r--r--changelogs/unreleased/52366-improved-group-lists-ui-spinners.yml5
-rw-r--r--changelogs/unreleased/52447-auto-devops-at-group-level.yml5
-rw-r--r--changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml5
-rw-r--r--changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml5
-rw-r--r--changelogs/unreleased/53138-add-metrics-usage-ping.yml5
-rw-r--r--changelogs/unreleased/53139-hide-tree-single-file.yml5
-rw-r--r--changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml6
-rw-r--r--changelogs/unreleased/53210-add-uniq-constraints-on-issues-and-mrs-labels.yml5
-rw-r--r--changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml5
-rw-r--r--changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml5
-rw-r--r--changelogs/unreleased/54417-graphql-type-authorization.yml5
-rw-r--r--changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml5
-rw-r--r--changelogs/unreleased/54670-external-diffs-when-outdated.yml5
-rw-r--r--changelogs/unreleased/54916-extended-tooltip-for-merge-request-links.yml5
-rw-r--r--changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml5
-rw-r--r--changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml5
-rw-r--r--changelogs/unreleased/55948-help-text-formatting-wiki.yml5
-rw-r--r--changelogs/unreleased/55964-fix-email-encoding.yml5
-rw-r--r--changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml5
-rw-r--r--changelogs/unreleased/56015-remove-remote-timeout.yml5
-rw-r--r--changelogs/unreleased/56089-merge-gitlab-keys.yml5
-rw-r--r--changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml5
-rw-r--r--changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml5
-rw-r--r--changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml5
-rw-r--r--changelogs/unreleased/56838-allow-guest-access-to-releases.yml5
-rw-r--r--changelogs/unreleased/56864-reopen-locked-mr.yml5
-rw-r--r--changelogs/unreleased/56954-improve-knative-after-installing-tiller.yml5
-rw-r--r--changelogs/unreleased/57017-add-toast-success-message.yml5
-rw-r--r--changelogs/unreleased/57077-add-salesforce-omniauth.yml5
-rw-r--r--changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml5
-rw-r--r--changelogs/unreleased/57131-external_auth_to_core.yml5
-rw-r--r--changelogs/unreleased/57171-add-dashboard-settings.yml5
-rw-r--r--changelogs/unreleased/57247-show-prioritized-labels-to-guests.yml5
-rw-r--r--changelogs/unreleased/57293-fix-image-rename.yml5
-rw-r--r--changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml5
-rw-r--r--changelogs/unreleased/57357-automate-base-domain-help-text.yml5
-rw-r--r--changelogs/unreleased/57364-improve-diff-nav-header.yml5
-rw-r--r--changelogs/unreleased/57409-loading-button-transition.yml5
-rw-r--r--changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml5
-rw-r--r--changelogs/unreleased/57493-add-limit-to-user-name.yml5
-rw-r--r--changelogs/unreleased/57540-filename-trailing-space.yml5
-rw-r--r--changelogs/unreleased/57602-create-cluster-validations.yml5
-rw-r--r--changelogs/unreleased/57648-make-emoji-picker-full-width-on-mobile.yml5
-rw-r--r--changelogs/unreleased/57655-fix-markdown-tables-border.yml5
-rw-r--r--changelogs/unreleased/57668-create-file-from-url.yml5
-rw-r--r--changelogs/unreleased/57669-fix-bug-clicking-file-header-refreshes-page.yml6
-rw-r--r--changelogs/unreleased/57815.yml5
-rw-r--r--changelogs/unreleased/57894-buttons-on-group-page-are-misaligned.yml5
-rw-r--r--changelogs/unreleased/57984-store-branch-name.yml5
-rw-r--r--changelogs/unreleased/58105-pipeline-author-and-commit-author-too-close-together-in-pipeline-list.yml5
-rw-r--r--changelogs/unreleased/58208-explicitly-set-masterauth.yml6
-rw-r--r--changelogs/unreleased/58252-web-ide-dropdown-duplicates.yml5
-rw-r--r--changelogs/unreleased/58293-extract-discussion-actions.yml5
-rw-r--r--changelogs/unreleased/58294-discussion-notes-component.yml5
-rw-r--r--changelogs/unreleased/58361-issue-create-system-note-timestamp.yml5
-rw-r--r--changelogs/unreleased/58375-api-controller.yml5
-rw-r--r--changelogs/unreleased/58375-reactive-caching-changes.yml5
-rw-r--r--changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml5
-rw-r--r--changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml5
-rw-r--r--changelogs/unreleased/58482-update-airminc-clair-local-scan-to-2-0-6.yml5
-rw-r--r--changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml5
-rw-r--r--changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml5
-rw-r--r--changelogs/unreleased/58748-update-nodejs-to-10-15-3.yml5
-rw-r--r--changelogs/unreleased/58751-fix-project-name-members-page.yml5
-rw-r--r--changelogs/unreleased/58781-silent-progress-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml5
-rw-r--r--changelogs/unreleased/58793-fix-nav-links-archived-project.yml5
-rw-r--r--changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml5
-rw-r--r--changelogs/unreleased/58805-allow-incomplete-commit-data-to-be-fetched-from-collection.yml5
-rw-r--r--changelogs/unreleased/58835-button-run-pipeline.yml5
-rw-r--r--changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml5
-rw-r--r--changelogs/unreleased/58850-fix-misplaced-swipe-view-26969.yml5
-rw-r--r--changelogs/unreleased/58883-fix-fetching-comments.yml5
-rw-r--r--changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml5
-rw-r--r--changelogs/unreleased/58933-broken-ui-on-commits-on-mobile.yml5
-rw-r--r--changelogs/unreleased/58971-sentry-api-keyerror.yml5
-rw-r--r--changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml5
-rw-r--r--changelogs/unreleased/59034-external-link-button.yml5
-rw-r--r--changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml5
-rw-r--r--changelogs/unreleased/59062-update-gitlab-markup-python-3.yml5
-rw-r--r--changelogs/unreleased/59079-fix-jupyter-render-loop.yml5
-rw-r--r--changelogs/unreleased/59117-inconsistent-hover-behavior-on-navbar-items.yml5
-rw-r--r--changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml5
-rw-r--r--changelogs/unreleased/59189-long-names-in-project-path-namespace-dropdown-breaks-past-container.yml5
-rw-r--r--changelogs/unreleased/59273-update-fugit.yml5
-rw-r--r--changelogs/unreleased/59275-cluster-form-hints.yml5
-rw-r--r--changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml5
-rw-r--r--changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml5
-rw-r--r--changelogs/unreleased/59352-fix-mr-discussion-expansion.yml5
-rw-r--r--changelogs/unreleased/59441-add-base-domain-to-cluster-api.yml5
-rw-r--r--changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml5
-rw-r--r--changelogs/unreleased/59514-uploading-images-base64.yml5
-rw-r--r--changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml5
-rw-r--r--changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml5
-rw-r--r--changelogs/unreleased/5966-rebase-with-block.yml5
-rw-r--r--changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml5
-rw-r--r--changelogs/unreleased/60026-group-member-count-bg.yml5
-rw-r--r--changelogs/unreleased/60068-avoid-null-domain-help-text.yml5
-rw-r--r--changelogs/unreleased/60116-fix-button-wrapping.yml5
-rw-r--r--changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml5
-rw-r--r--changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml5
-rw-r--r--changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml5
-rw-r--r--changelogs/unreleased/60387-use-icons-in-user-popovers.yml5
-rw-r--r--changelogs/unreleased/60462-empty-pipeline-section.yml5
-rw-r--r--changelogs/unreleased/60552-period-dropdown.yml5
-rw-r--r--changelogs/unreleased/60777-uninstall-button.yml5
-rw-r--r--changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml6
-rw-r--r--changelogs/unreleased/60874-fix-suggestion-misalignment.yml5
-rw-r--r--changelogs/unreleased/61036-fix-ingress-base-domain-text.yml5
-rw-r--r--changelogs/unreleased/61203-fix-lfs-ui-upload.yml5
-rw-r--r--changelogs/unreleased/61278-next.yml5
-rw-r--r--changelogs/unreleased/61469-align-play-icon.yml5
-rw-r--r--changelogs/unreleased/MaxWinterstein-master-patch-23232.yml5
-rw-r--r--changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml5
-rw-r--r--changelogs/unreleased/ac-package-storage-stats.yml5
-rw-r--r--changelogs/unreleased/add-ci-variable-protected-ref.yml5
-rw-r--r--changelogs/unreleased/add-runner-access-level-registration.yml5
-rw-r--r--changelogs/unreleased/add_backtrace_to_kubernetes_log.yml5
-rw-r--r--changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml5
-rw-r--r--changelogs/unreleased/allow-ref-name-caching-projects-controller.yml5
-rw-r--r--changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml5
-rw-r--r--changelogs/unreleased/an-use-labkit.yml5
-rw-r--r--changelogs/unreleased/autodevops_remote_private_helm_repository.yml6
-rw-r--r--changelogs/unreleased/avoid_es_loading_project_ci_status.yml5
-rw-r--r--changelogs/unreleased/bw-add-graphql-groups.yml5
-rw-r--r--changelogs/unreleased/ce-10725-restructure-project-merge-request-settings-page.yml5
-rw-r--r--changelogs/unreleased/ce-11430-update_clair_local_scan.yml5
-rw-r--r--changelogs/unreleased/ce-4681-autosave.yml5
-rw-r--r--changelogs/unreleased/ce-56153-error-tracking-counts.yml5
-rw-r--r--changelogs/unreleased/ce-its-simple-just-destroy-the-mirrors.yml5
-rw-r--r--changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml5
-rw-r--r--changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml5
-rw-r--r--changelogs/unreleased/ce-remove-already-signed-in.yml5
-rw-r--r--changelogs/unreleased/check-mergeability-in-merge-to-ref-service.yml5
-rw-r--r--changelogs/unreleased/ci-lint-ssl-error.yml6
-rw-r--r--changelogs/unreleased/create-label-and-list-checkbox.yml5
-rw-r--r--changelogs/unreleased/da-sentry-client-side-settings.yml5
-rw-r--r--changelogs/unreleased/delete-release-when-delete-tag.yml5
-rw-r--r--changelogs/unreleased/deploy-keys-ext.yml5
-rw-r--r--changelogs/unreleased/disallow-guests-to-access-releases.yml5
-rw-r--r--changelogs/unreleased/do-not-force-2fa.yml6
-rw-r--r--changelogs/unreleased/downloading-expired-artifacts.yml5
-rw-r--r--changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml5
-rw-r--r--changelogs/unreleased/duplicate-related-mrs.yml5
-rw-r--r--changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml5
-rw-r--r--changelogs/unreleased/expand-diff-performance.yml5
-rw-r--r--changelogs/unreleased/expose-group-id-on-home-panel.yml5
-rw-r--r--changelogs/unreleased/extend-cte-optimisations-to-projects.yml5
-rw-r--r--changelogs/unreleased/feat-sentry-environment.yml5
-rw-r--r--changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml5
-rw-r--r--changelogs/unreleased/feature-users-search-results.yml5
-rw-r--r--changelogs/unreleased/feature-webide_escaping.yml5
-rw-r--r--changelogs/unreleased/filter-merge-requests-by-target-branch.yml5
-rw-r--r--changelogs/unreleased/fix-UI-links-to-route-map-info.yml5
-rw-r--r--changelogs/unreleased/fix-api-ide-relative-url-root.yml5
-rw-r--r--changelogs/unreleased/fix-container-scanning-on-k8s.yml5
-rw-r--r--changelogs/unreleased/fix-expand-full-file-on-image.yml5
-rw-r--r--changelogs/unreleased/fix-extra-emails-for-custom-notifications.yml5
-rw-r--r--changelogs/unreleased/fix-hidden-statistics.yml5
-rw-r--r--changelogs/unreleased/fix-ide-relative-url-bug.yml5
-rw-r--r--changelogs/unreleased/fix-ide-web-worker-relative-url.yml5
-rw-r--r--changelogs/unreleased/fix-include-ci-yaml.yml5
-rw-r--r--changelogs/unreleased/fix-issues-time-counter.yml5
-rw-r--r--changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml6
-rw-r--r--changelogs/unreleased/fix-merge-request-pipeline-exist-method.yml5
-rw-r--r--changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml5
-rw-r--r--changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml5
-rw-r--r--changelogs/unreleased/fix-pipeline-entity.yml5
-rw-r--r--changelogs/unreleased/fix-review-app-env-url.yml5
-rw-r--r--changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml5
-rw-r--r--changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml5
-rw-r--r--changelogs/unreleased/fix-transfer-group-possibilities.yml5
-rw-r--r--changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml5
-rw-r--r--changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml5
-rw-r--r--changelogs/unreleased/fixed-web-ide-merge-request-review.yml5
-rw-r--r--changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml5
-rw-r--r--changelogs/unreleased/fj-59522-improve-search-controller-performance.yml5
-rw-r--r--changelogs/unreleased/friendly-wrap-component.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.29.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.32.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.33.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.36.0.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.42.0.yml5
-rw-r--r--changelogs/unreleased/gitlab-issue-54894.yml6
-rw-r--r--changelogs/unreleased/graphql-prometheus.yml5
-rw-r--r--changelogs/unreleased/graphql-resolvers-complexity.yml6
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-projects-pipelines.yml5
-rw-r--r--changelogs/unreleased/id-51433-sort-wiki-by-date.yml5
-rw-r--r--changelogs/unreleased/ide-fix-detect-mr-from-fork.yml5
-rw-r--r--changelogs/unreleased/instance_level_clusters.yml5
-rw-r--r--changelogs/unreleased/issue-42692-deployment-chat-notifications.yml5
-rw-r--r--changelogs/unreleased/issue-61038-deploy-chat-message-update.yml5
-rw-r--r--changelogs/unreleased/issue_57906_fix_github_import.yml5
-rw-r--r--changelogs/unreleased/issue_58547.yml5
-rw-r--r--changelogs/unreleased/jc-client-gitaly-session-id.yml5
-rw-r--r--changelogs/unreleased/jc-guard-against-empty-dereferenced_target.yml5
-rw-r--r--changelogs/unreleased/jc-update-list-last-commits.yml5
-rw-r--r--changelogs/unreleased/jv-dedup-activerecord.yml6
-rw-r--r--changelogs/unreleased/k8s_new_deployment_labels.yml5
-rw-r--r--changelogs/unreleased/knative-0-5.yml5
-rw-r--r--changelogs/unreleased/knative-prometheus.yml5
-rw-r--r--changelogs/unreleased/knative-update.yml5
-rw-r--r--changelogs/unreleased/kube_helm_auto_devops_213.yml5
-rw-r--r--changelogs/unreleased/limit-amount-of-created-pipelines.yml5
-rw-r--r--changelogs/unreleased/localize-notification-dropdown.yml5
-rw-r--r--changelogs/unreleased/markdown-autocomplete-escaping.yml5
-rw-r--r--changelogs/unreleased/member-access-granted-leave-email-fe.yml5
-rw-r--r--changelogs/unreleased/minimized-multiple-queries-ce.yml5
-rw-r--r--changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml5
-rw-r--r--changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml5
-rw-r--r--changelogs/unreleased/nfriend-update-job-detail-view-sidebar.yml5
-rw-r--r--changelogs/unreleased/nfriend-update-merge-request-widget-pipeline-block.yml6
-rw-r--r--changelogs/unreleased/nfriend-update-pipeline-detail-view.yml5
-rw-r--r--changelogs/unreleased/nfriend-update-pipeline-list-view.yml5
-rw-r--r--changelogs/unreleased/only-counted-active-milestones-as-started.yml5
-rw-r--r--changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml5
-rw-r--r--changelogs/unreleased/osw-multi-line-suggestions-parsing.yml5
-rw-r--r--changelogs/unreleased/osw-support-multi-line-suggestions.yml5
-rw-r--r--changelogs/unreleased/patch-49.yml5
-rw-r--r--changelogs/unreleased/persist-fulll-ref-path-for-mr-pipelines.yml5
-rw-r--r--changelogs/unreleased/pl-upgrade-letter_opener_web.yml5
-rw-r--r--changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml5
-rw-r--r--changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml5
-rw-r--r--changelogs/unreleased/rails5-1.yml5
-rw-r--r--changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml5
-rw-r--r--changelogs/unreleased/related-mr-link-cutoff.yml5
-rw-r--r--changelogs/unreleased/remove-ci-charts-undescriptive-header.yml5
-rw-r--r--changelogs/unreleased/remove-disabled-pages-domains-part-2.yml5
-rw-r--r--changelogs/unreleased/rename_auto_deploy_app_links.yml5
-rw-r--r--changelogs/unreleased/rewind-iid-on-pipelines.yml5
-rw-r--r--changelogs/unreleased/security-55503-fix-pdf-js-vulnerability.yml5
-rw-r--r--changelogs/unreleased/security-56224.yml5
-rw-r--r--changelogs/unreleased/security-56927-xss-resolve-conflicts-branch-name.yml5
-rw-r--r--changelogs/unreleased/security-exif-migration.yml5
-rw-r--r--changelogs/unreleased/security-id-potential-denial-languages.yml5
-rw-r--r--changelogs/unreleased/security-mass-assignment-on-project-update.yml5
-rw-r--r--changelogs/unreleased/sh-add-gitaly-backtrace.yml5
-rw-r--r--changelogs/unreleased/sh-add-gitaly-duration-logs.yml5
-rw-r--r--changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml5
-rw-r--r--changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml5
-rw-r--r--changelogs/unreleased/sh-avoid-fetching-temp-refs-within-project.yml5
-rw-r--r--changelogs/unreleased/sh-cache-pipeline-find-commits.yml5
-rw-r--r--changelogs/unreleased/sh-cache-root-ref-asymetrically.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-import-export.yml5
-rw-r--r--changelogs/unreleased/sh-clear-pipeline-status-cache-upon-destroy.yml5
-rw-r--r--changelogs/unreleased/sh-disable-batch-load-replace-methods.yml5
-rw-r--r--changelogs/unreleased/sh-disable-internal-ids-available-check.yml5
-rw-r--r--changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml5
-rw-r--r--changelogs/unreleased/sh-fix-gitaly-find-commit-caching.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-59065.yml5
-rw-r--r--changelogs/unreleased/sh-fix-merge-requests-api-remove-branch-param.yml5
-rw-r--r--changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml5
-rw-r--r--changelogs/unreleased/sh-fix-ref-name-caching.yml5
-rw-r--r--changelogs/unreleased/sh-fix-related-merge-requests-path.yml5
-rw-r--r--changelogs/unreleased/sh-git-gc-after-initial-fetch.yml5
-rw-r--r--changelogs/unreleased/sh-improve-find-commit-caching.yml5
-rw-r--r--changelogs/unreleased/sh-reject-info-refs-head-requests.yml5
-rw-r--r--changelogs/unreleased/sh-skip-sti-tables-reltuples.yml5
-rw-r--r--changelogs/unreleased/sh-update-rails-5-0-7-2.yml5
-rw-r--r--changelogs/unreleased/sh-upgrade-grpc-and-protobuf.yml5
-rw-r--r--changelogs/unreleased/show-disabled-mirrors.yml5
-rw-r--r--changelogs/unreleased/stop-signing-avatar-paths.yml5
-rw-r--r--changelogs/unreleased/support-negative-matches.yml5
-rw-r--r--changelogs/unreleased/tc-raw-log-in-plaintext-mail.yml5
-rw-r--r--changelogs/unreleased/tortuetorche-gitlab-ce-patch-auto-deploy-extra-values.yml5
-rw-r--r--changelogs/unreleased/tpresa-add-highest-role-to-user.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-3-0.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-0.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-shell.yml5
-rw-r--r--changelogs/unreleased/update-rack-oauth2.yml5
-rw-r--r--changelogs/unreleased/update-workhorse-8-5-1.yml5
-rw-r--r--changelogs/unreleased/update-workhorse-master.yml5
-rw-r--r--changelogs/unreleased/use-only-all-pipelines.yml5
-rw-r--r--changelogs/unreleased/use-pg-10-7.yml5
-rw-r--r--changelogs/unreleased/use-untrusted-regexp.yml5
-rw-r--r--changelogs/unreleased/weimeng-user-autocomplete-fix.yml5
-rw-r--r--changelogs/unreleased/wiki-search-results-fix.yml5
-rw-r--r--changelogs/unreleased/winh-toggle-comment-draft.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-add-template-link.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-improve-project-overview.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml5
-rw-r--r--changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml5
-rw-r--r--changelogs/unreleased/zj-git-2-21-tests.yml5
-rw-r--r--config/application.rb3
-rw-r--r--config/gitlab.yml.example12
-rw-r--r--config/initializers/1_settings.rb15
-rw-r--r--config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb94
-rw-r--r--config/initializers/active_record_build_select.rb24
-rw-r--r--config/initializers/active_record_locking.rb76
-rw-r--r--config/initializers/ar_native_database_types.rb3
-rw-r--r--config/initializers/config_initializers_active_record_locking.rb45
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb2
-rw-r--r--config/initializers/lograge.rb2
-rw-r--r--config/initializers/peek.rb14
-rw-r--r--config/initializers/postgresql_opclasses_support.rb2
-rw-r--r--config/initializers/sentry.rb1
-rw-r--r--config/initializers/sidekiq.rb17
-rw-r--r--config/initializers/tracing.rb16
-rw-r--r--config/karma.config.js5
-rw-r--r--config/locales/en.yml2
-rw-r--r--config/prometheus/common_metrics.yml163
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/project.rb8
-rw-r--r--danger/ce_ee_vue_templates/Dangerfile56
-rw-r--r--danger/commit_messages/Dangerfile2
-rw-r--r--danger/roulette/Dangerfile44
-rw-r--r--db/fixtures/development/09_issues.rb20
-rw-r--r--db/importers/common_metrics_importer.rb26
-rw-r--r--db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb1
-rw-r--r--db/migrate/20161031181638_add_unique_index_to_subscriptions.rb4
-rw-r--r--db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb7
-rw-r--r--db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb15
-rw-r--r--db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb17
-rw-r--r--db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb21
-rw-r--r--db/migrate/20190408163745_prometheus_knative05_fix.rb20
-rw-r--r--db/migrate/20190415030217_add_variable_type_to_ci_variables.rb17
-rw-r--r--db/migrate/20190415095825_add_packages_size_to_project_statistics.rb11
-rw-r--r--db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb17
-rw-r--r--db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb17
-rw-r--r--db/migrate/20190416213615_add_variable_type_to_ci_pipeline_variables.rb17
-rw-r--r--db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb17
-rw-r--r--db/migrate/20190418182545_create_merge_request_trains_table.rb18
-rw-r--r--db/migrate/20190422082247_create_project_metrics_settings.rb14
-rw-r--r--db/migrate/20190426180107_add_deployment_events_to_services.rb17
-rw-r--r--db/migrate/20190506135337_add_temporary_indexes_to_state_id.rb34
-rw-r--r--db/post_migrate/20190424134256_drop_projects_ci_id.rb28
-rw-r--r--db/post_migrate/20190506135400_schedule_sync_issuables_state_id_where_nil.rb63
-rw-r--r--db/schema.rb321
-rw-r--r--doc/README.md16
-rw-r--r--doc/administration/audit_events.md116
-rw-r--r--doc/administration/audit_log.pngbin0 -> 25767 bytes
-rw-r--r--doc/administration/auditor_access_form.pngbin0 -> 11910 bytes
-rw-r--r--doc/administration/auditor_users.md88
-rw-r--r--doc/administration/auth/README.md2
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md8
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/admin_group.pngbin0 -> 17543 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_link_final.pngbin0 -> 14524 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gifbin0 -> 1504079 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gifbin0 -> 1703084 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/multi_login.gifbin0 -> 180218 bytes
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md119
-rw-r--r--doc/administration/auth/ldap-ee.md557
-rw-r--r--doc/administration/auth/ldap.md7
-rw-r--r--doc/administration/auth/oidc.md105
-rw-r--r--doc/administration/auth/smartcard.md186
-rw-r--r--doc/administration/compliance.md6
-rw-r--r--doc/administration/container_registry.md46
-rw-r--r--doc/administration/custom_hooks.md69
-rw-r--r--doc/administration/database_load_balancing.md277
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md172
-rw-r--r--doc/administration/geo/disaster_recovery/bring_primary_back.md61
-rw-r--r--doc/administration/geo/disaster_recovery/img/replication-status.pngbin0 -> 7716 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/img/reverification-interval.pngbin0 -> 33620 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/img/verification-status-primary.pngbin0 -> 13329 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/img/verification-status-secondary.pngbin0 -> 12186 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/index.md322
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md227
-rw-r--r--doc/administration/geo/replication/configuration.md314
-rw-r--r--doc/administration/geo/replication/configuration_source.md172
-rw-r--r--doc/administration/geo/replication/database.md507
-rw-r--r--doc/administration/geo/replication/database_source.md439
-rw-r--r--doc/administration/geo/replication/docker_registry.md23
-rw-r--r--doc/administration/geo/replication/external_database.md219
-rw-r--r--doc/administration/geo/replication/faq.md62
-rw-r--r--doc/administration/geo/replication/high_availability.md295
-rw-r--r--doc/administration/geo/replication/img/geo_architecture.pngbin0 -> 116506 bytes
-rw-r--r--doc/administration/geo/replication/img/geo_node_dashboard.pngbin0 -> 26266 bytes
-rw-r--r--doc/administration/geo/replication/img/geo_node_healthcheck.pngbin0 -> 28285 bytes
-rw-r--r--doc/administration/geo/replication/img/geo_overview.pngbin0 -> 37246 bytes
-rw-r--r--doc/administration/geo/replication/index.md309
-rw-r--r--doc/administration/geo/replication/object_storage.md43
-rw-r--r--doc/administration/geo/replication/remove_geo_node.md50
-rw-r--r--doc/administration/geo/replication/security_review.md286
-rw-r--r--doc/administration/geo/replication/troubleshooting.md457
-rw-r--r--doc/administration/geo/replication/tuning.md17
-rw-r--r--doc/administration/geo/replication/updating_the_geo_nodes.md403
-rw-r--r--doc/administration/geo/replication/using_a_geo_server.md18
-rw-r--r--doc/administration/high_availability/README.md194
-rw-r--r--doc/administration/high_availability/alpha_database.md6
-rw-r--r--doc/administration/high_availability/consul.md105
-rw-r--r--doc/administration/high_availability/database.md1158
-rw-r--r--doc/administration/high_availability/gitaly.md90
-rw-r--r--doc/administration/high_availability/gitlab.md6
-rw-r--r--doc/administration/high_availability/nfs.md9
-rw-r--r--doc/administration/high_availability/nfs_host_client_setup.md135
-rw-r--r--doc/administration/high_availability/pg_ha_architecture.pngbin0 -> 18412 bytes
-rw-r--r--doc/administration/high_availability/pgbouncer.md132
-rw-r--r--doc/administration/high_availability/redis.md160
-rw-r--r--doc/administration/img/db_load_balancing_postgres_stats.pngbin0 -> 21543 bytes
-rw-r--r--doc/administration/img/high_availability/active-active-diagram.pngbin14649 -> 0 bytes
-rw-r--r--doc/administration/img/high_availability/active-passive-diagram.pngbin11699 -> 0 bytes
-rw-r--r--doc/administration/img/high_availability/fully-distributed.pngbin0 -> 46918 bytes
-rw-r--r--doc/administration/img/high_availability/geo-ha-diagram.pngbin0 -> 43826 bytes
-rw-r--r--doc/administration/img/high_availability/horizontal.pngbin0 -> 18660 bytes
-rw-r--r--doc/administration/img/high_availability/hybrid.pngbin0 -> 20698 bytes
-rw-r--r--doc/administration/img/instance_review_button.pngbin0 -> 24525 bytes
-rw-r--r--doc/administration/incoming_email.md2
-rw-r--r--doc/administration/index.md34
-rw-r--r--doc/administration/instance_review.md17
-rw-r--r--doc/administration/maven_packages.md5
-rw-r--r--doc/administration/maven_repository.md5
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md41
-rw-r--r--doc/administration/monitoring/prometheus/index.md24
-rw-r--r--doc/administration/monitoring/prometheus/pgbouncer_exporter.md34
-rw-r--r--doc/administration/npm_registry.md5
-rw-r--r--doc/administration/operations/cleaning_up_redis_sessions.md2
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md218
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md15
-rw-r--r--doc/administration/operations/img/sidekiq-cluster.pngbin0 -> 61923 bytes
-rw-r--r--doc/administration/operations/index.md1
-rw-r--r--doc/administration/packages.md174
-rw-r--r--doc/administration/pseudonymizer.md103
-rw-r--r--doc/administration/raketasks/geo.md57
-rw-r--r--doc/administration/raketasks/storage.md3
-rw-r--r--doc/administration/repository_storage_types.md10
-rw-r--r--doc/analytics/README.md5
-rw-r--r--doc/analytics/contribution_analytics.md5
-rw-r--r--doc/api/discussions.md15
-rw-r--r--doc/api/graphql/index.md6
-rw-r--r--doc/api/group_level_variables.md31
-rw-r--r--doc/api/merge_requests.md90
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/api/pipeline_schedules.md5
-rw-r--r--doc/api/pipelines.md3
-rw-r--r--doc/api/project_clusters.md1
-rw-r--r--doc/api/project_level_variables.md31
-rw-r--r--doc/api/project_templates.md2
-rw-r--r--doc/api/projects.md7
-rw-r--r--doc/api/runners.md1
-rw-r--r--doc/api/services.md4
-rw-r--r--doc/api/users.md2
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ee/index.md5
-rw-r--r--doc/ci/README.md209
-rw-r--r--doc/ci/ci_cd_for_external_repos/bitbucket_integration.md147
-rw-r--r--doc/ci/ci_cd_for_external_repos/github_integration.md111
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/bitbucket_app_password.pngbin0 -> 31488 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/bitbucket_webhook.pngbin0 -> 26142 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/ci_cd_for_external_repo.pngbin0 -> 66760 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/external_repository.pngbin0 -> 22832 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/github_omniauth.pngbin0 -> 29022 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.pngbin0 -> 9613 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/github_push_webhook.pngbin0 -> 45725 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/github_repo_list.pngbin0 -> 14282 bytes
-rw-r--r--doc/ci/ci_cd_for_external_repos/index.md31
-rw-r--r--doc/ci/environments.md177
-rw-r--r--doc/ci/environments/protected_environments.md25
-rw-r--r--doc/ci/examples/README.md48
-rw-r--r--doc/ci/examples/browser_performance.md2
-rw-r--r--doc/ci/examples/code_quality.md46
-rw-r--r--doc/ci/examples/container_scanning.md122
-rw-r--r--doc/ci/examples/dast.md105
-rw-r--r--doc/ci/examples/dependency_scanning.md5
-rw-r--r--doc/ci/examples/license_management.md5
-rw-r--r--doc/ci/examples/sast.md5
-rw-r--r--doc/ci/examples/sast_docker.md4
-rw-r--r--doc/ci/img/add_file_template_11_10.pngbin0 -> 55910 bytes
-rw-r--r--doc/ci/img/deployments_view.pngbin23352 -> 58498 bytes
-rw-r--r--doc/ci/img/environments_available.pngbin8464 -> 20410 bytes
-rw-r--r--doc/ci/img/environments_mr_review_app.pngbin13394 -> 30140 bytes
-rw-r--r--doc/ci/img/metrics_reports.pngbin0 -> 19450 bytes
-rw-r--r--doc/ci/img/multi_pipeline_mini_graph.gifbin0 -> 11466 bytes
-rw-r--r--doc/ci/img/multi_project_pipeline_graph.pngbin0 -> 10671 bytes
-rw-r--r--doc/ci/interactive_web_terminal/index.md5
-rw-r--r--doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.pngbin0 -> 139184 bytes
-rw-r--r--doc/ci/introduction/index.md61
-rw-r--r--doc/ci/junit_test_reports.md6
-rw-r--r--doc/ci/large_repositories/index.md39
-rw-r--r--doc/ci/merge_request_pipelines/index.md16
-rw-r--r--doc/ci/metrics_reports.md40
-rw-r--r--doc/ci/multi_project_pipeline_graphs.md5
-rw-r--r--doc/ci/multi_project_pipelines.md152
-rw-r--r--doc/ci/pipelines.md7
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/review_apps/index.md140
-rw-r--r--doc/ci/triggers/README.md60
-rw-r--r--doc/ci/variables/README.md96
-rw-r--r--doc/ci/variables/img/custom_variable_output.pngbin70552 -> 0 bytes
-rw-r--r--doc/ci/variables/img/custom_variables_output.pngbin0 -> 89185 bytes
-rw-r--r--doc/ci/variables/img/new_custom_variable_example.pngbin44164 -> 0 bytes
-rw-r--r--doc/ci/variables/img/new_custom_variables_example.pngbin0 -> 70758 bytes
-rw-r--r--doc/ci/variables/img/override_variable_manual_pipeline.pngbin29090 -> 41714 bytes
-rw-r--r--doc/ci/variables/predefined_variables.md39
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md19
-rw-r--r--doc/ci/yaml/README.md48
-rw-r--r--doc/customization/issue_and_merge_request_template.md5
-rw-r--r--doc/development/README.md7
-rw-r--r--doc/development/architecture.md148
-rw-r--r--doc/development/code_review.md64
-rw-r--r--doc/development/contributing/issue_workflow.md5
-rw-r--r--doc/development/contributing/merge_request_workflow.md2
-rw-r--r--doc/development/documentation/index.md96
-rw-r--r--doc/development/documentation/styleguide.md262
-rw-r--r--doc/development/ee_features.md42
-rw-r--r--doc/development/elasticsearch.md166
-rw-r--r--doc/development/fe_guide/frontend_faq.md27
-rw-r--r--doc/development/fe_guide/index.md4
-rw-r--r--doc/development/fe_guide/style_guide_scss.md2
-rw-r--r--doc/development/geo.md463
-rw-r--r--doc/development/gitaly.md4
-rw-r--r--doc/development/go_guide/index.md2
-rw-r--r--doc/development/i18n/externalization.md2
-rw-r--r--doc/development/i18n/proofreader.md15
-rw-r--r--doc/development/integrations/jira_connect.md41
-rw-r--r--doc/development/licensed_feature_availability.md37
-rw-r--r--doc/development/packages.md68
-rw-r--r--doc/development/rake_tasks.md34
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md5
-rw-r--r--doc/development/testing_guide/img/review_apps_cicd_architecture.pngbin73240 -> 136431 bytes
-rw-r--r--doc/development/testing_guide/review_apps.md162
-rw-r--r--doc/development/testing_guide/testing_levels.md29
-rw-r--r--doc/getting-started/subscription.md3
-rw-r--r--doc/git_hooks/git_hooks.md5
-rw-r--r--doc/gitlab-basics/README.md46
-rw-r--r--doc/gitlab-basics/create-project.md3
-rw-r--r--doc/gitlab-geo/README.md5
-rw-r--r--doc/gitlab-geo/after_setup.md5
-rw-r--r--doc/gitlab-geo/bring-primary-back.md5
-rw-r--r--doc/gitlab-geo/configuration.md5
-rw-r--r--doc/gitlab-geo/configuration_source.md5
-rw-r--r--doc/gitlab-geo/database.md5
-rw-r--r--doc/gitlab-geo/database_source.md5
-rw-r--r--doc/gitlab-geo/disaster-recovery.md5
-rw-r--r--doc/gitlab-geo/docker_registry.md5
-rw-r--r--doc/gitlab-geo/faq.md5
-rw-r--r--doc/gitlab-geo/ha.md5
-rw-r--r--doc/gitlab-geo/object_storage.md5
-rw-r--r--doc/gitlab-geo/planned-failover.md5
-rw-r--r--doc/gitlab-geo/security-review.md5
-rw-r--r--doc/gitlab-geo/ssh.md5
-rw-r--r--doc/gitlab-geo/troubleshooting.md5
-rw-r--r--doc/gitlab-geo/tuning.md5
-rw-r--r--doc/gitlab-geo/updating_the_geo_nodes.md5
-rw-r--r--doc/gitlab-geo/using_a_geo_server.md5
-rw-r--r--doc/install/installation.md20
-rw-r--r--doc/install/ldap.md7
-rw-r--r--doc/install/pivotal/index.md45
-rw-r--r--doc/install/requirements.md20
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/elasticsearch.md584
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/img/jenkins_gitlab_plugin_config.pngbin0 -> 10054 bytes
-rw-r--r--doc/integration/img/jenkins_gitlab_service.pngbin0 -> 19235 bytes
-rw-r--r--doc/integration/img/jenkins_gitlab_service_settings.pngbin0 -> 24094 bytes
-rw-r--r--doc/integration/img/jenkins_project.pngbin0 -> 42275 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_gl_setup_1.pngbin0 -> 36145 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_jira_setup_1.pngbin0 -> 29546 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_jira_setup_2.pngbin0 -> 17374 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_jira_setup_3.pngbin0 -> 37257 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_jira_setup_4.pngbin0 -> 21592 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_jira_setup_5.pngbin0 -> 11002 bytes
-rw-r--r--doc/integration/img/jira_dev_panel_manual_refresh.pngbin0 -> 23542 bytes
-rw-r--r--doc/integration/img/limit_namespace_filter.pngbin0 -> 12777 bytes
-rw-r--r--doc/integration/img/limit_namespaces_projects_options.pngbin0 -> 22650 bytes
-rw-r--r--doc/integration/img/salesforce_app_details.pngbin0 -> 116022 bytes
-rw-r--r--doc/integration/img/salesforce_app_secret_details.pngbin0 -> 199643 bytes
-rw-r--r--doc/integration/img/salesforce_oauth_app_details.pngbin0 -> 171542 bytes
-rw-r--r--doc/integration/img/ultra_auth_credentials.pngbin0 -> 53737 bytes
-rw-r--r--doc/integration/img/ultra_auth_edit_callback_url.pngbin0 -> 39410 bytes
-rw-r--r--doc/integration/img/ultra_auth_edit_callback_url_highlighted.pngbin0 -> 68383 bytes
-rw-r--r--doc/integration/jenkins.md129
-rw-r--r--doc/integration/jenkins_deprecated.md53
-rw-r--r--doc/integration/jira_development_panel.md141
-rw-r--r--doc/integration/kerberos.md315
-rw-r--r--doc/integration/omniauth.md3
-rw-r--r--doc/integration/salesforce.md79
-rw-r--r--doc/integration/saml.md330
-rw-r--r--doc/integration/ultra_auth.md78
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md32
-rw-r--r--doc/legal/individual_contributor_license_agreement.md28
-rw-r--r--doc/license/README.md5
-rw-r--r--doc/push_rules/push_rules.md156
-rw-r--r--doc/raketasks/cleanup.md10
-rw-r--r--doc/subscriptions/billing_table.pngbin0 -> 28321 bytes
-rw-r--r--doc/subscriptions/index.md103
-rw-r--r--doc/tools/email.md38
-rw-r--r--doc/tools/email1.pngbin0 -> 9590 bytes
-rw-r--r--doc/tools/email2.pngbin0 -> 14902 bytes
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/autodevops/index.md35
-rw-r--r--doc/topics/git/troubleshooting_git.md27
-rw-r--r--doc/update/10.0-ce-to-ee.md5
-rw-r--r--doc/update/10.1-ce-to-ee.md5
-rw-r--r--doc/update/10.2-ce-to-ee.md5
-rw-r--r--doc/update/10.3-ce-to-ee.md5
-rw-r--r--doc/update/10.4-ce-to-ee.md5
-rw-r--r--doc/update/10.5-ce-to-ee.md5
-rw-r--r--doc/update/10.6-ce-to-ee.md5
-rw-r--r--doc/update/10.7-ce-to-ee.md5
-rw-r--r--doc/update/10.8-ce-to-ee.md5
-rw-r--r--doc/update/11.0-ce-to-ee.md5
-rw-r--r--doc/update/11.1-ce-to-ee.md5
-rw-r--r--doc/update/11.2-ce-to-ee.md5
-rw-r--r--doc/update/11.3-ce-to-ee.md5
-rw-r--r--doc/update/11.4-ce-to-ee.md5
-rw-r--r--doc/update/11.5-ce-to-ee.md5
-rw-r--r--doc/update/11.6-ce-to-ee.md5
-rw-r--r--doc/update/11.7-ce-to-ee.md5
-rw-r--r--doc/update/11.8-ce-to-ee.md5
-rw-r--r--doc/update/6.0-ce-to-ee.md5
-rw-r--r--doc/update/6.1-ce-to-ee.md5
-rw-r--r--doc/update/6.2-ce-to-ee.md5
-rw-r--r--doc/update/6.3-ce-to-ee.md5
-rw-r--r--doc/update/6.4-ce-to-ee.md5
-rw-r--r--doc/update/6.5-ce-to-ee.md5
-rw-r--r--doc/update/6.6-ce-to-ee.md5
-rw-r--r--doc/update/6.7-ce-to-ee.md5
-rw-r--r--doc/update/6.8-ce-to-ee.md5
-rw-r--r--doc/update/6.9-ce-to-ee.md5
-rw-r--r--doc/update/7.0-ce-to-ee.md5
-rw-r--r--doc/update/7.1-ce-to-ee.md5
-rw-r--r--doc/update/7.10-ce-to-ee.md5
-rw-r--r--doc/update/7.11-ce-to-ee.md5
-rw-r--r--doc/update/7.12-ce-to-ee.md5
-rw-r--r--doc/update/7.13-ce-to-ee.md5
-rw-r--r--doc/update/7.14-ce-to-ee.md5
-rw-r--r--doc/update/7.3-ce-to-ee.md5
-rw-r--r--doc/update/7.4-ce-to-ee.md5
-rw-r--r--doc/update/7.5-ce-to-ee.md5
-rw-r--r--doc/update/7.6-ce-to-ee.md5
-rw-r--r--doc/update/7.7-ce-to-ee.md5
-rw-r--r--doc/update/7.8-ce-to-ee.md5
-rw-r--r--doc/update/7.9-ce-to-ee.md5
-rw-r--r--doc/update/8.0-ce-to-ee.md5
-rw-r--r--doc/update/8.1-ce-to-ee.md5
-rw-r--r--doc/update/8.10-ce-to-ee.md5
-rw-r--r--doc/update/8.11-ce-to-ee.md5
-rw-r--r--doc/update/8.12-ce-to-ee.md5
-rw-r--r--doc/update/8.13-ce-to-ee.md5
-rw-r--r--doc/update/8.14-ce-to-ee.md5
-rw-r--r--doc/update/8.15-ce-to-ee.md5
-rw-r--r--doc/update/8.16-ce-to-ee.md5
-rw-r--r--doc/update/8.17-ce-to-ee.md5
-rw-r--r--doc/update/8.2-ce-to-ee.md5
-rw-r--r--doc/update/8.3-ce-to-ee.md5
-rw-r--r--doc/update/8.4-ce-to-ee.md5
-rw-r--r--doc/update/8.5-ce-to-ee.md5
-rw-r--r--doc/update/8.6-ce-to-ee.md5
-rw-r--r--doc/update/8.7-ce-to-ee.md5
-rw-r--r--doc/update/8.8-ce-to-ee.md5
-rw-r--r--doc/update/8.9-ce-to-ee.md5
-rw-r--r--doc/update/9.0-ce-to-ee.md5
-rw-r--r--doc/update/9.1-ce-to-ee.md5
-rw-r--r--doc/update/9.2-ce-to-ee.md5
-rw-r--r--doc/update/9.3-ce-to-ee.md5
-rw-r--r--doc/update/9.4-ce-to-ee.md5
-rw-r--r--doc/update/9.5-ce-to-ee.md5
-rw-r--r--doc/update/mysql_to_postgresql.md2
-rw-r--r--doc/update/patch_versions.md10
-rw-r--r--doc/update/upgrading_from_ce_to_ee.md4
-rw-r--r--doc/update/upgrading_from_source.md3
-rw-r--r--doc/user/admin_area/geo_nodes.md70
-rw-r--r--doc/user/admin_area/img/admin_wrench.pngbin0 -> 3314 bytes
-rw-r--r--doc/user/admin_area/img/license_admin_area.pngbin0 -> 27826 bytes
-rw-r--r--doc/user/admin_area/img/license_details.pngbin0 -> 119290 bytes
-rw-r--r--doc/user/admin_area/img/license_no_license_message.pngbin0 -> 5778 bytes
-rw-r--r--doc/user/admin_area/img/license_upload.pngbin0 -> 10043 bytes
-rw-r--r--doc/user/admin_area/index.md73
-rw-r--r--doc/user/admin_area/license.md79
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md53
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md83
-rw-r--r--doc/user/admin_area/settings/email.md16
-rw-r--r--doc/user/admin_area/settings/external_authorization.md112
-rw-r--r--doc/user/admin_area/settings/img/additional_minutes.pngbin0 -> 32045 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_group_edit.pngbin0 -> 5869 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_groups.pngbin0 -> 12088 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_project_quota_view.pngbin0 -> 2670 bytes
-rw-r--r--doc/user/admin_area/settings/img/buy_btn.pngbin0 -> 26960 bytes
-rw-r--r--doc/user/admin_area/settings/img/buy_minutes_card.pngbin0 -> 29329 bytes
-rw-r--r--doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.pngbin0 -> 6000 bytes
-rw-r--r--doc/user/admin_area/settings/img/classification_label_on_project_page.pngbin0 -> 19568 bytes
-rw-r--r--doc/user/admin_area/settings/img/email_settings.pngbin0 -> 53267 bytes
-rw-r--r--doc/user/admin_area/settings/img/external_authorization_service_settings.pngbin0 -> 74753 bytes
-rw-r--r--doc/user/admin_area/settings/img/file_template_admin_area.pngbin0 -> 5624 bytes
-rw-r--r--doc/user/admin_area/settings/img/file_template_user_dropdown.pngbin0 -> 8067 bytes
-rw-r--r--doc/user/admin_area/settings/img/group_pipelines_quota.pngbin0 -> 7088 bytes
-rw-r--r--doc/user/admin_area/settings/img/group_quota_view.pngbin0 -> 1797 bytes
-rw-r--r--doc/user/admin_area/settings/img/group_settings.pngbin0 -> 3345 bytes
-rw-r--r--doc/user/admin_area/settings/img/mirror_settings.pngbin0 -> 9966 bytes
-rw-r--r--doc/user/admin_area/settings/index.md2
-rw-r--r--doc/user/admin_area/settings/instance_template_repository.md63
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md10
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning.pngbin0 -> 32549 bytes
-rw-r--r--doc/user/application_security/container_scanning/index.md202
-rw-r--r--doc/user/application_security/dast/img/dast_all.pngbin0 -> 25844 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_single.pngbin0 -> 69353 bytes
-rw-r--r--doc/user/application_security/dast/index.md262
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning.pngbin0 -> 16167 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/index.md225
-rw-r--r--doc/user/application_security/img/create_issue_with_list_hover.pngbin0 -> 106954 bytes
-rw-r--r--doc/user/application_security/img/interactive_reports.pngbin0 -> 23190 bytes
-rw-r--r--doc/user/application_security/img/issue.pngbin0 -> 4780 bytes
-rw-r--r--doc/user/application_security/img/vulnerability_solution.pngbin0 -> 3421 bytes
-rw-r--r--doc/user/application_security/index.md104
-rw-r--r--doc/user/application_security/license_management/img/license_management.pngbin0 -> 5184 bytes
-rw-r--r--doc/user/application_security/license_management/img/license_management_decision.pngbin0 -> 5981 bytes
-rw-r--r--doc/user/application_security/license_management/img/license_management_pipeline_tab.pngbin0 -> 12115 bytes
-rw-r--r--doc/user/application_security/license_management/img/license_management_settings.pngbin0 -> 13300 bytes
-rw-r--r--doc/user/application_security/license_management/index.md264
-rw-r--r--doc/user/application_security/sast/img/sast.pngbin0 -> 24876 bytes
-rw-r--r--doc/user/application_security/sast/img/security_report.pngbin0 -> 38475 bytes
-rw-r--r--doc/user/application_security/sast/index.md254
-rw-r--r--doc/user/application_security/security_dashboard/img/dashboard.pngbin0 -> 66467 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard.pngbin0 -> 49062 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md103
-rw-r--r--doc/user/discussions/img/mr_review_resolve.pngbin0 -> 21941 bytes
-rw-r--r--doc/user/discussions/img/mr_review_resolve2.pngbin0 -> 9430 bytes
-rw-r--r--doc/user/discussions/img/mr_review_second_comment.pngbin0 -> 11188 bytes
-rw-r--r--doc/user/discussions/img/mr_review_second_comment_added.pngbin0 -> 9673 bytes
-rw-r--r--doc/user/discussions/img/mr_review_start.pngbin0 -> 23491 bytes
-rw-r--r--doc/user/discussions/img/mr_review_unresolve.pngbin0 -> 11302 bytes
-rw-r--r--doc/user/discussions/img/mr_review_unresolve2.pngbin0 -> 8976 bytes
-rw-r--r--doc/user/discussions/img/pending_review_comment.pngbin0 -> 8793 bytes
-rw-r--r--doc/user/discussions/img/review_comment_quickactions.pngbin0 -> 12392 bytes
-rw-r--r--doc/user/discussions/img/review_preview.pngbin0 -> 19769 bytes
-rw-r--r--doc/user/discussions/index.md100
-rw-r--r--doc/user/group/clusters/index.md28
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_cal.pngbin0 -> 2029 bytes
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_graph.pngbin0 -> 35400 bytes
-rw-r--r--doc/user/group/contribution_analytics/img/group_stats_table.pngbin0 -> 8473 bytes
-rw-r--r--doc/user/group/contribution_analytics/index.md55
-rw-r--r--doc/user/group/epics/img/button_close_epic.pngbin0 -> 13850 bytes
-rw-r--r--doc/user/group/epics/img/button_reopen_epic.pngbin0 -> 14153 bytes
-rw-r--r--doc/user/group/epics/img/child_epics_roadmap.pngbin0 -> 30149 bytes
-rw-r--r--doc/user/group/epics/img/containing_epic.pngbin0 -> 159939 bytes
-rw-r--r--doc/user/group/epics/img/epic_view.pngbin0 -> 176759 bytes
-rw-r--r--doc/user/group/epics/img/epics_list_view.pngbin0 -> 96826 bytes
-rw-r--r--doc/user/group/epics/img/epics_search.pngbin0 -> 65963 bytes
-rw-r--r--doc/user/group/epics/img/epics_sort.pngbin0 -> 71177 bytes
-rw-r--r--doc/user/group/epics/index.md217
-rw-r--r--doc/user/group/img/group_file_template_dropdown.pngbin0 -> 9519 bytes
-rw-r--r--doc/user/group/img/group_file_template_settings.pngbin0 -> 6217 bytes
-rw-r--r--doc/user/group/img/group_issue_board.pngbin0 -> 63528 bytes
-rw-r--r--doc/user/group/img/member_lock.pngbin0 -> 3168 bytes
-rw-r--r--doc/user/group/img/membership_lock.pngbin4820 -> 0 bytes
-rw-r--r--doc/user/group/index.md125
-rw-r--r--doc/user/group/insights/img/insights_example_stacked_bar_chart.pngbin0 -> 86062 bytes
-rw-r--r--doc/user/group/insights/img/insights_group_configuration.pngbin0 -> 24107 bytes
-rw-r--r--doc/user/group/insights/index.md39
-rw-r--r--doc/user/group/issues_analytics/img/issues_created_per_month.pngbin0 -> 28508 bytes
-rw-r--r--doc/user/group/issues_analytics/index.md16
-rw-r--r--doc/user/group/roadmap/img/epics_state_dropdown.pngbin0 -> 3702 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_timeline_months.pngbin0 -> 7551 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_timeline_quarters.pngbin0 -> 7493 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_timeline_weeks.pngbin0 -> 8008 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_view.pngbin0 -> 49774 bytes
-rw-r--r--doc/user/group/roadmap/index.md64
-rw-r--r--doc/user/group/saml_sso/img/group_saml_configuration_information.pngbin0 -> 50435 bytes
-rw-r--r--doc/user/group/saml_sso/img/group_saml_settings.pngbin0 -> 89399 bytes
-rw-r--r--doc/user/group/saml_sso/img/scim_advanced.pngbin0 -> 21568 bytes
-rw-r--r--doc/user/group/saml_sso/img/scim_attribute_mapping.pngbin0 -> 95420 bytes
-rw-r--r--doc/user/group/saml_sso/img/scim_token.pngbin0 -> 154318 bytes
-rw-r--r--doc/user/group/saml_sso/img/unlink_group_saml.pngbin0 -> 27077 bytes
-rw-r--r--doc/user/group/saml_sso/index.md91
-rw-r--r--doc/user/group/saml_sso/scim_setup.md102
-rw-r--r--doc/user/group/security_dashboard/index.md5
-rw-r--r--doc/user/index.md16
-rw-r--r--doc/user/markdown.md3
-rw-r--r--doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.pngbin0 -> 3948 bytes
-rw-r--r--doc/user/operations_dashboard/img/index_operations_dashboard_with_projects.pngbin0 -> 30837 bytes
-rw-r--r--doc/user/operations_dashboard/index.md37
-rw-r--r--doc/user/permissions.md18
-rw-r--r--doc/user/profile/account/delete_account.md2
-rw-r--r--doc/user/profile/preferences.md1
-rw-r--r--doc/user/project/canary_deployments.md71
-rw-r--r--doc/user/project/ci_cd_for_external_repo.md5
-rw-r--r--doc/user/project/clusters/img/k8s_cluster_monitoring.pngbin0 -> 43150 bytes
-rw-r--r--doc/user/project/clusters/img/kubernetes_pod_logs.pngbin0 -> 147319 bytes
-rw-r--r--doc/user/project/clusters/img/pod_logs_deploy_board.pngbin0 -> 13291 bytes
-rw-r--r--doc/user/project/clusters/index.md135
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md21
-rw-r--r--doc/user/project/clusters/serverless/index.md15
-rw-r--r--doc/user/project/code_owners.md85
-rw-r--r--doc/user/project/deploy_boards.md132
-rw-r--r--doc/user/project/description_templates.md47
-rw-r--r--doc/user/project/file_lock.md107
-rw-r--r--doc/user/project/img/assignee_lists.pngbin0 -> 134635 bytes
-rw-r--r--doc/user/project/img/deploy_boards_canary_deployments.pngbin0 -> 17380 bytes
-rw-r--r--doc/user/project/img/deploy_boards_kubernetes_label.pngbin0 -> 43978 bytes
-rw-r--r--doc/user/project/img/deploy_boards_landing_page.pngbin0 -> 16107 bytes
-rw-r--r--doc/user/project/img/description_templates_default_settings.pngbin0 -> 26395 bytes
-rw-r--r--doc/user/project/img/file_lock.pngbin0 -> 26973 bytes
-rw-r--r--doc/user/project/img/file_lock_folders.pngbin0 -> 22900 bytes
-rw-r--r--doc/user/project/img/file_lock_list.pngbin0 -> 18243 bytes
-rw-r--r--doc/user/project/img/file_lock_merge_request_error_message.pngbin0 -> 24573 bytes
-rw-r--r--doc/user/project/img/file_lock_repository_view.pngbin0 -> 22947 bytes
-rw-r--r--doc/user/project/img/key_value_labels.pngbin0 -> 6208 bytes
-rw-r--r--doc/user/project/img/labels_epic_sidebar.pngbin0 -> 47869 bytes
-rw-r--r--doc/user/project/img/project_security_dashboard.pngbin0 -> 49062 bytes
-rw-r--r--doc/user/project/img/protected_branches_select_roles_and_users.pngbin0 -> 7155 bytes
-rw-r--r--doc/user/project/img/protected_branches_select_roles_and_users_list.pngbin0 -> 7408 bytes
-rw-r--r--doc/user/project/img/service_desk_confirmation_email.pngbin0 -> 24165 bytes
-rw-r--r--doc/user/project/img/service_desk_disabled.pngbin0 -> 18654 bytes
-rw-r--r--doc/user/project/img/service_desk_enabled.pngbin0 -> 33243 bytes
-rw-r--r--doc/user/project/img/service_desk_issue_tracker.pngbin0 -> 95087 bytes
-rw-r--r--doc/user/project/img/service_desk_nav_item.pngbin0 -> 30323 bytes
-rw-r--r--doc/user/project/img/service_desk_reply.pngbin0 -> 15186 bytes
-rw-r--r--doc/user/project/img/service_desk_thread.pngbin0 -> 60850 bytes
-rw-r--r--doc/user/project/import/gemnasium.md102
-rw-r--r--doc/user/project/import/img/gemnasium/connect_github.pngbin0 -> 49966 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/create_project.pngbin0 -> 85728 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/edit_gitlab-ci.pngbin0 -> 79052 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/pipeline.pngbin0 -> 40872 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/project_connected.pngbin0 -> 21575 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/report.pngbin0 -> 144883 bytes
-rw-r--r--doc/user/project/import/img/gemnasium/select_project.pngbin0 -> 8927 bytes
-rw-r--r--doc/user/project/import/index.md29
-rw-r--r--doc/user/project/index.md23
-rw-r--r--doc/user/project/insights/img/insights_example_bar_chart.pngbin0 -> 21767 bytes
-rw-r--r--doc/user/project/insights/img/insights_example_bar_time_series_chart.pngbin0 -> 34068 bytes
-rw-r--r--doc/user/project/insights/img/insights_example_line_chart.pngbin0 -> 120678 bytes
-rw-r--r--doc/user/project/insights/img/insights_example_pie_chart.pngbin0 -> 10889 bytes
-rw-r--r--doc/user/project/insights/img/insights_example_stacked_bar_chart.pngbin0 -> 81587 bytes
-rw-r--r--doc/user/project/insights/img/project_insights.pngbin0 -> 41210 bytes
-rw-r--r--doc/user/project/insights/index.md300
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md4
-rw-r--r--doc/user/project/integrations/github.md48
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md65
-rw-r--r--doc/user/project/integrations/img/github_configuration.pngbin0 -> 12515 bytes
-rw-r--r--doc/user/project/integrations/img/github_status_check_pipeline_update.pngbin0 -> 21075 bytes
-rw-r--r--doc/user/project/integrations/img/gitlab_slack_app_landing_page.pngbin0 -> 32992 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_add_metric.pngbin0 -> 53571 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_alert.pngbin0 -> 24452 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_service_alerts.pngbin0 -> 40727 bytes
-rw-r--r--doc/user/project/integrations/mock_ci.md2
-rw-r--r--doc/user/project/integrations/project_services.md8
-rw-r--r--doc/user/project/integrations/prometheus.md81
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md24
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md13
-rw-r--r--doc/user/project/issue_board.md2
-rw-r--r--doc/user/project/issues/create_new_issue.md20
-rw-r--r--doc/user/project/issues/csv_export.md77
-rw-r--r--doc/user/project/issues/img/create_issue_from_group_level_issue_tracker.pngbin0 -> 29358 bytes
-rw-r--r--doc/user/project/issues/img/csv_export_button.pngbin0 -> 7383 bytes
-rw-r--r--doc/user/project/issues/img/csv_export_modal.pngbin0 -> 16825 bytes
-rw-r--r--doc/user/project/issues/img/multiple_assignees.gifbin0 -> 877551 bytes
-rw-r--r--doc/user/project/issues/img/multiple_assignees_for_issues.pngbin0 -> 39710 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_add.pngbin0 -> 12900 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_remove.pngbin0 -> 5450 bytes
-rw-r--r--doc/user/project/issues/img/select_project_from_group_level_issue_tracker.pngbin0 -> 23706 bytes
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md4
-rw-r--r--doc/user/project/issues/multiple_assignees_for_issues.md41
-rw-r--r--doc/user/project/issues/related_issues.md45
-rw-r--r--doc/user/project/labels.md57
-rw-r--r--doc/user/project/maven_packages.md5
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md54
-rw-r--r--doc/user/project/merge_requests/code_quality.md82
-rw-r--r--doc/user/project/merge_requests/code_quality_diff.md6
-rw-r--r--doc/user/project/merge_requests/container_scanning.md5
-rw-r--r--doc/user/project/merge_requests/dast.md5
-rw-r--r--doc/user/project/merge_requests/dependency_scanning.md5
-rw-r--r--doc/user/project/merge_requests/img/approvals_can_override.pngbin0 -> 7634 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_premium_mr_widget.pngbin0 -> 76524 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_premium_project_edit.pngbin0 -> 47371 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_remove_on_push.pngbin0 -> 6551 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_starter_project_edit.pngbin0 -> 52442 bytes
-rw-r--r--doc/user/project/merge_requests/img/approvals_starter_project_empty.pngbin0 -> 50820 bytes
-rw-r--r--doc/user/project/merge_requests/img/approve.pngbin0 -> 19329 bytes
-rw-r--r--doc/user/project/merge_requests/img/approve_additionally.pngbin0 -> 22700 bytes
-rw-r--r--doc/user/project/merge_requests/img/browser_performance_testing.pngbin0 -> 52100 bytes
-rw-r--r--doc/user/project/merge_requests/img/code_quality.gifbin0 -> 2617453 bytes
-rw-r--r--doc/user/project/merge_requests/img/container_scanning.pngbin0 -> 32549 bytes
-rw-r--r--doc/user/project/merge_requests/img/create-issue-with-list-hover.pngbin0 -> 106954 bytes
-rw-r--r--doc/user/project/merge_requests/img/dast_all.pngbin0 -> 25844 bytes
-rw-r--r--doc/user/project/merge_requests/img/dast_single.pngbin0 -> 69353 bytes
-rw-r--r--doc/user/project/merge_requests/img/dependency_scanning.pngbin0 -> 16167 bytes
-rw-r--r--doc/user/project/merge_requests/img/filter_approver_merge_requests.pngbin0 -> 90764 bytes
-rw-r--r--doc/user/project/merge_requests/img/interactive_reports.pngbin0 -> 23190 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management.pngbin0 -> 5184 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_decision.pngbin0 -> 5981 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_pipeline_tab.pngbin0 -> 12115 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_settings.pngbin0 -> 13300 bytes
-rw-r--r--doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.pngbin0 -> 20867 bytes
-rw-r--r--doc/user/project/merge_requests/img/remove_approval.pngbin0 -> 21902 bytes
-rw-r--r--doc/user/project/merge_requests/img/sast.pngbin0 -> 24876 bytes
-rw-r--r--doc/user/project/merge_requests/img/security_report.pngbin0 -> 38475 bytes
-rw-r--r--doc/user/project/merge_requests/img/vulnerability_solution.pngbin0 -> 3421 bytes
-rw-r--r--doc/user/project/merge_requests/index.md114
-rw-r--r--doc/user/project/merge_requests/license_management.md5
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md319
-rw-r--r--doc/user/project/merge_requests/sast.md5
-rw-r--r--doc/user/project/merge_requests/sast_docker.md5
-rw-r--r--doc/user/project/milestones/burndown_charts.md70
-rw-r--r--doc/user/project/milestones/img/burndown_chart.pngbin0 -> 48403 bytes
-rw-r--r--doc/user/project/milestones/index.md37
-rw-r--r--doc/user/project/operations/feature_flags.md177
-rw-r--r--doc/user/project/operations/img/feature_flags_list.pngbin0 -> 14963 bytes
-rw-r--r--doc/user/project/operations/img/specs_list.pngbin0 -> 43574 bytes
-rw-r--r--doc/user/project/operations/index.md4
-rw-r--r--doc/user/project/operations/tracing.md34
-rw-r--r--doc/user/project/packages/img/maven_package_view.pngbin0 -> 16105 bytes
-rw-r--r--doc/user/project/packages/img/npm_package_view.pngbin0 -> 24443 bytes
-rw-r--r--doc/user/project/packages/maven.md5
-rw-r--r--doc/user/project/packages/maven_packages.md5
-rw-r--r--doc/user/project/packages/maven_repository.md341
-rw-r--r--doc/user/project/packages/npm_registry.md120
-rw-r--r--doc/user/project/pages/getting_started_part_four.md2
-rw-r--r--doc/user/project/pages/getting_started_part_two.md5
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedule_variables.pngbin5360 -> 17292 bytes
-rw-r--r--doc/user/project/pipelines/img/pipeline_schedules_new_form.pngbin23290 -> 62141 bytes
-rw-r--r--doc/user/project/pipelines/job_artifacts.md3
-rw-r--r--doc/user/project/pipelines/schedules.md2
-rw-r--r--doc/user/project/protected_branches.md24
-rw-r--r--doc/user/project/quick_actions.md11
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md2
-rw-r--r--doc/user/project/repository/img/download_source_code.pngbin0 -> 61467 bytes
-rw-r--r--doc/user/project/repository/index.md31
-rw-r--r--doc/user/project/repository/reducing_the_repo_size_using_git.md6
-rw-r--r--doc/user/project/security_dashboard.md5
-rw-r--r--doc/user/project/service_desk.md124
-rw-r--r--doc/user/project/settings/index.md8
-rw-r--r--doc/user/project/web_ide/index.md118
-rw-r--r--doc/user/search/advanced_global_search.md75
-rw-r--r--doc/user/search/advanced_search_syntax.md69
-rw-r--r--doc/user/search/img/advanced_global_search.pngbin0 -> 15017 bytes
-rw-r--r--doc/user/search/img/multiple_assignees.pngbin0 -> 18897 bytes
-rw-r--r--doc/user/search/index.md21
-rw-r--r--doc/workflow/README.md6
-rw-r--r--doc/workflow/ff_merge.md5
-rw-r--r--doc/workflow/git_annex.md238
-rw-r--r--doc/workflow/git_lfs.md5
-rw-r--r--doc/workflow/img/copy_ssh_public_key_button.pngbin0 -> 11225 bytes
-rw-r--r--doc/workflow/issue_weight.md22
-rw-r--r--doc/workflow/issue_weight/issue.pngbin0 -> 69564 bytes
-rw-r--r--doc/workflow/lfs/images/git-annex-branches.pngbin0 -> 32164 bytes
-rw-r--r--doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md261
-rw-r--r--doc/workflow/merge_request_approvals.md5
-rw-r--r--doc/workflow/notifications.md27
-rw-r--r--doc/workflow/rebase_before_merge.md5
-rw-r--r--doc/workflow/repository_mirroring.md6
-rw-r--r--doc/workflow/shortcuts.md8
-rw-r--r--doc/workflow/todos.md19
-rw-r--r--jest.config.js5
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/branches.rb4
-rw-r--r--lib/api/discussions.rb6
-rw-r--r--lib/api/entities.rb31
-rw-r--r--lib/api/events.rb49
-rw-r--r--lib/api/group_variables.rb2
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/events_helpers.rb31
-rw-r--r--lib/api/helpers/related_resources_helpers.rb4
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/issues.rb1
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/pipeline_schedules.rb2
-rw-r--r--lib/api/project_clusters.rb1
-rw-r--r--lib/api/project_events.rb29
-rw-r--r--lib/api/releases.rb16
-rw-r--r--lib/api/runner.rb4
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/api/variables.rb2
-rw-r--r--lib/api/wikis.rb3
-rw-r--r--lib/banzai/color_parser.rb20
-rw-r--r--lib/banzai/filter/autolink_filter.rb2
-rw-r--r--lib/banzai/filter/front_matter_filter.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb6
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb2
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb3
-rw-r--r--lib/declarative_policy/preferred_scope.rb1
-rw-r--r--lib/gitlab.rb10
-rw-r--r--lib/gitlab/action_view_output/context.rb41
-rw-r--r--lib/gitlab/auth/result.rb1
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb4
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb2
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb1
-rw-r--r--lib/gitlab/checks/branch_check.rb6
-rw-r--r--lib/gitlab/checks/lfs_check.rb1
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb4
-rw-r--r--lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb5
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/ci/config/external/processor.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb1
-rw-r--r--lib/gitlab/ci/pipeline/chain/skip.rb2
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb28
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb31
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb4
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb13
-rw-r--r--lib/gitlab/ci/status/build/failed_allowed.rb2
-rw-r--r--lib/gitlab/ci/status/stage/factory.rb3
-rw-r--r--lib/gitlab/ci/status/stage/play_manual.rb43
-rw-r--r--lib/gitlab/ci/status/success_warning.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml27
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml32
-rw-r--r--lib/gitlab/ci/templates/dotNET-Core.yml107
-rw-r--r--lib/gitlab/ci/trace.rb14
-rw-r--r--lib/gitlab/ci/variables/collection/item.rb6
-rw-r--r--lib/gitlab/content_disposition.rb4
-rw-r--r--lib/gitlab/danger/helper.rb11
-rw-r--r--lib/gitlab/data_builder/deployment.rb25
-rw-r--r--lib/gitlab/data_builder/push.rb12
-rw-r--r--lib/gitlab/discussions_diff/highlight_cache.rb13
-rw-r--r--lib/gitlab/file_detector.rb1
-rw-r--r--lib/gitlab/git/object_pool.rb4
-rw-r--r--lib/gitlab/git/pre_receive_error.rb2
-rw-r--r--lib/gitlab/git/repository.rb68
-rw-r--r--lib/gitlab/git/repository_cleaner.rb4
-rw-r--r--lib/gitlab/git/rugged_impl/commit.rb20
-rw-r--r--lib/gitlab/git/rugged_impl/repository.rb2
-rw-r--r--lib/gitlab/git/wiki.rb24
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb30
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb4
-rw-r--r--lib/gitlab/gitaly_client/cleanup_service.rb33
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/object_pool_service.rb9
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb58
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb6
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb8
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb23
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb1
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb1
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb2
-rw-r--r--lib/gitlab/github_import/representation/note.rb2
-rw-r--r--lib/gitlab/gl_repository.rb12
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb2
-rw-r--r--lib/gitlab/gon_helper.rb7
-rw-r--r--lib/gitlab/grape_logging/loggers/correlation_id_logger.rb2
-rw-r--r--lib/gitlab/graphql/authorize.rb2
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb90
-rw-r--r--lib/gitlab/graphql/connections/keyset_connection.rb16
-rw-r--r--lib/gitlab/graphql/errors.rb1
-rw-r--r--lib/gitlab/graphql/generic_tracing.rb59
-rw-r--r--lib/gitlab/graphql/tracing.rb43
-rw-r--r--lib/gitlab/group_search_results.rb6
-rw-r--r--lib/gitlab/health_checks/metric.rb1
-rw-r--r--lib/gitlab/health_checks/result.rb1
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb5
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/json_logger.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb7
-rw-r--r--lib/gitlab/kubernetes/helm/delete_command.rb55
-rw-r--r--lib/gitlab/metrics/dashboard/base_service.rb73
-rw-r--r--lib/gitlab/metrics/dashboard/finder.rb51
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb46
-rw-r--r--lib/gitlab/metrics/dashboard/project_dashboard_service.rb47
-rw-r--r--lib/gitlab/metrics/dashboard/stages/base_stage.rb58
-rw-r--r--lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb23
-rw-r--r--lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb106
-rw-r--r--lib/gitlab/metrics/dashboard/stages/sorter.rb34
-rw-r--r--lib/gitlab/metrics/dashboard/system_dashboard_service.rb47
-rw-r--r--lib/gitlab/metrics/metric.rb2
-rw-r--r--lib/gitlab/middleware/correlation_id.rb2
-rw-r--r--lib/gitlab/middleware/read_only.rb2
-rw-r--r--lib/gitlab/middleware/release_env.rb1
-rw-r--r--lib/gitlab/namespaced_session_store.rb22
-rw-r--r--lib/gitlab/performance_bar/peek_query_tracker.rb6
-rw-r--r--lib/gitlab/profiler.rb6
-rw-r--r--lib/gitlab/project_search_results.rb4
-rw-r--r--lib/gitlab/prometheus/query_variables.rb6
-rw-r--r--lib/gitlab/push_options.rb2
-rw-r--r--lib/gitlab/quick_actions/commit_actions.rb9
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb32
-rw-r--r--lib/gitlab/quick_actions/issue_actions.rb33
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb46
-rw-r--r--lib/gitlab/quick_actions/spend_time_and_date_separator.rb2
-rw-r--r--lib/gitlab/sanitizers/svg.rb2
-rw-r--r--lib/gitlab/search_results.rb90
-rw-r--r--lib/gitlab/sentry.rb2
-rw-r--r--lib/gitlab/session.rb27
-rw-r--r--lib/gitlab/sherlock/middleware.rb4
-rw-r--r--lib/gitlab/sherlock/query.rb2
-rw-r--r--lib/gitlab/sidekiq_config.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_injector.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/correlation_logger.rb4
-rw-r--r--lib/gitlab/slash_commands/result.rb1
-rw-r--r--lib/gitlab/sql/pattern.rb2
-rw-r--r--lib/gitlab/tracing/common.rb69
-rw-r--r--lib/gitlab/tracing/factory.rb61
-rw-r--r--lib/gitlab/tracing/grpc_interceptor.rb54
-rw-r--r--lib/gitlab/tracing/jaeger_factory.rb97
-rw-r--r--lib/gitlab/tracing/rack_middleware.rb46
-rw-r--r--lib/gitlab/tracing/rails/action_view_subscriber.rb75
-rw-r--r--lib/gitlab/tracing/rails/active_record_subscriber.rb49
-rw-r--r--lib/gitlab/tracing/rails/rails_common.rb24
-rw-r--r--lib/gitlab/tracing/sidekiq/client_middleware.rb26
-rw-r--r--lib/gitlab/tracing/sidekiq/server_middleware.rb26
-rw-r--r--lib/gitlab/tracing/sidekiq/sidekiq_common.rb22
-rw-r--r--lib/gitlab/url_builder.rb4
-rw-r--r--lib/gitlab/usage_data.rb2
-rw-r--r--lib/gitlab/user_extractor.rb2
-rw-r--r--lib/gitlab/workhorse.rb65
-rw-r--r--lib/haml_lint/inline_javascript.rb1
-rw-r--r--lib/peek/views/tracing.rb6
-rw-r--r--lib/quality/kubernetes_client.rb2
-rw-r--r--lib/quality/seeders/issues.rb58
-rw-r--r--lib/system_check/app/git_version_check.rb2
-rw-r--r--lib/system_check/app/ruby_version_check.rb2
-rw-r--r--lib/tasks/gemojione.rake2
-rw-r--r--lib/tasks/gitlab/seed.rake34
-rw-r--r--locale/gitlab.pot895
-rw-r--r--package.json9
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/admin/menu.rb9
-rw-r--r--qa/qa/page/admin/settings/component/performance_bar.rb27
-rw-r--r--qa/qa/page/admin/settings/metrics_and_profiling.rb23
-rw-r--r--qa/qa/page/component/note.rb2
-rw-r--r--qa/qa/page/layout/performance_bar.rb35
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb1
-rw-r--r--qa/qa/runtime/key/ecdsa.rb1
-rw-r--r--qa/qa/runtime/key/ed25519.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb34
-rw-r--r--qa/qa/specs/helpers/quarantine.rb16
-rw-r--r--qa/qa/vendor/github/page/login.rb4
-rw-r--r--qa/spec/git/repository_spec.rb9
-rw-r--r--rubocop/cop/gitlab/finder_with_find_by.rb4
-rw-r--r--rubocop/cop/include_action_view_context.rb31
-rw-r--r--rubocop/qa_helpers.rb2
-rw-r--r--rubocop/rubocop.rb2
-rw-r--r--rubocop/spec_helpers.rb6
-rwxr-xr-xscripts/lint-doc.sh2
-rwxr-xr-xscripts/review_apps/review-apps.sh108
-rw-r--r--scripts/utils.sh136
-rw-r--r--security.txt6
-rw-r--r--spec/controllers/admin/clusters/applications_controller_spec.rb149
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb540
-rw-r--r--spec/controllers/application_controller_spec.rb2
-rw-r--r--spec/controllers/concerns/enforces_admin_authentication_spec.rb40
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb110
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb27
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb6
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb62
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb35
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb2
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb122
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb3
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb9
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb224
-rw-r--r--spec/controllers/projects/stages_controller_spec.rb72
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb22
-rw-r--r--spec/controllers/search_controller_spec.rb24
-rw-r--r--spec/db/importers/common_metrics_importer_spec.rb44
-rw-r--r--spec/factories/ci/pipeline_schedule_variables.rb1
-rw-r--r--spec/factories/clusters/applications/helm.rb19
-rw-r--r--spec/factories/clusters/clusters.rb2
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/merge_requests.rb6
-rw-r--r--spec/factories/pages_domains.rb4
-rw-r--r--spec/factories/pool_repositories.rb1
-rw-r--r--spec/factories/project_metrics_settings.rb8
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/features/admin/admin_settings_spec.rb46
-rw-r--r--spec/features/admin/admin_users_spec.rb7
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb14
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb20
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb221
-rw-r--r--spec/features/groups/members/leave_group_spec.rb26
-rw-r--r--spec/features/issuables/markdown_references/jira_spec.rb2
-rw-r--r--spec/features/issuables/sorting_list_spec.rb28
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb34
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb121
-rw-r--r--spec/features/issues_spec.rb12
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb114
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb22
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb22
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb20
-rw-r--r--spec/features/projects/labels/user_views_labels_spec.rb5
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb9
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb13
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb60
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb31
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb25
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb3
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb10
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/features/protected_tags_spec.rb13
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb36
-rw-r--r--spec/features/users/login_spec.rb16
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb16
-rw-r--r--spec/finders/cluster_ancestors_finder_spec.rb29
-rw-r--r--spec/finders/issues_finder_spec.rb8
-rw-r--r--spec/finders/merge_requests_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json2
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json3
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json2
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule_variable.json10
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json35
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json22
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/tag_release.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/releases.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tag.json2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml36
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json13
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json20
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json17
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json20
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js136
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js251
-rw-r--r--spec/frontend/clusters/components/applications_spec.js2
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js32
-rw-r--r--spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js56
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js162
-rw-r--r--spec/frontend/clusters/services/mock_data.js8
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js76
-rw-r--r--spec/frontend/environment.js7
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js55
-rw-r--r--spec/frontend/helpers/fixtures.js4
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/mutations_spec.js41
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js185
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js58
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js104
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js284
-rw-r--r--spec/frontend/mr_popover/index_spec.js20
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js104
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js139
-rw-r--r--spec/frontend/operation_settings/components/external_dashboard_spec.js100
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js114
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js198
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js66
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js121
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js134
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js107
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js75
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js97
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js139
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js32
-rw-r--r--spec/graphql/features/authorization_spec.rb77
-rw-r--r--spec/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb15
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb8
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb160
-rw-r--r--spec/graphql/resolvers/project_resolver_spec.rb8
-rw-r--r--spec/graphql/types/base_field_spec.rb26
-rw-r--r--spec/graphql/types/group_type_spec.rb11
-rw-r--r--spec/graphql/types/namespace_type.rb7
-rw-r--r--spec/graphql/types/query_type_spec.rb2
-rw-r--r--spec/helpers/labels_helper_spec.rb48
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/helpers/storage_helper_spec.rb24
-rw-r--r--spec/initializers/secret_token_spec.rb4
-rw-r--r--spec/javascripts/api_spec.js14
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js4
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js4
-rw-r--r--spec/javascripts/boards/board_list_common_spec.js58
-rw-r--r--spec/javascripts/boards/board_list_spec.js51
-rw-r--r--spec/javascripts/boards/issue_card_spec.js6
-rw-r--r--spec/javascripts/boards/list_spec.js3
-rw-r--r--spec/javascripts/boards/mock_data.js1
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js14
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js111
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js3
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js1
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js92
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js94
-rw-r--r--spec/javascripts/fixtures/.gitignore2
-rw-r--r--spec/javascripts/fixtures/static/images/green_box.png (renamed from spec/javascripts/fixtures/images/green_box.png)bin1306 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/one_white_pixel.png (renamed from spec/javascripts/fixtures/one_white_pixel.png)bin68 -> 68 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/red_box.png (renamed from spec/javascripts/fixtures/images/red_box.png)bin1305 -> 1305 bytes
-rw-r--r--spec/javascripts/fixtures/static/projects.json (renamed from spec/javascripts/fixtures/projects.json)0
-rw-r--r--spec/javascripts/gl_dropdown_spec.js4
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js4
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js19
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js5
-rw-r--r--spec/javascripts/ide/stores/modules/file_templates/actions_spec.js54
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js19
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js188
-rw-r--r--spec/javascripts/import_projects/components/imported_project_table_row_spec.js51
-rw-r--r--spec/javascripts/import_projects/components/provider_repo_table_row_spec.js96
-rw-r--r--spec/javascripts/import_projects/store/actions_spec.js284
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js47
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/fields/title_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js25
-rw-r--r--spec/javascripts/issue_spec.js1
-rw-r--r--spec/javascripts/jobs/mock_data.js6
-rw-r--r--spec/javascripts/merge_request_spec.js2
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js101
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js24
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js23
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js130
-rw-r--r--spec/javascripts/pdf/index_spec.js4
-rw-r--r--spec/javascripts/pdf/page_spec.js4
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js22
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js11
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js49
-rw-r--r--spec/javascripts/pipelines/pipeline_triggerer_spec.js54
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js48
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js4
-rw-r--r--spec/javascripts/raven/index_spec.js10
-rw-r--r--spec/javascripts/raven/raven_config_spec.js10
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js10
-rw-r--r--spec/javascripts/test_constants.js10
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js77
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js1
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js95
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js114
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js55
-rw-r--r--spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js194
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js75
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js51
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/notes/system_note_spec.js59
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js86
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js121
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js130
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js95
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js108
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js76
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js40
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js42
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js43
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js98
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js137
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js18
-rw-r--r--spec/lib/api/helpers/related_resources_helpers_spec.rb34
-rw-r--r--spec/lib/api/helpers_spec.rb12
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb118
-rw-r--r--spec/lib/gitlab/checks/lfs_check_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/stage/play_manual_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb14
-rw-r--r--spec/lib/gitlab/correlation_id_spec.rb77
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb21
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb39
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb11
-rw-r--r--spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb110
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb37
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb41
-rw-r--r--spec/lib/gitlab/git/repository_cleaner_spec.rb71
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb198
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb16
-rw-r--r--spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb11
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb30
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb42
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb3
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb142
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb5
-rw-r--r--spec/lib/gitlab/graphql/generic_tracing_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/tracing_spec.rb33
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml7
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb22
-rw-r--r--spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb72
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb105
-rw-r--r--spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb32
-rw-r--r--spec/lib/gitlab/namespaced_session_store_spec.rb22
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb10
-rw-r--r--spec/lib/gitlab/profiler_spec.rb6
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb28
-rw-r--r--spec/lib/gitlab/sentry_spec.rb4
-rw-r--r--spec/lib/gitlab/session_spec.rb27
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/tracing/factory_spec.rb43
-rw-r--r--spec/lib/gitlab/tracing/grpc_interceptor_spec.rb47
-rw-r--r--spec/lib/gitlab/tracing/jaeger_factory_spec.rb71
-rw-r--r--spec/lib/gitlab/tracing/rack_middleware_spec.rb62
-rw-r--r--spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb147
-rw-r--r--spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb73
-rw-r--r--spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb43
-rw-r--r--spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb43
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb82
-rw-r--r--spec/lib/quality/kubernetes_client_spec.rb4
-rw-r--r--spec/lib/quality/seeders/issues_spec.rb18
-rw-r--r--spec/mailers/notify_spec.rb4
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb57
-rw-r--r--spec/models/application_record_spec.rb19
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/blob_spec.rb15
-rw-r--r--spec/models/ci/bridge_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb89
-rw-r--r--spec/models/ci/group_variable_spec.rb3
-rw-r--r--spec/models/ci/job_artifact_spec.rb44
-rw-r--r--spec/models/ci/legacy_stage_spec.rb2
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb9
-rw-r--r--spec/models/ci/pipeline_schedule_variable_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb124
-rw-r--r--spec/models/ci/pipeline_variable_spec.rb3
-rw-r--r--spec/models/ci/runner_spec.rb5
-rw-r--r--spec/models/ci/stage_spec.rb2
-rw-r--r--spec/models/ci/variable_spec.rb3
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb6
-rw-r--r--spec/models/clusters/applications/helm_spec.rb8
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb6
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb9
-rw-r--r--spec/models/clusters/applications/knative_spec.rb8
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb53
-rw-r--r--spec/models/clusters/applications/runner_spec.rb30
-rw-r--r--spec/models/clusters/cluster_spec.rb27
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb12
-rw-r--r--spec/models/commit_status_spec.rb4
-rw-r--r--spec/models/concerns/has_ref_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb27
-rw-r--r--spec/models/internal_id_spec.rb62
-rw-r--r--spec/models/issue_spec.rb23
-rw-r--r--spec/models/member_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb69
-rw-r--r--spec/models/namespace_spec.rb64
-rw-r--r--spec/models/note_diff_file_spec.rb27
-rw-r--r--spec/models/notification_recipient_spec.rb40
-rw-r--r--spec/models/pages_domain_spec.rb28
-rw-r--r--spec/models/pool_repository_spec.rb2
-rw-r--r--spec/models/project_metrics_setting_spec.rb55
-rw-r--r--spec/models/project_services/chat_message/deployment_message_spec.rb153
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb11
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb106
-rw-r--r--spec/models/project_statistics_spec.rb30
-rw-r--r--spec/models/project_wiki_spec.rb74
-rw-r--r--spec/models/release_spec.rb5
-rw-r--r--spec/models/remote_mirror_spec.rb16
-rw-r--r--spec/models/repository_spec.rb170
-rw-r--r--spec/models/wiki_page_spec.rb66
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb16
-rw-r--r--spec/policies/clusters/instance_policy_spec.rb36
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb31
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb12
-rw-r--r--spec/presenters/label_presenter_spec.rb65
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb50
-rw-r--r--spec/presenters/project_presenter_spec.rb19
-rw-r--r--spec/rack_servers/configs/puma.rb32
-rw-r--r--spec/rack_servers/puma_spec.rb20
-rw-r--r--spec/requests/api/badges_spec.rb6
-rw-r--r--spec/requests/api/discussions_spec.rb4
-rw-r--r--spec/requests/api/events_spec.rb135
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb118
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/requests/api/group_variables_spec.rb8
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb10
-rw-r--r--spec/requests/api/issues_spec.rb10
-rw-r--r--spec/requests/api/jobs_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb27
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb7
-rw-r--r--spec/requests/api/pipelines_spec.rb9
-rw-r--r--spec/requests/api/project_clusters_spec.rb2
-rw-r--r--spec/requests/api/project_events_spec.rb156
-rw-r--r--spec/requests/api/projects_spec.rb39
-rw-r--r--spec/requests/api/releases_spec.rb49
-rw-r--r--spec/requests/api/runner_spec.rb28
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/variables_spec.rb8
-rw-r--r--spec/requests/rack_attack_global_spec.rb4
-rw-r--r--spec/routing/group_routing_spec.rb14
-rw-r--r--spec/routing/uploads_routing_spec.rb22
-rw-r--r--spec/rubocop/cop/include_action_view_context_spec.rb45
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/serializers/stage_entity_spec.rb28
-rw-r--r--spec/serializers/stage_serializer_spec.rb31
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb77
-rw-r--r--spec/services/ci/play_manual_stage_service_spec.rb79
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb76
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb6
-rw-r--r--spec/services/clusters/applications/check_uninstall_progress_service_spec.rb145
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb2
-rw-r--r--spec/services/clusters/applications/destroy_service_spec.rb63
-rw-r--r--spec/services/clusters/applications/uninstall_service_spec.rb77
-rw-r--r--spec/services/clusters/build_service_spec.rb8
-rw-r--r--spec/services/clusters/refresh_service_spec.rb32
-rw-r--r--spec/services/compare_service_spec.rb13
-rw-r--r--spec/services/delete_branch_service_spec.rb24
-rw-r--r--spec/services/lfs/file_transformer_spec.rb19
-rw-r--r--spec/services/merge_requests/close_service_spec.rb8
-rw-r--r--spec/services/merge_requests/create_service_spec.rb6
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb2
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb8
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb18
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb22
-rw-r--r--spec/services/notification_service_spec.rb62
-rw-r--r--spec/services/projects/cleanup_service_spec.rb89
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb3
-rw-r--r--spec/services/projects/import_service_spec.rb45
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb3
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb1
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb153
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb1
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb148
-rw-r--r--spec/services/projects/operations/update_service_spec.rb50
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/services/service_response_spec.rb57
-rw-r--r--spec/services/system_hooks_service_spec.rb12
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb14
-rw-r--r--spec/services/update_deployment_service_spec.rb1
-rw-r--r--spec/services/users/destroy_service_spec.rb4
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb2
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb12
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/capybara.rb11
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb6
-rw-r--r--spec/support/helpers/email_helpers.rb4
-rw-r--r--spec/support/helpers/features/notes_helpers.rb12
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb17
-rw-r--r--spec/support/helpers/graphql_helpers.rb16
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb3
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb43
-rw-r--r--spec/support/helpers/mobile_helpers.rb2
-rw-r--r--spec/support/helpers/notification_helpers.rb8
-rw-r--r--spec/support/helpers/query_recorder.rb2
-rw-r--r--spec/support/helpers/repo_helpers.rb78
-rw-r--r--spec/support/helpers/select2_helper.rb4
-rw-r--r--spec/support/helpers/test_request_helpers.rb4
-rw-r--r--spec/support/helpers/wait_for_requests.rb14
-rw-r--r--spec/support/helpers/wait_helpers.rb20
-rw-r--r--spec/support/protected_branch_helpers.rb30
-rw-r--r--spec/support/protected_tag_helpers.rb18
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb4
-rw-r--r--spec/support/shared_context/policies/project_policy_shared_context.rb3
-rw-r--r--spec/support/shared_examples/application_setting_examples.rb39
-rw-r--r--spec/support/shared_examples/ci/stage_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb42
-rw-r--r--spec/support/shared_examples/models/chat_service_spec.rb8
-rw-r--r--spec/support/shared_examples/models/ci_variable_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/models/cluster_application_core_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb26
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb65
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_spec.rb76
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb24
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb14
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb32
-rw-r--r--spec/uploaders/object_storage_spec.rb8
-rw-r--r--spec/views/notify/pipeline_failed_email.text.erb_spec.rb39
-rw-r--r--spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb28
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb27
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb41
-rw-r--r--spec/views/shared/_label_row.html.haml.rb32
-rw-r--r--spec/workers/build_success_worker_spec.rb1
-rw-r--r--spec/workers/cluster_configure_worker_spec.rb71
-rw-r--r--spec/workers/cluster_project_configure_worker_spec.rb15
-rw-r--r--spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb32
-rw-r--r--spec/workers/deployments/finished_worker_spec.rb39
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb13
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb42
-rw-r--r--spec/workers/pages_domain_verification_cron_worker_spec.rb8
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb12
-rw-r--r--yarn.lock153
2217 files changed, 44776 insertions, 12900 deletions
diff --git a/.gitignore b/.gitignore
index 0696dd217af..627c806787b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,8 +59,6 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
-/spec/javascripts/fixtures/blob/pdf/
-/spec/javascripts/fixtures/blob/balsamiq/
/rails_best_practices_output.html
/tags
/tmp/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 72385b93bd9..44beccd966a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29"
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
@@ -28,6 +28,8 @@ stages:
- prepare
- merge
- test
+ - review
+ - qa
- post-test
- pages
- post-cleanup
diff --git a/.gitlab/CODEOWNERS.disabled b/.gitlab/CODEOWNERS.disabled
index f7e2c06dae5..52fb651f551 100644
--- a/.gitlab/CODEOWNERS.disabled
+++ b/.gitlab/CODEOWNERS.disabled
@@ -1,6 +1,6 @@
# Backend Maintainers are the default for all ruby files
-*.rb @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
-*.rake @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern
+*.rb @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern @mayra-cabrera
+*.rake @ashmckenzie @ayufan @dbalexandre @DouweM @dzaporozhets @godfat @grzesiek @mkozono @nick.thomas @rspeicher @rymai @smcgivern @mayra-cabrera
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @axil @marcia
diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml
index e15f8ed91e0..c384bcdcdfc 100644
--- a/.gitlab/ci/cng.gitlab-ci.yml
+++ b/.gitlab/ci/cng.gitlab-ci.yml
@@ -9,7 +9,7 @@ cloud-native-image:
cache: {}
when: manual
script:
- - gem install gitlab --no-document
+ - install_gitlab_gem
- CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng
only:
- tags@gitlab-org/gitlab-ce
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index e38b878972d..fbf8925e30a 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -16,7 +16,7 @@
gitlab:assets:compile:
<<: *assets-compile-cache
extends: .dedicated-no-docs-pull-cache-job
- image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
+ image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.21-chrome-73.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1
dependencies:
- setup-test-env
services:
@@ -38,6 +38,10 @@ gitlab:assets:compile:
- bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
- scripts/clean-old-cached-assets
+ # Play dependent manual jobs
+ - install_api_client_dependencies_with_apt
+ - play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
+ - play_job "schedule:review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
artifacts:
name: webpack-report
expire_in: 31d
@@ -103,7 +107,8 @@ gitlab:ui:visual:
- $CI_COMMIT_MESSAGE =~ /\[skip visual\]/i
artifacts:
paths:
- - tests/__image_snapshots__/
+ - gitlab-ui/tests/__image_snapshots__/
+ when: always
karma:
extends: .dedicated-no-docs-pull-cache-job
@@ -234,4 +239,4 @@ jsdoc:
name: jsdoc
expire_in: 31d
paths:
- - jsdoc/ \ No newline at end of file
+ - jsdoc/
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index c48672183b9..466c47b37c7 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -1,6 +1,6 @@
.dedicated-runner:
retry:
- max: 1 # This is confusing but this means "2 runs at max".
+ max: 2 # This is confusing but this means "3 runs at max".
when:
- unknown_failure
- api_failure
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 07b38c9aa85..85c6409186e 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -1,20 +1,17 @@
package-and-qa:
image: ruby:2.5-alpine
- stage: test
+ stage: qa
+ when: manual
before_script: []
dependencies: []
cache: {}
variables:
GIT_DEPTH: "1"
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
retry: 0
script:
- - apk add --update openssl curl jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
- - wait_for_job_to_be_done "gitlab:assets:compile"
+ - source scripts/utils.sh
+ - install_gitlab_gem
- ./scripts/trigger-build omnibus
- when: manual
only:
- /.+/@gitlab-org/gitlab-ce
- /.+/@gitlab-org/gitlab-ee
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index b926fb77a08..35c5f67427e 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -6,7 +6,7 @@
.use-pg-10: &use-pg-10
services:
- - postgres:10.0
+ - postgres:10.7
- redis:alpine
.use-mysql: &use-mysql
@@ -86,7 +86,7 @@
.rspec-metadata-pg-10: &rspec-metadata-pg-10
<<: *rspec-metadata
<<: *use-pg-10
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.21-chrome-73.0-node-10.x-yarn-1.12-postgresql-10-graphicsmagick-1.3.29"
.rspec-metadata-mysql: &rspec-metadata-mysql
<<: *rspec-metadata
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 9655498073e..d0e09dbf2f8 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -1,8 +1,8 @@
include:
- - local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
+ - template: Code-Quality.gitlab-ci.yml
code_quality:
- extends: .dedicated-runner
+ extends: .dedicated-no-docs-no-db-pull-cache-job
# gitlab-org runners set `privileged: false` but we need to have it set to true
# since we're using Docker in Docker
tags: []
@@ -13,7 +13,7 @@ code_quality:
SETUP_DB: "false"
sast:
- extends: .dedicated-runner
+ extends: .dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
SAST_CONFIDENCE_LEVEL: 2
@@ -26,18 +26,38 @@ sast:
services:
- docker:stable-dind
script:
+ - | # 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
+ }
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
- --volume "$PWD:/code"
- --volume /var/run/docker.sock:/var/run/docker.sock
+ - |
+ docker run \
+ $(propagate_env_vars \
+ SAST_ANALYZER_IMAGES \
+ SAST_ANALYZER_IMAGE_PREFIX \
+ SAST_ANALYZER_IMAGE_TAG \
+ SAST_DEFAULT_ANALYZERS \
+ SAST_BRAKEMAN_LEVEL \
+ SAST_GOSEC_LEVEL \
+ SAST_FLAWFINDER_LEVEL \
+ SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
+ SAST_RUN_ANALYZER_TIMEOUT \
+ ) \
+ --volume "$PWD:/code" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
artifacts:
reports:
sast: gl-sast-report.json
dependency_scanning:
- extends: .dedicated-runner
+ extends: .dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -50,10 +70,28 @@ dependency_scanning:
- docker:stable-dind
script:
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
- --volume "$PWD:/code"
- --volume /var/run/docker.sock:/var/run/docker.sock
+ - | # 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
+ }
+ - |
+ docker run \
+ $(propagate_env_vars \
+ DS_ANALYZER_IMAGES \
+ DS_ANALYZER_IMAGE_PREFIX \
+ DS_ANALYZER_IMAGE_TAG \
+ DS_DEFAULT_ANALYZERS \
+ DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ DS_RUN_ANALYZER_TIMEOUT \
+ ) \
+ --volume "$PWD:/code" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
artifacts:
reports:
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index a7f6caedd37..ae16549ef6b 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -26,13 +26,10 @@
extends: .dedicated-runner
<<: *review-only
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- stage: test
cache: {}
dependencies: []
- environment: &review-environment
- name: review/${CI_COMMIT_REF_NAME}
- url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
- before_script: []
+ before_script:
+ - source scripts/utils.sh
.review-docker: &review-docker
<<: *review-base
@@ -43,7 +40,6 @@
- gitlab-org
- docker
variables: &review-docker-variables
- GIT_DEPTH: "1"
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
LATEST_QA_IMAGE: "gitlab/${CI_PROJECT_NAME}-qa:nightly"
@@ -51,10 +47,7 @@
build-qa-image:
<<: *review-docker
- variables:
- <<: *review-docker-variables
- GIT_DEPTH: "20"
- stage: prepare
+ stage: test
script:
- time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/
- echo "${CI_JOB_TOKEN}" | docker login --username gitlab-ci-token --password-stdin ${CI_REGISTRY}
@@ -63,16 +56,14 @@ build-qa-image:
.review-build-cng-base: &review-build-cng-base
image: ruby:2.5-alpine
stage: test
- before_script: []
+ when: manual
+ before_script:
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - install_gitlab_gem
dependencies: []
cache: {}
- variables:
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
script:
- - apk add --update openssl curl jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
- - wait_for_job_to_be_done "gitlab:assets:compile"
- BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
review-build-cng:
@@ -83,28 +74,34 @@ schedule:review-build-cng:
<<: *review-schedules-only
<<: *review-build-cng-base
-
.review-deploy-base: &review-deploy-base
<<: *review-base
+ stage: review
+ retry: 2
allow_failure: true
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
GITLAB_HELM_CHART_REF: "master"
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
- environment:
- <<: *review-environment
+ environment: &review-environment
+ name: review/${CI_COMMIT_REF_NAME}
+ url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
on_stop: review-stop
before_script:
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
- - apk update && apk add jq
- - gem install gitlab --no-document
- - source ./scripts/review_apps/review-apps.sh
+ - echo "${CI_ENVIRONMENT_URL}" > review_app_url.txt
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
+ - source scripts/review_apps/review-apps.sh
script:
- - wait_for_job_to_be_done "review-build-cng"
- perform_review_app_deployment
+ artifacts:
+ paths:
+ - review_app_url.txt
+ expire_in: 2 days
+ when: always
review-deploy:
<<: *review-deploy-base
@@ -113,15 +110,29 @@ schedule:review-deploy:
<<: *review-deploy-base
<<: *review-schedules-only
script:
- - wait_for_job_to_be_done "schedule:review-build-cng"
- perform_review_app_deployment
+review-stop:
+ <<: *review-base
+ stage: review
+ when: manual
+ allow_failure: true
+ variables:
+ GIT_DEPTH: "1"
+ environment:
+ <<: *review-environment
+ action: stop
+ script:
+ - source scripts/review_apps/review-apps.sh
+ - delete
+ - cleanup
+
.review-qa-base: &review-qa-base
<<: *review-docker
+ stage: qa
allow_failure: true
variables:
<<: *review-docker-variables
- API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}"
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
GITLAB_USERNAME: "root"
@@ -131,39 +142,45 @@ schedule:review-deploy:
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
QA_DEBUG: "true"
+ dependencies:
+ - review-deploy
artifacts:
paths:
- ./qa/gitlab-qa-run-*
expire_in: 7 days
when: always
before_script:
- - echo "${QA_IMAGE}"
+ - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- - apk update && apk add curl jq
- - source ./scripts/review_apps/review-apps.sh
+ - echo "${QA_IMAGE}"
+ - source scripts/utils.sh
+ - install_api_client_dependencies_with_apk
- gem install gitlab-qa --no-document ${GITLAB_QA_VERSION:+ --version ${GITLAB_QA_VERSION}}
review-qa-smoke:
<<: *review-qa-base
+ retry: 2
script:
- - wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
review-qa-all:
<<: *review-qa-base
+ when: manual
script:
- - wait_for_job_to_be_done "review-deploy"
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
- when: manual
.review-performance-base: &review-performance-base
<<: *review-qa-base
- script:
- - wait_for_job_to_be_done "review-deploy"
+ stage: qa
+ before_script:
+ - export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
+ - echo "${CI_ENVIRONMENT_URL}"
- mkdir -p gitlab-exporter
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- - mkdir sitespeed-results
- - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
+ - mkdir -p sitespeed-results
+ script:
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
+ after_script:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
paths:
@@ -174,41 +191,26 @@ review-qa-all:
review-performance:
<<: *review-performance-base
-review-stop:
- extends: .single-script-job-dedicated-runner
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
- allow_failure: true
- variables:
- SCRIPT_NAME: "review_apps/review-apps.sh"
- when: manual
- environment:
- <<: *review-environment
- action: stop
- script:
- - source $(basename "${SCRIPT_NAME}")
- - delete
- - cleanup
+schedule:review-performance:
+ <<: *review-performance-base
+ <<: *review-schedules-only
+ dependencies:
+ - schedule:review-deploy
schedule:review-cleanup:
<<: *review-base
<<: *review-schedules-only
stage: build
allow_failure: true
- variables:
- GIT_DEPTH: "1"
environment:
name: review/auto-cleanup
+ action: stop
before_script:
- - gem install gitlab --no-document
+ - source scripts/utils.sh
+ - install_gitlab_gem
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
-schedule:review-performance:
- <<: *review-performance-base
- <<: *review-schedules-only
- script:
- - wait_for_job_to_be_done "schedule:review-deploy"
-
danger-review:
extends: .dedicated-pull-cache-job
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
@@ -229,4 +231,3 @@ danger-review:
- node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- danger --fail-on-errors=true
-
diff --git a/.gitlab/issue_templates/Database Reviewer.md b/.gitlab/issue_templates/Database Reviewer.md
index a5e7e42fd14..acbaf5c1965 100644
--- a/.gitlab/issue_templates/Database Reviewer.md
+++ b/.gitlab/issue_templates/Database Reviewer.md
@@ -1,6 +1,8 @@
#### Database Reviewer Checklist
-Thank you for becoming a ~database reviewer! Please work on the list below to complete your setup. For any question, reach out to #database an mention @abrandl.
+Thank you for becoming a ~database reviewer! Please work on the list
+below to complete your setup. For any question, reach out to #database
+an mention `@abrandl`.
- [ ] Change issue title to include your name: `Database Reviewer Checklist: Your Name`
- [ ] Review general [code review guide](https://docs.gitlab.com/ee/development/code_review.html)
@@ -12,7 +14,7 @@ Thank you for becoming a ~database reviewer! Please work on the list below to co
- [ ] Read [Understanding EXPLAIN plans](https://docs.gitlab.com/ee/development/understanding_explain_plans.html)
- [ ] Review [database best practices](https://docs.gitlab.com/ee/development/#best-practices)
- [ ] Review how we use [database instances restored from a backup](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd) for testing and make sure you're set up to execute pipelines (check [README.md](https://ops.gitlab.net/gitlab-com/gl-infra/gitlab-restore/postgres-gprd/blob/master/README.md) and reach out to @abrandl since this is currently subject to being changed)
-- [ ] Get yourself added to [@gl-database](https://gitlab.com/groups/gl-database/-/group_members) group and respond to @-mentions to the group (reach out to any maintainer on the group to get added). You will get TODOs on gitlab.com for group mentions.
+- [ ] Get yourself added to [`@gl-database`](https://gitlab.com/groups/gl-database/-/group_members) group and respond to @-mentions to the group (reach out to any maintainer on the group to get added). You will get TODOs on gitlab.com for group mentions.
- [ ] Make sure you have proper access to at least a read-only replica in staging and production
- [ ] Indicate in `data/team.yml` your role as a database reviewer ([example MR](https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/19600/diffs)). Assign MR to your manager for merge.
- [ ] Send one MR to improve the [review documentation](https://about.gitlab.com/handbook/engineering/workflow/code-review/database.html) or the [issue template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/issue_templates/Database%20Reviewer.md)
@@ -25,7 +27,7 @@ You're all set! Watch out for TODOs on GitLab.com.
###### Where to go for questions?
-Reach out to `#database` on Slack and mention @abrandl for any questions.
+Reach out to `#database` on Slack and mention `@abrandl` for any questions.
cc @abrandl
diff --git a/.gitlab/issue_templates/Refactoring.md b/.gitlab/issue_templates/Refactoring.md
new file mode 100644
index 00000000000..cd0ce8486f0
--- /dev/null
+++ b/.gitlab/issue_templates/Refactoring.md
@@ -0,0 +1,41 @@
+## Summary
+
+<!--
+Please briefly describe what part of the code base needs to be refactored.
+-->
+
+## Improvements
+
+<!--
+Explain the benefits of refactoring this code.
+See also https://about.gitlab.com/handbook/values/index.html#say-why-not-just-what
+-->
+
+## Risks
+
+<!--
+Please list features that can break because of this refactoring and how you intend to solve that.
+-->
+
+## Involved components
+
+<!--
+List files or directories that will be changed by the refactoring.
+-->
+
+## Optional: Intended side effects
+
+<!--
+If the refactoring involves changes apart from the main improvements (such as a better UI), list them here.
+It may be a good idea to create separate issues and link them here.
+-->
+
+
+## Optional: Missing test coverage
+
+<!--
+If you are aware of tests that need to be written or adjusted apart from unit tests for the changed components,
+please list them here.
+-->
+
+/label ~backstage
diff --git a/.haml-lint.yml b/.haml-lint.yml
index bad918ef35d..e9cc4a91a21 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -97,6 +97,7 @@ linters:
- Cop/LineBreakAroundConditionalBlock
- Cop/ProjectPathHelper
- GitlabSecurity/PublicSend
+ - Layout/EmptyLineAfterGuardClause
- Layout/LeadingCommentSpace
- Layout/SpaceAfterColon
- Layout/SpaceAfterComma
@@ -112,11 +113,13 @@ linters:
- Lint/LiteralInInterpolation
- Lint/ParenthesesAsGroupedExpression
- Lint/RedundantWithIndex
+ - Lint/SafeNavigationConsistency
- Lint/Syntax
- Metrics/BlockNesting
- Naming/VariableName
- Performance/RedundantMatch
- Performance/StringReplacement
+ - Rails/LinkToBlank
- Rails/Presence
- Rails/RequestReferer
- Style/AndOr
@@ -134,6 +137,7 @@ linters:
- Style/TernaryParentheses
- Style/TrailingCommaInHashLiteral
- Style/UnlessElse
+ - Style/UnneededCondition
- Style/WordArray
- Style/ZeroLengthPredicate
diff --git a/.rubocop.yml b/.rubocop.yml
index aa49f41ebf4..e5fe527e611 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -75,6 +75,7 @@ Naming/FileName:
- 'qa/spec/**/*'
- 'qa/qa/specs/**/*'
- 'qa/bin/*'
+ - 'ee/bin/*'
- 'config/**/*'
- 'ee/config/**/*'
- 'lib/generators/**/*'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 77ad4753c84..63b1685feda 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,42 +1,154 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
-# on 2018-01-18 18:23:26 +0100 using RuboCop version 0.52.1.
+# on 2019-05-04 16:01:00 +0000 using RuboCop version 0.68.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 181
+# Offense count: 264
Capybara/CurrentPathExpectation:
Enabled: false
-# Offense count: 167
+# Offense count: 1097
# Cop supports --auto-correct.
-Layout/EmptyLinesAroundArguments:
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: with_first_argument, with_fixed_indentation
+Layout/AlignArguments:
Enabled: false
-# Offense count: 83
+# Offense count: 824
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
+# SupportedHashRocketStyles: key, separator, table
+# SupportedColonStyles: key, separator, table
+# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
+Layout/AlignHash:
+ Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Layout/ClosingHeredocIndentation:
+ Exclude:
+ - 'app/graphql/mutations/merge_requests/set_wip.rb'
+ - 'ee/db/geo/migrate/20180322062741_migrate_ci_job_artifacts_to_separate_registry.rb'
+ - 'ee/db/migrate/20160204190809_update_jenkins_service_category.rb'
+ - 'ee/lib/gitlab/background_migration/prune_orphaned_geo_events.rb'
+ - 'ee/lib/gitlab/geo/health_check.rb'
+ - 'lib/gitlab/background_migration/populate_untracked_uploads.rb'
+ - 'qa/qa/service/kubernetes_cluster.rb'
+ - 'spec/features/merge_request/user_sees_diff_spec.rb'
+ - 'spec/lib/gitlab/asciidoc_spec.rb'
+ - 'spec/lib/gitlab/checks/project_moved_spec.rb'
+ - 'spec/rubocop/cop/active_record_association_reload_spec.rb'
+ - 'spec/services/task_list_toggle_service_spec.rb'
+
+# Offense count: 14
+# Cop supports --auto-correct.
+Layout/ClosingParenthesisIndentation:
+ Exclude:
+ - 'db/post_migrate/20180704145007_update_project_indexes.rb'
+ - 'ee/db/geo/migrate/20180405074130_add_partial_index_project_repository_verification.rb'
+ - 'ee/db/migrate/20180308234102_add_partial_index_to_project_repository_states_checksum_columns.rb'
+ - 'ee/db/post_migrate/20180605213516_fix_partial_index_to_project_repository_states_checksum_columns.rb'
+ - 'ee/lib/ee/gitlab/usage_data.rb'
+ - 'spec/services/issues/resolve_discussions_spec.rb'
+ - 'spec/services/projects/update_service_spec.rb'
+ - 'spec/support/helpers/stub_object_storage.rb'
+ - 'spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: leading, trailing
+Layout/DotPosition:
+ Exclude:
+ - 'app/models/concerns/relative_positioning.rb'
+ - 'app/models/group.rb'
+
+# Offense count: 69
+# Cop supports --auto-correct.
+Layout/EmptyLinesAroundArguments:
+ Exclude:
+ - 'app/models/concerns/discussion_on_diff.rb'
+ - 'app/models/concerns/resolvable_discussion.rb'
+ - 'app/models/diff_discussion.rb'
+ - 'app/models/discussion.rb'
+ - 'ee/app/helpers/license_helper.rb'
+ - 'ee/spec/models/geo/project_registry_spec.rb'
+ - 'lib/banzai/pipeline/broadcast_message_pipeline.rb'
+ - 'lib/banzai/pipeline/gfm_pipeline.rb'
+ - 'lib/banzai/pipeline/single_line_pipeline.rb'
+ - 'spec/features/markdown/copy_as_gfm_spec.rb'
+
+# Offense count: 160
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
-Layout/IndentArray:
+Layout/IndentFirstArrayElement:
Enabled: false
-# Offense count: 237
+# Offense count: 631
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
-Layout/IndentHash:
+Layout/IndentFirstHashElement:
Enabled: false
-# Offense count: 93
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: consistent, align_parentheses
+Layout/IndentFirstParameter:
+ Exclude:
+ - 'app/models/ci/pipeline_schedule.rb'
+ - 'lib/gitlab/cross_project_access.rb'
+ - 'lib/gitlab/data_builder/push.rb'
+ - 'spec/support/helpers/repo_helpers.rb'
+ - 'spec/support/helpers/stub_object_storage.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Layout/LeadingBlankLines:
+ Exclude:
+ - 'app/workers/update_project_statistics_worker.rb'
+ - 'db/migrate/20161007073613_create_user_activities.rb'
+ - 'ee/spec/helpers/boards_helper_spec.rb'
+ - 'lib/tasks/yarn.rake'
+ - 'spec/javascripts/fixtures/merge_requests_diffs.rb'
+
+# Offense count: 30
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: aligned, indented
+Layout/MultilineOperationIndentation:
+ Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Layout/RescueEnsureAlignment:
+ Exclude:
+ - 'app/models/blob_viewer/dependency_manager.rb'
+ - 'app/models/ci/pipeline.rb'
+ - 'app/models/project.rb'
+ - 'app/services/prometheus/proxy_service.rb'
+ - 'app/workers/delete_stored_files_worker.rb'
+ - 'app/workers/reactive_caching_worker.rb'
+ - 'config/initializers/1_settings.rb'
+ - 'config/initializers/trusted_proxies.rb'
+ - 'ee/db/migrate/20151113115819_canonicalize_kerberos_identities.rb'
+ - 'lib/gitlab/background_migration/archive_legacy_traces.rb'
+ - 'lib/gitlab/highlight.rb'
+ - 'lib/tasks/gitlab/lfs/migrate.rake'
+
+# Offense count: 344
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
Layout/SpaceInLambdaLiteral:
Enabled: false
-# Offense count: 327
+# Offense count: 583
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
@@ -44,20 +156,27 @@ Layout/SpaceInLambdaLiteral:
Layout/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 156
+# Offense count: 255
# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: space, no_space
Layout/SpaceInsideParens:
Enabled: false
-# Offense count: 26
+# Offense count: 1
+Lint/DisjunctiveAssignmentInConstructor:
+ Exclude:
+ - 'app/models/uploads/base.rb'
+
+# Offense count: 19
Lint/DuplicateMethods:
Exclude:
- - 'app/models/application_setting.rb'
- 'app/models/commit.rb'
- 'app/models/note.rb'
- - 'app/services/merge_requests/merge_service.rb'
- 'lib/bitbucket/representation/repo.rb'
- 'lib/declarative_policy/base.rb'
+ - 'lib/gitlab/auth/ldap/person.rb'
+ - 'lib/gitlab/auth/o_auth/user.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'lib/gitlab/cycle_analytics/base_event_fetcher.rb'
- 'lib/gitlab/diff/formatters/base_formatter.rb'
@@ -65,22 +184,34 @@ Lint/DuplicateMethods:
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
- - 'lib/gitlab/auth/ldap/person.rb'
- - 'lib/gitlab/auth/o_auth/user.rb'
-# Offense count: 4
+# Offense count: 2
Lint/InterpolationCheck:
Exclude:
- 'spec/features/issues/filtered_search/filter_issues_spec.rb'
- - 'spec/features/users_spec.rb'
- 'spec/services/quick_actions/interpret_service_spec.rb'
-# Offense count: 206
+# Offense count: 326
# Configuration parameters: MaximumRangeSize.
Lint/MissingCopEnableDirective:
Enabled: false
-# Offense count: 9
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: Whitelist.
+# Whitelist: present?, blank?, presence, try, try!
+Lint/SafeNavigationConsistency:
+ Exclude:
+ - 'lib/gitlab/gpg/commit.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Lint/ToJSON:
+ Exclude:
+ - 'lib/gitlab/cycle_analytics/usage_data.rb'
+ - 'lib/gitlab/template/base_template.rb'
+
+# Offense count: 7
Lint/UriEscapeUnescape:
Exclude:
- 'app/controllers/application_controller.rb'
@@ -88,13 +219,25 @@ Lint/UriEscapeUnescape:
- 'spec/lib/google_api/auth_spec.rb'
- 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/internal_spec.rb'
- - 'spec/requests/api/issues_spec.rb'
# Offense count: 1
-# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# Configuration parameters: CheckForMethodsWithNoSideEffects.
+Lint/Void:
+ Exclude:
+ - 'lib/gitlab/git/diff_collection.rb'
+
+# Offense count: 158
+# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
- Max: 1310
+ Max: 176
+
+# Offense count: 94
+# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
+# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
+Naming/FileName:
+ Enabled: false
# Offense count: 11
# Configuration parameters: EnforcedStyle.
@@ -107,61 +250,85 @@ Naming/HeredocDelimiterCase:
- 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/helpers/seed_repo.rb'
-# Offense count: 112
+# Offense count: 197
# Configuration parameters: Blacklist.
-# Blacklist: END, (?-mix:EO[A-Z]{1})
+# Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$))
Naming/HeredocDelimiterNaming:
Enabled: false
+# Offense count: 125
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredName.
+Naming/RescuedExceptionsVariableName:
+ Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Performance/InefficientHashSearch:
+ Exclude:
+ - 'app/controllers/concerns/sessionless_authentication.rb'
+ - 'app/models/note.rb'
+ - 'app/models/user_preference.rb'
+ - 'ee/app/models/ee/project.rb'
+ - 'lib/gitlab/import_export/members_mapper.rb'
+ - 'qa/spec/spec_helper.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Performance/ReverseEach:
+ Exclude:
+ - 'app/models/commit.rb'
+ - 'db/migrate/20190222051615_add_indexes_for_merge_request_diffs_query.rb'
+ - 'lib/gitlab/profiler.rb'
-# Offense count: 3821
+# Offense count: 7081
# Configuration parameters: Prefixes.
# Prefixes: when, with, without
RSpec/ContextWording:
Enabled: false
-# Offense count: 293
+# Offense count: 407
+# Cop supports --auto-correct.
RSpec/EmptyLineAfterFinalLet:
Enabled: false
-# Offense count: 188
+# Offense count: 232
+# Cop supports --auto-correct.
RSpec/EmptyLineAfterSubject:
Enabled: false
-# Offense count: 258
+# Offense count: 719
+# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: method_call, block
RSpec/ExpectChange:
Enabled: false
-# Offense count: 221
+# Offense count: 512
RSpec/ExpectInHook:
Enabled: false
-# Offense count: 19
+# Offense count: 10
+# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: it_behaves_like, it_should_behave_like
RSpec/ItBehavesLike:
Exclude:
- 'spec/lib/gitlab/git/commit_spec.rb'
- 'spec/lib/gitlab/git/repository_spec.rb'
- - 'spec/lib/gitlab/shell_spec.rb'
- 'spec/services/notification_service_spec.rb'
- - 'spec/workers/git_garbage_collect_worker_spec.rb'
-# Offense count: 5
+# Offense count: 3
RSpec/IteratedExpectation:
Exclude:
- 'spec/features/admin/admin_settings_spec.rb'
- - 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
- - 'spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb'
- 'spec/lib/gitlab/gitlab_import/client_spec.rb'
- 'spec/lib/gitlab/legacy_github_import/client_spec.rb'
-# Offense count: 75
+# Offense count: 68
+# Cop supports --auto-correct.
RSpec/LetBeforeExamples:
Exclude:
- - 'spec/controllers/projects/commit_controller_spec.rb'
- 'spec/lib/banzai/filter/issue_reference_filter_spec.rb'
- 'spec/lib/banzai/filter/user_reference_filter_spec.rb'
- 'spec/lib/gitlab/email/handler/create_issue_handler_spec.rb'
@@ -170,12 +337,11 @@ RSpec/LetBeforeExamples:
- 'spec/models/commit_range_spec.rb'
- 'spec/models/milestone_spec.rb'
- 'spec/models/project_services/packagist_service_spec.rb'
- - 'spec/models/repository_spec.rb'
- 'spec/rubocop/cop/migration/update_column_in_batches_spec.rb'
- 'spec/serializers/pipeline_details_entity_spec.rb'
- - 'spec/views/ci/lints/show.html.haml_spec.rb'
# Offense count: 1
+# Cop supports --auto-correct.
RSpec/MultipleSubjects:
Exclude:
- 'spec/services/merge_requests/create_from_issue_service_spec.rb'
@@ -188,95 +354,136 @@ RSpec/OverwritingSetup:
- 'spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb'
- 'spec/services/notes/quick_actions_service_spec.rb'
-# Offense count: 965
+# Offense count: 1828
+# Cop supports --auto-correct.
# Configuration parameters: Strict, EnforcedStyle.
# SupportedStyles: inflected, explicit
RSpec/PredicateMatcher:
Enabled: false
-# Offense count: 35
+# Offense count: 57
RSpec/RepeatedExample:
Enabled: false
-# Offense count: 140
+# Offense count: 474
+# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: and_return, block
RSpec/ReturnFromStub:
Enabled: false
-# Offense count: 112
+# Offense count: 188
RSpec/ScatteredLet:
Enabled: false
-# Offense count: 22
+# Offense count: 10
RSpec/ScatteredSetup:
Exclude:
- 'spec/controllers/projects/templates_controller_spec.rb'
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- - 'spec/lib/gitlab/git/env_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/services/projects/create_service_spec.rb'
# Offense count: 1
+# Cop supports --auto-correct.
RSpec/SharedContext:
Exclude:
- 'spec/features/admin/admin_groups_spec.rb'
-# Offense count: 5
+# Offense count: 4
RSpec/VoidExpect:
Exclude:
- - 'spec/features/projects/artifacts/download_spec.rb'
- 'spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb'
- 'spec/models/ci/group_spec.rb'
- 'spec/models/ci/runner_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
-# Offense count: 41
+# Offense count: 8
+# Cop supports --auto-correct.
+Rails/BelongsTo:
+ Exclude:
+ - 'app/models/deployment.rb'
+ - 'app/models/environment.rb'
+ - 'ee/app/models/prometheus_alert.rb'
+ - 'ee/app/models/prometheus_alert_event.rb'
+
+# Offense count: 80
# Configuration parameters: Include.
# Include: db/migrate/*.rb
Rails/CreateTableWithTimestamps:
Enabled: false
-# Offense count: 155
+# Offense count: 222
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: slashes, arguments
Rails/FilePath:
Enabled: false
-# Offense count: 121
+# Offense count: 167
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Enabled: false
-# Offense count: 157
+# 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
+Rails/IgnoredSkipActionFilterOption:
+ Exclude:
+ - 'app/controllers/projects/snippets_controller.rb'
+ - 'app/controllers/snippets_controller.rb'
+
+# Offense count: 87
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/InverseOf:
Enabled: false
-# Offense count: 48
+# Offense count: 46
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb
Rails/LexicallyScopedActionFilter:
Enabled: false
-# Offense count: 14
+# Offense count: 4
+# Cop supports --auto-correct.
+Rails/LinkToBlank:
+ Exclude:
+ - 'app/helpers/projects_helper.rb'
+ - 'app/helpers/wiki_helper.rb'
+ - 'ee/app/helpers/ee/user_callouts_helper.rb'
+ - 'ee/app/helpers/license_helper.rb'
+
+# Offense count: 11
# Cop supports --auto-correct.
Rails/Presence:
Exclude:
- - 'app/controllers/projects/blob_controller.rb'
- 'app/models/ci/pipeline.rb'
- 'app/models/clusters/platforms/kubernetes.rb'
- 'app/models/concerns/mentionable.rb'
- - 'app/models/concerns/token_authenticatable.rb'
- 'app/models/project_services/hipchat_service.rb'
- 'app/models/project_services/irker_service.rb'
- 'app/models/project_services/jira_service.rb'
- 'app/models/project_services/kubernetes_service.rb'
- 'app/models/project_services/packagist_service.rb'
- 'app/models/wiki_page.rb'
- - 'lib/gitlab/git/hook.rb'
- 'lib/gitlab/github_import/importer/releases_importer.rb'
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/RedundantAllowNil:
+ Exclude:
+ - 'app/models/application_setting.rb'
+
# Offense count: 2
# Configuration parameters: Include.
# Include: db/migrate/*.rb
@@ -284,8 +491,8 @@ Rails/ReversibleMigration:
Exclude:
- 'db/migrate/20160824103857_drop_unused_ci_tables.rb'
-# Offense count: 446
-# Configuration parameters: Blacklist.
+# Offense count: 860
+# Configuration parameters: Blacklist, Whitelist.
# Blacklist: decrement!, decrement_counter, increment!, increment_counter, toggle!, touch, update_all, update_attribute, update_column, update_columns, update_counters
Rails/SkipsModelValidations:
Enabled: false
@@ -297,21 +504,25 @@ Rails/UnknownEnv:
Exclude:
- 'db/migrate/20171124125748_populate_missing_merge_request_statuses.rb'
-# Offense count: 13
+# Offense count: 11
# Cop supports --auto-correct.
Security/YAMLLoad:
Exclude:
- - 'config/initializers/carrierwave.rb'
- 'lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb'
- 'lib/gitlab/redis/wrapper.rb'
- 'lib/system_check/incoming_email/imap_authentication_check.rb'
- 'spec/config/mail_room_spec.rb'
- 'spec/initializers/secret_token_spec.rb'
- 'spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb'
- - 'spec/models/clusters/platforms/kubernetes_spec.rb'
- 'spec/models/project_services/kubernetes_service_spec.rb'
-# Offense count: 64
+# Offense count: 34
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: inline, group
+Style/AccessModifierDeclarations:
+ Enabled: false
+
+# Offense count: 121
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: percent_q, bare_percent
@@ -324,17 +535,13 @@ Style/CommentedKeyword:
- 'lib/tasks/gitlab/backup.rake'
- 'spec/tasks/gitlab/backup_rake_spec.rb'
-# Offense count: 30
-Style/DateTime:
- Enabled: false
-
# Offense count: 1
# Cop supports --auto-correct.
Style/Dir:
Exclude:
- 'qa/qa.rb'
-# Offense count: 9
+# Offense count: 7
# Cop supports --auto-correct.
Style/EachWithObject:
Exclude:
@@ -345,46 +552,42 @@ Style/EachWithObject:
- 'lib/gitlab/i18n/po_linter.rb'
- 'lib/gitlab/import_export/members_mapper.rb'
- 'lib/gitlab/import_export/relation_factory.rb'
- - 'scripts/static-analysis'
-# Offense count: 24
+# Offense count: 34
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: empty, nil, both
Style/EmptyElse:
Enabled: false
-# Offense count: 14
+# Offense count: 11
# Cop supports --auto-correct.
Style/EmptyLambdaParameter:
Exclude:
- 'app/models/ci/build.rb'
- 'app/models/ci/runner.rb'
-# Offense count: 12
+# Offense count: 9
# Cop supports --auto-correct.
Style/EmptyLiteral:
Exclude:
- - 'features/steps/project/commits/commits.rb'
- 'lib/gitlab/fogbugz_import/importer.rb'
- 'lib/gitlab/git/diff_collection.rb'
- 'lib/gitlab/gitaly_client.rb'
- - 'scripts/trigger-build'
- - 'spec/features/merge_requests/versions_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb'
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
-# Offense count: 102
+# Offense count: 180
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Enabled: false
-# Offense count: 23
+# Offense count: 40
# Cop supports --auto-correct.
Style/Encoding:
Enabled: false
@@ -394,52 +597,52 @@ Style/EvalWithLocation:
Exclude:
- 'app/models/service.rb'
-# Offense count: 35
+# Offense count: 203
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
-# Offense count: 384
+# Offense count: 669
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 22
+# Offense count: 27
Style/IfInsideElse:
Enabled: false
-# Offense count: 809
+# Offense count: 1346
# Cop supports --auto-correct.
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 75
+# Offense count: 186
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: line_count_dependent, lambda, literal
Style/Lambda:
Enabled: false
-# Offense count: 11
+# Offense count: 3
# Cop supports --auto-correct.
Style/LineEndConcatenation:
Exclude:
- - 'app/helpers/tree_helper.rb'
- - 'spec/features/issuables/markdown_references_spec.rb'
- - 'spec/lib/gitlab/checks/project_moved_spec.rb'
- 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb'
- 'spec/lib/gitlab/incoming_email_spec.rb'
# Offense count: 18
-Style/MethodMissing:
+Style/MethodMissingSuper:
Enabled: false
-# Offense count: 7
+# Offense count: 18
+Style/MissingRespondToMissing:
+ Enabled: false
+
+# Offense count: 6
Style/MixinUsage:
Exclude:
- - 'features/support/env.rb'
- 'spec/factories/ci/builds.rb'
- 'spec/factories/ci/job_artifacts.rb'
- 'spec/factories/lfs_objects.rb'
@@ -447,59 +650,63 @@ Style/MixinUsage:
- 'spec/lib/gitlab/import_export/project_tree_restorer_spec.rb'
- 'spec/lib/gitlab/import_export/version_checker_spec.rb'
-# Offense count: 6
+# Offense count: 4
# Cop supports --auto-correct.
Style/MultilineIfModifier:
Exclude:
- 'app/helpers/snippets_helper.rb'
- 'app/models/project_wiki.rb'
- 'app/services/ci/process_pipeline_service.rb'
- - 'app/services/create_deployment_service.rb'
- 'lib/api/commit_statuses.rb'
- - 'lib/gitlab/ci/trace.rb'
-# Offense count: 25
+# Offense count: 72
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: literals, strict
+Style/MutableConstant:
+ Enabled: false
+
+# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: Whitelist.
# Whitelist: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with
Style/NestedParenthesizedCalls:
Enabled: false
-# Offense count: 19
+# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Enabled: false
-# Offense count: 61
+# Offense count: 67
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 114
+# Offense count: 186
# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect, EnforcedStyle.
+# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Enabled: false
-# Offense count: 4
+# Offense count: 2
# Cop supports --auto-correct.
Style/OrAssignment:
Exclude:
- - 'app/models/concerns/token_authenticatable.rb'
- 'lib/api/commit_statuses.rb'
- 'lib/gitlab/project_transfer.rb'
-# Offense count: 50
+# Offense count: 79
# Cop supports --auto-correct.
Style/ParallelAssignment:
Enabled: false
-# Offense count: 917
+# Offense count: 1390
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -521,25 +728,20 @@ Style/PerlBackrefs:
- 'lib/gitlab/search_results.rb'
- 'lib/gitlab/sherlock/query.rb'
-# Offense count: 87
+# Offense count: 129
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
-# Offense count: 9
+# Offense count: 3
# Cop supports --auto-correct.
Style/RedundantBegin:
Exclude:
- - 'app/controllers/projects/clusters/gcp_controller.rb'
- 'app/models/merge_request.rb'
- 'app/services/projects/import_service.rb'
- - 'lib/api/branches.rb'
- - 'lib/gitlab/current_settings.rb'
- - 'lib/gitlab/git/commit.rb'
- 'lib/gitlab/health_checks/base_abstract_check.rb'
- - 'lib/tasks/gitlab/task_helpers.rb'
# Offense count: 1
# Cop supports --auto-correct.
@@ -547,7 +749,7 @@ Style/RedundantConditional:
Exclude:
- 'lib/system_check/helpers.rb'
-# Offense count: 57
+# Offense count: 360
# Cop supports --auto-correct.
Style/RedundantFreeze:
Enabled: false
@@ -567,41 +769,36 @@ Style/RedundantReturn:
- 'lib/gitlab/utils.rb'
- 'lib/google_api/auth.rb'
-# Offense count: 460
+# Offense count: 700
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 142
+# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
- Enabled: true
- EnforcedStyle: mixed
- AllowInnerSlashes: false
+ Enabled: false
-# Offense count: 36
+# Offense count: 41
# Cop supports --auto-correct.
Style/RescueModifier:
Enabled: false
-# Offense count: 107
+# Offense count: 197
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, explicit
Style/RescueStandardError:
Enabled: false
-# Offense count: 8
+# Offense count: 5
# Cop supports --auto-correct.
Style/SelfAssignment:
Exclude:
- 'app/models/concerns/bulk_member_access_load.rb'
- 'app/serializers/base_serializer.rb'
- - 'app/services/notification_service.rb'
- - 'lib/api/runners.rb'
- - 'spec/features/merge_requests/diff_notes_resolve_spec.rb'
- 'spec/features/projects/clusters/interchangeability_spec.rb'
- 'spec/support/import_export/configuration_helper.rb'
@@ -612,7 +809,7 @@ Style/SingleLineMethods:
Exclude:
- 'lib/gitlab/ci/ansi2html.rb'
-# Offense count: 66
+# Offense count: 91
# Cop supports --auto-correct.
# Configuration parameters: .
# SupportedStyles: use_perl_names, use_english_names
@@ -625,21 +822,21 @@ Style/StderrPuts:
Exclude:
- 'config/initializers/rspec_profiling.rb'
-# Offense count: 45
+# Offense count: 65
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 106
+# Offense count: 187
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
Style/SymbolProc:
Enabled: false
-# Offense count: 9
+# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
@@ -647,45 +844,65 @@ Style/TernaryParentheses:
Exclude:
- 'app/finders/projects_finder.rb'
- 'app/helpers/namespaces_helper.rb'
- - 'features/support/capybara.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/support/capybara.rb'
-# Offense count: 17
+# Offense count: 3
# Cop supports --auto-correct.
-# Configuration parameters: AllowNamedUnderscoreVariables.
-Style/TrailingUnderscoreVariable:
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInArguments:
Exclude:
- - 'app/controllers/admin/background_jobs_controller.rb'
- - 'app/controllers/invites_controller.rb'
- - 'app/helpers/tab_helper.rb'
- - 'lib/backup/manager.rb'
- - 'lib/gitlab/logger.rb'
- - 'lib/gitlab/upgrader.rb'
- - 'lib/system_check/app/migrations_are_up_check.rb'
- - 'lib/system_check/incoming_email/mail_room_running_check.rb'
- - 'lib/tasks/gitlab/check.rake'
- - 'lib/tasks/gitlab/task_helpers.rb'
- - 'spec/lib/gitlab/etag_caching/middleware_spec.rb'
- - 'spec/services/quick_actions/interpret_service_spec.rb'
+ - 'spec/features/markdown/copy_as_gfm_spec.rb'
-# Offense count: 4
+# Offense count: 10
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInArrayLiteral:
+ Exclude:
+ - 'ee/spec/models/project_spec.rb'
+ - 'spec/lib/gitlab/diff/position_tracer_spec.rb'
+ - 'spec/lib/gitlab/metrics/dashboard/processor_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInHashLiteral:
+ Exclude:
+ - 'lib/gitlab/ci/ansi2html.rb'
+ - 'lib/gitlab/kubernetes.rb'
+
+# Offense count: 2
# Cop supports --auto-correct.
Style/UnlessElse:
Exclude:
- 'lib/backup/manager.rb'
- 'lib/gitlab/project_search_results.rb'
- - 'lib/tasks/gitlab/check.rake'
- - 'spec/features/issues/award_emoji_spec.rb'
-# Offense count: 31
+# Offense count: 10
+# Cop supports --auto-correct.
+Style/UnneededCondition:
+ Exclude:
+ - 'app/helpers/button_helper.rb'
+ - 'app/helpers/environment_helper.rb'
+ - 'app/models/project.rb'
+ - 'app/services/issuable/clone/base_service.rb'
+ - 'app/services/prometheus/adapter_service.rb'
+ - 'lib/gitlab/email/message/repository_push.rb'
+ - 'lib/gitlab/prometheus_client.rb'
+ - 'spec/lib/rspec_flaky/flaky_example_spec.rb'
+
+# Offense count: 73
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Enabled: false
-# Offense count: 22840
-# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
-# URISchemes: http, https
-Metrics/LineLength:
- Max: 1310
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/UnneededSort:
+ Exclude:
+ - 'app/models/concerns/resolvable_discussion.rb'
+ - 'lib/gitlab/highlight.rb'
diff --git a/.stylelintrc b/.stylelintrc
index 241d2c94a88..b0ace93e04f 100644
--- a/.stylelintrc
+++ b/.stylelintrc
@@ -2,7 +2,7 @@
"plugins":[
"./scripts/frontend/stylelint/stylelint-duplicate-selectors.js",
"./scripts/frontend/stylelint/stylelint-utility-classes.js",
- "stylelint-scss",
+ "stylelint-scss"
],
"rules":{
"at-rule-blacklist":[
@@ -64,7 +64,7 @@
"number-leading-zero":"always",
"number-no-trailing-zeros":true,
"property-no-unknown":true,
- "property-no-vendor-prefix":true,
+ "property-no-vendor-prefix": [true, { "ignoreProperties": ["user-select"] }],
"rule-empty-line-before":[
"always-multi-line",
{
@@ -94,7 +94,7 @@
{
"message":"Selector should be written in lowercase with hyphens (selector-class-pattern)",
"severity": "warning"
- },
+ }
],
"selector-list-comma-newline-after":"always",
"selector-max-compound-selectors":[3, { "severity": "warning" }],
@@ -104,8 +104,8 @@
"selector-pseudo-element-no-unknown":true,
"shorthand-property-no-redundant-values":true,
"string-quotes":"single",
- "value-no-vendor-prefix":[true, { ignoreValues: ["sticky"] }],
+ "value-no-vendor-prefix":[true, { "ignoreValues": ["sticky"] }],
"stylelint-gitlab/duplicate-selectors":[true,{ "severity": "warning" }],
- "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }],
+ "stylelint-gitlab/utility-classes":[true,{ "severity": "warning" }]
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41506746c98..17b185358ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,315 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.10.4 (2019-05-01)
+
+### Fixed (12 changes)
+
+- Fix MR popover on ToDos page. !27382
+- Fix 500 in general pipeline settings when passing an invalid build timeout. !27416
+- Fix bug where system note MR has no popover. !27589
+- Fix bug when project export to remote url fails. !27614
+- `on_stop` is not automatically triggered with pipelines for merge requests. !27618
+- Update Workhorse to v8.5.2. !27631
+- Show proper wiki links in search results. !27634
+- Make `CI_COMMIT_REF_NAME` and `SLUG` variable idempotent. !27663
+- Fix Kubernetes service template deployment jobs broken as of 11.10.0. !27687
+- Prevent text selection when dragging in issue boards. !27724
+- Fix pipelines for merge requests does not show pipeline page when source branch is removed. !27803
+- Fix Metrics Environments dropdown.
+
+### Performance (2 changes)
+
+- Prevent concurrent execution of PipelineScheduleWorker. !27781
+- Fix slow performance with compiling HAML templates. !27782
+
+
+## 11.10.3 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
+## 11.10.2 (2019-04-25)
+
+### Security (4 changes)
+
+- Loosen regex for exception sanitization. !3076
+- Resolve: moving an issue to private repo leaks namespace and project name.
+- Escape path in new merge request mail.
+- Stop sending emails to users who can't read commit.
+
+
+## 11.10.1 (2019-04-23)
+
+### Fixed (2 changes)
+
+- Upgrade Gitaly to 1.34.0. !27494
+- Fix filtering of labels from system note link. !27507
+
+### Changed (1 change)
+
+- Disable just-in-time Kubernetes resource creation for project level clusters. !27352
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+### Other (1 change)
+
+- Bump required Ruby version check to 2.5.3. !27495
+
+
+## 11.10.0 (2019-04-22)
+
+### Security (9 changes)
+
+- Update Rails to 5.0.7.2. !27022
+- Disallow guest users from accessing Releases.
+- Return cached languages if they've been detected before.
+- Added rake task for removing EXIF data from existing uploads.
+- Disallow updating namespace when updating a project.
+- Fix XSS in resolve conflicts form.
+- Hide "related branches" when user does not have permission.
+- Fix PDF.js vulnerability.
+- Use UntrustedRegexp for matching refs policy.
+
+### Fixed (81 changes, 21 of them are from the community)
+
+- Update `border-radius` of form controls and remove extra space above page titles. !24497
+- Disallow reopening of a locked merge request. !24882 (Jan Beckmann)
+- Align EmailValidator to validate_email gem implementation. !24971 (Horatiu Eugen Vlad)
+- add a uniq constraints on issues and mrs labels. !25435 (Antoine Huret)
+- Display draft when toggling replies. !25563
+- Fix markdown table header and table content borders. !25666
+- Fix authorized application count. !25715 (moyuru)
+- Added "Add List" checkbox to create label dropdown to make creation of list optional. !25716 (Tucker Chapman)
+- Makes emoji picker full width on mobile. !25883 (Jacopo Beschi @jacopo-beschi)
+- Don't cutoff letters in MR and Issue links. !25910 (gfyoung)
+- Fix unwanted character replacement on project members page caused by usage of sanitize function. !25946 (Elias Werberich)
+- Fix UI for closed MR when source project is removed. !25967 (Takuya Noguchi)
+- Keep inline as much as possible in system notes on issuable. !25968 (Takuya Noguchi)
+- Fixes long review app subdomains. !25990 (walkafwalka)
+- Fix counting of groups in admin dashboard. !26009
+- Disable inaccessible navigation links upon archiving a project. !26020 (Elias Werberich)
+- Fixed - Create project label window is cut off at the bottom. !26049
+- Fix error shown when loading links to specific comments. !26092
+- Fix group transfer selection possibilities. !26123 (Peter Marko)
+- Fix UI layout on Commits on mobile. !26133 (Takuya Noguchi)
+- Fix continuous bitbucket import loading spinner. !26175
+- Resolves Branch name is lost if I change commit mode in Web IDE. !26180
+- Fix removing remote mirror failure which leaves unnecessary refs behind. !26213
+- Fix Error 500 when user commits Wiki page with no commit message. !26247
+- Handle missing keys in sentry api response. !26264
+- Implemented whitespace-trimming for file names in Web IDE. !26270
+- Fix misalignment of group overview page buttons. !26292
+- Reject HEAD requests to info/refs endpoint. !26334
+- Prevent namespace dropdown in new project form from exceeding container. !26343
+- Fix hover animation consistency in top navbar items. !26345
+- Exclude system notes from commits in merge request discussions. !26396
+- Resolve Code in other column of side-by-side diff is highlighted when selecting code on one side. !26423
+- Prevent fade out transition on loading-button component. !26428
+- Fix merge commits being used as default squash commit messages. !26445
+- Expand resolved discussion when linking to a comment in the discussion. !26483
+- Show statistics also when repository is disabled. !26509 (Peter Marko)
+- Fix multiple series queries on metrics dashboard. !26514
+- Releases will now be automatically deleted when deleting corresponding tag. !26530
+- Make stylistic improvements to diff nav header. !26557
+- Clear pipeline status cache after destruction of pipeline. !26575
+- Update fugit which fixes a potential infinite loop. !26579
+- Fixes job link in artifacts page breadcrumb. !26592
+- Fix quick actions add label name middle word overlaps. !26602 (Jacopo Beschi @jacopo-beschi)
+- Fix Auto DevOps missing domain error handling. !26627
+- Fix jupyter rendering bug that ended in an infinite loop. !26656 (ROSPARS Benoit)
+- Use a fixed git abbrev parameter when we fetch a git revision. !26707
+- Enabled text selection highlighting in diffs in Web IDE. !26721 (Isaac Smith)
+- Remove `path` and `branch` labels from metrics. !26744
+- Resolve "Hide Kubernetes cluster warning if project has cluster related". !26749
+- Fix long label overflow on metrics dashboard. !26775
+- Group transfer now properly redirects to edit on failure. !26837
+- Only execute system hooks once when pushing tags. !26888
+- Fix UI anchor links after docs refactor. !26890
+- Fix MWPS does not work for merge request pipelines. !26906
+- Create pipelines for merge requests only when source branch is updated. !26921
+- Fix notfication emails having wrong encoding. !26931
+- Allow task lists that follow a blockquote to work correctly. !26937
+- Fix image diff swipe view on commit and compare pages. !26968 (ftab)
+- Fix IDE detection of MR from fork with same branch name. !26986
+- Fix single string values for the 'include' keyword validation of gitlab-ci.yml. !26998 (Paul Bonaud (@paulrbr))
+- Do not display Ingress IP help text when there isn’t an Ingress IP assigned. !27057
+- Fix real-time updates for projects that contain a reserved word. !27060
+- Remove duplicates from issue related merge requests. !27067
+- Add to white-space nowrap to all buttons. !27069
+- Handle possible HTTP exception for Sentry client. !27080
+- Guard against nil dereferenced_target. !27192
+- Update GitLab Workhorse to v8.5.1. !27217
+- Fix long file header names bug in diffs. !27233
+- Always return the deployment in the UpdateDeploymentService#execute method. !27322
+- Fix remove_source_branch merge request API handling. !27392
+- Fixed bug with hashes in urls in WebIDE. !54376 (Kieran Andrews)
+- Fix bug where MR popover doesn't go away on mouse leave.
+- Only consider active milestones when using the special Started milestone filter.
+- Scroll to diff file content when clicking on file header name and it is not a link to other page.
+- Remove non-functional add issue button on welcome list.
+- Fixed expand full file button showing on images.
+- Fixed Web IDE web workers not working with relative URLs.
+- Fixed Web IDE not loading merge request files.
+- Fixed duplicated diff too large error message.
+- Fixed sticky headers in merge request creation diffs.
+- Fix bug when reopening milestone from index page.
+
+### Deprecated (1 change)
+
+- Allow to use untrusted Regexp via feature flag. !26905
+
+### Changed (35 changes, 4 of them are from the community)
+
+- Create MR pipelines with `refs/merge-requests/:iid/head`. !25504
+- Create Kubernetes resources for projects when their deployment jobs run. !25586
+- Remove unnecessary folder prefix from environment name. !25600
+- Update deploy boards to additionally select on "app.gitlab.com" annotations. !25623
+- Allow failed custom hook script errors to safely appear in GitLab UI by filtering error messages by the prefix GL-HOOK-ERR:. !25625
+- Add link on two-factor authorization settings page to leave group that enforces two-factor authorization. !25731
+- Reduce height of instance system header and footer. !25752
+- Unify behaviour of 'Copy commit SHA to clipboard' to use full commit SHA. !25829 (Max Winterstein)
+- Show loading spinner while Ingress/Knative IP is being assigned. !25912
+- Hashed Storage: Prevent a migration and rollback running at the same time. !25976
+- Make time counters show 'just now' for everything under one minute. !25992 (Sergiu Marton)
+- Allow filtering labels list by one or two characters. !26012
+- Implements the creation strategy for multi-line suggestions. !26057
+- Automate base domain help text on Clusters page. !26124
+- Set user.name limit to 128 characters. !26146
+- Update gitlab-markup to 1.7.0 which requies python3. !26246
+- Update system message banner font size to 12px. !26293
+- Extend timezone dropdown. !26311
+- Upgrade to Gitaly v1.29.0. !26406
+- Automatically set Prometheus step interval. !26441
+- Knative version bump 0.2.2 -> 0.3.0. !26459 (Chris Baumbauer)
+- Display cluster form validation error messages inline. !26502
+- Split Auto-DevOps.gitlab-ci.yml into reusable templates. !26520
+- Update spinners in group list component. !26572
+- Allow removing last owner from subgroup if parent group has owners. !26718
+- Check mergeability in MergeToRefService. !26757
+- Show download diff links for closed MRs. !26772
+- Fix Container Scanning in Kubernetes Runners. !26793
+- Move "Authorize project access with external service" to Core. !26823
+- Localize notifications dropdown. !26844
+- Order labels alphabetically in issue boards. !26927
+- Upgrade to Gitaly v1.32.0. !26989
+- Upgrade to Gitaly v1.33.0. !27065
+- collapse file tree by default if the merge request changes only one file. (Riccardo Padovani <riccardo@rpadovani.com>)
+- Removes the undescriptive CI Charts header.
+
+### Performance (17 changes)
+
+- Drop legacy artifacts usage as there are no leftovers. !24294
+- Cache Repository#root_ref within a request. !25903
+- Allow ref name caching CommitService#find_commit. !26248
+- Avoid loading pipeline status in project search. !26342
+- Fix some N+1s in loading routes and counting members for groups in @-autocomplete. !26491
+- GitHub import: Run housekeeping after initial import. !26600
+- Add initial complexity limits to GraphQL queries. !26629
+- Cache FindCommit results in pipelines view. !26776
+- Fix and expand Gitaly FindCommit caching. !27018
+- Enable FindCommit caching for project and commits pages. !27048
+- Expand FindCommit caching to blob and refs. !27084
+- Enable Gitaly FindCommit caching for TreeController. !27100
+- Improve performance of PR import. !27121
+- Process at most 4 pipelines during push. !27205
+- Disable method instrumentation for diffs. !27235
+- Speed up filtering issues in a project when searching.
+- Speed up generation of avatar URLs when using object storage.
+
+### Added (35 changes, 6 of them are from the community)
+
+- Add users search results to global search. !21197 (Alexis Reigel)
+- Add target branch filter to merge requests search bar. !24380 (Hiroyuki Sato)
+- Add Knative metrics to Prometheus. !24663 (Chris Baumbauer <cab@cabnetworks.net>)
+- Support multi-line suggestions. !25211
+- Allow to sort wiki pages by date and title. !25365
+- Allow external diffs to be used conditionally. !25432
+- Add usage counts for error tracking feature. !25472
+- Enable/disable Auto DevOps at the Group level. !25533
+- Update pipeline list view to accommodate post-merge pipeline information. !25690
+- GraphQL Types can be made to always authorize access to resources of that Type. !25724
+- Update clair-local-scan to 2.0.6. !25743 (Takuya Noguchi)
+- Update pipeline block on merge request page to accommodate post-merge pipeline information. !25745
+- Support multiple queries per chart on metrics dash. !25758
+- Update pipeline detail view to accommodate post-merge pipelines. !25775
+- Update job detail sidebar to accommodate post-merge pipeline information. !25777
+- Add merge request pipeline flag to pipeline entity. !25846
+- Expose group id on home panel. !25897 (Peter Marko)
+- Move allow developers to create projects in groups to Core. !25975
+- Add two new warning messages to the MR widget about merge request pipelines. !25983
+- Support installing Group runner on group-level cluster. !26260
+- Improve the Knative installation on Clusters. !26339
+- Show error when namespace/svc account missing. !26362
+- Add select by title to milestones API. !26573
+- Implemented support for creation of new files from URL in Web IDE. !26622
+- Add control for masking variable values in runner logs. !26751
+- Allow merge requests to be created via git push options. !26752
+- Create a shortcut for a new MR in the Web IDE. !26792
+- Allow reactive caching to be used in services. !26839
+- Add a Prometheus API per environment. !26841
+- Allow merge requests to be set to merge when pipeline succeeds via git push options. !26842
+- Use gitlabktl to build and deploy GitLab Serverless Functions. !26926
+- Make touch events work on image diff swipe view and onion skin. !26971 (ftab)
+- Add extended merge request tooltip.
+- Added prometheus monitoring to GraphQL.
+- Adding highest role property to admin's user details page.
+
+### Other (29 changes, 6 of them are from the community)
+
+- Update rack-oauth2 1.2.1 -> 1.9.3. !17868
+- Merge the gitlab-shell "gitlab-keys" functionality into GitLab CE. !25598
+- Refactor all_pipelines in Merge request. !25676
+- Show error backtrace when logging errors to kubernetes.log. !25726
+- Apply recaptcha API change in 4.0. !25921 (Praveen Arimbrathodiyil)
+- Remove fake repository_path response. !25942 (Fabio Papa)
+- Use curl silent/show-error options on Auto DevOps. !25954 (Takuya Noguchi)
+- Explicitly set master_auth setting to enable basic auth and client certificate for new GKE clusters. !26018
+- Project: Improve empty repository state UI. !26024
+- Externalize strings from `/app/views/projects/pipelines`. !26035 (George Tsiolis)
+- Prepare multi-line suggestions for rendering in Markdown. !26107
+- Improve mobile UI on User Profile page. !26240 (Takuya Noguchi)
+- Update GitLab Runner Helm Chart to 0.3.0/11.9.0. !26467
+- Improve project merge request settings. !26495
+- Bump kubectl to 1.11.9 and Helm to 2.13.1 in Auto-DevOps.gitlab-ci.yml. !26534
+- Upgrade bootstrap_form Gem. !26568
+- Add API access check to Graphql. !26570
+- Change project avatar remove button to a link. !26589
+- Log Gitaly RPC duration to api_json.log and production_json.log. !26652
+- Add cluster domain to Project Cluster API. !26735
+- Move project tags to separate line. !26797
+- Changed button label at /pipelines/new. !26893 (antfobe,leonardofl)
+- Update GitLab Shell to v9.0.0. !27002
+- Migrate clusters tests to jest. !27013
+- Rewrite related MRs widget with Vue. !27027
+- Restore HipChat project service. !27172
+- Externalize admin deploy keys strings.
+- Removes EE differences for environments_table.vue.
+- Removes EE differences for environment_item.vue.
+
+
+## 11.9.10 (2019-04-26)
+
+### Security (5 changes)
+
+- Loosen regex for exception sanitization. !3077
+- Resolve: moving an issue to private repo leaks namespace and project name.
+- Escape path in new merge request mail.
+- Stop sending emails to users who can't read commit.
+- Upgrade Rails to 5.0.7.2.
+
+
+## 11.9.9 (2019-04-23)
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+
## 11.9.8 (2019-04-11)
### Deprecated (1 change)
@@ -353,6 +662,35 @@ entry.
- Removes EE differences for jobs/getters.js.
+## 11.8.10 (2019-04-30)
+
+### Security (1 change)
+
+- Allow to see project events only with api scope token.
+
+
+## 11.8.8 (2019-04-23)
+
+### Fixed (5 changes)
+
+- Bring back Rugged implementation of find_commit. !25477
+- Fix bug in BitBucket imports with SHA shorter than 40 chars. !26050
+- Fix health checks not working behind load balancers. !26055
+- Fix error creating a merge request when diff includes a null byte. !26190
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+### Other (4 changes)
+
+- Bring back Rugged implementation of GetTreeEntries. !25674
+- Bring back Rugged implementation of CommitIsAncestor. !25702
+- Bring back Rugged implementation of TreeEntry. !25706
+- Bring back Rugged implementation of commit_tree_entry. !25896
+
+
## 11.8.3 (2019-03-19)
### Security (1 change)
@@ -622,6 +960,25 @@ entry.
- Creates mixin to reduce code duplication between CE and EE in graph component.
+## 11.7.12 (2019-04-23)
+
+### Fixed (2 changes)
+
+- Bring back Rugged implementation of find_commit. !25477
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+### Other (4 changes)
+
+- Bring back Rugged implementation of GetTreeEntries. !25674
+- Bring back Rugged implementation of CommitIsAncestor. !25702
+- Bring back Rugged implementation of TreeEntry. !25706
+- Bring back Rugged implementation of commit_tree_entry. !25896
+
+
## 11.7.11 (2019-04-09)
- No changes.
@@ -897,6 +1254,30 @@ entry.
- Update url placeholder for the sentry configuration page. !24338
+## 11.6.11 (2019-04-23)
+
+### Security (2 changes)
+
+- Fixed ability to see private groups by users not belonging to given group.
+- Fix XSS in resolve conflicts form.
+
+### Fixed (2 changes)
+
+- Bring back Rugged implementation of find_commit. !25477
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+### Other (4 changes)
+
+- Bring back Rugged implementation of GetTreeEntries. !25674
+- Bring back Rugged implementation of CommitIsAncestor. !25702
+- Bring back Rugged implementation of TreeEntry. !25706
+- Bring back Rugged implementation of commit_tree_entry. !25896
+
+
## 11.6.10 (2019-02-28)
### Security (21 changes)
@@ -1268,6 +1649,25 @@ entry.
- Enable Rubocop on lib/gitlab. (gfyoung)
+## 11.5.11 (2019-04-23)
+
+### Fixed (2 changes)
+
+- Bring back Rugged implementation of find_commit. !25477
+- Avoid excessive recursive calls with Rugged TreeEntries. !26813
+
+### Performance (1 change)
+
+- Bring back Rugged implementation of ListCommitsByOid. !27441
+
+### Other (4 changes)
+
+- Bring back Rugged implementation of GetTreeEntries. !25674
+- Bring back Rugged implementation of CommitIsAncestor. !25702
+- Bring back Rugged implementation of TreeEntry. !25706
+- Bring back Rugged implementation of commit_tree_entry. !25896
+
+
## 11.5.8 (2019-01-28)
### Security (21 changes)
diff --git a/Dangerfile b/Dangerfile
index 3e8cb456003..9e3a08949b0 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
danger.import_plugin('danger/plugins/helper.rb')
unless helper.release_automation?
@@ -16,4 +17,5 @@ unless helper.release_automation?
danger.import_dangerfile(path: 'danger/roulette')
danger.import_dangerfile(path: 'danger/single_codebase')
danger.import_dangerfile(path: 'danger/gitlab_ui_wg')
+ danger.import_dangerfile(path: 'danger/ce_ee_vue_templates')
end
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7aa332e4163..a50908ca3da 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.33.0
+1.42.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index acd405b1d62..df5119ec64e 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.6.0
+8.7.0
diff --git a/Gemfile b/Gemfile
index e38c1f03ca0..19432758b34 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '5.0.7.2'
+gem 'rails', '5.1.7'
# Improves copy-on-write performance for MRI
gem 'nakayoshi_fork', '~> 0.0.4'
@@ -18,7 +18,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 1.1', group: :postgres
gem 'rugged', '~> 0.28'
-gem 'grape-path-helpers', '~> 1.0'
+gem 'grape-path-helpers', '~> 1.1'
gem 'faraday', '~> 0.12'
@@ -41,6 +41,9 @@ 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.0'
+gem "omniauth-ultraauth", '~> 0.0.2'
+gem 'omniauth-salesforce', '~> 1.0.5'
gem 'rack-oauth2', '~> 1.9.3'
gem 'jwt', '~> 2.1.0'
@@ -78,6 +81,7 @@ gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.8.0'
gem 'graphiql-rails', '~> 1.4.10'
+gem 'apollo_upload_server', '~> 2.0.0.beta3'
# Disable strong_params so that Mash does not respond to :permitted?
gem 'hashie-forbidden_attributes'
@@ -128,7 +132,7 @@ gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 3.1'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
-gem 'nokogiri', '~> 1.10.1'
+gem 'nokogiri', '~> 1.10.3'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
@@ -157,7 +161,7 @@ gem 'state_machines-activerecord', '~> 0.5.1'
gem 'acts-as-taggable-on', '~> 6.0'
# Background jobs
-gem 'sidekiq', '~> 5.2.1'
+gem 'sidekiq', '~> 5.2.7'
gem 'sidekiq-cron', '~> 1.0'
gem 'redis-namespace', '~> 1.6.0'
gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch'
@@ -256,8 +260,7 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0'
-gem 'sass-rails', '~> 5.0.6'
-gem 'sass', '~> 3.5'
+gem 'sassc-rails', '~> 2.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.5.2'
@@ -273,6 +276,9 @@ gem 'sentry-raven', '~> 2.7'
gem 'premailer-rails', '~> 1.9.7'
+# LabKit: Tracing and Correlation
+gem 'gitlab-labkit', '~> 0.2.0'
+
# I18n
gem 'ruby_parser', '~> 3.8', require: false
gem 'rails-i18n', '~> 5.1'
@@ -280,7 +286,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.3'
gem 'gettext', '~> 3.2.2', require: false, group: :development
-gem 'batch-loader', '~> 1.2.2'
+gem 'batch-loader', '~> 1.4.0'
# Perf bar
gem 'peek', '~> 1.0.1'
@@ -300,17 +306,11 @@ group :metrics do
gem 'raindrops', '~> 0.18'
end
-group :tracing do
- # OpenTracing
- gem 'opentracing', '~> 0.4.3'
- gem 'jaeger-client', '~> 0.10.0'
-end
-
group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 4.2', require: false
- gem 'letter_opener_web', '~> 1.3.0'
+ gem 'letter_opener_web', '~> 1.3.4'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
# Better errors handler
@@ -351,13 +351,14 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'gitlab-styles', '~> 2.5', require: false
+ gem 'gitlab-styles', '~> 2.6', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
- gem 'rubocop', '~> 0.54.0'
+ gem 'rubocop', '~> 0.68.1'
+ gem 'rubocop-performance', '~> 1.1.0'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
- gem 'haml_lint', '~> 0.28.0', require: false
+ gem 'haml_lint', '~> 0.30.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -419,11 +420,11 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.19.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.27.0', require: 'gitaly'
-gem 'grpc', '~> 1.15.0'
+gem 'grpc', '~> 1.19.0'
-gem 'google-protobuf', '~> 3.6'
+gem 'google-protobuf', '~> 3.7.1'
gem 'toml-rb', '~> 1.0.0', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 36c24265e48..3dd1adb5f63 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,41 +4,41 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
- actioncable (5.0.7.2)
- actionpack (= 5.0.7.2)
- nio4r (>= 1.2, < 3.0)
+ actioncable (5.1.7)
+ actionpack (= 5.1.7)
+ nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.7.2)
- actionpack (= 5.0.7.2)
- actionview (= 5.0.7.2)
- activejob (= 5.0.7.2)
+ actionmailer (5.1.7)
+ actionpack (= 5.1.7)
+ actionview (= 5.1.7)
+ activejob (= 5.1.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (5.0.7.2)
- actionview (= 5.0.7.2)
- activesupport (= 5.0.7.2)
+ actionpack (5.1.7)
+ actionview (= 5.1.7)
+ activesupport (= 5.1.7)
rack (~> 2.0)
- rack-test (~> 0.6.3)
+ rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.7.2)
- activesupport (= 5.0.7.2)
+ actionview (5.1.7)
+ activesupport (= 5.1.7)
builder (~> 3.1)
- erubis (~> 2.7.0)
+ erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
- activejob (5.0.7.2)
- activesupport (= 5.0.7.2)
+ activejob (5.1.7)
+ activesupport (= 5.1.7)
globalid (>= 0.3.6)
- activemodel (5.0.7.2)
- activesupport (= 5.0.7.2)
- activerecord (5.0.7.2)
- activemodel (= 5.0.7.2)
- activesupport (= 5.0.7.2)
- arel (~> 7.0)
+ activemodel (5.1.7)
+ activesupport (= 5.1.7)
+ activerecord (5.1.7)
+ activemodel (= 5.1.7)
+ activesupport (= 5.1.7)
+ arel (~> 8.0)
activerecord_sane_schema_dumper (1.0)
rails (>= 5, < 6)
- activesupport (5.0.7.2)
+ activesupport (5.1.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -52,7 +52,10 @@ GEM
public_suffix (>= 2.0.2, < 4.0)
aes_key_wrap (1.0.1)
akismet (2.0.0)
- arel (7.1.4)
+ apollo_upload_server (2.0.0.beta.3)
+ graphql (>= 1.8)
+ rails (>= 4.2)
+ arel (8.0.0)
asana (0.8.1)
faraday (~> 0.9)
faraday_middleware (~> 0.9)
@@ -73,7 +76,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.2.2)
+ batch-loader (1.4.0)
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -185,8 +188,7 @@ GEM
mail (~> 2.7)
encryptor (3.0.0)
equalizer (0.0.11)
- erubi (1.7.1)
- erubis (2.7.0)
+ erubi (1.8.0)
escape_utils (1.2.1)
et-orbi (1.1.7)
tzinfo
@@ -257,8 +259,8 @@ GEM
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
- font-awesome-rails (4.7.0.1)
- railties (>= 3.2, < 5.1)
+ font-awesome-rails (4.7.0.4)
+ railties (>= 3.2, < 6.0)
foreman (0.84.0)
thor (~> 0.19.1)
formatador (0.2.5)
@@ -281,17 +283,24 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.19.0)
+ gitaly-proto (1.27.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
activerecord (>= 3.2.0, < 6.0)
+ gitlab-labkit (0.2.0)
+ actionpack (~> 5)
+ activesupport (~> 5)
+ grpc (~> 1.15)
+ jaeger-client (~> 0.10)
+ opentracing (~> 0.4)
gitlab-markup (1.7.0)
gitlab-sidekiq-fetcher (0.4.0)
sidekiq (~> 5)
- gitlab-styles (2.5.2)
- rubocop (~> 0.54.0)
+ gitlab-styles (2.6.2)
+ rubocop (~> 0.68.1)
rubocop-gitlab-security (~> 0.1.0)
+ rubocop-performance (~> 1.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.1.1)
net-ldap (~> 0.16)
@@ -311,8 +320,8 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.6.1)
- googleapis-common-protos-types (1.0.3)
+ google-protobuf (3.7.1)
+ googleapis-common-protos-types (1.0.4)
google-protobuf (~> 3.0)
googleauth (0.6.6)
faraday (~> 0.12)
@@ -333,8 +342,8 @@ GEM
grape-entity (0.7.1)
activesupport (>= 4.0)
multi_json (>= 1.3.2)
- grape-path-helpers (1.0.6)
- activesupport (>= 4, < 5.1)
+ grape-path-helpers (1.1.0)
+ activesupport
grape (~> 1.0)
rake (~> 12)
grape_logging (1.7.0)
@@ -343,13 +352,13 @@ GEM
railties
sprockets-rails
graphql (1.8.1)
- grpc (1.15.0)
+ grpc (1.19.0)
google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0)
haml (5.0.4)
temple (>= 0.8.0)
tilt
- haml_lint (0.28.0)
+ haml_lint (0.30.0)
haml (>= 4.0, < 5.1)
rainbow
rake (>= 10, < 13)
@@ -399,6 +408,7 @@ GEM
jaeger-client (0.10.0)
opentracing (~> 0.3)
thrift
+ jaro_winkler (1.5.2)
jira-ruby (1.4.1)
activesupport
multipart-post
@@ -436,9 +446,9 @@ GEM
rest-client (~> 2.0)
launchy (2.4.3)
addressable (~> 2.3)
- letter_opener (1.4.1)
+ letter_opener (1.7.0)
launchy (~> 2.2)
- letter_opener_web (1.3.0)
+ letter_opener_web (1.3.4)
actionmailer (>= 3.2)
letter_opener (~> 1.0)
railties (>= 3.2)
@@ -488,7 +498,7 @@ GEM
net-ssh (5.0.1)
netrc (0.11.0)
nio4r (2.3.1)
- nokogiri (1.10.2)
+ nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
nokogiri
@@ -543,6 +553,9 @@ GEM
omniauth (~> 1.9)
omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
+ omniauth-salesforce (1.0.5)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.0)
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
@@ -551,17 +564,33 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
+ omniauth-ultraauth (0.0.2)
+ omniauth_openid_connect (~> 0.3.0)
omniauth_crowd (2.2.3)
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- opentracing (0.4.3)
+ omniauth_openid_connect (0.3.0)
+ addressable (~> 2.5)
+ omniauth (~> 1.3)
+ openid_connect (~> 1.1)
+ openid_connect (1.1.6)
+ activemodel
+ attr_required (>= 1.0.0)
+ json-jwt (>= 1.5.0)
+ rack-oauth2 (>= 1.6.1)
+ swd (>= 1.0.0)
+ tzinfo
+ validate_email
+ validate_url
+ webfinger (>= 1.0.1)
+ opentracing (0.5.0)
optimist (3.0.0)
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (1.0.0)
- parallel (1.12.1)
+ parallel (1.17.0)
parser (2.5.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
@@ -591,7 +620,6 @@ GEM
pg (1.1.4)
po_to_json (1.0.1)
json (>= 1.6.0)
- powerpack (0.1.1)
premailer (1.10.4)
addressable
css_parser (>= 1.4.10)
@@ -636,19 +664,19 @@ GEM
rack
rack-proxy (0.6.0)
rack
- rack-test (0.6.3)
- rack (>= 1.0)
- rails (5.0.7.2)
- actioncable (= 5.0.7.2)
- actionmailer (= 5.0.7.2)
- actionpack (= 5.0.7.2)
- actionview (= 5.0.7.2)
- activejob (= 5.0.7.2)
- activemodel (= 5.0.7.2)
- activerecord (= 5.0.7.2)
- activesupport (= 5.0.7.2)
+ rack-test (1.1.0)
+ rack (>= 1.0, < 3)
+ rails (5.1.7)
+ actioncable (= 5.1.7)
+ actionmailer (= 5.1.7)
+ actionpack (= 5.1.7)
+ actionview (= 5.1.7)
+ activejob (= 5.1.7)
+ activemodel (= 5.1.7)
+ activerecord (= 5.1.7)
+ activesupport (= 5.1.7)
bundler (>= 1.3.0)
- railties (= 5.0.7.2)
+ railties (= 5.1.7)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2)
actionpack (~> 5.x, >= 5.0.1)
@@ -662,9 +690,9 @@ GEM
rails-i18n (5.1.1)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
- railties (5.0.7.2)
- actionpack (= 5.0.7.2)
- activesupport (= 5.0.7.2)
+ railties (5.1.7)
+ actionpack (= 5.1.7)
+ activesupport (= 5.1.7)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -763,15 +791,17 @@ GEM
pg
rails
sqlite3
- rubocop (0.54.0)
+ rubocop (0.68.1)
+ jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
- parser (>= 2.5)
- powerpack (~> 0.1)
+ parser (>= 2.5, != 2.5.1.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (~> 1.0, >= 1.0.1)
+ unicode-display_width (>= 1.4.0, < 1.6)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
+ rubocop-performance (1.1.0)
+ rubocop (>= 0.67.0)
rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
@@ -779,7 +809,7 @@ GEM
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.17.0)
- ruby-progressbar (1.9.0)
+ ruby-progressbar (1.10.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
ruby_parser (3.11.0)
@@ -798,12 +828,15 @@ GEM
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
- sass-rails (5.0.6)
- railties (>= 4.0.0, < 6)
- sass (~> 3.1)
- sprockets (>= 2.8, < 4.0)
- sprockets-rails (>= 2.0, < 4.0)
- tilt (>= 1.1, < 3)
+ sassc (2.0.1)
+ ffi (~> 1.9)
+ rake
+ sassc-rails (2.1.0)
+ railties (>= 4.0.0)
+ sassc (>= 2.0)
+ sprockets (> 3.0)
+ sprockets-rails
+ tilt
sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0)
@@ -824,7 +857,7 @@ GEM
rack
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
- sidekiq (5.2.5)
+ sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0)
@@ -865,6 +898,10 @@ GEM
state_machines-activerecord (0.5.1)
activerecord (>= 4.1, < 6.0)
state_machines-activemodel (>= 0.5.0)
+ swd (1.1.2)
+ activesupport (>= 3)
+ attr_required (>= 0.0.5)
+ httpclient (>= 2.4)
sys-filesystem (1.1.6)
ffi
sys-proctable (1.2.1)
@@ -900,7 +937,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.2)
+ unicode-display_width (1.5.0)
unicorn (5.4.1)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -916,6 +953,12 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.3.1.2, < 2.6)
procto (~> 0.0.2)
+ validate_email (0.1.6)
+ activemodel (>= 3.0)
+ mail (>= 2.2.5)
+ validate_url (1.0.8)
+ activemodel (>= 3.0.0)
+ public_suffix
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
@@ -928,6 +971,9 @@ GEM
vmstat (2.3.0)
warden (1.2.7)
rack (>= 1.0)
+ webfinger (1.1.0)
+ activesupport
+ httpclient (>= 2.4)
webmock (3.5.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
@@ -956,6 +1002,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
akismet (~> 2.0)
+ apollo_upload_server (~> 2.0.0.beta3)
asana (~> 0.8.1)
asciidoctor (~> 1.5.8)
asciidoctor-plantuml (= 0.0.8)
@@ -963,7 +1010,7 @@ DEPENDENCIES
awesome_print
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader (~> 1.2.2)
+ batch-loader (~> 1.4.0)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.5.0)
@@ -1020,25 +1067,26 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.19.0)
+ gitaly-proto (~> 1.27.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
+ gitlab-labkit (~> 0.2.0)
gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0)
- gitlab-styles (~> 2.5)
+ gitlab-styles (~> 2.6)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
- google-protobuf (~> 3.6)
+ google-protobuf (~> 3.7.1)
gpgme (~> 2.0.18)
grape (~> 1.1.0)
grape-entity (~> 0.7.1)
- grape-path-helpers (~> 1.0)
+ grape-path-helpers (~> 1.1)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
- grpc (~> 1.15.0)
- haml_lint (~> 0.28.0)
+ grpc (~> 1.19.0)
+ haml_lint (~> 0.30.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
@@ -1049,7 +1097,6 @@ DEPENDENCIES
httparty (~> 0.16.4)
icalendar
influxdb (~> 0.2)
- jaeger-client (~> 0.10.0)
jira-ruby (~> 1.4)
js_regex (~> 3.1)
json-schema (~> 2.8.0)
@@ -1057,7 +1104,7 @@ DEPENDENCIES
kaminari (~> 1.0)
knapsack (~> 1.17)
kubeclient (~> 4.2.2)
- letter_opener_web (~> 1.3.0)
+ letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
lograge (~> 0.5)
@@ -1071,7 +1118,7 @@ DEPENDENCIES
nakayoshi_fork (~> 0.0.4)
net-ldap
net-ssh (~> 5.0)
- nokogiri (~> 1.10.1)
+ nokogiri (~> 1.10.3)
oauth2 (~> 1.4)
octokit (~> 4.9)
omniauth (~> 1.8)
@@ -1085,11 +1132,13 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.6.0)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
+ omniauth-salesforce (~> 1.0.5)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
+ omniauth-ultraauth (~> 0.0.2)
omniauth_crowd (~> 2.2.0)
- opentracing (~> 0.4.3)
+ omniauth_openid_connect (~> 0.3.0)
org-ruby (~> 0.9.12)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
@@ -1109,7 +1158,7 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
- rails (= 5.0.7.2)
+ rails (= 5.1.7)
rails-controller-testing
rails-i18n (~> 5.1)
rainbow (~> 3.0)
@@ -1132,7 +1181,8 @@ DEPENDENCIES
rspec-set (~> 0.1.3)
rspec_junit_formatter
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.54.0)
+ rubocop (~> 0.68.1)
+ rubocop-performance (~> 1.1.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
@@ -1141,8 +1191,7 @@ DEPENDENCIES
rubyzip (~> 1.2.2)
rugged (~> 0.28)
sanitize (~> 4.6)
- sass (~> 3.5)
- sass-rails (~> 5.0.6)
+ sassc-rails (~> 2.1.0)
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
selenium-webdriver (~> 3.141)
@@ -1150,7 +1199,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 3.1.2)
- sidekiq (~> 5.2.1)
+ sidekiq (~> 5.2.7)
sidekiq-cron (~> 1.0)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
diff --git a/VERSION b/VERSION
index 1a954017bf3..fd59314f1ad 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.10.0-pre
+11.11.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 8754c253881..e583a8affd4 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
+import { joinPaths } from './lib/utils/url_utility';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -339,11 +340,7 @@ const Api = {
},
buildUrl(url) {
- let urlRoot = '';
- if (gon.relative_url_root != null) {
- urlRoot = gon.relative_url_root;
- }
- return urlRoot + url.replace(':version', gon.api_version);
+ return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
};
diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
new file mode 100644
index 00000000000..20c31d9f8a4
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
@@ -0,0 +1,13 @@
+export default {
+ computed: {
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
+
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
+
+ return title;
+ },
+ },
+};
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index 9a33a060c76..c3541e62568 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Clipboard from 'clipboard';
+import { sprintf, __ } from '~/locale';
function showTooltip(target, title) {
const $target = $(target);
@@ -16,7 +17,7 @@ function showTooltip(target, title) {
}
function genericSuccess(e) {
- showTooltip(e.trigger, 'Copied');
+ showTooltip(e.trigger, __('Copied'));
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
@@ -33,7 +34,7 @@ function genericError(e) {
} else {
key = 'Ctrl';
}
- showTooltip(e.trigger, `Press ${key}-C to copy`);
+ showTooltip(e.trigger, sprintf(__(`Press %{key}-C to copy`), { key }));
}
export default function initCopyToClipboard() {
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 55c68139ded..b7200150925 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { parseBoolean } from '~/lib/utils/common_utils';
-import GfmAutoComplete from '~/gfm_auto_complete';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
export default function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
index 20c7fa8a9ab..9a2e2c03213 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import { Node } from 'tiptap';
+import { __ } from '~/locale';
// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter
export default class TableOfContents extends Node {
@@ -22,7 +23,7 @@ export default class TableOfContents extends Node {
priority: 51,
},
],
- toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'],
+ toDOM: () => ['p', { class: 'table-of-contents' }, __('Table of Contents')],
};
}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 7adccbb062f..35874140bf9 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -22,7 +22,7 @@ function MarkdownPreview() {}
// Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
-MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.';
+MarkdownPreview.prototype.emptyMessage = __('Nothing to preview.');
MarkdownPreview.prototype.ajaxCache = {};
@@ -40,7 +40,7 @@ MarkdownPreview.prototype.showPreview = function($form) {
preview.text(this.emptyMessage);
this.hideReferencedUsers($form);
} else {
- preview.addClass('md-preview-loading').text('Loading...');
+ preview.addClass('md-preview-loading').text(__('Loading...'));
this.fetchMarkdownPreview(
mdText,
url,
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index c1ea67f9293..530ab0bd4d9 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import '../commons/bootstrap';
import { isInIssuePage } from '../lib/utils/common_utils';
+import { __ } from '~/locale';
// Quick Submit behavior
//
@@ -65,7 +66,9 @@ $(document).on(
}
const $this = $(this);
- const title = isMac() ? 'You can also press &#8984;-Enter' : 'You can also press Ctrl-Enter';
+ const title = isMac()
+ ? __('You can also press &#8984;-Enter')
+ : __('You can also press Ctrl-Enter');
$this.tooltip({
container: 'body',
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index b88e69a07bf..2e537d8c000 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,8 +1,9 @@
import Flash from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
+import { __ } from '~/locale';
function onError() {
- const flash = new Flash('Balsamiq file could not be loaded.');
+ const flash = new Flash(__('Balsamiq file could not be loaded.'));
return flash;
}
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index cd3251ad1ca..9010cd0c3c1 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -5,6 +5,7 @@ import Dropzone from 'dropzone';
import { visitUrl } from '../lib/utils/url_utility';
import { HIDDEN_CLASS } from '../lib/utils/constants';
import csrf from '../lib/utils/csrf';
+import { sprintf, __ } from '~/locale';
Dropzone.autoDiscover = false;
@@ -73,7 +74,7 @@ export default class BlobFileDropzone {
.html(errorMessage)
.text();
$('.dropzone-alerts')
- .html(`Error uploading file: "${stripped}"`)
+ .html(sprintf(__('Error uploading file: %{stripped}'), { stripped }))
.show();
this.removeFile(file);
},
@@ -84,7 +85,7 @@ export default class BlobFileDropzone {
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
- alert('Please select a file');
+ alert(__('Please select a file'));
return false;
}
toggleLoading(submitButton, submitButtonLoadingIcon, true);
diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js
index 57c1baa9886..dbff03dc734 100644
--- a/app/assets/javascripts/blob/sketch/index.js
+++ b/app/assets/javascripts/blob/sketch/index.js
@@ -1,5 +1,6 @@
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
+import { __ } from '~/locale';
export default class SketchLoader {
constructor(container) {
@@ -56,10 +57,10 @@ export default class SketchLoader {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
- errorMsg.textContent = `
+ errorMsg.textContent = __(`
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
- `;
+ `);
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
index 4718b642617..659d57e6a6f 100644
--- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -1,11 +1,12 @@
import FileTemplateSelector from '../file_template_selector';
+import { __ } from '~/locale';
export default class DockerfileSelector extends FileTemplateSelector {
constructor({ mediator }) {
super(mediator);
this.config = {
key: 'dockerfile',
- name: 'Dockerfile',
+ name: __('Dockerfile'),
pattern: /(Dockerfile)/,
type: 'dockerfiles',
dropdown: '.js-dockerfile-selector',
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index d0359fc5fe9..d246a1f6064 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Flash from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
+import { __ } from '~/locale';
export default class BlobViewer {
constructor() {
@@ -26,7 +27,7 @@ export default class BlobViewer {
promise
.then(module => module.default(viewer))
.catch(error => {
- Flash('Error loading file viewer.');
+ Flash(__('Error loading file viewer.'));
throw error;
});
@@ -106,16 +107,19 @@ export default class BlobViewer {
if (!this.copySourceBtn) return;
if (this.simpleViewer.getAttribute('data-loaded')) {
- this.copySourceBtn.setAttribute('title', 'Copy source to clipboard');
+ this.copySourceBtn.setAttribute('title', __('Copy source to clipboard'));
this.copySourceBtn.classList.remove('disabled');
} else if (this.activeViewer === this.simpleViewer) {
this.copySourceBtn.setAttribute(
'title',
- 'Wait for the source to load to copy it to the clipboard',
+ __('Wait for the source to load to copy it to the clipboard'),
);
this.copySourceBtn.classList.add('disabled');
} else {
- this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard');
+ this.copySourceBtn.setAttribute(
+ 'title',
+ __('Switch to the source to copy it to the clipboard'),
+ );
this.copySourceBtn.classList.add('disabled');
}
@@ -158,7 +162,7 @@ export default class BlobViewer {
this.toggleCopyButtonState();
})
- .catch(() => new Flash('Error loading viewer'));
+ .catch(() => new Flash(__('Error loading viewer')));
}
static loadViewer(viewerParam) {
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
new file mode 100644
index 00000000000..3178bda93b8
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -0,0 +1,7 @@
+export function getMilestone() {
+ return null;
+}
+
+export default {
+ getMilestone,
+};
diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue
index 667eea17d44..47a46502bff 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.vue
+++ b/app/assets/javascripts/boards/components/board_blank_state.vue
@@ -60,11 +60,15 @@ export default {
</script>
<template>
- <div class="board-blank-state">
+ <div class="board-blank-state p-3">
<p>Add the following default lists to your Issue Board with one click:</p>
- <ul class="board-blank-state-list">
+ <ul class="list-unstyled board-blank-state-list">
<li v-for="(label, index) in predefinedLabels" :key="index">
- <span :style="{ backgroundColor: label.color }" class="label-color"> </span>
+ <span
+ :style="{ backgroundColor: label.color }"
+ class="label-color position-relative d-inline-block rounded"
+ >
+ </span>
{{ label.title }}
</li>
</ul>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index f569322ab70..b8882203cc7 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -83,7 +83,7 @@ export default {
}"
:index="index"
:data-issue-id="issue.id"
- class="board-card"
+ class="board-card p-3 rounded"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)"
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index a5f9d65e4d5..a06db359c94 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
export default Vue.extend({
props: {
@@ -13,7 +14,7 @@ export default Vue.extend({
$(this.$el).tooltip('hide');
// eslint-disable-next-line no-alert
- if (window.confirm('Are you sure you want to delete this list?')) {
+ if (window.confirm(__('Are you sure you want to delete this list?'))) {
this.list.destroy();
}
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index f3f341ece5c..c9972d051aa 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -221,7 +221,10 @@ export default {
</script>
<template>
- <div class="board-list-component">
+ <div
+ :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
+ class="board-list-component position-relative h-100"
+ >
<div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues">
<gl-loading-icon />
</div>
@@ -236,7 +239,7 @@ export default {
:data-board="list.id"
:data-board-type="list.type"
:class="{ 'is-smaller': showIssueForm }"
- class="board-list js-board-list"
+ class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list"
>
<board-card
v-for="(issue, index) in issues"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 28d96dab605..dc1bdc23b5e 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import { GlButton } from '@gitlab/ui';
+import { getMilestone } from 'ee_else_ce/boards/boards_util';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import ListIssue from '../models/issue';
@@ -51,11 +52,14 @@ export default {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
+ const milestone = getMilestone(this.list);
+
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
assignees,
+ milestone,
project_id: this.selectedProject.id,
});
@@ -95,7 +99,7 @@ export default {
<template>
<div class="board-new-issue-form">
- <div class="board-card">
+ <div class="board-card position-relative p-3 rounded">
<form @submit="submit($event)">
<div v-if="error" class="flash-container">
<div class="flash-alert">An error occurred. Please try again.</div>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 915d1676e62..c587b276fa3 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -45,7 +45,7 @@ export default Vue.extend({
return Object.keys(this.issue).length;
},
milestoneTitle() {
- return this.issue.milestone ? this.issue.milestone.title : 'No Milestone';
+ return this.issue.milestone ? this.issue.milestone.title : __('No Milestone');
},
canRemove() {
return !this.list.preset;
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index be0de63e772..17de7b2cf1e 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -4,6 +4,7 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import IssueDueDate from './issue_due_date.vue';
@@ -19,11 +20,13 @@ export default {
TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
+ IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssueCardInnerScopedLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [issueCardInner],
props: {
issue: {
type: Object,
@@ -135,14 +138,6 @@ export default {
this.applyFilter(filter);
},
- filterByWeight(weight) {
- if (!this.updateFilters) return;
-
- const issueWeight = encodeURIComponent(weight);
- const filter = `weight=${issueWeight}`;
-
- this.applyFilter(filter);
- },
applyFilter(filter) {
const filterPath = boardsStore.filter.path.split('&');
const filterIndex = filterPath.indexOf(filter);
@@ -173,7 +168,7 @@ export default {
</script>
<template>
<div>
- <div class="board-card-header">
+ <div class="d-flex board-card-header" dir="auto">
<h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
@@ -214,11 +209,11 @@ export default {
</div>
<div class="board-card-footer d-flex justify-content-between align-items-end">
<div
- class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container"
+ class="d-flex align-items-start flex-wrap-reverse board-card-number-container overflow-hidden js-board-card-number-container"
>
<span
v-if="issue.referencePath"
- class="board-card-number d-flex append-right-8 prepend-top-8"
+ class="board-card-number overflow-hidden d-flex append-right-8 prepend-top-8"
>
<tooltip-on-truncate
v-if="issueReferencePath"
@@ -232,10 +227,14 @@ export default {
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" /><issue-time-estimate
v-if="issue.timeEstimate"
:estimate="issue.timeEstimate"
+ /><issue-card-weight
+ v-if="issue.weight"
+ :weight="issue.weight"
+ @click="filterByWeight(issue.weight)"
/>
</span>
</div>
- <div class="board-card-assignee">
+ <div class="board-card-assignee d-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 9bc66978198..3bc7f13a9e6 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -82,7 +82,11 @@ export default {
<template>
<span>
<span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
- <icon :class="{ 'text-danger': isPastDue, 'board-card-info-icon': true }" name="calendar" />
+ <icon
+ :class="{ 'text-danger': isPastDue }"
+ class="board-card-info-icon align-top"
+ name="calendar"
+ />
<time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index 5acc3025b2c..98c1d29db16 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -28,7 +28,7 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
- <icon name="hourglass" css-classes="board-card-info-icon" /><time
+ <icon name="hourglass" css-classes="board-card-info-icon align-top" /><time
class="board-card-info-text"
>{{ timeEstimate }}</time
>
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue
index 2a0008467c4..091700de93f 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.vue
+++ b/app/assets/javascripts/boards/components/modal/empty_state.vue
@@ -42,8 +42,8 @@ export default {
</script>
<template>
- <section class="empty-state">
- <div class="row">
+ <section class="empty-state d-flex mt-0 h-100">
+ <div class="row w-100 my-auto mx-0">
<div class="col-12 col-md-6 order-md-last">
<aside class="svg-content d-none d-md-block"><img :src="emptyStateSvg" /></aside>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 1f0961e02d8..1cfa6d39362 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -50,8 +50,8 @@ export default {
</script>
<template>
<div>
- <header class="add-issues-header form-actions">
- <h2>
+ <header class="add-issues-header border-top-0 form-actions">
+ <h2 class="m-0">
Add issues
<button
type="button"
@@ -65,7 +65,7 @@ export default {
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0" />
- <div v-if="showSearch" class="add-issues-search append-bottom-10">
+ <div v-if="showSearch" class="d-flex append-bottom-10">
<modal-filters :store="filter" />
<button
ref="selectAllBtn"
diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue
index 1e5761cf268..8e09e265cfb 100644
--- a/app/assets/javascripts/boards/components/modal/index.vue
+++ b/app/assets/javascripts/boards/components/modal/index.vue
@@ -143,8 +143,11 @@ export default {
};
</script>
<template>
- <div v-if="showAddIssuesModal" class="add-issues-modal">
- <div class="add-issues-container">
+ <div
+ v-if="showAddIssuesModal"
+ class="add-issues-modal d-flex position-fixed position-top-0 position-bottom-0 position-left-0 position-right-0 h-100"
+ >
+ <div class="add-issues-container d-flex flex-column m-auto rounded">
<modal-header
:project-id="projectId"
:milestone-path="milestonePath"
@@ -161,8 +164,10 @@ export default {
:new-issue-path="newIssuePath"
:empty-state-svg="emptyStateSvg"
/>
- <section v-if="loading || filterLoading" class="add-issues-list text-center">
- <div class="add-issues-list-loading"><gl-loading-icon /></div>
+ <section v-if="loading || filterLoading" class="add-issues-list d-flex h-100 text-center">
+ <div class="add-issues-list-loading w-100 align-self-center">
+ <gl-loading-icon size="md" />
+ </div>
</section>
<modal-footer />
</div>
diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue
index e9ed2de859d..28d2019af2f 100644
--- a/app/assets/javascripts/boards/components/modal/list.vue
+++ b/app/assets/javascripts/boards/components/modal/list.vue
@@ -117,7 +117,7 @@ export default {
};
</script>
<template>
- <section ref="list" class="add-issues-list add-issues-list-columns">
+ <section ref="list" class="add-issues-list add-issues-list-columns d-flex h-100">
<div
v-if="issuesCount > 0 && issues.length === 0"
class="empty-state add-issues-empty-state-filter text-center"
@@ -129,7 +129,7 @@ export default {
<div v-for="issue in group" v-if="showIssue(issue)" :key="issue.id" class="board-card-parent">
<div
:class="{ 'is-active': issue.selected }"
- class="board-card"
+ class="board-card position-relative p-3 rounded"
@click="toggleIssue($event, issue)"
>
<issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" />
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 009ae5dd331..4995a8d9367 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -123,7 +123,7 @@ export default () => {
this.loading = false;
})
.catch(() => {
- Flash('An error occurred while fetching the board lists. Please try again.');
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
});
},
methods: {
@@ -223,7 +223,7 @@ export default () => {
},
tooltipTitle() {
if (this.disabled) {
- return 'Please add a list to your board first';
+ return __('Please add a list to your board first');
}
return '';
diff --git a/app/assets/javascripts/boards/mixins/issue_card_inner.js b/app/assets/javascripts/boards/mixins/issue_card_inner.js
new file mode 100644
index 00000000000..8000237da6d
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/issue_card_inner.js
@@ -0,0 +1,5 @@
+export default {
+ methods: {
+ filterByWeight() {},
+ },
+};
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 6cf77705847..7e5d0e0f888 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -90,6 +90,7 @@ class List {
this.id = data.id;
this.type = data.list_type;
this.position = data.position;
+ this.label = data.label;
return this.getIssues();
});
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 6b1e37d3f24..70861fbf2b3 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -5,7 +5,9 @@ import $ from 'jquery';
import _ from 'underscore';
import Vue from 'vue';
import Cookies from 'js-cookie';
+import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
const boardsStore = {
disabled: false,
@@ -16,7 +18,13 @@ const boardsStore = {
filter: {
path: '',
},
- state: {},
+ state: {
+ currentBoard: {
+ labels: [],
+ },
+ currentPage: '',
+ reload: false,
+ },
detail: {
issue: {},
},
@@ -31,6 +39,10 @@ const boardsStore = {
issue: {},
};
},
+ showPage(page) {
+ this.state.reload = false;
+ this.state.currentPage = page;
+ },
addList(listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
this.state.lists.push(list);
@@ -67,7 +79,7 @@ const boardsStore = {
this.addList({
id: 'blank',
list_type: 'blank',
- title: 'Welcome to your Issue Board!',
+ title: __('Welcome to your Issue Board!'),
position: 0,
});
@@ -178,6 +190,8 @@ const boardsStore = {
},
};
+BoardsStoreEE.initEESpecific(boardsStore);
+
// hacks added in order to allow milestone_select to function properly
// TODO: remove these
diff --git a/app/assets/javascripts/boards/stores/boards_store_ee.js b/app/assets/javascripts/boards/stores/boards_store_ee.js
new file mode 100644
index 00000000000..09e3a938fbe
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store_ee.js
@@ -0,0 +1,5 @@
+// this is just to make ee_else_ce happy and will be cleaned up in https://gitlab.com/gitlab-org/gitlab-ce/issues/59807
+
+export default {
+ initEESpecific() {},
+};
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index da3100b9386..0390a3bf96a 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -26,6 +26,10 @@ export default class VariableList {
selector: '.js-ci-variable-input-id',
default: '',
},
+ variable_type: {
+ selector: '.js-ci-variable-input-variable-type',
+ default: 'env_var',
+ },
key: {
selector: '.js-ci-variable-input-key',
default: '',
diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
index e7111c666a2..fdbefd8c313 100644
--- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
@@ -19,6 +19,7 @@ export default function setupNativeFormVariableList({ container, formField = 'va
const isTouched = variableList.checkIfRowTouched($lastRow);
if (!isTouched) {
$lastRow.find('input, textarea').attr('name', '');
+ $lastRow.find('select').attr('name', '');
}
});
}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index df855261b3c..561b6bdd9f1 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,25 +1,20 @@
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
- UPGRADE_REQUEST_FAILURE,
- INGRESS,
- INGRESS_DOMAIN_SUFFIX,
-} from './constants';
+import { APPLICATION_STATUS, INGRESS, INGRESS_DOMAIN_SUFFIX } 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';
+Vue.use(GlToast);
+
/**
* Cluster page has 2 separate parts:
* Toggle button and applications section
@@ -134,20 +129,20 @@ export default class Clusters {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('upgradeApplication', data => this.upgradeApplication(data));
- eventHub.$on('upgradeFailed', appId => this.upgradeFailed(appId));
eventHub.$on('dismissUpgradeSuccess', appId => this.dismissUpgradeSuccess(appId));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
+ eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
}
removeListeners() {
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('upgradeApplication', this.upgradeApplication);
- eventHub.$off('upgradeFailed', this.upgradeFailed);
eventHub.$off('dismissUpgradeSuccess', this.dismissUpgradeSuccess);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
+ eventHub.$off('uninstallApplication');
}
initPolling() {
@@ -256,14 +251,14 @@ export default class Clusters {
}
}
- installApplication(data) {
- const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED);
+ installApplication({ id: appId, params }) {
this.store.updateAppProperty(appId, 'requestReason', null);
this.store.updateAppProperty(appId, 'statusReason', null);
- return this.service.installApplication(appId, data.params).catch(() => {
- this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE);
+ this.store.installApplication(appId);
+
+ return this.service.installApplication(appId, params).catch(() => {
+ this.store.notifyInstallFailure(appId);
this.store.updateAppProperty(
appId,
'requestReason',
@@ -272,29 +267,39 @@ export default class Clusters {
});
}
+ uninstallApplication({ id: appId }) {
+ this.store.updateAppProperty(appId, 'requestReason', null);
+ this.store.updateAppProperty(appId, 'statusReason', null);
+
+ this.store.uninstallApplication(appId);
+
+ return this.service.uninstallApplication(appId).catch(() => {
+ this.store.notifyUninstallFailure(appId);
+ this.store.updateAppProperty(
+ appId,
+ 'requestReason',
+ s__('ClusterIntegration|Request to begin uninstalling failed'),
+ );
+ });
+ }
+
upgradeApplication(data) {
const appId = data.id;
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUESTED);
- this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING);
- this.service.installApplication(appId, data.params).catch(() => this.upgradeFailed(appId));
- }
- upgradeFailed(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', UPGRADE_REQUEST_FAILURE);
+ this.store.updateApplication(appId);
+ this.service.installApplication(appId, data.params).catch(() => {
+ this.store.notifyUpdateFailure(appId);
+ });
}
dismissUpgradeSuccess(appId) {
- this.store.updateAppProperty(appId, 'requestStatus', null);
+ this.store.acknowledgeSuccessfulUpdate(appId);
}
- toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) {
- const { externalIp, status } = ingressNewState;
- const helpTextHidden = status !== APPLICATION_STATUS.INSTALLED || !externalIp;
- const domainSnippetText = `${externalIp}${INGRESS_DOMAIN_SUFFIX}`;
-
- if (ingressPreviousState.status !== status) {
- this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden);
- this.ingressDomainSnippet.textContent = domainSnippetText;
+ toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
+ if (externalIp !== newExternalIp) {
+ this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
+ this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`;
}
}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 19e5ac1567d..5f7675bb432 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -1,17 +1,15 @@
<script>
/* eslint-disable vue/require-default-prop */
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
-import {
- APPLICATION_STATUS,
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- UPGRADE_REQUESTED,
-} from '../constants';
+import UninstallApplicationButton from './uninstall_application_button.vue';
+import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
+
+import { APPLICATION_STATUS } from '../constants';
export default {
components: {
@@ -19,6 +17,11 @@ export default {
identicon,
TimeagoTooltip,
GlLink,
+ UninstallApplicationButton,
+ UninstallApplicationConfirmationModal,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
id: {
@@ -47,6 +50,11 @@ export default {
required: false,
default: false,
},
+ uninstallable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
status: {
type: String,
required: false,
@@ -55,13 +63,19 @@ export default {
type: String,
required: false,
},
- requestStatus: {
+ requestReason: {
type: String,
required: false,
},
- requestReason: {
- type: String,
+ installed: {
+ type: Boolean,
required: false,
+ default: false,
+ },
+ installFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
},
version: {
type: String,
@@ -75,6 +89,31 @@ export default {
type: Boolean,
required: false,
},
+ updateSuccessful: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ uninstallFailed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ uninstallSuccessful: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateAcknowledged: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
installApplicationRequestParams: {
type: Object,
required: false,
@@ -89,29 +128,12 @@ export default {
return Object.values(APPLICATION_STATUS).includes(this.status);
},
isInstalling() {
- return (
- this.status === APPLICATION_STATUS.SCHEDULED ||
- this.status === APPLICATION_STATUS.INSTALLING ||
- (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled)
- );
- },
- isInstalled() {
- return (
- this.status === APPLICATION_STATUS.INSTALLED ||
- this.status === APPLICATION_STATUS.UPDATED ||
- this.status === APPLICATION_STATUS.UPDATING ||
- this.status === APPLICATION_STATUS.UPDATE_ERRORED
- );
+ return this.status === APPLICATION_STATUS.INSTALLING;
},
canInstall() {
- if (this.isInstalling) {
- return false;
- }
-
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.ERROR ||
this.isUnknownStatus
);
},
@@ -125,8 +147,14 @@ export default {
rowJsClass() {
return `js-cluster-application-row-${this.id}`;
},
+ displayUninstallButton() {
+ return this.installed && this.uninstallable;
+ },
+ displayInstallButton() {
+ return !this.installed || !this.uninstallable;
+ },
installButtonLoading() {
- return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling;
+ return !this.status || this.isInstalling;
},
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
@@ -145,7 +173,7 @@ export default {
label = s__('ClusterIntegration|Install');
} else if (this.isInstalling) {
label = s__('ClusterIntegration|Installing');
- } else if (this.isInstalled) {
+ } else if (this.installed) {
label = s__('ClusterIntegration|Installed');
}
@@ -158,18 +186,23 @@ export default {
return s__('ClusterIntegration|Manage');
},
hasError() {
- return (
- !this.isInstalling &&
- (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE)
- );
+ return this.installFailed || this.uninstallFailed;
},
generalErrorDescription() {
- return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), {
- title: this.title,
- });
+ let errorDescription;
+
+ if (this.installFailed) {
+ errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
+ } else if (this.uninstallFailed) {
+ errorDescription = s__(
+ 'ClusterIntegration|Something went wrong while uninstalling %{title}',
+ );
+ }
+
+ return sprintf(errorDescription, { title: this.title });
},
versionLabel() {
- if (this.upgradeFailed) {
+ if (this.updateFailed) {
return s__('ClusterIntegration|Upgrade failed');
} else if (this.isUpgrading) {
return s__('ClusterIntegration|Upgrading');
@@ -177,19 +210,6 @@ export default {
return s__('ClusterIntegration|Upgraded');
},
- upgradeRequested() {
- return this.requestStatus === UPGRADE_REQUESTED;
- },
- upgradeSuccessful() {
- return this.status === APPLICATION_STATUS.UPDATED;
- },
- upgradeFailed() {
- if (this.isUpgrading) {
- return false;
- }
-
- return this.status === APPLICATION_STATUS.UPDATE_ERRORED;
- },
upgradeFailureDescription() {
return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
},
@@ -200,11 +220,11 @@ export default {
},
upgradeButtonLabel() {
let label;
- if (this.upgradeAvailable && !this.upgradeFailed && !this.isUpgrading) {
+ if (this.upgradeAvailable && !this.updateFailed && !this.isUpgrading) {
label = s__('ClusterIntegration|Upgrade');
} else if (this.isUpgrading) {
label = s__('ClusterIntegration|Updating');
- } else if (this.upgradeFailed) {
+ } else if (this.updateFailed) {
label = s__('ClusterIntegration|Retry update');
}
@@ -212,24 +232,29 @@ export default {
},
isUpgrading() {
// Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
- return (
- this.status === APPLICATION_STATUS.UPDATING ||
- (this.upgradeRequested && !this.upgradeSuccessful)
- );
+ return this.status === APPLICATION_STATUS.UPDATING;
},
shouldShowUpgradeDetails() {
// This method only returns true when;
// Upgrade was successful OR Upgrade failed
// AND new upgrade is unavailable AND version information is present.
- return (
- (this.upgradeSuccessful || this.upgradeFailed) && !this.upgradeAvailable && this.version
- );
+ return (this.updateSuccessful || this.updateFailed) && !this.upgradeAvailable && this.version;
+ },
+ uninstallSuccessDescription() {
+ return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
+ title: this.title,
+ });
},
},
watch: {
- status() {
- if (this.status === APPLICATION_STATUS.UPDATE_ERRORED) {
- eventHub.$emit('upgradeFailed', this.id);
+ updateSuccessful(updateSuccessful) {
+ if (updateSuccessful) {
+ this.$toast.show(this.upgradeSuccessDescription);
+ }
+ },
+ uninstallSuccessful(uninstallSuccessful) {
+ if (uninstallSuccessful) {
+ this.$toast.show(this.uninstallSuccessDescription);
}
},
},
@@ -246,8 +271,10 @@ export default {
params: this.installApplicationRequestParams,
});
},
- dismissUpgradeSuccess() {
- eventHub.$emit('dismissUpgradeSuccess', this.id);
+ uninstallConfirmed() {
+ eventHub.$emit('uninstallApplication', {
+ id: this.id,
+ });
},
},
};
@@ -257,7 +284,7 @@ export default {
<div
:class="[
rowJsClass,
- isInstalled && 'cluster-application-installed',
+ installed && 'cluster-application-installed',
disabled && 'cluster-application-disabled',
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
@@ -280,16 +307,12 @@ export default {
target="blank"
rel="noopener noreferrer"
class="js-cluster-application-title"
+ >{{ title }}</a
>
- {{ title }}
- </a>
- <span v-else class="js-cluster-application-title"> {{ title }} </span>
+ <span v-else class="js-cluster-application-title">{{ title }}</span>
</strong>
<slot name="description"></slot>
- <div
- v-if="hasError || isUnknownStatus"
- class="cluster-application-error text-danger prepend-top-10"
- >
+ <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10">
<p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }}
</p>
@@ -308,39 +331,25 @@ export default {
class="form-text text-muted label p-0 js-cluster-application-upgrade-details"
>
{{ versionLabel }}
-
- <span v-if="upgradeSuccessful"> to</span>
+ <span v-if="updateSuccessful">to</span>
<gl-link
- v-if="upgradeSuccessful"
+ v-if="updateSuccessful"
:href="chartRepo"
target="_blank"
class="js-cluster-application-upgrade-version"
+ >chart v{{ version }}</gl-link
>
- chart v{{ version }}
- </gl-link>
</div>
<div
- v-if="upgradeFailed && !isUpgrading"
+ v-if="updateFailed && !isUpgrading"
class="bs-callout bs-callout-danger cluster-application-banner mt-2 mb-0 js-cluster-application-upgrade-failure-message"
>
{{ upgradeFailureDescription }}
</div>
-
- <div
- v-if="upgradeRequested && upgradeSuccessful"
- class="bs-callout bs-callout-success cluster-application-banner mt-2 mb-0 p-0 pl-3"
- >
- {{ upgradeSuccessDescription }}
-
- <button class="close cluster-application-banner-close" @click="dismissUpgradeSuccess">
- &times;
- </button>
- </div>
-
<loading-button
- v-if="upgradeAvailable || upgradeFailed || isUpgrading"
+ v-if="upgradeAvailable || updateFailed || isUpgrading"
class="btn btn-primary js-cluster-application-upgrade-button mt-2"
:loading="isUpgrading"
:disabled="isUpgrading"
@@ -354,18 +363,30 @@ export default {
role="gridcell"
>
<div v-if="showManageButton" class="btn-group table-action-buttons">
- <a :href="manageLink" :class="{ disabled: disabled }" class="btn">
- {{ manageButtonLabel }}
- </a>
+ <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
+ manageButtonLabel
+ }}</a>
</div>
<div class="btn-group table-action-buttons">
<loading-button
+ v-if="displayInstallButton"
:loading="installButtonLoading"
:disabled="disabled || installButtonDisabled"
:label="installButtonLabel"
class="js-cluster-application-install-button"
@click="installClicked"
/>
+ <uninstall-application-button
+ v-if="displayUninstallButton"
+ v-gl-modal-directive="'uninstall-' + id"
+ :status="status"
+ class="js-cluster-application-uninstall-button"
+ />
+ <uninstall-application-confirmation-modal
+ :application="id"
+ :application-title="title"
+ @confirm="uninstallConfirmed()"
+ />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index d54f9ce552c..73760da9b98 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -224,9 +224,9 @@ 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>
+ <a :href="helpPath">{{ __('More information') }}</a>
</p>
<div class="cluster-application-list prepend-top-10">
@@ -238,15 +238,20 @@ export default {
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
+ :installed="applications.helm.installed"
+ :install-failed="applications.helm.installFailed"
+ :uninstallable="applications.helm.uninstallable"
+ :uninstall-successful="applications.helm.uninstallSuccessful"
+ :uninstall-failed="applications.helm.uninstallFailed"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
<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>
@@ -254,7 +259,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
@@ -265,6 +270,11 @@ export default {
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
+ :installed="applications.ingress.installed"
+ :install-failed="applications.ingress.installFailed"
+ :uninstallable="applications.ingress.uninstallable"
+ :uninstall-successful="applications.ingress.uninstallSuccessful"
+ :uninstall-failed="applications.ingress.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
@@ -272,16 +282,14 @@ 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>
<template v-if="ingressInstalled">
<div class="form-group">
- <label for="ingress-endpoint">
- {{ s__('ClusterIntegration|Ingress Endpoint') }}
- </label>
+ <label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
<div v-if="ingressExternalEndpoint" class="input-group">
<input
id="ingress-endpoint"
@@ -307,25 +315,24 @@ 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') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</div>
<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') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</template>
<template v-if="!ingressInstalled">
@@ -341,7 +348,12 @@ export default {
:status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason"
+ :installed="applications.cert_manager.installed"
+ :install-failed="applications.cert_manager.installFailed"
:install-application-request-params="{ email: applications.cert_manager.email }"
+ :uninstallable="applications.cert_manager.uninstallable"
+ :uninstall-successful="applications.cert_manager.uninstallSuccessful"
+ :uninstall-failed="applications.cert_manager.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
@@ -349,9 +361,9 @@ export default {
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
- <label for="cert-manager-issuer-email">
- {{ s__('ClusterIntegration|Issuer Email') }}
- </label>
+ <label for="cert-manager-issuer-email">{{
+ s__('ClusterIntegration|Issuer Email')
+ }}</label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
@@ -363,22 +375,20 @@ 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"
target="_blank"
rel="noopener noreferrer"
+ >{{ __('More information') }}</a
>
- {{ __('More information') }}
- </a>
</p>
</div>
</div>
</template>
</application-row>
<application-row
- v-if="isProjectCluster"
id="prometheus"
:logo-url="prometheusLogo"
:title="applications.prometheus.title"
@@ -387,6 +397,11 @@ export default {
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
+ :installed="applications.prometheus.installed"
+ :install-failed="applications.prometheus.installFailed"
+ :uninstallable="applications.prometheus.uninstallable"
+ :uninstall-successful="applications.prometheus.uninstallSuccessful"
+ :uninstall-failed="applications.prometheus.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
@@ -403,15 +418,22 @@ export default {
:version="applications.runner.version"
:chart-repo="applications.runner.chartRepo"
:upgrade-available="applications.runner.upgradeAvailable"
+ :installed="applications.runner.installed"
+ :install-failed="applications.runner.installFailed"
+ :update-successful="applications.runner.updateSuccessful"
+ :update-failed="applications.runner.updateFailed"
+ :uninstallable="applications.runner.uninstallable"
+ :uninstall-successful="applications.runner.uninstallSuccessful"
+ :uninstall-failed="applications.runner.uninstallFailed"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
<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>
@@ -424,6 +446,11 @@ export default {
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
+ :installed="applications.jupyter.installed"
+ :install-failed="applications.jupyter.installFailed"
+ :uninstallable="applications.jupyter.uninstallable"
+ :uninstall-successful="applications.jupyter.uninstallSuccessful"
+ :uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
@@ -432,18 +459,16 @@ 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>
<template v-if="ingressExternalEndpoint">
<div class="form-group">
- <label for="jupyter-hostname">
- {{ s__('ClusterIntegration|Jupyter Hostname') }}
- </label>
+ <label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
<div class="input-group">
<input
@@ -464,11 +489,11 @@ 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') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
</div>
</template>
@@ -483,8 +508,14 @@ export default {
:status-reason="applications.knative.statusReason"
:request-status="applications.knative.requestStatus"
:request-reason="applications.knative.requestReason"
+ :installed="applications.knative.installed"
+ :install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
+ :uninstallable="applications.knative.uninstallable"
+ :uninstall-successful="applications.knative.uninstallSuccessful"
+ :uninstall-failed="applications.knative.uninstallFailed"
:disabled="!helmInstalled"
+ v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
<div slot="description">
@@ -492,20 +523,20 @@ export default {
<p v-if="!rbac" class="rbac-notice bs-callout bs-callout-info append-bottom-0">
{{
s__(`ClusterIntegration|You must have an RBAC-enabled cluster
- to install Knative.`)
+ to install Knative.`)
}}
- <a :href="helpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="helpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
<br />
</span>
<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>
@@ -516,9 +547,7 @@ export default {
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
- <strong>
- {{ s__('ClusterIntegration|Knative Domain Name:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
@@ -531,9 +560,7 @@ export default {
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
- <strong>
- {{ s__('ClusterIntegration|Knative Endpoint:') }}
- </strong>
+ <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
@@ -565,9 +592,9 @@ export default {
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
- {{ __('More information') }}
- </a>
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
+ __('More information')
+ }}</a>
</p>
<p
@@ -576,8 +603,8 @@ export default {
>
{{
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.`)
}}
</p>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
new file mode 100644
index 00000000000..ef4bcbe14dd
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
@@ -0,0 +1,33 @@
+<script>
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+export default {
+ components: {
+ LoadingButton,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ disabled() {
+ return [UNINSTALLING, UPDATING].includes(this.status);
+ },
+ loading() {
+ return this.status === UNINSTALLING;
+ },
+ label() {
+ return this.loading ? this.__('Uninstalling') : this.__('Uninstall');
+ },
+ },
+};
+</script>
+
+<template>
+ <loading-button :label="label" :disabled="disabled" :loading="loading" />
+</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
new file mode 100644
index 00000000000..65827f1cb6a
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
+import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
+import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
+
+const CUSTOM_APP_WARNING_TEXT = {
+ [INGRESS]: s__(
+ 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
+ ),
+ [CERT_MANAGER]: s__(
+ 'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.',
+ ),
+ [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
+ [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
+ [KNATIVE]: s__('ClusterIntegration|The associated IP will be deleted and cannot be restored.'),
+ [JUPYTER]: '',
+};
+
+export default {
+ components: {
+ GlModal,
+ },
+ mixins: [trackUninstallButtonClickMixin],
+ props: {
+ application: {
+ type: String,
+ required: true,
+ },
+ applicationTitle: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
+ appTitle: this.applicationTitle,
+ });
+ },
+ warningText() {
+ return sprintf(
+ s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
+ {
+ appTitle: this.applicationTitle,
+ },
+ );
+ },
+ customAppWarningText() {
+ return CUSTOM_APP_WARNING_TEXT[this.application];
+ },
+ modalId() {
+ return `uninstall-${this.application}`;
+ },
+ },
+ methods: {
+ confirmUninstall() {
+ this.trackUninstallButtonClick(this.application);
+ this.$emit('confirm');
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ ok-variant="danger"
+ cancel-variant="light"
+ :ok-title="title"
+ :modal-id="modalId"
+ :title="title"
+ @ok="confirmUninstall()"
+ >{{ warningText }} {{ customAppWarningText }}</gl-modal
+ >
+</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 67f481f2afb..8fd752092c9 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -7,6 +7,7 @@ export const CLUSTER_TYPE = {
// These need to match what is returned from the server
export const APPLICATION_STATUS = {
+ NO_STATUS: null,
NOT_INSTALLABLE: 'not_installable',
INSTALLABLE: 'installable',
SCHEDULED: 'scheduled',
@@ -15,17 +16,35 @@ export const APPLICATION_STATUS = {
UPDATING: 'updating',
UPDATED: 'updated',
UPDATE_ERRORED: 'update_errored',
+ UNINSTALLING: 'uninstalling',
+ UNINSTALL_ERRORED: 'uninstall_errored',
ERROR: 'errored',
};
+/*
+ * The application cannot be in any of the following states without
+ * not being installed.
+ */
+export const APPLICATION_INSTALLED_STATUSES = [
+ APPLICATION_STATUS.INSTALLED,
+ APPLICATION_STATUS.UPDATING,
+ APPLICATION_STATUS.UNINSTALLING,
+];
+
// These are only used client-side
-export const REQUEST_SUBMITTED = 'request-submitted';
-export const REQUEST_FAILURE = 'request-failure';
-export const UPGRADE_REQUESTED = 'upgrade-requested';
-export const UPGRADE_REQUEST_FAILURE = 'upgrade-request-failure';
+
+export const UPDATE_EVENT = 'update';
+export const INSTALL_EVENT = 'install';
+export const UNINSTALL_EVENT = 'uninstall';
+
+export const HELM = 'helm';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
+export const PROMETHEUS = 'prometheus';
+
+export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
+
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
diff --git a/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js b/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js
new file mode 100644
index 00000000000..18f65b234d3
--- /dev/null
+++ b/app/assets/javascripts/clusters/mixins/track_uninstall_button_click.js
@@ -0,0 +1,5 @@
+export default {
+ methods: {
+ trackUninstallButtonClick: () => {},
+ },
+};
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
new file mode 100644
index 00000000000..14b80a116a7
--- /dev/null
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -0,0 +1,175 @@
+import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+ UNINSTALLING,
+ UNINSTALL_ERRORED,
+} = APPLICATION_STATUS;
+
+const applicationStateMachine = {
+ /* When the application initially loads, it will have `NO_STATUS`
+ * It will transition from `NO_STATUS` once the async backend call is completed
+ */
+ [NO_STATUS]: {
+ on: {
+ [SCHEDULED]: {
+ target: INSTALLING,
+ },
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ [INSTALLING]: {
+ target: INSTALLING,
+ },
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ [UPDATING]: {
+ target: UPDATING,
+ },
+ [UPDATED]: {
+ target: INSTALLED,
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ [UNINSTALLING]: {
+ target: UNINSTALLING,
+ },
+ [UNINSTALL_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ uninstallFailed: true,
+ },
+ },
+ },
+ },
+ [NOT_INSTALLABLE]: {
+ on: {
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ },
+ },
+ },
+ [INSTALLABLE]: {
+ on: {
+ [INSTALL_EVENT]: {
+ target: INSTALLING,
+ effects: {
+ installFailed: false,
+ },
+ },
+ // This is possible in artificial environments for E2E testing
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ },
+ },
+ [INSTALLING]: {
+ on: {
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ },
+ },
+ [INSTALLED]: {
+ on: {
+ [UPDATE_EVENT]: {
+ target: UPDATING,
+ effects: {
+ updateFailed: false,
+ updateSuccessful: false,
+ },
+ },
+ [UNINSTALL_EVENT]: {
+ target: UNINSTALLING,
+ effects: {
+ uninstallFailed: false,
+ uninstallSuccessful: false,
+ },
+ },
+ },
+ },
+ [UPDATING]: {
+ on: {
+ [UPDATED]: {
+ target: INSTALLED,
+ effects: {
+ updateSuccessful: true,
+ updateAcknowledged: false,
+ },
+ },
+ [UPDATE_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ updateFailed: true,
+ },
+ },
+ },
+ },
+ [UNINSTALLING]: {
+ on: {
+ [INSTALLABLE]: {
+ target: INSTALLABLE,
+ effects: {
+ uninstallSuccessful: true,
+ },
+ },
+ [UNINSTALL_ERRORED]: {
+ target: INSTALLED,
+ effects: {
+ uninstallFailed: true,
+ },
+ },
+ },
+ },
+};
+
+/**
+ * Determines an application new state based on the application current state
+ * and an event. If the application current state cannot handle a given event,
+ * the current state is returned.
+ *
+ * @param {*} application
+ * @param {*} event
+ */
+const transitionApplicationState = (application, event) => {
+ const newState = applicationStateMachine[application.status].on[event];
+
+ return newState
+ ? {
+ ...application,
+ status: newState.target,
+ ...newState.effects,
+ }
+ : application;
+};
+
+export default transitionApplicationState;
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index dea33ac44c5..01f3732de7e 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -29,6 +29,10 @@ export default class ClusterService {
return axios.patch(this.appUpdateEndpointMap[appId], params);
}
+ uninstallApplication(appId, params) {
+ return axios.delete(this.appInstallEndpointMap[appId], params);
+ }
+
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 92993337f02..1b4d7e8372c 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,6 +1,31 @@
import { s__ } from '../../locale';
import { parseBoolean } from '../../lib/utils/common_utils';
-import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER, RUNNER } from '../constants';
+import {
+ INGRESS,
+ JUPYTER,
+ KNATIVE,
+ CERT_MANAGER,
+ RUNNER,
+ APPLICATION_INSTALLED_STATUSES,
+ APPLICATION_STATUS,
+ INSTALL_EVENT,
+ UPDATE_EVENT,
+ UNINSTALL_EVENT,
+} from '../constants';
+import transitionApplicationState from '../services/application_state_machine';
+
+const isApplicationInstalled = appStatus => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
+
+const applicationInitialState = {
+ status: null,
+ statusReason: null,
+ requestReason: null,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallFailed: false,
+ uninstallSuccessful: false,
+};
export default class ClusterStore {
constructor() {
@@ -12,60 +37,42 @@ export default class ClusterStore {
statusReason: null,
applications: {
helm: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|Helm Tiller'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
},
ingress: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|Ingress'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
externalIp: null,
externalHostname: null,
},
cert_manager: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|Cert-Manager'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
email: null,
},
runner: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|GitLab Runner'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
version: null,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
upgradeAvailable: null,
+ updateAcknowledged: true,
+ updateSuccessful: false,
+ updateFailed: false,
},
prometheus: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|Prometheus'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
},
jupyter: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|JupyterHub'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
hostname: null,
},
knative: {
+ ...applicationInitialState,
title: s__('ClusterIntegration|Knative'),
- status: null,
- statusReason: null,
- requestStatus: null,
- requestReason: null,
hostname: null,
isEditingHostName: false,
externalIp: null,
@@ -97,6 +104,40 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
+ installApplication(appId) {
+ this.handleApplicationEvent(appId, INSTALL_EVENT);
+ }
+
+ notifyInstallFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
+ }
+
+ updateApplication(appId) {
+ this.handleApplicationEvent(appId, UPDATE_EVENT);
+ }
+
+ notifyUpdateFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
+ }
+
+ uninstallApplication(appId) {
+ this.handleApplicationEvent(appId, UNINSTALL_EVENT);
+ }
+
+ notifyUninstallFailure(appId) {
+ this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
+ }
+
+ handleApplicationEvent(appId, event) {
+ const currentAppState = this.state.applications[appId];
+
+ this.state.applications[appId] = transitionApplicationState(currentAppState, event);
+ }
+
+ acknowledgeSuccessfulUpdate(appId) {
+ this.state.applications[appId].updateAcknowledged = true;
+ }
+
updateAppProperty(appId, prop, value) {
this.state.applications[appId][prop] = value;
}
@@ -112,12 +153,17 @@ export default class ClusterStore {
status_reason: statusReason,
version,
update_available: upgradeAvailable,
+ can_uninstall: uninstallable,
} = serverAppEntry;
+ const currentApplicationState = this.state.applications[appId] || {};
+ const nextApplicationState = transitionApplicationState(currentApplicationState, status);
this.state.applications[appId] = {
- ...(this.state.applications[appId] || {}),
- status,
+ ...currentApplicationState,
+ ...nextApplicationState,
statusReason,
+ installed: isApplicationInstalled(nextApplicationState.status),
+ uninstallable,
};
if (appId === INGRESS) {
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 4de425b48e7..3f0a9f2602c 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -12,6 +12,7 @@ import stageStagingComponent from './components/stage_staging_component.vue';
import stageTestComponent from './components/stage_test_component.vue';
import CycleAnalyticsService from './cycle_analytics_service';
import CycleAnalyticsStore from './cycle_analytics_store';
+import { __ } from '~/locale';
Vue.use(Translate);
@@ -61,7 +62,7 @@ export default () => {
methods: {
handleError() {
this.store.setErrorState(true);
- return new Flash('There was an error while fetching cycle analytics data.');
+ return new Flash(__('There was an error while fetching cycle analytics data.'));
},
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index 4ae4ceabc21..f66e07ba31a 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
const CommentAndResolveBtn = Vue.extend({
props: {
@@ -31,15 +32,15 @@ const CommentAndResolveBtn = Vue.extend({
buttonText: function() {
if (this.isDiscussionResolved) {
if (this.textareaIsEmpty) {
- return 'Unresolve discussion';
+ return __('Unresolve discussion');
} else {
- return 'Comment & unresolve discussion';
+ return __('Comment & unresolve discussion');
}
} else {
if (this.textareaIsEmpty) {
- return 'Resolve discussion';
+ return __('Resolve discussion');
} else {
- return 'Comment & resolve discussion';
+ return __('Comment & resolve discussion');
}
}
},
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 5bdeaaade68..b5a781cbc92 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -5,6 +5,7 @@ import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
import Notes from '../../notes';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
+import { n__ } from '~/locale';
const DiffNoteAvatars = Vue.extend({
components: {
@@ -44,7 +45,7 @@ const DiffNoteAvatars = Vue.extend({
if (this.discussion) {
const extra = this.discussion.notesCount() - this.shownAvatars;
- return `${extra} more comment${extra > 1 ? 's' : ''}`;
+ return n__('%d more comment', '%d more comments', extra);
}
return '';
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 8542a6e718a..fe4088cadda 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __ } from '~/locale';
import DiscussionMixins from '../mixins/discussion';
@@ -23,9 +24,9 @@ const JumpToDiscussion = Vue.extend({
computed: {
buttonText: function() {
if (this.discussionId) {
- return 'Jump to next unresolved discussion';
+ return __('Jump to next unresolved discussion');
} else {
- return 'Jump to first unresolved discussion';
+ return __('Jump to first unresolved discussion');
}
},
allResolved: function() {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index a69b34b0db8..87e7dd18e0c 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -4,6 +4,7 @@
import $ from 'jquery';
import Vue from 'vue';
import Flash from '../../flash';
+import { sprintf, __ } from '~/locale';
const ResolveBtn = Vue.extend({
props: {
@@ -55,12 +56,14 @@ const ResolveBtn = Vue.extend({
},
buttonText() {
if (this.isResolved) {
- return `Resolved by ${this.resolvedByName}`;
+ return sprintf(__('Resolved by %{resolvedByName}'), {
+ resolvedByName: this.resolvedByName,
+ });
} else if (this.canResolve) {
- return 'Mark as resolved';
+ return __('Mark as resolved');
}
- return 'Unable to resolve';
+ return __('Unable to resolve');
},
isResolved() {
if (this.note) {
@@ -132,7 +135,8 @@ const ResolveBtn = Vue.extend({
this.updateTooltip();
})
.catch(
- () => new Flash('An error occurred when trying to resolve a comment. Please try again.'),
+ () =>
+ new Flash(__('An error occurred when trying to resolve a comment. Please try again.')),
);
},
},
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
index 6fcad187b35..4b204fdfeb0 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js
@@ -3,6 +3,7 @@
/* global ResolveService */
import Vue from 'vue';
+import { __ } from '~/locale';
const ResolveDiscussionBtn = Vue.extend({
props: {
@@ -41,9 +42,9 @@ const ResolveDiscussionBtn = Vue.extend({
},
buttonText: function() {
if (this.isDiscussionResolved) {
- return 'Unresolve discussion';
+ return __('Unresolve discussion');
} else {
- return 'Resolve discussion';
+ return __('Resolve discussion');
}
},
loading: function() {
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index e69eaad4423..0687028ca54 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -3,6 +3,7 @@
import Vue from 'vue';
import Flash from '../../flash';
import '../../vue_shared/vue_resource_interceptor';
+import { __ } from '~/locale';
window.gl = window.gl || {};
@@ -49,7 +50,8 @@ class ResolveServiceClass {
discussion.updateHeadline(data);
})
.catch(
- () => new Flash('An error occurred when trying to resolve a discussion. Please try again.'),
+ () =>
+ new Flash(__('An error occurred when trying to resolve a discussion. Please try again.')),
);
}
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index c02a8740a42..a767379d662 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -113,9 +113,10 @@ export default {
<commit-pipeline-status
v-if="commit.pipeline_status_path"
:endpoint="commit.pipeline_status_path"
+ class="d-inline-flex"
/>
<div class="commit-sha-group">
- <div class="label label-monospace" v-text="commit.short_id"></div>
+ <div class="label label-monospace monospace" v-text="commit.short_id"></div>
<clipboard-button
:text="commit.id"
:title="__('Copy commit SHA to clipboard')"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 8d09c2a7399..2b3d6d1a3fa 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form';
import draftCommentsMixin from 'ee_else_ce/diffs/mixins/draft_comments';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
@@ -16,6 +17,7 @@ import { diffViewerModes } from '~/ide/constants';
export default {
components: {
+ GlLoadingIcon,
InlineDiffView,
ParallelDiffView,
DiffViewer,
@@ -108,6 +110,7 @@ export default {
:diff-lines="diffFile.parallel_diff_lines || []"
:help-page-path="helpPagePath"
/>
+ <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 32e5fa5bf8b..392de1c9f23 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -112,9 +112,7 @@ export default {
const truncatedContentSha = _.escape(truncateSha(this.diffFile.content_sha));
return sprintf(
s__('MergeRequests|View file @ %{commitId}'),
- {
- commitId: `<span class="commit-sha">${truncatedContentSha}</span>`,
- },
+ { commitId: truncatedContentSha },
false,
);
},
@@ -303,7 +301,7 @@ export default {
class="view-file js-view-file-button"
:title="viewFileButtonText"
>
- <icon name="external-link" />
+ <icon name="doc-text" />
</gl-button>
<a
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index 6709df48637..1281f9b17ef 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -84,8 +84,6 @@ export default {
},
shouldShowCommentButton() {
return (
- this.isLoggedIn &&
- this.showCommentButton &&
this.isHover &&
!this.isMatchLine &&
!this.isContextLine &&
@@ -102,6 +100,9 @@ export default {
}
return this.showCommentButton && this.hasDiscussions;
},
+ shouldRenderCommentButton() {
+ return this.isLoggedIn && this.showCommentButton;
+ },
},
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
@@ -167,6 +168,7 @@ export default {
>
<template v-else>
<button
+ v-if="shouldRenderCommentButton"
v-show="shouldShowCommentButton"
type="button"
class="add-diff-note js-add-diff-note-button qa-diff-comment"
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index d174b13e133..0f3e9208d21 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -89,17 +89,19 @@ export default {
classNameMap() {
const { type } = this.line;
- return {
- hll: this.isHighlighted,
- [type]: type,
- [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
- [LINE_HOVER_CLASS_NAME]:
- this.isLoggedIn &&
- this.isHover &&
- !this.isMatchLine &&
- !this.isContextLine &&
- !this.isMetaLine,
- };
+ return [
+ type,
+ {
+ hll: this.isHighlighted,
+ [LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
+ [LINE_HOVER_CLASS_NAME]:
+ this.isLoggedIn &&
+ this.isHover &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.isMetaLine,
+ },
+ ];
},
lineNumber() {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index c764cbeb8e0..2d5262baeec 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,12 +1,11 @@
<script>
-import { mapGetters, mapActions, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
OLD_LINE_TYPE,
CONTEXT_LINE_TYPE,
CONTEXT_LINE_CLASS_NAME,
- PARALLEL_DIFF_VIEW_TYPE,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
} from '../constants';
@@ -45,16 +44,16 @@ export default {
return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow;
},
}),
- ...mapGetters('diffs', ['isInlineView']),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
},
classNameMap() {
- return {
- [this.line.type]: this.line.type,
- [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
- [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
- };
+ return [
+ this.line.type,
+ {
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
+ },
+ ];
},
inlineRowId() {
return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 4feb73cfef2..d84e1af11f3 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -50,3 +50,10 @@ export const LEFT_LINE_KEY = 'left';
export const CENTERED_LIMITED_CONTAINER_CLASSES =
'container-limited limit-container-width mx-lg-auto px-3';
+
+export const MAX_RENDERING_DIFF_LINES = 500;
+export const MAX_RENDERING_BULK_ROWS = 30;
+export const MIN_RENDERING_MS = 2;
+export const START_RENDERING_INDEX = 200;
+export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
+export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index b58ae0d248c..386d08aed2b 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -7,7 +7,12 @@ import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/uti
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import TreeWorker from '../workers/tree_worker';
import eventHub from '../../notes/event_hub';
-import { getDiffPositionByLineCode, getNoteFormData } from './utils';
+import {
+ getDiffPositionByLineCode,
+ getNoteFormData,
+ convertExpandLines,
+ idleCallback,
+} from './utils';
import * as types from './mutation_types';
import {
PARALLEL_DIFF_VIEW_TYPE,
@@ -17,6 +22,16 @@ import {
TREE_LIST_STORAGE_KEY,
WHITESPACE_STORAGE_KEY,
TREE_LIST_WIDTH_STORAGE_KEY,
+ OLD_LINE_KEY,
+ NEW_LINE_KEY,
+ TYPE_KEY,
+ LEFT_LINE_KEY,
+ MAX_RENDERING_DIFF_LINES,
+ MAX_RENDERING_BULK_ROWS,
+ MIN_RENDERING_MS,
+ START_RENDERING_INDEX,
+ INLINE_DIFF_LINES_KEY,
+ PARALLEL_DIFF_LINES_KEY,
} from '../constants';
import { diffViewerModes } from '~/ide/constants';
@@ -313,13 +328,98 @@ export const cacheTreeListWidth = (_, size) => {
};
export const requestFullDiff = ({ commit }, filePath) => commit(types.REQUEST_FULL_DIFF, filePath);
-export const receiveFullDiffSucess = ({ commit }, { filePath, data }) =>
- commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath, data });
+export const receiveFullDiffSucess = ({ commit }, { filePath }) =>
+ commit(types.RECEIVE_FULL_DIFF_SUCCESS, { filePath });
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
};
+export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
+ const expandedDiffLines = {
+ highlighted_diff_lines: convertExpandLines({
+ diffLines: file.highlighted_diff_lines,
+ typeKey: TYPE_KEY,
+ oldLineKey: OLD_LINE_KEY,
+ newLineKey: NEW_LINE_KEY,
+ data,
+ mapLine: ({ line, oldLine, newLine }) =>
+ Object.assign(line, {
+ old_line: oldLine,
+ new_line: newLine,
+ line_code: `${file.file_hash}_${oldLine}_${newLine}`,
+ }),
+ }),
+ parallel_diff_lines: convertExpandLines({
+ diffLines: file.parallel_diff_lines,
+ typeKey: [LEFT_LINE_KEY, TYPE_KEY],
+ oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
+ newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
+ data,
+ mapLine: ({ line, oldLine, newLine }) => ({
+ left: {
+ ...line,
+ old_line: oldLine,
+ line_code: `${file.file_hash}_${oldLine}_${newLine}`,
+ },
+ right: {
+ ...line,
+ new_line: newLine,
+ line_code: `${file.file_hash}_${newLine}_${oldLine}`,
+ },
+ }),
+ }),
+ };
+ const currentDiffLinesKey =
+ state.diffViewType === INLINE_DIFF_VIEW_TYPE ? INLINE_DIFF_LINES_KEY : PARALLEL_DIFF_LINES_KEY;
+ const hiddenDiffLinesKey =
+ state.diffViewType === INLINE_DIFF_VIEW_TYPE ? PARALLEL_DIFF_LINES_KEY : INLINE_DIFF_LINES_KEY;
+
+ commit(types.SET_HIDDEN_VIEW_DIFF_FILE_LINES, {
+ filePath: file.file_path,
+ lines: expandedDiffLines[hiddenDiffLinesKey],
+ });
+
+ if (expandedDiffLines[currentDiffLinesKey].length > MAX_RENDERING_DIFF_LINES) {
+ let index = START_RENDERING_INDEX;
+ commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
+ filePath: file.file_path,
+ lines: expandedDiffLines[currentDiffLinesKey].slice(0, index),
+ });
+ commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
+
+ const idleCb = t => {
+ const startIndex = index;
+
+ while (
+ t.timeRemaining() >= MIN_RENDERING_MS &&
+ index !== expandedDiffLines[currentDiffLinesKey].length &&
+ index - startIndex !== MAX_RENDERING_BULK_ROWS
+ ) {
+ const line = expandedDiffLines[currentDiffLinesKey][index];
+
+ if (line) {
+ commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line });
+ index += 1;
+ }
+ }
+
+ if (index !== expandedDiffLines[currentDiffLinesKey].length) {
+ idleCallback(idleCb);
+ } else {
+ commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
+ }
+ };
+
+ idleCallback(idleCb);
+ } else {
+ commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
+ filePath: file.file_path,
+ lines: expandedDiffLines[currentDiffLinesKey],
+ });
+ }
+};
+
export const fetchFullDiff = ({ dispatch }, file) =>
axios
.get(file.context_lines_path, {
@@ -328,8 +428,10 @@ export const fetchFullDiff = ({ dispatch }, file) =>
from_merge_request: true,
},
})
- .then(({ data }) => dispatch('receiveFullDiffSucess', { filePath: file.file_path, data }))
- .then(() => scrollToElement(`#${file.file_hash}`))
+ .then(({ data }) => {
+ dispatch('receiveFullDiffSucess', { filePath: file.file_path });
+ dispatch('setExpandedDiffLines', { file, data });
+ })
.catch(() => dispatch('receiveFullDiffError', file.file_path));
export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
@@ -340,7 +442,6 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
if (file.isShowingFullFile) {
dispatch('loadCollapsedDiff', file)
.then(() => dispatch('assignDiscussionsToDiff', getters.getDiffFileDiscussions(file)))
- .then(() => scrollToElement(`#${file.file_hash}`))
.catch(() => dispatch('receiveFullDiffError', filePath));
} else {
dispatch('fetchFullDiff', file);
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index adf56eba3f8..6bb24c97139 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -28,3 +28,8 @@ export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
+
+export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES';
+export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
+export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
+export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 572fbfb5be4..67bc1724738 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -6,10 +6,8 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
- convertExpandLines,
} from './utils';
import * as types from './mutation_types';
-import { OLD_LINE_KEY, NEW_LINE_KEY, TYPE_KEY, LEFT_LINE_KEY } from '../constants';
export default {
[types.SET_BASE_CONFIG](state, options) {
@@ -265,45 +263,11 @@ export default {
file.isLoadingFullFile = false;
},
- [types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath, data }) {
+ [types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath }) {
const file = findDiffFile(state.diffFiles, filePath, 'file_path');
file.isShowingFullFile = true;
file.isLoadingFullFile = false;
-
- file.highlighted_diff_lines = convertExpandLines({
- diffLines: file.highlighted_diff_lines,
- typeKey: [TYPE_KEY],
- oldLineKey: [OLD_LINE_KEY],
- newLineKey: [NEW_LINE_KEY],
- data,
- mapLine: ({ line, oldLine, newLine }) => ({
- ...line,
- old_line: oldLine,
- new_line: newLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- }),
- });
-
- file.parallel_diff_lines = convertExpandLines({
- diffLines: file.parallel_diff_lines,
- typeKey: [LEFT_LINE_KEY, TYPE_KEY],
- oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
- newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
- data,
- mapLine: ({ line, oldLine, newLine }) => ({
- left: {
- ...line,
- old_line: oldLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- },
- right: {
- ...line,
- new_line: newLine,
- line_code: `${file.file_hash}_${newLine}_${oldLine}`,
- },
- }),
- });
},
[types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
@@ -312,4 +276,30 @@ export default {
file.viewer.collapsed = collapsed;
}
},
+ [types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
+ const file = state.diffFiles.find(f => f.file_path === filePath);
+ const hiddenDiffLinesKey =
+ state.diffViewType === 'inline' ? 'parallel_diff_lines' : 'highlighted_diff_lines';
+
+ file[hiddenDiffLinesKey] = lines;
+ },
+ [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
+ const file = state.diffFiles.find(f => f.file_path === filePath);
+ const currentDiffLinesKey =
+ state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
+
+ file[currentDiffLinesKey] = lines;
+ },
+ [types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) {
+ const file = state.diffFiles.find(f => f.file_path === filePath);
+ const currentDiffLinesKey =
+ state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
+
+ file[currentDiffLinesKey].push(line);
+ },
+ [types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) {
+ const file = state.diffFiles.find(f => f.file_path === filePath);
+
+ file.renderingLines = !file.renderingLines;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 27a79369a24..71956255eef 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -253,6 +253,7 @@ export function prepareDiffData(diffData) {
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
+ renderingLines: false,
});
}
}
@@ -423,27 +424,33 @@ export const convertExpandLines = ({
mapLine,
}) => {
const dataLength = data.length;
+ const lines = [];
+
+ for (let i = 0, diffLinesLength = diffLines.length; i < diffLinesLength; i += 1) {
+ const line = diffLines[i];
- return diffLines.reduce((acc, line, i) => {
if (_.property(typeKey)(line) === 'match') {
const beforeLine = diffLines[i - 1];
const afterLine = diffLines[i + 1];
- const beforeLineIndex = _.property(newLineKey)(beforeLine) || 0;
- const afterLineIndex = _.property(newLineKey)(afterLine) - 1 || dataLength;
-
- acc.push(
- ...data.slice(beforeLineIndex, afterLineIndex).map((l, index) => ({
- ...mapLine({
- line: { ...l, hasForm: false, discussions: [] },
+ const newLineProperty = _.property(newLineKey);
+ const beforeLineIndex = newLineProperty(beforeLine) || 0;
+ const afterLineIndex = newLineProperty(afterLine) - 1 || dataLength;
+
+ lines.push(
+ ...data.slice(beforeLineIndex, afterLineIndex).map((l, index) =>
+ mapLine({
+ line: Object.assign(l, { hasForm: false, discussions: [] }),
oldLine: (_.property(oldLineKey)(beforeLine) || 0) + index + 1,
- newLine: (_.property(newLineKey)(beforeLine) || 0) + index + 1,
+ newLine: (newLineProperty(beforeLine) || 0) + index + 1,
}),
- })),
+ ),
);
} else {
- acc.push(line);
+ lines.push(line);
}
+ }
- return acc;
- }, []);
+ return lines;
};
+
+export const idleCallback = cb => requestIdleCallback(cb);
diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js
index 534d737c77e..415c463fd19 100644
--- a/app/assets/javascripts/diffs/workers/tree_worker.js
+++ b/app/assets/javascripts/diffs/workers/tree_worker.js
@@ -4,6 +4,11 @@ import { generateTreeList } from '../store/utils';
// eslint-disable-next-line no-restricted-globals
self.addEventListener('message', e => {
const { data } = e;
+
+ if (data === undefined) {
+ return;
+ }
+
const { treeEntries, tree } = generateTreeList(data);
// eslint-disable-next-line no-restricted-globals
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index cb1b1173190..3c650397a19 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -104,7 +104,7 @@ class DueDateSelect {
const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
} else {
- this.displayedDate = 'No due date';
+ this.displayedDate = __('None');
}
}
@@ -132,7 +132,7 @@ class DueDateSelect {
submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const hasDueDate = this.displayedDate !== 'No due date';
+ const hasDueDate = this.displayedDate !== __('None');
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn();
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 0b24d9fc920..e020628a473 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default IssuableTokenKeys => {
const wipToken = {
key: 'wip',
@@ -5,7 +7,7 @@ export default IssuableTokenKeys => {
param: '',
symbol: '',
icon: 'admin',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: true,
capitalizeTokenValue: true,
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index d9a4d06b549..dad188f6f98 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownEmoji extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -14,7 +15,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 0264f934914..a2312de289d 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -3,6 +3,7 @@ import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class DropdownNonUser extends FilteredSearchDropdown {
constructor(options = {}) {
@@ -17,7 +18,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
preprocessing,
onError() {
/* eslint-disable no-new */
- new Flash('An error occurred fetching the dropdown data.');
+ new Flash(__('An error occurred fetching the dropdown data.'));
/* eslint-enable no-new */
},
},
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index efd03ec952f..78fbb3696cc 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -14,6 +14,7 @@ import FilteredSearchTokenizer from './filtered_search_tokenizer';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
import DropdownUtils from './dropdown_utils';
+import { __ } from '~/locale';
export default class FilteredSearchManager {
constructor({
@@ -64,7 +65,7 @@ export default class FilteredSearchManager {
.catch(error => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new
- new Flash('An error occurred while parsing recent searches');
+ new Flash(__('An error occurred while parsing recent searches'));
// Gracefully fail to empty array
return [];
})
@@ -340,7 +341,7 @@ export default class FilteredSearchManager {
handleInputPlaceholder() {
const query = DropdownUtils.getSearchQuery();
- const placeholder = 'Search or filter results...';
+ const placeholder = __('Search or filter results...');
const currentPlaceholder = this.filteredSearchInput.placeholder;
if (query.length === 0 && currentPlaceholder !== placeholder) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 11ed85504ec..0a9579bf491 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class FilteredSearchTokenKeys {
constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) {
this.tokenKeys = tokenKeys;
@@ -79,7 +81,7 @@ export default class FilteredSearchTokenKeys {
param: '',
symbol: '',
icon: 'eye-slash',
- tag: 'Yes or No',
+ tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
uppercaseTokenName: false,
capitalizeTokenValue: true,
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 fd61030eb13..6c3d9e33420 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,4 +1,5 @@
import FilteredSearchTokenKeys from './filtered_search_token_keys';
+import { __ } from '~/locale';
export const tokenKeys = [
{
@@ -60,52 +61,52 @@ export const conditions = [
{
url: 'assignee_id=None',
tokenKey: 'assignee',
- value: 'None',
+ value: __('None'),
},
{
url: 'assignee_id=Any',
tokenKey: 'assignee',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=None',
tokenKey: 'milestone',
- value: 'None',
+ value: __('None'),
},
{
url: 'milestone_title=Any',
tokenKey: 'milestone',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
- value: 'Upcoming',
+ value: __('Upcoming'),
},
{
url: 'milestone_title=%23started',
tokenKey: 'milestone',
- value: 'Started',
+ value: __('Started'),
},
{
url: 'label_name[]=None',
tokenKey: 'label',
- value: 'None',
+ value: __('None'),
},
{
url: 'label_name[]=Any',
tokenKey: 'label',
- value: 'Any',
+ value: __('Any'),
},
{
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
- value: 'None',
+ value: __('None'),
},
{
url: 'my_reaction_emoji=Any',
tokenKey: 'my-reaction',
- value: 'Any',
+ value: __('Any'),
},
];
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
index 5917b223d63..011b37e218d 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service_error.js
@@ -1,7 +1,9 @@
+import { __ } from '~/locale';
+
class RecentSearchesServiceError {
constructor(message) {
this.name = 'RecentSearchesServiceError';
- this.message = message || 'Recent Searches Service is unavailable';
+ this.message = message || __('Recent Searches Service is unavailable');
}
}
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index a9d5ba8faa8..38327472cb3 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -5,6 +5,7 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import Flash from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
+import { __ } from '~/locale';
export default class VisualTokenValue {
constructor(tokenValue, tokenType) {
@@ -77,7 +78,7 @@ export default class VisualTokenValue {
matchingLabel.text_color,
);
})
- .catch(() => new Flash('An error occurred while fetching label colors.'));
+ .catch(() => new Flash(__('An error occurred while fetching label colors.')));
}
static setTokenStyle(tokenValueContainer, backgroundColor, textColor) {
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index c81e754df4c..f437954881c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -461,7 +461,10 @@ class GfmAutoComplete {
// We can ignore this for quick actions because they are processed
// before Markdown.
if (!this.setting.skipMarkdownCharacterTest) {
- withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
+ withoutAt = withoutAt
+ .replace(/(~~|`|\*)/g, '\\$1')
+ .replace(/(\b)(_+)/g, '$1\\$2') // only escape underscores at the start
+ .replace(/(_+)(\b)/g, '\\$1$2'); // or end of words
}
return `${at}${withoutAt}`;
@@ -474,6 +477,16 @@ class GfmAutoComplete {
}
return null;
},
+ highlighter(li, query) {
+ // override default behaviour to escape dot character
+ // see https://github.com/ichord/At.js/pull/576
+ if (!query) {
+ return li;
+ }
+ const escapedQuery = query.replace(/[.+]/, '\\$&');
+ const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
+ return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
+ },
};
}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index f5e2e46237f..5a6d44ef838 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import autosize from 'autosize';
-import GfmAutoComplete, * as GFMConfig from './gfm_auto_complete';
+import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
@@ -8,7 +8,7 @@ export default class GLForm {
constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = Object.assign({}, GFMConfig.defaultAutocompleteConfig, enableGFM);
+ this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM);
// Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index 26510fcdb2a..ce0c9256148 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from '~/locale';
export default class TransferDropdown {
constructor() {
@@ -13,7 +14,7 @@ export default class TransferDropdown {
}
buildDropdown() {
- const extraOptions = [{ id: '', text: 'No parent group' }, 'divider'];
+ const extraOptions = [{ id: '', text: __('No parent group') }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index 00b2d236da3..6b0aa5b2b2b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -108,6 +108,7 @@ export default {
:placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
+ dir="auto"
name="commit-message"
@scroll="handleScroll"
@input="onInput"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index ec759043efc..188518dd419 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -57,6 +57,8 @@ export default {
type: 'blob',
content: result,
base64: !isText,
+ binary: !isText,
+ rawPath: !isText ? target.result : '',
});
},
readFile(file) {
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 94a9e87369c..e15b2a6f76b 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,5 +1,6 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import flash from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
@@ -35,7 +36,7 @@ export default {
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
- return this.file && this.file.binary && !this.file.content;
+ return this.file && this.file.binary;
},
showContentViewer() {
return (
@@ -56,6 +57,10 @@ export default {
active: this.file.viewMode === 'preview',
};
},
+ fileType() {
+ const info = viewerInformationForPath(this.file.path);
+ return (info && info.id) || '';
+ },
},
watch: {
file(newVal, oldVal) {
@@ -258,6 +263,7 @@ export default {
:path="file.rawPath || file.path"
:file-size="file.size"
:project-path="file.projectId"
+ :type="fileType"
/>
<diff-viewer
v-if="showDiffViewer"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 7c560c89695..e30670e119f 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -72,4 +72,11 @@ export const modalTypes = {
tree: 'tree',
};
+export const commitActionTypes = {
+ move: 'move',
+ delete: 'delete',
+ create: 'create',
+ update: 'update',
+};
+
export const packageJsonPath = 'package.json';
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 229ef168926..8c84b98a108 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import { join as joinPath } from 'path';
+import { joinPaths } from '~/lib/utils/url_utility';
import flash from '~/flash';
import store from './stores';
+import { __ } from '~/locale';
Vue.use(VueRouter);
@@ -34,7 +35,7 @@ const EmptyRouterComponent = {
const router = new VueRouter({
mode: 'history',
- base: `${gon.relative_url_root}/-/ide/`,
+ base: joinPaths(gon.relative_url_root || '', '/-/ide/'),
routes: [
{
path: '/project/:namespace+/:project',
@@ -46,11 +47,11 @@ const router = new VueRouter({
},
{
path: ':targetmode(edit|tree|blob)/:branchid+/',
- redirect: to => joinPath(to.path, '/-/'),
+ redirect: to => joinPaths(to.path, '/-/'),
},
{
path: ':targetmode(edit|tree|blob)',
- redirect: to => joinPath(to.path, '/master/-/'),
+ redirect: to => joinPaths(to.path, '/master/-/'),
},
{
path: 'merge_requests/:mrid',
@@ -58,7 +59,7 @@ const router = new VueRouter({
},
{
path: '',
- redirect: to => joinPath(to.path, '/edit/master/-/'),
+ redirect: to => joinPaths(to.path, '/edit/master/-/'),
},
],
},
@@ -94,7 +95,7 @@ router.beforeEach((to, from, next) => {
})
.catch(e => {
flash(
- 'Error while loading the project data. Please try again.',
+ __('Error while loading the project data. Please try again.'),
'alert',
document,
null,
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index df100f753d7..b8abaa41f23 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -22,6 +22,8 @@ export const decorateFiles = ({
tempFile = false,
content = '',
base64 = false,
+ binary = false,
+ rawPath = '',
}) => {
const treeList = [];
const entries = {};
@@ -90,6 +92,8 @@ export const decorateFiles = ({
changed: tempFile,
content,
base64,
+ binary,
+ rawPath,
previewMode: viewerInformationForPath(name),
parentPath,
});
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7b660bda081..fd678e6e10c 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -53,7 +53,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
export const createTempEntry = (
{ state, commit, dispatch },
- { name, type, content = '', base64 = false },
+ { name, type, content = '', base64 = false, binary = false, rawPath = '' },
) =>
new Promise(resolve => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
@@ -79,8 +79,10 @@ export const createTempEntry = (
branchId: state.currentBranchId,
type,
tempFile: true,
- base64,
content,
+ base64,
+ binary,
+ rawPath,
});
const { file, parentPath } = data;
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index e74b880e02c..e7e8ac6d80b 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,6 @@
-import { __ } from '../../../locale';
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
+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';
@@ -69,7 +70,7 @@ export const getFileData = (
const url = file.prevPath ? file.url.replace(file.path, file.prevPath) : file.url;
return service
- .getFileData(`${gon.relative_url_root ? gon.relative_url_root : ''}${url.replace('/-/', '/')}`)
+ .getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
.then(({ data, headers }) => {
const normalizedHeaders = normalizeHeaders(headers);
setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
index b7090e09daf..59ead8a3dcf 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -23,22 +23,27 @@ export const receiveTemplateTypesError = ({ commit, dispatch }) => {
export const receiveTemplateTypesSuccess = ({ commit }, templates) =>
commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates);
-export const fetchTemplateTypes = ({ dispatch, state, rootState }, page = 1) => {
+export const fetchTemplateTypes = ({ dispatch, state, rootState }) => {
if (!Object.keys(state.selectedTemplateType).length) return Promise.reject();
dispatch('requestTemplateTypes');
- return Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, { page })
- .then(({ data, headers }) => {
- const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10);
+ const fetchPages = (page = 1, prev = []) =>
+ Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, {
+ page,
+ per_page: 100,
+ })
+ .then(({ data, headers }) => {
+ const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10);
+ const nextData = prev.concat(data);
- dispatch('receiveTemplateTypesSuccess', data);
+ dispatch('receiveTemplateTypesSuccess', nextData);
- if (nextPage) {
- dispatch('fetchTemplateTypes', nextPage);
- }
- })
- .catch(() => dispatch('receiveTemplateTypesError'));
+ return nextPage ? fetchPages(nextPage, nextData) : nextData;
+ })
+ .catch(() => dispatch('receiveTemplateTypesError'));
+
+ return fetchPages();
};
export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 628babe6a01..f10891a8e5b 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,4 +1,5 @@
import { activityBarViews } from '../../../constants';
+import { __ } from '~/locale';
export const templateTypes = () => [
{
@@ -10,11 +11,11 @@ export const templateTypes = () => [
key: 'gitignores',
},
{
- name: 'LICENSE',
+ name: __('LICENSE'),
key: 'licenses',
},
{
- name: 'Dockerfile',
+ name: __('Dockerfile'),
key: 'dockerfiles',
},
];
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
index 25a65b047f1..7fc1c9134a7 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js
@@ -3,13 +3,14 @@ import * as types from './mutation_types';
export default {
[types.REQUEST_TEMPLATE_TYPES](state) {
state.isLoading = true;
+ state.templates = [];
},
[types.RECEIVE_TEMPLATE_TYPES_ERROR](state) {
state.isLoading = false;
},
[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) {
state.isLoading = false;
- state.templates = state.templates.concat(templates);
+ state.templates = templates;
},
[types.SET_SELECTED_TEMPLATE_TYPE](state, type) {
state.selectedTemplateType = type;
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 9b9f4b21f1c..344b189decf 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -213,7 +213,7 @@ export default {
? `${slashedParentPath}${oldEntry.name}`
: `${slashedParentPath}${name}`;
- state.entries[newPath] = {
+ Vue.set(state.entries, newPath, {
...oldEntry,
id: newPath,
key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
@@ -225,7 +225,7 @@ export default {
tree: [],
parentPath,
raw: '',
- };
+ });
oldEntry.moved = true;
oldEntry.movedPath = newPath;
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 3ab8f3f11be..bcc9ca60d9b 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,3 +1,5 @@
+import { commitActionTypes } from '../constants';
+
export const dataStructure = () => ({
id: '',
// Key will contain a mixture of ID and path
@@ -69,6 +71,8 @@ export const decorateData = entity => {
changed = false,
parentTreeUrl = '',
base64 = false,
+ binary = false,
+ rawPath = '',
previewMode,
file_lock,
html,
@@ -92,6 +96,8 @@ export const decorateData = entity => {
renderError,
content,
base64,
+ binary,
+ rawPath,
previewMode,
file_lock,
html,
@@ -110,14 +116,14 @@ export const setPageTitle = title => {
export const commitActionForFile = file => {
if (file.prevPath) {
- return 'move';
+ return commitActionTypes.move;
} else if (file.deleted) {
- return 'delete';
+ return commitActionTypes.delete;
} else if (file.tempFile) {
- return 'create';
+ return commitActionTypes.create;
}
- return 'update';
+ return commitActionTypes.update;
};
export const getCommitFiles = stagedFiles =>
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 777f8fa6691..00eb0afb3bf 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -74,7 +74,7 @@ export default {
<gl-loading-icon
v-if="isLoadingRepos"
class="js-loading-button-icon import-projects-loading-icon"
- :size="4"
+ size="md"
/>
<div v-else-if="hasProviderRepos || hasImportedProjects" class="table-responsive">
<table class="table import-table">
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index f666e2ebf33..ff1fd1e598e 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -7,6 +7,8 @@ import mutations from './mutations';
Vue.use(Vuex);
+export { state, actions, getters, mutations };
+
export default () =>
new Vuex.Store({
state: state(),
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 08b858305ab..a7746bb3a0b 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
+import { __ } from '~/locale';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -65,10 +66,10 @@ export default class IntegrationSettingsForm {
* Toggle Submit button label based on Integration status and ability to test service
*/
toggleSubmitBtnLabel(serviceActive) {
- let btnLabel = 'Save changes';
+ let btnLabel = __('Save changes');
if (serviceActive && this.canTestService) {
- btnLabel = 'Test settings and save changes';
+ btnLabel = __('Test settings and save changes');
}
this.$submitBtnLabel.text(btnLabel);
@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) {
flashActions = {
- title: 'Save anyway',
+ title: __('Save anyway'),
clickHandler: e => {
e.preventDefault();
this.$form.submit();
@@ -121,7 +122,7 @@ export default class IntegrationSettingsForm {
this.toggleSubmitBtnState(false);
})
.catch(() => {
- flash('Something went wrong on our end.');
+ flash(__('Something went wrong on our end.'));
this.toggleSubmitBtnState(false);
});
}
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 9336b71cfd7..7576d36f27d 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import Autosave from './autosave';
import UsersSelect from './users_select';
-import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index bd757a76ee7..ab0b4231255 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -159,9 +159,23 @@ export default {
return !!this.state.updatedAt;
},
issueChanged() {
- const descriptionChanged = this.initialDescriptionText !== this.store.formState.description;
- const titleChanged = this.initialTitleText !== this.store.formState.title;
- return descriptionChanged || titleChanged;
+ const {
+ store: {
+ formState: { description, title },
+ },
+ initialDescriptionText,
+ initialTitleText,
+ } = this;
+
+ if (initialDescriptionText || description) {
+ return initialDescriptionText !== description;
+ }
+
+ if (initialTitleText || title) {
+ return initialTitleText !== title;
+ }
+
+ return false;
},
defaultErrorMessage() {
return sprintf(s__('Error updating %{issuableType}'), { issuableType: this.issuableType });
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 732184dc782..f2462e50093 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -145,9 +145,11 @@ export default {
></div>
<textarea
v-if="descriptionText"
+ ref="textarea"
v-model="descriptionText"
:data-update-url="updateUrl"
class="hidden js-task-list-field"
+ dir="auto"
>
</textarea>
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 299130e56ae..d27dd873125 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -53,6 +53,7 @@ export default {
v-model="formState.description"
class="note-textarea js-gfm-input js-autosize markdown-area
qa-description-textarea"
+ dir="auto"
data-supports-quick-actions="false"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index c3d7ba4907f..ce4baf17d09 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -17,8 +17,10 @@ export default {
<label class="sr-only" for="issuable-title"> Title </label>
<input
id="issuable-title"
+ ref="input"
v-model="formState.title"
class="form-control qa-title-input"
+ dir="auto"
type="text"
placeholder="Title"
aria-label="Title"
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index eade31f1d14..528ccb77efc 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -1,9 +1,12 @@
<script>
+import $ from 'jquery';
import lockedWarning from './locked_warning.vue';
import titleField from './fields/title.vue';
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
+import Autosave from '~/autosave';
+import eventHub from '../event_hub';
export default {
components: {
@@ -68,6 +71,47 @@ export default {
return this.issuableTemplates.length;
},
},
+ created() {
+ eventHub.$on('delete.issuable', this.resetAutosave);
+ eventHub.$on('update.issuable', this.resetAutosave);
+ eventHub.$on('close.form', this.resetAutosave);
+ },
+ mounted() {
+ this.initAutosave();
+ },
+ beforeDestroy() {
+ eventHub.$off('delete.issuable', this.resetAutosave);
+ eventHub.$off('update.issuable', this.resetAutosave);
+ eventHub.$off('close.form', this.resetAutosave);
+ },
+ methods: {
+ initAutosave() {
+ const {
+ description: {
+ $refs: { textarea },
+ },
+ title: {
+ $refs: { input },
+ },
+ } = this.$refs;
+
+ this.autosaveDescription = new Autosave($(textarea), [
+ document.location.pathname,
+ document.location.search,
+ 'description',
+ ]);
+
+ this.autosaveTitle = new Autosave($(input), [
+ document.location.pathname,
+ document.location.search,
+ 'title',
+ ]);
+ },
+ resetAutosave() {
+ this.autosaveDescription.reset();
+ this.autosaveTitle.reset();
+ },
+ },
};
</script>
@@ -89,10 +133,11 @@ export default {
'col-12': !hasIssuableTemplates,
}"
>
- <title-field :form-state="formState" :issuable-templates="issuableTemplates" />
+ <title-field ref="title" :form-state="formState" :issuable-templates="issuableTemplates" />
</div>
</div>
<description-field
+ ref="description"
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 3b5c95ccded..d2f33dc31a7 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -72,6 +72,7 @@ export default {
'issue-realtime-trigger-pulse': pulseAnimation,
}"
class="title"
+ dir="auto"
v-html="titleHtml"
></h2>
<button
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index f134a54dd53..4d304c5fe69 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -14,13 +14,16 @@ export default class LabelManager {
this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
this.$badgeItemTemplate = $('#js-badge-item-template');
- this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
- filter: '.empty-message',
- forceFallback: true,
- fallbackClass: 'is-dragging',
- dataIdAttr: 'data-id',
- onUpdate: this.onPrioritySortUpdate.bind(this),
- });
+
+ if ('sortable' in this.prioritizedLabels.data()) {
+ Sortable.create(this.prioritizedLabels.get(0), {
+ filter: '.empty-message',
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ dataIdAttr: 'data-id',
+ onUpdate: this.onPrioritySortUpdate.bind(this),
+ });
+ }
this.bindEvents();
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index ae02559415c..498c2348ca2 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -3,10 +3,17 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
import csrf from '~/lib/utils/csrf';
-export default (resolvers = {}) =>
- new ApolloClient({
+export default (resolvers = {}, baseUrl = '') => {
+ let uri = `${gon.relative_url_root}/api/graphql`;
+
+ if (baseUrl) {
+ // Prepend baseUrl and ensure that `///` are replaced with `/`
+ uri = `${baseUrl}${uri}`.replace(/\/{3,}/g, '/');
+ }
+
+ return new ApolloClient({
link: createUploadLink({
- uri: `${gon.relative_url_root}/api/graphql`,
+ uri,
headers: {
[csrf.headerKey]: csrf.token,
},
@@ -14,3 +21,4 @@ export default (resolvers = {}) =>
cache: new InMemoryCache(),
resolvers,
});
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 4ba84589705..bdfd06fc250 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -120,3 +120,5 @@ export function webIDEUrl(route = undefined) {
}
return returnUrl;
}
+
+export { join as joinPaths } from 'path';
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 37b5409a51d..37b17f0fe23 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,3 +1,5 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
// tell webpack to load assets from origin so that web workers don't break
// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
@@ -5,7 +7,7 @@ export function resetServiceWorkersPublicPath() {
// the webpack publicPath setting at runtime.
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
+ const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
// monaco-editor-webpack-plugin currently (incorrectly) references the
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1b722c0505a..b503c746801 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -135,6 +135,24 @@ 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/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 0333335de06..88bc0940741 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -3,15 +3,16 @@
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
+import { s__ } from '~/locale';
(global => {
global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = Cookies.get('diff_view');
- const HEAD_HEADER_TEXT = 'HEAD//our changes';
- const ORIGIN_HEADER_TEXT = 'origin//their changes';
- const HEAD_BUTTON_TITLE = 'Use ours';
- const ORIGIN_BUTTON_TITLE = 'Use theirs';
+ const HEAD_HEADER_TEXT = s__('MergeConflict|HEAD//our changes');
+ const ORIGIN_HEADER_TEXT = s__('MergeConflict|origin//their changes');
+ const HEAD_BUTTON_TITLE = s__('MergeConflict|Use ours');
+ const ORIGIN_BUTTON_TITLE = s__('MergeConflict|Use theirs');
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const EDIT_RESOLVE_MODE = 'edit';
const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
@@ -173,7 +174,7 @@ import Cookies from 'js-cookie';
getConflictsCountText() {
const count = this.getConflictsCount();
- const text = count > 1 ? 'conflicts' : 'conflict';
+ const text = count > 1 ? s__('MergeConflict|conflicts') : s__('MergeConflict|conflict');
return `${count} ${text}`;
},
@@ -348,8 +349,8 @@ import Cookies from 'js-cookie';
},
getCommitButtonText() {
- const initial = 'Commit to source branch';
- const inProgress = 'Committing...';
+ const initial = s__('MergeConflict|Commit to source branch');
+ const inProgress = s__('MergeConflict|Committing...');
return this.state ? (this.state.isSubmitting ? inProgress : initial) : initial;
},
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index 7badd68089c..d8d203e0616 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -8,6 +8,7 @@ import './components/diff_file_editor';
import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight';
+import { __ } from '~/locale';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
@@ -92,7 +93,7 @@ export default function initMergeConflicts() {
})
.catch(() => {
mergeConflictsStore.setSubmitState(false);
- createFlash('Failed to save merge conflicts resolutions. Please try again!');
+ createFlash(__('Failed to save merge conflicts resolutions. Please try again!'));
});
},
},
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 196b84621b6..33e9b1c4e46 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -87,7 +87,7 @@ export default class MirrorRepos {
project: {
remote_mirrors_attributes: {
id: $target.data('mirrorId'),
- enabled: 0,
+ _destroy: 1,
},
},
};
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 547c078ec55..bb5ae6ce2d1 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -24,12 +24,6 @@ export default class SSHMirror {
this.$wellAuthTypeChanging = this.$form.find('.js-well-changing-auth');
this.$wellPasswordAuth = this.$form.find('.js-well-password-auth');
- this.$wellSSHAuth = this.$form.find('.js-well-ssh-auth');
- this.$sshPublicKeyWrap = this.$form.find('.js-ssh-public-key-wrap');
- this.$regeneratePublicSshKeyButton = this.$wellSSHAuth.find('.js-btn-regenerate-ssh-key');
- this.$regeneratePublicSshKeyModal = this.$wellSSHAuth.find(
- '.js-regenerate-public-ssh-key-confirm-modal',
- );
}
init() {
@@ -40,15 +34,6 @@ export default class SSHMirror {
this.$dropdownAuthType.on('change', e => this.handleAuthTypeChange(e));
this.$btnDetectHostKeys.on('click', e => this.handleDetectHostKeys(e));
this.$btnSSHHostsShowAdvanced.on('click', e => this.handleSSHHostsAdvanced(e));
- this.$regeneratePublicSshKeyButton.on('click', () =>
- this.$regeneratePublicSshKeyModal.toggle(true),
- );
- $('.js-confirm', this.$regeneratePublicSshKeyModal).on('click', e =>
- this.regeneratePublicSshKey(e),
- );
- $('.js-cancel', this.$regeneratePublicSshKeyModal).on('click', () =>
- this.$regeneratePublicSshKeyModal.toggle(false),
- );
}
/**
@@ -162,54 +147,11 @@ export default class SSHMirror {
* Authentication method dropdown change event listener
*/
handleAuthTypeChange() {
- const projectMirrorAuthTypeEndpoint = `${this.$form.attr('action')}.json`;
- const $sshPublicKey = this.$sshPublicKeyWrap.find('.ssh-public-key');
const selectedAuthType = this.$dropdownAuthType.val();
this.$wellPasswordAuth.collapse('hide');
- this.$wellSSHAuth.collapse('hide');
this.updateHiddenAuthType(selectedAuthType);
-
- // This request should happen only if selected Auth type was SSH
- // and SSH Public key was not present on page load
- if (selectedAuthType === AUTH_METHOD.SSH && !$sshPublicKey.text().trim()) {
- if (!this.$wellSSHAuth.length) return;
-
- // Construct request body
- const authTypeData = {
- project: {
- ...this.$regeneratePublicSshKeyButton.data().projectData,
- },
- };
-
- this.$wellAuthTypeChanging.collapse('show');
- this.$dropdownAuthType.disable();
-
- axios
- .put(projectMirrorAuthTypeEndpoint, JSON.stringify(authTypeData), {
- headers: {
- 'Content-Type': 'application/json; charset=utf-8',
- },
- })
- .then(({ data }) => {
- // Show SSH public key container and fill in public key
- this.toggleAuthWell(selectedAuthType);
- this.toggleSSHAuthWellMessage(true);
- this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
-
- this.$wellAuthTypeChanging.collapse('hide');
- this.$dropdownAuthType.enable();
- })
- .catch(() => {
- Flash(__('Something went wrong on our end.'));
-
- this.$wellAuthTypeChanging.collapse('hide');
- this.$dropdownAuthType.enable();
- });
- } else {
- this.toggleAuthWell(selectedAuthType);
- this.$wellSSHAuth.find('.js-ssh-public-key-present').collapse('show');
- }
+ this.toggleAuthWell(selectedAuthType);
}
/**
@@ -235,7 +177,6 @@ export default class SSHMirror {
*/
toggleAuthWell(authType) {
this.$wellPasswordAuth.collapse(authType === AUTH_METHOD.PASSWORD ? 'show' : 'hide');
- this.$wellSSHAuth.collapse(authType === AUTH_METHOD.SSH ? 'show' : 'hide');
this.updateHiddenAuthType(authType);
}
@@ -244,64 +185,11 @@ export default class SSHMirror {
this.$hiddenAuthType.prop('disabled', authType === AUTH_METHOD.SSH);
}
- /**
- * Toggle SSH auth information message
- */
- toggleSSHAuthWellMessage(sshKeyPresent) {
- this.$sshPublicKeyWrap.collapse(sshKeyPresent ? 'show' : 'hide');
- this.$wellSSHAuth.find('.js-ssh-public-key-present').collapse(sshKeyPresent ? 'show' : 'hide');
- this.$regeneratePublicSshKeyButton.collapse(sshKeyPresent ? 'show' : 'hide');
- this.$wellSSHAuth.find('.js-ssh-public-key-pending').collapse(sshKeyPresent ? 'hide' : 'show');
- }
-
- /**
- * Sets SSH Public key to Clipboard button and shows it on UI.
- */
- setSSHPublicKey(sshPublicKey) {
- this.$sshPublicKeyWrap.find('.ssh-public-key').text(sshPublicKey);
- this.$sshPublicKeyWrap
- .find('.btn-copy-ssh-public-key')
- .attr('data-clipboard-text', sshPublicKey);
- }
-
- regeneratePublicSshKey(event) {
- event.preventDefault();
-
- this.$regeneratePublicSshKeyModal.toggle(false);
-
- const button = this.$regeneratePublicSshKeyButton;
- const spinner = $('.js-spinner', button);
- const endpoint = button.data('endpoint');
- const authTypeData = {
- project: {
- ...this.$regeneratePublicSshKeyButton.data().projectData,
- },
- };
-
- button.attr('disabled', 'disabled');
- spinner.removeClass('d-none');
-
- axios
- .patch(endpoint, authTypeData)
- .then(({ data }) => {
- button.removeAttr('disabled');
- spinner.addClass('d-none');
-
- this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
- })
- .catch(() => {
- Flash(_('Unable to regenerate public ssh key.'));
- });
- }
-
destroy() {
this.$repositoryUrl.off('keyup');
this.$form.find('.js-known-hosts').off('keyup');
this.$dropdownAuthType.off('change');
this.$btnDetectHostKeys.off('click');
this.$btnSSHHostsShowAdvanced.off('click');
- this.$regeneratePublicSshKeyButton.off('click');
- $('.js-confirm', this.$regeneratePublicSshKeyModal).off('click');
- $('.js-cancel', this.$regeneratePublicSshKeyModal).off('click');
}
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index f2bd4150b6d..a2bf58d007c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,16 +1,17 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee';
+import { getParameterValues } from '~/lib/utils/url_utility';
import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service';
import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
-import { timeWindows } from '../constants';
+import { timeWindows, timeWindowsKeyNames } from '../constants';
import { getTimeDiff } from '../utils';
const sidebarAnimationDuration = 150;
@@ -22,11 +23,18 @@ export default {
GraphGroup,
EmptyState,
Icon,
+ GlButton,
GlDropdown,
GlDropdownItem,
+ GlLink,
},
props: {
+ externalDashboardPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
hasMetrics: {
type: Boolean,
required: false,
@@ -102,6 +110,7 @@ export default {
showEmptyState: true,
elWidth: 0,
selectedTimeWindow: '',
+ selectedTimeWindowKey: '',
};
},
created() {
@@ -110,9 +119,16 @@ export default {
deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
});
-
this.timeWindows = timeWindows;
- this.selectedTimeWindow = this.timeWindows.eightHours;
+ this.selectedTimeWindowKey =
+ _.escape(getParameterValues('time_window')[0]) || timeWindowsKeyNames.eightHours;
+
+ // Set default time window if the selectedTimeWindowKey is bogus
+ if (!Object.keys(this.timeWindows).includes(this.selectedTimeWindowKey)) {
+ this.selectedTimeWindowKey = timeWindowsKeyNames.eightHours;
+ }
+
+ this.selectedTimeWindow = this.timeWindows[this.selectedTimeWindowKey];
},
beforeDestroy() {
if (sidebarMutationObserver) {
@@ -120,9 +136,10 @@ export default {
}
},
mounted() {
+ const startEndWindow = getTimeDiff(this.timeWindows[this.selectedTimeWindowKey]);
this.servicePromises = [
this.service
- .getGraphsData()
+ .getGraphsData(startEndWindow)
.then(data => this.store.storeMetrics(data))
.catch(() => Flash(s__('Metrics|There was an error while retrieving metrics'))),
this.service
@@ -176,22 +193,6 @@ export default {
this.state = 'unableToConnect';
});
},
- getGraphsDataWithTime(timeFrame) {
- this.state = 'loading';
- this.showEmptyState = true;
- this.service
- .getGraphsData(getTimeDiff(this.timeWindows[timeFrame]))
- .then(data => {
- this.store.storeMetrics(data);
- this.selectedTimeWindow = this.timeWindows[timeFrame];
- })
- .catch(() => {
- Flash(s__('Metrics|Not enough data to display'));
- })
- .finally(() => {
- this.showEmptyState = false;
- });
- },
onSidebarMutation() {
setTimeout(() => {
this.elWidth = this.$el.clientWidth;
@@ -200,6 +201,9 @@ export default {
activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindow;
},
+ setTimeWindowParameter(key) {
+ return `?time_window=${key}`;
+ },
},
};
</script>
@@ -221,6 +225,7 @@ export default {
<gl-dropdown-item
v-for="environment in store.environmentsData"
:key="environment.id"
+ :href="environment.metrics_path"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
>{{ environment.name }}</gl-dropdown-item
@@ -238,11 +243,19 @@ export default {
v-for="(value, key) in timeWindows"
:key="key"
:active="activeTimeWindow(key)"
- @click="getGraphsDataWithTime(key)"
- >{{ value }}</gl-dropdown-item
+ ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
>
</gl-dropdown>
</div>
+ <gl-button
+ v-if="externalDashboardPath.length"
+ class="js-external-dashboard-link"
+ variant="primary"
+ :href="externalDashboardPath"
+ >
+ {{ __('View full dashboard') }}
+ <icon name="external-link" />
+ </gl-button>
</div>
<graph-group
v-for="(groupData, index) in store.groups"
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index e97320fd682..26f1bf3f68d 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -18,3 +18,12 @@ export const timeWindows = {
threeDays: __('3 days'),
oneWeek: __('1 week'),
};
+
+export const timeWindowsKeyNames = {
+ thirtyMinutes: 'thirtyMinutes',
+ threeHours: 'threeHours',
+ eightHours: 'eightHours',
+ oneDay: 'oneDay',
+ threeDays: 'threeDays',
+ oneWeek: 'oneWeek',
+};
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 2b4ddd7afbc..08dc57d545c 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import Dashboard from './components/dashboard.vue';
+import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
-export default () => {
+export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
@@ -15,6 +15,7 @@ export default () => {
...el.dataset,
hasMetrics: parseBoolean(el.dataset.hasMetrics),
showTimeWindowDropdown: gon.features.metricsTimeWindow,
+ ...props,
},
});
},
diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js
index 5fcc2c8cfac..1efa5189996 100644
--- a/app/assets/javascripts/monitoring/services/monitoring_service.js
+++ b/app/assets/javascripts/monitoring/services/monitoring_service.js
@@ -1,7 +1,7 @@
import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
-import { s__ } from '../../locale';
+import { s__, __ } from '../../locale';
const MAX_REQUESTS = 3;
@@ -15,7 +15,7 @@ function backOffRequest(makeRequestCallback) {
if (requestCounter < MAX_REQUESTS) {
next();
} else {
- stop(new Error('Failed to connect to the prometheus server'));
+ stop(new Error(__('Failed to connect to the prometheus server')));
}
} else {
stop(resp);
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 9761fe168be..013fb0d4540 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -45,14 +45,13 @@ function removeTimeSeriesNoData(queries) {
// ]
function groupQueriesByChartInfo(metrics) {
const metricsByChart = metrics.reduce((accumulator, metric) => {
- const { id, queries, ...chart } = metric;
+ const { queries, ...chart } = metric;
+ const metricId = chart.id ? chart.id.toString() : null;
const chartKey = `${chart.title}|${chart.y_label}`;
accumulator[chartKey] = accumulator[chartKey] || { ...chart, queries: [] };
- queries.forEach(queryAttrs =>
- accumulator[chartKey].queries.push({ metricId: id.toString(), ...queryAttrs }),
- );
+ queries.forEach(queryAttrs => accumulator[chartKey].queries.push({ metricId, ...queryAttrs }));
return accumulator;
}, {});
diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js
index 433df844c80..c13c417cc18 100644
--- a/app/assets/javascripts/mr_popover/constants.js
+++ b/app/assets/javascripts/mr_popover/constants.js
@@ -1,10 +1,12 @@
+import { __ } from '~/locale';
+
export const mrStates = {
merged: 'merged',
closed: 'closed',
};
export const humanMRStates = {
- merged: 'Merged',
- closed: 'Closed',
- open: 'Open',
+ merged: __('Merged'),
+ closed: __('Closed'),
+ open: __('Open'),
};
diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js
index 9a97e98f9db..18c0e201300 100644
--- a/app/assets/javascripts/mr_popover/index.js
+++ b/app/assets/javascripts/mr_popover/index.js
@@ -22,13 +22,10 @@ const handleUserPopoverMouseOut = ({ target }) => {
* Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes.
* loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover
*/
-const handleMRPopoverMount = apolloProvider => ({ target }) => {
+const handleMRPopoverMount = ({ apolloProvider, projectPath, mrTitle, iid }) => ({ target }) => {
// Add listener to actually remove it again
target.addEventListener('mouseleave', handleUserPopoverMouseOut);
- const { projectPath, mrTitle, iid } = target.dataset;
- const mergeRequest = {};
-
renderFn = setTimeout(() => {
const MRPopoverComponent = Vue.extend(MRPopover);
renderedPopover = new MRPopoverComponent({
@@ -36,7 +33,6 @@ const handleMRPopoverMount = apolloProvider => ({ target }) => {
target,
projectPath,
mergeRequestIID: iid,
- mergeRequest,
mergeRequestTitle: mrTitle,
},
apolloProvider,
@@ -57,8 +53,13 @@ export default elements => {
const listenerAddedAttr = 'data-mr-listener-added';
mrLinks.forEach(el => {
- if (!el.getAttribute(listenerAddedAttr)) {
- el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider));
+ const { projectPath, mrTitle, iid } = el.dataset;
+
+ if (!el.getAttribute(listenerAddedAttr) && projectPath && mrTitle && iid) {
+ el.addEventListener(
+ 'mouseenter',
+ handleMRPopoverMount({ apolloProvider, projectPath, mrTitle, iid }),
+ );
el.setAttribute(listenerAddedAttr, true);
}
});
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
new file mode 100644
index 00000000000..b817d38960c
--- /dev/null
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -0,0 +1,22 @@
+import Flash from '~/flash';
+import { __, sprintf } from '~/locale';
+import { getParameterByName } from '~/lib/utils/common_utils';
+
+const PARAMETER_NAME = 'leave';
+const LEAVE_LINK_SELECTOR = '.js-leave-link';
+
+export default function leaveByUrl(namespaceType) {
+ if (!namespaceType) throw new Error('namespaceType not provided');
+
+ const param = getParameterByName(PARAMETER_NAME);
+ if (!param) return;
+
+ const leaveLink = document.querySelector(LEAVE_LINK_SELECTOR);
+ if (leaveLink) {
+ leaveLink.click();
+ } else {
+ Flash(
+ sprintf(__('You do not have permission to leave this %{namespaceType}.'), { namespaceType }),
+ );
+ }
+}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index caf22f71bf9..688c06878ac 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -351,6 +351,7 @@ Please check your network connection and try again.`;
ref="textarea"
slot="textarea"
v-model="note"
+ dir="auto"
:disabled="isSubmitting"
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
new file mode 100644
index 00000000000..22cca756ef6
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -0,0 +1,58 @@
+<script>
+import ReplyPlaceholder from './discussion_reply_placeholder.vue';
+import ResolveDiscussionButton from './discussion_resolve_button.vue';
+import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
+import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
+
+export default {
+ name: 'DiscussionActions',
+ components: {
+ ReplyPlaceholder,
+ ResolveDiscussionButton,
+ ResolveWithIssueButton,
+ JumpToNextDiscussionButton,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ isResolving: {
+ type: Boolean,
+ required: true,
+ },
+ resolveButtonTitle: {
+ type: String,
+ required: true,
+ },
+ resolveWithIssuePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ shouldShowJumpToNextDiscussion: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-with-resolve-btn">
+ <reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" />
+ <resolve-discussion-button
+ v-if="discussion.resolvable"
+ :is-resolving="isResolving"
+ :button-title="resolveButtonTitle"
+ @onClick="$emit('resolve')"
+ />
+ <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group">
+ <resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" />
+ <jump-to-next-discussion-button
+ v-if="shouldShowJumpToNextDiscussion"
+ @onClick="$emit('jumpToNextDiscussion')"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
new file mode 100644
index 00000000000..5b6163a6214
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -0,0 +1,155 @@
+<script>
+import { mapGetters } from 'vuex';
+import { SYSTEM_NOTE } from '../constants';
+import { __ } from '~/locale';
+import NoteableNote from './noteable_note.vue';
+import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import ToggleRepliesWidget from './toggle_replies_widget.vue';
+import NoteEditedText from './note_edited_text.vue';
+
+export default {
+ name: 'DiscussionNotes',
+ components: {
+ ToggleRepliesWidget,
+ NoteEditedText,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ isExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ diffLine: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ line: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ shouldGroupReplies: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpPagePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ ...mapGetters(['userCanReply']),
+ hasReplies() {
+ return !!this.replies.length;
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
+ firstNote() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ resolvedText() {
+ return this.discussion.resolved_by_push ? __('Automatically resolved') : __('Resolved');
+ },
+ commit() {
+ if (!this.discussion.for_commit) {
+ return null;
+ }
+
+ return {
+ id: this.discussion.commit_id,
+ url: this.discussion.discussion_path,
+ };
+ },
+ },
+ methods: {
+ componentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === SYSTEM_NOTE) {
+ return PlaceholderSystemNote;
+ }
+
+ return PlaceholderNote;
+ }
+
+ if (note.system) {
+ return SystemNote;
+ }
+
+ return NoteableNote;
+ },
+ componentData(note) {
+ return note.isPlaceholderNote ? note.notes[0] : note;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-notes">
+ <ul class="notes">
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(firstNote)"
+ :note="componentData(firstNote)"
+ :line="line"
+ :commit="commit"
+ :help-page-path="helpPagePath"
+ :show-reply-button="userCanReply"
+ @handle-delete-note="$emit('deleteNote')"
+ @start-replying="$emit('startReplying')"
+ >
+ <note-edited-text
+ v-if="discussion.resolved"
+ slot="discussion-resolved-text"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="$emit('toggleDiscussion')"
+ />
+ <template v-if="isExpanded">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="line"
+ @handle-delete-note="$emit('deleteNote')"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ :help-page-path="helpPagePath"
+ :line="diffLine"
+ @handle-delete-note="$emit('deleteNote')"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index aabb77f6a85..7e8f23d6a96 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import resolvedStatusMixin from 'ee_else_ce/batch_comments/mixins/resolved_status';
import ReplyButton from './note_actions/reply_button.vue';
export default {
@@ -14,6 +15,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [resolvedStatusMixin],
props: {
authorId: {
type: Number,
@@ -98,15 +100,6 @@ export default {
currentUserId() {
return this.getUserDataByProp('id');
},
- resolveButtonTitle() {
- let title = 'Mark as resolved';
-
- if (this.resolvedBy) {
- title = `Resolved by ${this.resolvedBy.name}`;
- }
-
- return title;
- },
},
methods: {
onEdit() {
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index fbf75ed0e41..88454c3fb4c 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -83,10 +83,12 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelForm', shouldConfirm, isDirty);
},
- applySuggestion({ suggestionId, flashContainer, callback }) {
+ applySuggestion({ suggestionId, flashContainer, callback = () => {} }) {
const { discussion_id: discussionId, id: noteId } = this.note;
- this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer, callback });
+ return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then(
+ callback,
+ );
},
},
};
@@ -122,6 +124,7 @@ export default {
v-model="note.note"
:data-update-url="note.path"
class="hidden js-task-list-field"
+ dir="auto"
></textarea>
<note-edited-text
v-if="note.last_edited_at"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 471323bfc83..acbb91ce7be 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -8,6 +8,7 @@ import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
import { __ } from '~/locale';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
+import noteFormMixin from 'ee_else_ce/notes/mixins/note_form';
export default {
name: 'NoteForm',
@@ -15,7 +16,7 @@ export default {
issueWarning,
markdownField,
},
- mixins: [issuableStateMixin, resolvable],
+ mixins: [issuableStateMixin, resolvable, noteFormMixin],
props: {
noteBody: {
type: String,
@@ -195,21 +196,6 @@ export default {
return shouldResolve || shouldToggleState;
},
- handleKeySubmit() {
- this.handleUpdate();
- },
- handleUpdate(shouldResolve) {
- const beforeSubmitDiscussionState = this.discussionResolved;
- this.isSubmitting = true;
-
- this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
- this.isSubmitting = false;
-
- if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) {
- this.resolveHandler(beforeSubmitDiscussionState);
- }
- });
- },
editMyLastNote() {
if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
@@ -268,6 +254,7 @@ export default {
:data-supports-quick-actions="!isEditing"
name="note[note]"
class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ dir="auto"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleKeySubmit()"
@@ -278,28 +265,74 @@ export default {
></textarea>
</markdown-field>
<div class="note-form-actions clearfix">
- <button
- :disabled="isDisabled"
- type="button"
- class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button"
- @click="handleUpdate()"
- >
- {{ saveButtonTitle }}
- </button>
- <button
- v-if="discussion.resolvable"
- class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
- @click.prevent="handleUpdate(true)"
- >
- {{ resolveButtonTitle }}
- </button>
- <button
- class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
- type="button"
- @click="cancelHandler()"
- >
- Cancel
- </button>
+ <template v-if="showBatchCommentsActions">
+ <p v-if="showResolveDiscussionToggle">
+ <label>
+ <template v-if="discussionResolved">
+ <input
+ v-model="isUnresolving"
+ type="checkbox"
+ class="qa-unresolve-review-discussion"
+ />
+ {{ __('Unresolve discussion') }}
+ </template>
+ <template v-else>
+ <input v-model="isResolving" type="checkbox" class="qa-resolve-review-discussion" />
+ {{ __('Resolve discussion') }}
+ </template>
+ </label>
+ </p>
+ <div>
+ <button
+ :disabled="isDisabled"
+ type="button"
+ class="btn btn-success qa-start-review"
+ @click="handleAddToReview"
+ >
+ <template v-if="hasDrafts">{{ __('Add to review') }}</template>
+ <template v-else>{{ __('Start a review') }}</template>
+ </button>
+ <button
+ :disabled="isDisabled"
+ type="button"
+ class="btn qa-comment-now"
+ @click="handleUpdate()"
+ >
+ {{ __('Add comment now') }}
+ </button>
+ <button
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ type="button"
+ @click="cancelHandler()"
+ >
+ {{ __('Cancel') }}
+ </button>
+ </div>
+ </template>
+ <template v-else>
+ <button
+ :disabled="isDisabled"
+ type="button"
+ class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button"
+ @click="handleUpdate()"
+ >
+ {{ saveButtonTitle }}
+ </button>
+ <button
+ v-if="discussion.resolvable"
+ class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
+ @click.prevent="handleUpdate(true)"
+ >
+ {{ resolveButtonTitle }}
+ </button>
+ <button
+ class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
+ type="button"
+ @click="cancelHandler()"
+ >
+ Cancel
+ </button>
+ </template>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index a3d664a738f..2c549e7abdd 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -5,51 +5,36 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
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 { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
-import resolveDiscussionButton from './discussion_resolve_button.vue';
-import toggleRepliesWidget from './toggle_replies_widget.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 placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
-import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import discussionNavigation from '../mixins/discussion_navigation';
-import ReplyPlaceholder from './discussion_reply_placeholder.vue';
-import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
-import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
import eventHub from '../event_hub';
+import DiscussionNotes from './discussion_notes.vue';
+import DiscussionActions from './discussion_actions.vue';
export default {
name: 'NoteableDiscussion',
components: {
icon,
- noteableNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
noteEditedText,
noteForm,
- resolveDiscussionButton,
- jumpToNextDiscussionButton,
- toggleRepliesWidget,
- ReplyPlaceholder,
- placeholderNote,
- placeholderSystemNote,
- ResolveWithIssueButton,
- systemNote,
DraftNote: () => import('ee_component/batch_comments/components/draft_note.vue'),
TimelineEntryItem,
+ DiscussionNotes,
+ DiscussionActions,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -97,6 +82,7 @@ export default {
...mapGetters([
'convertedDisscussionIds',
'getNoteableData',
+ 'userCanReply',
'nextUnresolvedDiscussionId',
'unresolvedDiscussionsCount',
'hasUnresolvedDiscussions',
@@ -108,21 +94,12 @@ export default {
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
},
- canReply() {
- return this.getNoteableData.current_user.can_create_note;
- },
newNotePath() {
return this.getNoteableData.create_note_path;
},
- hasReplies() {
- return this.discussion.notes.length > 1;
- },
firstNote() {
return this.discussion.notes.slice(0, 1)[0];
},
- replies() {
- return this.discussion.notes.slice(1);
- },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -228,18 +205,8 @@ export default {
return null;
},
- commit() {
- if (!this.discussion.for_commit) {
- return null;
- }
-
- return {
- id: this.discussion.commit_id,
- url: this.discussion.discussion_path,
- };
- },
resolveWithIssuePath() {
- return !this.discussionResolved && this.discussion.resolve_with_issue_path;
+ return !this.discussionResolved ? this.discussion.resolve_with_issue_path : '';
},
},
created() {
@@ -258,24 +225,6 @@ export default {
'removeConvertedDiscussion',
]),
truncateSha,
- componentName(note) {
- if (note.isPlaceholderNote) {
- if (note.placeholderType === SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
-
- return placeholderNote;
- }
-
- if (note.system) {
- return systemNote;
- }
-
- return noteableNote;
- },
- componentData(note) {
- return note.isPlaceholderNote ? note.notes[0] : note;
- },
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
@@ -405,106 +354,56 @@ Please check your network connection and try again.`;
v-bind="wrapperComponentProps"
class="card discussion-wrapper"
>
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(firstNote)"
- :note="componentData(firstNote)"
- :line="line"
- :commit="commit"
- :help-page-path="helpPagePath"
- :show-reply-button="canReply"
- @handleDeleteNote="deleteNoteHandler"
- @startReplying="showReplyForm"
- >
- <note-edited-text
- v-if="discussion.resolved"
- slot="discussion-resolved-text"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- <slot slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="!isExpanded"
- :replies="replies"
- @toggle="toggleDiscussionHandler"
+ <discussion-notes
+ :discussion="discussion"
+ :diff-line="diffLine"
+ :help-page-path="helpPagePath"
+ :is-expanded="isExpanded"
+ :line="line"
+ :should-group-replies="shouldGroupReplies"
+ @startReplying="showReplyForm"
+ @toggleDiscussion="toggleDiscussionHandler"
+ @deleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #footer="{ showReplies }">
+ <draft-note
+ v-if="showDraft(discussion.reply_id)"
+ :key="`draft_${discussion.id}`"
+ :draft="draftForDiscussion(discussion.reply_id)"
+ />
+ <div
+ v-else-if="showReplies"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <discussion-actions
+ v-if="!isReplying && userCanReply"
+ :discussion="discussion"
+ :is-resolving="isResolving"
+ :resolve-button-title="resolveButtonTitle"
+ :resolve-with-issue-path="resolveWithIssuePath"
+ :should-show-jump-to-next-discussion="shouldShowJumpToNextDiscussion"
+ @showReplyForm="showReplyForm"
+ @resolve="resolveHandler"
+ @jumpToNextDiscussion="jumpToNextDiscussion"
/>
- <template v-if="isExpanded">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
- :line="line"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
- <component
- :is="componentName(note)"
- v-for="(note, index) in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- :help-page-path="helpPagePath"
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
:line="diffLine"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- </template>
- </ul>
- <div
- v-if="isExpanded || !hasReplies"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <template v-if="!isReplying && canReply">
- <div class="discussion-with-resolve-btn">
- <reply-placeholder class="qa-discussion-reply" @onClick="showReplyForm" />
- <resolve-discussion-button
- v-if="discussion.resolvable"
- :is-resolving="isResolving"
- :button-title="resolveButtonTitle"
- @onClick="resolveHandler"
- />
- <div
- v-if="discussion.resolvable"
- class="btn-group discussion-actions ml-sm-2"
- role="group"
- >
- <resolve-with-issue-button
- v-if="resolveWithIssuePath"
- :url="resolveWithIssuePath"
- />
- <jump-to-next-discussion-button
- v-if="shouldShowJumpToNextDiscussion"
- @onClick="jumpToNextDiscussion"
- />
- </div>
- </div>
- </template>
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- :line="diffLine"
- save-button-title="Comment"
- :autosave-key="autosaveKey"
- @handleFormUpdateAddToReview="addReplyToReview"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
- </div>
+ save-button-title="Comment"
+ :autosave-key="autosaveKey"
+ @handleFormUpdateAddToReview="addReplyToReview"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!userCanReply" />
+ </div>
+ </template>
+ </discussion-notes>
</component>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index e2bd59f7631..0f1976db37d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -67,6 +67,7 @@ export default {
'isLoading',
'commentsDisabled',
'getNoteableData',
+ 'userCanReply',
]),
noteableType() {
return this.noteableData.noteableType;
@@ -83,7 +84,7 @@ export default {
return this.discussions;
},
canReply() {
- return this.getNoteableData.current_user.can_create_note && !this.commentsDisabled;
+ return this.userCanReply && !this.commentsDisabled;
},
},
watch: {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 4f45f912479..b161773f5f1 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
import Autosave from '../../autosave';
import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
+import { s__ } from '~/locale';
export default {
methods: {
initAutoSave(noteable, extraKeys = []) {
let keys = [
- 'Note',
+ s__('Autosave|Note'),
capitalizeFirstCharacter(noteable.noteable_type || noteable.noteableType),
noteable.id,
];
diff --git a/app/assets/javascripts/notes/mixins/note_form.js b/app/assets/javascripts/notes/mixins/note_form.js
new file mode 100644
index 00000000000..b74879f2256
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/note_form.js
@@ -0,0 +1,24 @@
+export default {
+ data() {
+ return {
+ showBatchCommentsActions: false,
+ };
+ },
+ methods: {
+ handleKeySubmit() {
+ this.handleUpdate();
+ },
+ handleUpdate(shouldResolve) {
+ const beforeSubmitDiscussionState = this.discussionResolved;
+ this.isSubmitting = true;
+
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
+ this.isSubmitting = false;
+
+ if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1a0dba69a7c..bac124be34c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -142,6 +142,23 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }) => {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ const isResolved = getters.isDiscussionResolved(discussionId);
+
+ if (!discussion) {
+ return Promise.reject();
+ } else if (isResolved) {
+ return Promise.resolve();
+ }
+
+ return dispatch('toggleResolveNote', {
+ endpoint: discussion.resolve_path,
+ isResolved,
+ discussion: true,
+ });
+};
+
export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
service
.toggleResolveNote(endpoint, isResolved)
@@ -255,7 +272,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
eTagPoll.makeRequest();
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', noteData.flashContainer);
+ Flash(__('Commands applied'), 'notice', noteData.flashContainer);
}
if (commandsChanges) {
@@ -269,7 +286,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
})
.catch(() => {
Flash(
- 'Something went wrong while adding your award. Please try again.',
+ __('Something went wrong while adding your award. Please try again.'),
'alert',
noteData.flashContainer,
);
@@ -311,7 +328,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
data: state,
successCallback: resp =>
resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
- errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
+ errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
});
if (!Visibility.hidden()) {
@@ -347,7 +364,7 @@ export const fetchData = ({ commit, state, getters }) => {
.poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
- .catch(() => Flash('Something went wrong while fetching latest comments.'));
+ .catch(() => Flash(__('Something went wrong while fetching latest comments.')));
};
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
@@ -420,15 +437,13 @@ export const updateResolvableDiscussonsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = (
- { commit },
- { discussionId, noteId, suggestionId, flashContainer, callback },
-) => {
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+) =>
service
.applySuggestion(suggestionId)
- .then(() => {
- commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId });
- callback();
- })
+ .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }))
+ .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {}))
.catch(err => {
const defaultMessage = __(
'Something went wrong while applying the suggestion. Please try again.',
@@ -436,9 +451,7 @@ export const submitSuggestion = (
const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage;
Flash(__(flashMessage), 'alert', flashContainer);
- callback();
});
-};
export const convertToDiscussion = ({ commit }, noteId) =>
commit(types.CONVERT_TO_DISCUSSION, noteId);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index fcc8889b0c7..2d150e64ef7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -20,6 +20,8 @@ export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+export const userCanReply = state => !!state.noteableData.current_user.can_create_note;
+
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 4b0feb0f94d..029fde348fb 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -1,12 +1,13 @@
import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
+import { sprintf, __ } from '~/locale';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const getQuickActionText = note => {
- let text = 'Applying command';
+ let text = __('Applying command');
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter(command => {
@@ -16,10 +17,10 @@ export const getQuickActionText = note => {
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
- text = 'Applying multiple commands';
+ text = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- text = `Applying command to ${commandDescription}`;
+ text = sprintf(__('Applying command to %{commandDescription}', { commandDescription }));
}
}
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
new file mode 100644
index 00000000000..0a87d193b72
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ },
+ props: {
+ externalDashboardPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ externalDashboardHelpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings expanded">
+ <div class="settings-header">
+ <h4 class="js-section-header">
+ {{ s__('ExternalMetrics|External Dashboard') }}
+ </h4>
+ <p class="js-section-sub-header">
+ {{
+ s__(
+ 'ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards.',
+ )
+ }}
+ <gl-link :href="externalDashboardHelpPagePath">{{ __('Learn more') }}</gl-link>
+ </p>
+ </div>
+ <div class="settings-content">
+ <form>
+ <gl-form-group
+ :label="s__('ExternalMetrics|Full dashboard URL')"
+ :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
+ >
+ <gl-form-input
+ :value="externalDashboardPath"
+ placeholder="https://my-org.gitlab.io/my-dashboards"
+ />
+ </gl-form-group>
+ <gl-button variant="success">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </form>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
new file mode 100644
index 00000000000..1171f3ece9f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import ExternalDashboardForm from './components/external_dashboard.vue';
+
+export default () => {
+ /**
+ * This check can be removed when we remove
+ * the :grafana_dashboard_link feature flag
+ */
+ if (!gon.features.grafanaDashboardLink) {
+ return null;
+ }
+
+ const el = document.querySelector('.js-operation-settings');
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(ExternalDashboardForm, {
+ props: {
+ ...el.dataset,
+ expanded: false,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index d5ded3f9a79..6e00e31b828 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -22,7 +22,7 @@ export default () => {
_.debounce(function onMessageInput() {
const message = $(this).val();
if (message === '') {
- $('.js-broadcast-message-preview').text('Your message here');
+ $('.js-broadcast-message-preview').text(__('Your message here'));
} else {
axios
.post(previewPath, {
diff --git a/app/assets/javascripts/pages/admin/clusters/destroy/index.js b/app/assets/javascripts/pages/admin/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/destroy/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/admin/clusters/edit/index.js b/app/assets/javascripts/pages/admin/clusters/edit/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/edit/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js
new file mode 100644
index 00000000000..d0c9ae66c6a
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/index.js
@@ -0,0 +1,21 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+function initGcpSignupCallout() {
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
+}
+
+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();
+ }
+});
diff --git a/app/assets/javascripts/pages/admin/clusters/index/index.js b/app/assets/javascripts/pages/admin/clusters/index/index.js
new file mode 100644
index 00000000000..30d519d0e37
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/index/index.js
@@ -0,0 +1,6 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const callout = document.querySelector('.gcp-signup-offer');
+ PersistentUserCallout.factory(callout);
+});
diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/clusters/show/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index af924e74f1f..82ee5ead83d 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -1,5 +1,7 @@
+import leaveByUrl from '~/namespaces/leave_by_url';
import initGroupDetails from '../shared/group_details';
document.addEventListener('DOMContentLoaded', () => {
+ leaveByUrl('group');
initGroupDetails();
});
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index 0dd0d5336fc..13cb0d6f74b 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
import createFlash from '~/flash';
-import GfmAutoComplete from '~/gfm_auto_complete';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import emojiRegex from 'emoji-regex';
import EmojiMenu from './emoji_menu';
+import { __ } from '~/locale';
const defaultStatusEmoji = 'speech_balloon';
@@ -48,7 +49,7 @@ document.addEventListener('DOMContentLoaded', () => {
const EMOJI_REGEX = emojiRegex();
if (EMOJI_REGEX.test(userNameInput.value)) {
// set field to invalid so it gets detected by GlFieldErrors
- userNameInput.setCustomValidity('Invalid field');
+ userNameInput.setCustomValidity(__('Invalid field'));
} else {
userNameInput.setCustomValidity('');
}
@@ -81,5 +82,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
})
- .catch(() => createFlash('Failed to load emoji list.'));
+ .catch(() => createFlash(__('Failed to load emoji list.')));
});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 8bf0c2edc71..c34aff02111 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -4,9 +4,9 @@ import IssuableIndex from '~/issuable_index';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
-import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
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 73c745179be..5270a7924ec 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,5 +1,7 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
+import mountOperationSettings from '~/operation_settings';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
+ mountOperationSettings();
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
index bc5c29d12b5..ac0dca31c37 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const visibilityOptions = {
PRIVATE: 0,
INTERNAL: 10,
@@ -5,9 +7,11 @@ export const visibilityOptions = {
};
export const visibilityLevelDescriptions = {
- [visibilityOptions.PRIVATE]:
+ [visibilityOptions.PRIVATE]: __(
'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
- [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
- [visibilityOptions.PUBLIC]:
+ ),
+ [visibilityOptions.INTERNAL]: __('The project can be accessed by any user who is logged in.'),
+ [visibilityOptions.PUBLIC]: __(
'The project can be accessed by anyone, regardless of authentication.',
+ ),
};
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 7302c1ab202..869f70e7d33 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -9,6 +9,7 @@ import Activities from '~/activities';
import { ajaxGet } from '~/lib/utils/common_utils';
import GpgBadges from '~/gpg_badges';
import initReadMore from '~/read_more';
+import leaveByUrl from '~/namespaces/leave_by_url';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
@@ -44,4 +45,5 @@ document.addEventListener('DOMContentLoaded', () => {
});
GpgBadges.fetch();
+ leaveByUrl('project');
});
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 0c896c8599e..d5a8e712d6b 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import Flash from '~/flash';
import Api from '~/api';
+import { __ } from '~/locale';
export default class Search {
constructor() {
@@ -24,7 +25,7 @@ export default class Search {
data(term, callback) {
return Api.groups(term, {}, data => {
data.unshift({
- full_name: 'Any',
+ full_name: __('Any'),
});
data.splice(1, 0, 'divider');
return callback(data);
@@ -54,14 +55,14 @@ export default class Search {
this.getProjectsData(term)
.then(data => {
data.unshift({
- name_with_namespace: 'Any',
+ name_with_namespace: __('Any'),
});
data.splice(1, 0, 'divider');
return data;
})
.then(data => callback(data))
- .catch(() => new Flash('Error fetching projects'));
+ .catch(() => new Flash(__('Error fetching projects')));
},
id(obj) {
return obj.id;
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 61204c37307..693125f8a38 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -6,7 +6,7 @@ import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
-import { __ } from '~/locale';
+import { n__, s__, __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold };
@@ -35,9 +35,9 @@ function formatTooltipText({ date, count }) {
const dateDayName = getDayName(dateObject);
const dateText = dateFormat(dateObject, 'mmm d, yyyy');
- let contribText = 'No contributions';
+ let contribText = __('No contributions');
if (count > 0) {
- contribText = `${count} contribution${count > 1 ? 's' : ''}`;
+ contribText = n__('%d contribution', '%d contributions', count);
}
return `${contribText}<br />${dateDayName} ${dateText}`;
}
@@ -65,18 +65,18 @@ export default class ActivityCalendar {
this.daySize = 15;
this.daySizeWithSpace = this.daySize + this.daySpace * 2;
this.monthNames = [
- 'Jan',
- 'Feb',
- 'Mar',
- 'Apr',
- 'May',
- 'Jun',
- 'Jul',
- 'Aug',
- 'Sep',
- 'Oct',
- 'Nov',
- 'Dec',
+ __('Jan'),
+ __('Feb'),
+ __('Mar'),
+ __('Apr'),
+ __('May'),
+ __('Jun'),
+ __('Jul'),
+ __('Aug'),
+ __('Sep'),
+ __('Oct'),
+ __('Nov'),
+ __('Dec'),
];
this.months = [];
this.firstDayOfWeek = firstDayOfWeek;
@@ -199,27 +199,27 @@ export default class ActivityCalendar {
renderDayTitles() {
const days = [
{
- text: 'M',
+ text: s__('DayTitle|M'),
y: 29 + this.dayYPos(1),
},
{
- text: 'W',
+ text: s__('DayTitle|W'),
y: 29 + this.dayYPos(3),
},
{
- text: 'F',
+ text: s__('DayTitle|F'),
y: 29 + this.dayYPos(5),
},
];
if (this.firstDayOfWeek === firstDayOfWeekChoices.monday) {
days.push({
- text: 'S',
+ text: s__('DayTitle|S'),
y: 29 + this.dayYPos(7),
});
} else if (this.firstDayOfWeek === firstDayOfWeekChoices.saturday) {
days.push({
- text: 'S',
+ text: s__('DayTitle|S'),
y: 29 + this.dayYPos(6),
});
}
@@ -253,11 +253,11 @@ export default class ActivityCalendar {
renderKey() {
const keyValues = [
- 'no contributions',
- '1-9 contributions',
- '10-19 contributions',
- '20-29 contributions',
- '30+ contributions',
+ __('no contributions'),
+ __('1-9 contributions'),
+ __('10-19 contributions'),
+ __('20-29 contributions'),
+ __('30+ contributions'),
];
const keyColors = [
'#ededed',
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index c729198c1d3..8f3ba9779fb 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,9 +1,11 @@
<script>
import GlModal from '~/vue_shared/components/gl_modal.vue';
+import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlModal,
+ Icon,
},
props: {
currentRequest: {
@@ -38,7 +40,11 @@ export default {
};
</script>
<template>
- <div v-if="currentRequest.details" :id="`peek-view-${metric}`" class="view">
+ <div
+ v-if="currentRequest.details"
+ :id="`peek-view-${metric}`"
+ class="view qa-performance-bar-detailed-metric"
+ >
<button
:data-target="`#modal-peek-${metric}-details`"
class="btn-blank btn-link bold"
@@ -57,9 +63,31 @@ export default {
<template v-if="detailsList.length">
<tr v-for="(item, index) in detailsList" :key="index">
<td>
- <strong>{{ item.duration }}ms</strong>
+ <span>{{ item.duration }}ms</span>
+ </td>
+ <td>
+ <div class="js-toggle-container">
+ <div
+ v-for="(key, keyIndex) in keys"
+ :key="key"
+ class="break-word"
+ :class="{ 'mb-3 bold': keyIndex == 0 }"
+ >
+ {{ item[key] }}
+ <button
+ v-if="keyIndex == 0 && item.backtrace"
+ class="text-expander js-toggle-button"
+ type="button"
+ :aria-label="__('Toggle backtrace')"
+ >
+ <icon :size="12" name="ellipsis_h" />
+ </button>
+ </div>
+ <pre v-if="item.backtrace" class="backtrace-row js-toggle-content mt-2">{{
+ item.backtrace
+ }}</pre>
+ </div>
</td>
- <td v-for="key in keys" :key="key" class="break-word">{{ item[key] }}</td>
</tr>
</template>
<template v-else>
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 1ec2784cc5a..48515cf785c 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -92,7 +92,7 @@ export default {
</script>
<template>
<div id="js-peek" :class="env">
- <div v-if="currentRequest" class="d-flex container-fluid container-limited">
+ <div v-if="currentRequest" class="d-flex container-fluid container-limited qa-performance-bar">
<div id="peek-view-host" class="view">
<span
v-if="hasHost"
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index fdb5c0d6939..297507b85af 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -37,7 +37,12 @@ export default {
<template>
<div id="peek-request-selector">
<select v-model="currentRequestId">
- <option v-for="request in requests" :key="request.id" :value="request.id">
+ <option
+ v-for="request in requests"
+ :key="request.id"
+ :value="request.id"
+ class="qa-performance-bar-request"
+ >
{{ truncatedUrl(request.url) }}
</option>
</select>
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index 8ca539351a7..3c85bb61ce8 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@@ -20,6 +20,7 @@ export default {
components: {
Icon,
GlButton,
+ GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -41,6 +42,7 @@ export default {
data() {
return {
isDisabled: false,
+ isLoading: false,
};
},
computed: {
@@ -59,15 +61,19 @@ export default {
onClickAction() {
this.$root.$emit('bv::hide::tooltip', `js-ci-action-${this.link}`);
this.isDisabled = true;
+ this.isLoading = true;
axios
.post(`${this.link}.json`)
.then(() => {
this.isDisabled = false;
+ this.isLoading = false;
+
this.$emit('pipelineActionRequestComplete');
})
.catch(() => {
this.isDisabled = false;
+ this.isLoading = false;
createFlash(__('An error occurred while making the request.'));
});
@@ -82,10 +88,10 @@ 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"
@click="onClickAction"
>
- <icon :name="actionIcon" />
+ <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
+ <icon v-else :name="actionIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index a49dc311bd0..ba0dea626dc 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -24,6 +24,7 @@ export default {
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
+ :action="stage.status.action"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 348c407f1b5..d5c124dc0ca 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -3,11 +3,13 @@ import _ from 'underscore';
import stageColumnMixin from 'ee_else_ce/pipelines/mixins/stage_column_mixin';
import JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
+import ActionComponent from './action_component.vue';
export default {
components: {
JobItem,
JobGroupDropdown,
+ ActionComponent,
},
mixins: [stageColumnMixin],
props: {
@@ -29,6 +31,16 @@ export default {
required: false,
default: '',
},
+ action: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ hasAction() {
+ return !_.isEmpty(this.action);
+ },
},
methods: {
groupId(group) {
@@ -42,7 +54,18 @@ export default {
</script>
<template>
<li :class="stageConnectorClass" class="stage-column">
- <div class="stage-name">{{ title }}</div>
+ <div class="stage-name position-relative">
+ {{ title }}
+ <action-component
+ v-if="hasAction"
+ :action-icon="action.icon"
+ :tooltip-text="action.title"
+ :link="action.path"
+ class="js-stage-action stage-action position-absolute position-top-0 rounded"
+ @pipelineActionRequestComplete="pipelineActionRequestComplete"
+ />
+ </div>
+
<div class="builds-container">
<ul>
<li
diff --git a/app/assets/javascripts/pipelines/components/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipeline_triggerer.vue
new file mode 100644
index 00000000000..740b54cd8e0
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipeline_triggerer.vue
@@ -0,0 +1,35 @@
+<script>
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+export default {
+ components: {
+ UserAvatarLink,
+ },
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ user() {
+ return this.pipeline.user;
+ },
+ },
+};
+</script>
+<template>
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-triggerer">
+ <user-avatar-link
+ v-if="user"
+ :link-href="user.path"
+ :img-src="user.avatar_url"
+ :img-size="26"
+ :tooltip-text="user.name"
+ class="prepend-left-default js-pipeline-url-user"
+ />
+ <span v-else class="prepend-left-default js-pipeline-url-api api">
+ {{ s__('Pipelines|API') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 3e7bf20470c..c41ecab1294 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -59,19 +59,10 @@ export default {
};
</script>
<template>
- <div class="table-section section-15 d-none d-sm-none d-md-block pipeline-tags">
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags">
<gl-link :href="pipeline.path" class="js-pipeline-url-link">
<span class="pipeline-id">#{{ pipeline.id }}</span>
</gl-link>
- <span>by</span>
- <user-avatar-link
- v-if="user"
- :link-href="user.path"
- :img-src="user.avatar_url"
- :tooltip-text="user.name"
- class="js-pipeline-url-user"
- />
- <span v-if="!user" class="js-pipeline-url-api api"> API </span>
<div class="label-container">
<span
v-if="pipeline.flags.latest"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index fcd1f119df0..03d332cd430 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,4 +1,5 @@
<script>
+import { GlTooltipDirective } from '@gitlab/ui';
import PipelinesTableRowComponent from './pipelines_table_row.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
import eventHub from '../event_hub';
@@ -13,6 +14,9 @@ export default {
PipelinesTableRowComponent,
PipelineStopModal,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
pipelines: {
type: Array,
@@ -62,16 +66,19 @@ export default {
<template>
<div class="ci-table">
<div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-10 js-pipeline-status pipeline-status" role="rowheader">
+ <div class="table-section section-10 js-pipeline-status" role="rowheader">
{{ s__('Pipeline|Status') }}
</div>
- <div class="table-section section-15 js-pipeline-info pipeline-info" role="rowheader">
+ <div class="table-section section-10 js-pipeline-info pipeline-info" role="rowheader">
{{ s__('Pipeline|Pipeline') }}
</div>
+ <div class="table-section section-10 js-triggerer-info triggerer-info" role="rowheader">
+ {{ s__('Pipeline|Triggerer') }}
+ </div>
<div class="table-section section-20 js-pipeline-commit pipeline-commit" role="rowheader">
{{ s__('Pipeline|Commit') }}
</div>
- <div class="table-section section-20 js-pipeline-stages pipeline-stages" role="rowheader">
+ <div class="table-section section-15 js-pipeline-stages pipeline-stages" role="rowheader">
{{ s__('Pipeline|Stages') }}
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 1c44427e720..e32e2f785bd 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -5,6 +5,7 @@ import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import CiBadge from '../../vue_shared/components/ci_badge_link.vue';
import PipelineStage from './stage.vue';
import PipelineUrl from './pipeline_url.vue';
+import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
import CommitComponent from '../../vue_shared/components/commit.vue';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
@@ -23,6 +24,7 @@ export default {
CommitComponent,
PipelineStage,
PipelineUrl,
+ PipelineTriggerer,
CiBadge,
PipelinesTimeago,
LoadingButton,
@@ -264,8 +266,9 @@ export default {
</div>
<pipeline-url :pipeline="pipeline" :auto-devops-help-path="autoDevopsHelpPath" />
+ <pipeline-triggerer :pipeline="pipeline" />
- <div class="table-section section-20">
+ <div class="table-section section-wrap section-20">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Commit') }}</div>
<div class="table-mobile-content">
<commit-component
@@ -281,7 +284,7 @@ export default {
</div>
</div>
- <div class="table-section section-wrap section-20 stage-cell">
+ <div class="table-section section-wrap section-15 stage-cell">
<div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Stages') }}</div>
<div class="table-mobile-content">
<template v-if="pipeline.details.stages.length > 0">
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 6660f8120f8..b8976f77bac 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -34,6 +34,7 @@ export default () => {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
+ mediator: this.mediator,
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 784eec1ea55..ea82ff4e340 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { slugifyWithHyphens } from '../lib/utils/text_utility';
+import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false;
@@ -114,71 +115,71 @@ const bindEvents = () => {
const value = $(this).val();
const templates = {
rails: {
- text: 'Ruby on Rails',
+ text: s__('ProjectTemplates|Ruby on Rails'),
icon: '.template-option .icon-rails',
},
express: {
- text: 'NodeJS Express',
+ text: s__('ProjectTemplates|NodeJS Express'),
icon: '.template-option .icon-express',
},
spring: {
- text: 'Spring',
+ text: s__('ProjectTemplates|Spring'),
icon: '.template-option .icon-spring',
},
iosswift: {
- text: 'iOS (Swift)',
+ text: s__('ProjectTemplates|iOS (Swift)'),
icon: '.template-option svg.icon-gitlab',
},
dotnetcore: {
- text: '.NET Core',
+ text: s__('ProjectTemplates|.NET Core'),
icon: '.template-option .icon-dotnet',
},
android: {
- text: 'Android',
+ text: s__('ProjectTemplates|Android'),
icon: '.template-option svg.icon-android',
},
gomicro: {
- text: 'Go Micro',
+ text: s__('ProjectTemplates|Go Micro'),
icon: '.template-option .icon-gomicro',
},
hugo: {
- text: 'Pages/Hugo',
+ text: s__('ProjectTemplates|Pages/Hugo'),
icon: '.template-option .icon-hugo',
},
jekyll: {
- text: 'Pages/Jekyll',
+ text: s__('ProjectTemplates|Pages/Jekyll'),
icon: '.template-option .icon-jekyll',
},
plainhtml: {
- text: 'Pages/Plain HTML',
+ text: s__('ProjectTemplates|Pages/Plain HTML'),
icon: '.template-option .icon-plainhtml',
},
gitbook: {
- text: 'Pages/GitBook',
+ text: s__('ProjectTemplates|Pages/GitBook'),
icon: '.template-option .icon-gitbook',
},
hexo: {
- text: 'Pages/Hexo',
+ text: s__('ProjectTemplates|Pages/Hexo'),
icon: '.template-option .icon-hexo',
},
nfhugo: {
- text: 'Netlify/Hugo',
+ text: s__('ProjectTemplates|Netlify/Hugo'),
icon: '.template-option .icon-netlify',
},
nfjekyll: {
- text: 'Netlify/Jekyll',
+ text: s__('ProjectTemplates|Netlify/Jekyll'),
icon: '.template-option .icon-netlify',
},
nfplainhtml: {
- text: 'Netlify/Plain HTML',
+ text: s__('ProjectTemplates|Netlify/Plain HTML'),
icon: '.template-option .icon-netlify',
},
nfgitbook: {
- text: 'Netlify/GitBook',
+ text: s__('ProjectTemplates|Netlify/GitBook'),
icon: '.template-option .icon-netlify',
},
nfhexo: {
- text: 'Netlify/Hexo',
+ text: s__('ProjectTemplates|Netlify/Hexo'),
icon: '.template-option .icon-netlify',
},
};
@@ -217,6 +218,12 @@ const bindEvents = () => {
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
+ $('.js-import-git-toggle-button').on('click', () => {
+ const $projectMirror = $('#project_mirror');
+
+ $projectMirror.attr('disabled', !$projectMirror.attr('disabled'));
+ });
+
$projectName.on('keyup change', () => {
onProjectNameChange($projectName, $projectPath);
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
index b803da798d5..def2f091947 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class ProtectedTagAccessDropdown {
constructor(options) {
this.options = options;
@@ -15,7 +17,7 @@ export default class ProtectedTagAccessDropdown {
if ($el.is('.is-active')) {
return item.text;
}
- return 'Select';
+ return __('Select');
},
clicked(options) {
options.e.preventDefault();
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index fddf2674cbb..03a5fe6b353 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagCreate {
constructor() {
@@ -27,7 +28,7 @@ export default class ProtectedTagCreate {
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
- defaultToggleLabel: 'Protected Tag',
+ defaultToggleLabel: __('Protected Tag'),
fieldName: 'protected_tag[name]',
onSelect: this.onSelectCallback,
getData: ProtectedTagCreate.getProtectedTags,
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index c52497e62f2..70bfd71abce 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,6 +1,7 @@
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
+import { __ } from '~/locale';
export default class ProtectedTagEdit {
constructor(options) {
@@ -47,7 +48,11 @@ export default class ProtectedTagEdit {
.catch(() => {
this.$allowedToCreateDropdownButton.enable();
- flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
+ flash(
+ __('Failed to update tag!'),
+ 'alert',
+ document.querySelector('.js-protected-tags-list'),
+ );
});
}
}
diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js
index edc2293915f..4dd0175e528 100644
--- a/app/assets/javascripts/raven/index.js
+++ b/app/assets/javascripts/raven/index.js
@@ -4,8 +4,11 @@ const index = function index() {
RavenConfig.init({
sentryDsn: gon.sentry_dsn,
currentUserId: gon.current_user_id,
- whitelistUrls: [gon.gitlab_url],
- isProduction: process.env.NODE_ENV,
+ whitelistUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
release: gon.revision,
tags: {
revision: gon.revision,
diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js
index 338006ce2b9..7259e0df104 100644
--- a/app/assets/javascripts/raven/raven_config.js
+++ b/app/assets/javascripts/raven/raven_config.js
@@ -1,5 +1,6 @@
import Raven from 'raven-js';
import $ from 'jquery';
+import { __ } from '~/locale';
const IGNORE_ERRORS = [
// Random plugins/extensions
@@ -9,9 +10,9 @@ const IGNORE_ERRORS = [
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
- "Can't find variable: ZiteReader",
- 'jigsaw is not defined',
- 'ComboSearch is not defined',
+ __("Can't find variable: ZiteReader"),
+ __('jigsaw is not defined'),
+ __('ComboSearch is not defined'),
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
@@ -61,7 +62,7 @@ const RavenConfig = {
release: this.options.release,
tags: this.options.tags,
whitelistUrls: this.options.whitelistUrls,
- environment: this.options.isProduction ? 'production' : 'development',
+ environment: this.options.environment,
ignoreErrors: this.IGNORE_ERRORS,
ignoreUrls: this.IGNORE_URLS,
shouldSendCallback: this.shouldSendSample.bind(this),
@@ -80,7 +81,7 @@ const RavenConfig = {
handleRavenErrors(event, req, config, err) {
const error = err || req.statusText;
- const responseText = req.responseText || 'Unknown response text';
+ const responseText = req.responseText || __('Unknown response text');
Raven.captureMessage(error, {
extra: {
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 7ed1b407ddd..0958b9fa926 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -86,7 +86,7 @@ export default {
</div>
<div
- v-if="assets.links.length || assets.sources.length"
+ v-if="assets.links.length || (assets.sources && assets.sources.length)"
class="card-text prepend-top-default"
>
<b>
@@ -103,7 +103,7 @@ export default {
</li>
</ul>
- <div v-if="assets.sources.length" class="dropdown">
+ <div v-if="assets.sources && assets.sources.length" class="dropdown">
<button
type="button"
class="btn btn-link"
diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js
index 35632218269..10560d0ae8e 100644
--- a/app/assets/javascripts/reports/store/utils.js
+++ b/app/assets/javascripts/reports/store/utils.js
@@ -1,4 +1,4 @@
-import { sprintf, n__, s__ } from '~/locale';
+import { sprintf, n__, s__, __ } from '~/locale';
import {
STATUS_FAILED,
STATUS_SUCCESS,
@@ -38,12 +38,12 @@ const textBuilder = results => {
export const summaryTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
- return `${name} contained ${resultsString}`;
+ return sprintf(__('%{name} contained %{resultsString}'), { name, resultsString });
};
export const reportTextBuilder = (name = '', results = {}) => {
const resultsString = textBuilder(results);
- return `${name} found ${resultsString}`;
+ return sprintf(__('%{name} found %{resultsString}'), { name, resultsString });
};
export const statusIcon = status => {
diff --git a/app/assets/javascripts/serverless/components/url.vue b/app/assets/javascripts/serverless/components/url.vue
index ca53bf6c52a..e47a03f1939 100644
--- a/app/assets/javascripts/serverless/components/url.vue
+++ b/app/assets/javascripts/serverless/components/url.vue
@@ -20,7 +20,7 @@ export default {
<template>
<div class="clipboard-group">
- <div class="url-text-field label label-monospace">{{ uri }}</div>
+ <div class="url-text-field label label-monospace monospace">{{ uri }}</div>
<clipboard-button
:text="uri"
:title="s__('ServerlessURL|Copy URL to clipboard')"
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 7f86741ed29..c0659a0173a 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -2,7 +2,7 @@
import $ from 'jquery';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
-import GfmAutoComplete from '~/gfm_auto_complete';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { __, s__ } from '~/locale';
import Api from '~/api';
import { GlModal, GlTooltipDirective } from '@gitlab/ui';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index ce378e24289..0074d7099dc 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -153,7 +153,7 @@ export default {
data-placement="left"
data-boundary="viewport"
>
- <i v-if="hasNoUsers" aria-label="No Assignee" class="fa fa-user"> </i>
+ <i v-if="hasNoUsers" aria-label="None" class="fa fa-user"> </i>
<button
v-for="(user, index) in users"
v-if="shouldRenderCollapsedAssignee(index)"
@@ -184,8 +184,8 @@ export default {
<i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
</span>
<template v-if="hasNoUsers">
- <span class="assign-yourself no-value">
- No assignee
+ <span class="assign-yourself no-value qa-assign-yourself">
+ None
<template v-if="editable">
- <button type="button" class="btn-link" @click="assignSelf">assign yourself</button>
</template>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 225ebb61195..110175a6779 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import _ from 'underscore';
+import { __ } from '~/locale';
function isValidProjectId(id) {
return id > 0;
@@ -40,7 +41,9 @@ class SidebarMoveIssue {
this.mediator
.fetchAutocompleteProjects(searchTerm)
.then(callback)
- .catch(() => new window.Flash('An error occurred while fetching projects autocomplete.'));
+ .catch(
+ () => new window.Flash(__('An error occurred while fetching projects autocomplete.')),
+ );
},
renderRow: project => `
<li>
@@ -72,7 +75,7 @@ class SidebarMoveIssue {
this.$confirmButton.disable().addClass('is-loading');
this.mediator.moveIssue().catch(() => {
- window.Flash('An error occurred while moving the issue.');
+ window.Flash(__('An error occurred while moving the issue.'));
this.$confirmButton.enable().removeClass('is-loading');
});
}
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 3e040ec8428..22ac8df9699 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -2,6 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
import Store from './stores/sidebar_store';
+import { __ } from '~/locale';
export default class SidebarMediator {
constructor(options) {
@@ -45,7 +46,7 @@ export default class SidebarMediator {
.then(data => {
this.processFetchedData(data);
})
- .catch(() => new Flash('Error occurred when fetching sidebar data'));
+ .catch(() => new Flash(__('Error occurred when fetching sidebar data')));
}
processFetchedData(data) {
diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js
index 873a506a92f..fe08d2c7ebb 100644
--- a/app/assets/javascripts/snippet/snippet_embed.js
+++ b/app/assets/javascripts/snippet/snippet_embed.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default () => {
const { protocol, host, pathname } = window.location;
const shareBtn = document.querySelector('.js-share-btn');
@@ -10,7 +12,7 @@ export default () => {
shareBtn.classList.add('is-active');
embedBtn.classList.remove('is-active');
snippetUrlArea.value = url;
- embedAction.innerText = 'Share';
+ embedAction.innerText = __('Share');
});
embedBtn.addEventListener('click', () => {
@@ -18,6 +20,6 @@ export default () => {
shareBtn.classList.remove('is-active');
const scriptTag = `<script src="${url}.js"></script>`;
snippetUrlArea.value = scriptTag;
- embedAction.innerText = 'Embed';
+ embedAction.innerText = __('Embed');
});
};
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 6065770e68d..78609ce0610 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -3,6 +3,7 @@
import $ from 'jquery';
import Api from '../api';
import TemplateSelector from '../blob/template_selector';
+import { __ } from '~/locale';
export default class IssuableTemplateSelector extends TemplateSelector {
constructor(...args) {
@@ -25,7 +26,7 @@ export default class IssuableTemplateSelector extends TemplateSelector {
$('.no-template', this.dropdown.parent()).on('click', () => {
this.currentTemplate.content = '';
this.setInputValueToTemplateContent();
- $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
+ $('.dropdown-toggle-text', this.dropdown).text(__('Choose a template'));
});
}
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index e5dd7a465ea..9c7c10d9864 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -4,6 +4,7 @@ import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
+import { __ } from '~/locale';
const SCROLL_MARGIN = 5;
@@ -78,7 +79,8 @@ export default class GLTerminal {
}
handleSocketFailure() {
- this.terminal.write('\r\nConnection failure');
+ this.terminal.write('\r\n');
+ this.terminal.write(__('Connection failure'));
}
addScrollListener(onScrollLimit) {
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 1a98564ff55..ca0fc0700ad 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class U2FError {
constructor(errorCode, u2fFlowType) {
this.errorCode = errorCode;
@@ -8,15 +10,17 @@ export default class U2FError {
message() {
if (this.errorCode === window.u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) {
- return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.';
+ return __(
+ 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.',
+ );
} else if (this.errorCode === window.u2f.ErrorCodes.DEVICE_INELIGIBLE) {
if (this.u2fFlowType === 'authenticate') {
- return 'This device has not been registered with us.';
+ return __('This device has not been registered with us.');
}
if (this.u2fFlowType === 'register') {
- return 'This device has already been registered with us.';
+ return __('This device has already been registered with us.');
}
}
- return 'There was a problem communicating with your device.';
+ return __('There was a problem communicating with your device.');
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
new file mode 100644
index 00000000000..040315b3c66
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_alert_message.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { WARNING, DANGER, WARNING_MESSAGE_CLASS, DANGER_MESSAGE_CLASS } from '../constants';
+
+export default {
+ name: 'MrWidgetAlertMessage',
+ components: {
+ GlLink,
+ Icon,
+ },
+ props: {
+ type: {
+ type: String,
+ required: false,
+ default: DANGER,
+ validator: value => [WARNING, DANGER].includes(value),
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ },
+ computed: {
+ messageClass() {
+ if (this.type === WARNING) {
+ return WARNING_MESSAGE_CLASS;
+ } else if (this.type === DANGER) {
+ return DANGER_MESSAGE_CLASS;
+ }
+
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="m-3 ml-5" :class="messageClass">
+ <slot></slot>
+ <gl-link v-if="helpPath" :href="helpPath" target="_blank">
+ <icon :size="16" name="question-o" class="align-middle" />
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index f5a1ff2f6fd..f5fa68308bc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -94,8 +94,8 @@ export default {
</script>
<template>
- <div v-if="hasPipeline || hasCIError" class="ci-widget media js-ci-widget">
- <template v-if="hasCIError">
+ <div class="ci-widget media js-ci-widget">
+ <template v-if="!hasPipeline || hasCIError">
<div
class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
index a38f25cce35..acd8037cfb2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_edit.vue
@@ -30,6 +30,7 @@ export default {
:id="inputId"
:value="value"
class="form-control js-gfm-input append-bottom-default commit-message-edit"
+ dir="auto"
required="required"
rows="7"
@input="$emit('input', $event.target.value)"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index bb76eb1030d..851939d5d4e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -3,6 +3,7 @@ import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
import { __ } from '~/locale';
+import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import MergeRequest from '../../../merge_request';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -21,6 +22,7 @@ export default {
CommitEdit,
CommitMessageDropdown,
},
+ mixins: [readyToMergeMixin],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -94,15 +96,6 @@ export default {
shouldShowMergeOptionsDropdown() {
return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
},
- isMergeButtonDisabled() {
- const { commitMessage } = this;
- return Boolean(
- !commitMessage.length ||
- !this.shouldShowMergeControls ||
- this.isMakingRequest ||
- this.mr.preventMerge,
- );
- },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
new file mode 100644
index 00000000000..0a29d55fbd6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -0,0 +1,5 @@
+export const WARNING = 'warning';
+export const DANGER = 'danger';
+
+export const WARNING_MESSAGE_CLASS = 'warning_message';
+export const DANGER_MESSAGE_CLASS = 'danger_message';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
new file mode 100644
index 00000000000..b2e64506472
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -0,0 +1,13 @@
+export default {
+ computed: {
+ isMergeButtonDisabled() {
+ const { commitMessage } = this;
+ return Boolean(
+ !commitMessage.length ||
+ !this.shouldShowMergeControls ||
+ this.isMakingRequest ||
+ this.mr.preventMerge,
+ );
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 57c4dfbe3b7..705ee05e29f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -12,6 +12,7 @@ import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
+import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
import ClosedState from './components/states/mr_widget_closed.vue';
import MergingState from './components/states/mr_widget_merging.vue';
@@ -46,6 +47,7 @@ export default {
MrWidgetPipelineContainer,
Deployment,
'mr-widget-related-links': WidgetRelatedLinks,
+ MrWidgetAlertMessage,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState,
@@ -110,6 +112,19 @@ export default {
shouldRenderMergedPipeline() {
return this.mr.state === 'merged' && !_.isEmpty(this.mr.mergePipeline);
},
+ showMergePipelineForkWarning() {
+ return Boolean(
+ this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId,
+ );
+ },
+ showTargetBranchAdvancedError() {
+ return Boolean(
+ this.mr.isOpen &&
+ this.mr.pipeline &&
+ this.mr.pipeline.target_sha &&
+ this.mr.pipeline.target_sha !== this.mr.targetBranchSha,
+ );
+ },
},
watch: {
state(newVal, oldVal) {
@@ -328,6 +343,30 @@ export default {
:related-links="mr.relatedLinks"
/>
+ <mr-widget-alert-message
+ v-if="showMergePipelineForkWarning"
+ type="warning"
+ :help-path="mr.mergeRequestPipelinesHelpPath"
+ >
+ {{
+ s__(
+ 'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result',
+ )
+ }}
+ </mr-widget-alert-message>
+
+ <mr-widget-alert-message
+ v-if="showTargetBranchAdvancedError"
+ type="danger"
+ :help-path="mr.mergeRequestPipelinesHelpPath"
+ >
+ {{
+ s__(
+ 'mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging',
+ )
+ }}
+ </mr-widget-alert-message>
+
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 58363f632a9..45708d78886 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -28,9 +28,11 @@ export default class MergeRequestStore {
this.iid = data.iid;
this.title = data.title;
this.targetBranch = data.target_branch;
+ this.targetBranchSha = data.target_branch_sha;
this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
+ this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
@@ -99,6 +101,9 @@ export default class MergeRequestStore {
this.allowCollaboration = data.allow_collaboration;
this.targetProjectFullPath = data.target_project_full_path;
this.sourceProjectFullPath = data.source_project_full_path;
+ this.sourceProjectId = data.source_project_id;
+ this.targetProjectId = data.target_project_id;
+ this.mergePipelinesEnabled = data.merge_pipelines_enabled;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
@@ -112,7 +117,7 @@ export default class MergeRequestStore {
this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing =
- this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
+ this.ciStatus === 'success' || this.ciStatus === 'success-with-warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 944b9c0c083..3ba946e6447 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -133,7 +133,7 @@ export default {
};
</script>
<template>
- <div class="branch-commit">
+ <div class="branch-commit cgray">
<template v-if="shouldShowRefInfo">
<div class="icon-container">
<icon v-if="tag" name="tag" />
@@ -174,7 +174,7 @@ export default {
:tooltip-text="author.username"
class="avatar-image-container"
/>
- <gl-link :href="commitUrl" class="commit-row-message"> {{ title }} </gl-link>
+ <gl-link :href="commitUrl" class="commit-row-message cgray"> {{ title }} </gl-link>
</span>
<span v-else> Can't find HEAD commit for this branch </span>
</div>
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 4155e1bab9c..1e6f4c376c1 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
@@ -1,5 +1,4 @@
<script>
-import { viewerInformationForPath } from './lib/viewer_utils';
import MarkdownViewer from './viewers/markdown_viewer.vue';
import ImageViewer from './viewers/image_viewer.vue';
import DownloadViewer from './viewers/download_viewer.vue';
@@ -24,15 +23,18 @@ export default {
required: false,
default: '',
},
+ type: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
viewer() {
if (!this.path) return null;
+ if (!this.type) return DownloadViewer;
- const previewInfo = viewerInformationForPath(this.path);
- if (!previewInfo) return DownloadViewer;
-
- switch (previewInfo.id) {
+ switch (this.type) {
case 'markdown':
return MarkdownViewer;
case 'image':
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
index f01a51da0b3..ba63683f5c0 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -1,10 +1,12 @@
+import { __ } from '~/locale';
+
const viewers = {
image: {
id: 'image',
},
markdown: {
id: 'markdown',
- previewTitle: 'Preview Markdown',
+ previewTitle: __('Preview Markdown'),
},
};
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
index ad3b3b81ac5..8d77b156aa4 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue
@@ -58,12 +58,11 @@ export default {
const moveX = e.pageX || e.touches[0].pageX;
let leftValue = moveX - this.$refs.swipeFrame.getBoundingClientRect().left;
- const spaceLeft = 20;
const { clientWidth } = this.$refs.swipeFrame;
if (leftValue <= 0) {
leftValue = 0;
- } else if (leftValue > clientWidth - spaceLeft) {
- leftValue = clientWidth - spaceLeft;
+ } else if (leftValue > clientWidth) {
+ leftValue = clientWidth;
}
this.swipeWrapWidth = (leftValue / clientWidth) * 100;
@@ -80,7 +79,7 @@ export default {
document.body.removeEventListener('touchmove', this.dragMove);
},
prepareSwipe() {
- if (this.swipeOldImgInfo && this.swipeNewImgInfo) {
+ if (this.swipeOldImgInfo && this.swipeNewImgInfo && this.swipeOldImgInfo.renderedWidth > 0) {
// Add 2 for border width
this.swipeMaxWidth =
Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2;
@@ -101,6 +100,8 @@ export default {
},
resize: _.throttle(function throttledResize() {
this.swipeBarPos = 0;
+ this.swipeWrapWidth = 0;
+ this.prepareSwipe();
}, 400),
},
};
@@ -111,6 +112,8 @@ export default {
<div
ref="swipeFrame"
:style="{
+ width: swipeMaxPixelWidth,
+ height: swipeMaxPixelHeight,
'user-select': dragging ? 'none' : null,
}"
class="swipe-frame"
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
index d5d967e25bf..53e6efa6ea3 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
@@ -17,15 +17,13 @@ export default {
required: true,
},
},
- data() {
- return {
- milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null,
- milestoneStart: this.milestone.start_date
- ? parsePikadayDate(this.milestone.start_date)
- : null,
- };
- },
computed: {
+ milestoneDue() {
+ return this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null;
+ },
+ milestoneStart() {
+ return this.milestone.start_date ? parsePikadayDate(this.milestone.start_date) : null;
+ },
isMilestoneStarted() {
if (!this.milestoneStart) {
return false;
@@ -72,7 +70,7 @@ export default {
<template>
<div ref="milestoneDetails" class="issue-milestone-details">
<icon :size="16" class="inline icon" name="clock" />
- <span class="milestone-title">{{ milestone.title }}</span>
+ <span class="milestone-title d-inline-block">{{ milestone.title }}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<span class="bold">{{ __('Milestone') }}</span> <br />
<span>{{ milestone.title }}</span> <br />
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index d4d18614f93..b807a35b421 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,4 +1,5 @@
<script>
+import '~/commons/bootstrap';
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from '../../components/issue/issue_milestone.vue';
@@ -45,10 +46,10 @@ export default {
'issuable-info-container': !canReorder,
'card-body': canReorder,
}"
- class="item-body"
+ class="item-body d-flex align-items-center p-2 p-lg-3 p-xl-2 pl-xl-3"
>
- <div class="item-contents">
- <div class="item-title d-flex align-items-center">
+ <div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
+ <div class="item-title d-flex align-items-center mb-1 mb-xl-0">
<icon
v-if="hasState"
v-tooltip
@@ -65,13 +66,15 @@ export default {
name="eye-slash"
:size="16"
:title="__('Confidential')"
- class="confidential-icon append-right-4"
+ class="confidential-icon append-right-4 align-self-baseline align-self-md-auto mt-xl-0"
:aria-label="__('Confidential')"
/>
<a :href="computedPath" class="sortable-link">{{ title }}</a>
</div>
- <div class="item-meta">
- <div class="d-flex align-items-center item-path-id">
+ <div class="item-meta d-flex flex-wrap mt-xl-0 justify-content-xl-end flex-xl-nowrap">
+ <div
+ class="d-flex align-items-center item-path-id order-md-0 mt-md-0 mt-1 ml-xl-2 mr-xl-auto"
+ >
<icon
v-if="hasState"
v-tooltip
@@ -81,11 +84,16 @@ export default {
:title="stateTitle"
:aria-label="state"
data-html="true"
+ class="d-xl-none"
/>
- <span v-tooltip :title="itemPath" class="path-id-text">{{ itemPath }}</span>
+ <span v-tooltip :title="itemPath" class="path-id-text d-inline-block">{{
+ itemPath
+ }}</span>
{{ pathIdSeparator }}{{ itemId }}
</div>
- <div class="item-meta-child d-flex align-items-center">
+ <div
+ class="item-meta-child d-flex align-items-center order-0 flex-wrap mr-md-1 ml-md-auto ml-xl-2 flex-xl-nowrap"
+ >
<span v-if="hasPipeline" class="mr-ci-status pr-2">
<a :href="pipelineStatus.details_path">
<ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
@@ -102,7 +110,7 @@ export default {
<issue-assignees
v-if="assignees.length"
:assignees="assignees"
- class="item-assignees d-inline-flex"
+ class="item-assignees d-inline-flex align-items-center align-self-end ml-auto ml-md-0 mb-md-0 order-2 flex-xl-grow-0 mt-xl-0 mr-xl-1"
/>
</div>
</div>
@@ -112,7 +120,7 @@ export default {
v-tooltip
:disabled="removeDisabled"
type="button"
- class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button"
+ class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button mr-xl-0 align-self-xl-center"
title="Remove"
aria-label="Remove"
@click="onRemoveRequest"
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 c5a2aa1f2af..32783b85df4 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
@@ -1,8 +1,10 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
export default {
- components: { Icon },
+ components: { Icon, GlButton, GlLoadingIcon },
+ directives: { 'gl-tooltip': GlTooltipDirective },
props: {
canApply: {
type: Boolean,
@@ -21,7 +23,6 @@ export default {
},
data() {
return {
- isAppliedSuccessfully: false,
isApplying: false,
};
},
@@ -47,14 +48,19 @@ export default {
</a>
</div>
<span v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</span>
- <button
- v-if="canApply"
- type="button"
- class="btn qa-apply-btn"
+ <div v-if="isApplying" class="d-flex align-items-center text-secondary">
+ <gl-loading-icon class="d-flex-center mr-2" />
+ <span>{{ __('Applying suggestion') }}</span>
+ </div>
+ <gl-button
+ v-else-if="canApply"
+ v-gl-tooltip.viewport="__('This also resolves the discussion')"
+ class="btn-inverted qa-apply-btn"
:disabled="isApplying"
+ variant="success"
@click="applySuggestion"
>
{{ __('Apply suggestion') }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index cafd3a515ea..c09bdfec250 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -17,10 +17,10 @@ export default {
<template>
<tr class="line_holder" :class="lineType">
- <td class="diff-line-num old_line" :class="lineType">
+ <td class="diff-line-num old_line border-top-0 border-bottom-0" :class="lineType">
{{ line.old_line }}
</td>
- <td class="diff-line-num new_line" :class="lineType">
+ <td class="diff-line-num new_line border-top-0 border-bottom-0" :class="lineType">
{{ line.new_line }}
</td>
<td class="line_content" :class="lineType">
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 acc179b3834..3c86b7e4c61 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -22,6 +22,7 @@ import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
+import initMRPopovers from '~/mr_popover/';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -71,6 +72,9 @@ export default {
);
},
},
+ mounted() {
+ initMRPopovers(this.$el.querySelectorAll('.gfm-merge_request'));
+ },
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
index 3074ea859cc..6d2612556ff 100644
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import 'select2/select2';
+import 'select2';
export default {
name: 'Select2Select',
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
index 373794fb1f2..05446903286 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -14,10 +14,12 @@ export default {
},
computed: {
labelsList() {
- const labelsString = this.labels
- .slice(0, 5)
- .map(label => label.title)
- .join(', ');
+ const labelsString = this.labels.length
+ ? this.labels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ')
+ : s__('LabelSelect|Labels');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
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 f9773622001..4dbfd1ba6f4 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
@@ -1,11 +1,13 @@
<script>
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
export default {
name: 'UserPopover',
components: {
+ Icon,
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
@@ -68,16 +70,27 @@ export default {
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</div>
<div class="text-secondary">
- <div v-if="user.bio" class="js-bio">{{ user.bio }}</div>
- <div v-if="user.organization" class="js-organization">{{ user.organization }}</div>
+ <div v-if="user.bio" class="js-bio d-flex mb-1">
+ <icon name="profile" css-classes="category-icon" />
+ <span class="ml-1">{{ user.bio }}</span>
+ </div>
+ <div v-if="user.organization" class="js-organization d-flex mb-1">
+ <icon v-show="!jobInfoIsLoading" name="work" css-classes="category-icon" />
+ <span class="ml-1">{{ user.organization }}</span>
+ </div>
<gl-skeleton-loading
v-if="jobInfoIsLoading"
:lines="1"
class="animation-container-small mb-1"
/>
</div>
- <div class="text-secondary">
- {{ user.location }}
+ <div class="js-location text-secondary d-flex">
+ <icon
+ v-show="!locationIsLoading && user.location"
+ name="location"
+ css-classes="category-icon"
+ />
+ <span class="ml-1">{{ user.location }}</span>
<gl-skeleton-loading
v-if="locationIsLoading"
:lines="1"
diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js
index 549d27e96d9..2d1f7a1cfd0 100644
--- a/app/assets/javascripts/vue_shared/directives/tooltip.js
+++ b/app/assets/javascripts/vue_shared/directives/tooltip.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import '~/commons/bootstrap';
export default {
bind(el) {
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 42ede599bc6..a104d035a9a 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -8,14 +8,10 @@
&-warning {
background-color: $orange-100;
}
-
- &-failed {
- background-color: $red-100;
- }
}
&-body {
- height: 120px;
+ min-height: 120px;
&-warning {
background-color: $orange-50;
@@ -26,20 +22,14 @@
}
}
- &-time-ago {
- &-icon {
- color: $gray-500;
- }
+ &-icon {
+ color: $gray-500;
}
&-footer {
border-radius: $gl-padding;
height: $gl-padding-32;
- &-failed {
- background-color: $red-100;
- }
-
&-arrow {
color: $gray-300;
}
@@ -56,6 +46,13 @@
}
}
+ &-header,
+ &-footer {
+ &-failed {
+ background-color: $red-100;
+ }
+ }
+
&-skeleton-info {
border-radius: $gl-padding;
height: $gl-padding;
diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss
index 7d46b262a69..d0aa6ec78aa 100644
--- a/app/assets/stylesheets/components/popover.scss
+++ b/app/assets/stylesheets/components/popover.scss
@@ -5,6 +5,30 @@
padding: $gl-padding-8;
font-size: $gl-font-size-small;
line-height: $gl-line-height;
+
+ .category-icon {
+ color: $gray-600;
+ }
+ }
+
+ &.blue {
+ background-color: $blue-600;
+
+ .popover-body {
+ color: $white-light;
+ }
+
+ &.bs-popover-bottom {
+ .arrow::after {
+ border-bottom-color: $blue-600;
+ }
+ }
+
+ &.bs-popover-top {
+ .arrow::after {
+ border-top-color: $blue-600;
+ }
+ }
}
}
@@ -14,3 +38,16 @@
line-height: 1.33;
}
}
+
+.onboarding-welcome-page {
+ .popover {
+ min-width: auto;
+ max-width: 40%;
+
+ .popover-body {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ font-size: $gl-font-size-small;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 628dffc39f1..7f9cf1266b1 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -11,20 +11,14 @@ $item-weight-max-width: 48px;
}
}
+.sortable-link {
+ max-width: 85%;
+}
+
.item-body {
- display: flex;
position: relative;
- align-items: center;
- padding: $gl-padding-8;
line-height: $gl-line-height;
- .item-contents {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- flex-grow: 1;
- }
-
.issue-token-state-icon-open {
color: $green-500;
}
@@ -52,157 +46,130 @@ $item-weight-max-width: 48px;
}
.confidential-icon {
- align-self: baseline;
color: $orange-600;
- margin-right: $gl-padding-4;
}
.item-title {
flex-basis: 100%;
- margin-bottom: $gl-padding-8;
font-size: $gl-font-size-small;
&.mr-title {
font-weight: $gl-font-weight-bold;
}
- .sortable-link {
- max-width: 85%;
- }
-
.issue-token-state-icon-open,
.issue-token-state-icon-closed {
display: none;
}
}
- .item-meta {
- display: flex;
- flex-wrap: wrap;
- flex-basis: 100%;
- font-size: $gl-font-size-small;
+ .item-path-id .path-id-text,
+ .item-milestone .milestone-title,
+ .item-due-date,
+ .item-weight .board-card-info-text {
color: $gl-text-color-secondary;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+}
- .item-meta-child {
- order: 0;
- display: flex;
- flex-wrap: wrap;
- flex-basis: 100%;
-
- .item-due-date,
- .item-weight {
- margin-left: $gl-padding-8;
- }
+.item-meta {
+ flex-basis: 100%;
+ font-size: $gl-font-size-small;
+ color: $gl-text-color-secondary;
- .item-milestone,
- .item-weight {
- cursor: help;
- }
+ .item-meta-child {
+ flex-basis: 100%;
+ }
- .item-milestone {
- text-decoration: none;
- max-width: $item-milestone-max-width;
- }
+ .item-milestone,
+ .item-weight {
+ cursor: help;
+ }
- .item-due-date {
- margin-right: 0;
- }
+ .item-milestone {
+ text-decoration: none;
+ max-width: $item-milestone-max-width;
- .item-weight {
- margin-right: 0;
- max-width: $item-weight-max-width;
- }
+ .ic-clock {
+ color: $gl-text-color-tertiary;
+ margin-right: $gl-padding-4;
}
+ }
- .item-path-id .path-id-text,
- .item-milestone .milestone-title,
- .item-due-date,
- .item-weight .board-card-info-text {
- color: $gl-text-color-secondary;
- display: inline-block;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- }
+ .item-weight {
+ max-width: $item-weight-max-width;
+ }
- .item-path-id {
- margin-top: $gl-padding-4;
- font-size: $gl-font-size-xs;
- white-space: nowrap;
+ .item-assignees {
+ .user-avatar-link {
+ margin-right: -$gl-padding-4;
- .path-id-text {
- font-weight: $gl-font-weight-bold;
- max-width: $item-path-max-width;
+ &:nth-of-type(1) {
+ z-index: 2;
}
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: block;
+ &:nth-of-type(2) {
+ z-index: 1;
}
- &:not(.mr-item-path) {
- order: 1;
+ &:last-child {
+ margin-right: 0;
}
}
- .item-milestone .ic-clock {
- color: $gl-text-color-tertiary;
- margin-right: $gl-padding-4;
+ .avatar {
+ height: $gl-padding;
+ width: $gl-padding;
+ margin-right: 0;
+ vertical-align: bottom;
}
- .item-assignees {
- order: 2;
- align-self: flex-end;
- align-items: center;
- margin-left: auto;
-
- .user-avatar-link {
- margin-right: -$gl-padding-4;
-
- &:nth-of-type(1) {
- z-index: 2;
- }
+ .avatar-counter {
+ height: $gl-padding;
+ border: 1px solid transparent;
+ background-color: $gl-text-color-tertiary;
+ font-weight: $gl-font-weight-bold;
+ padding: 0 $gl-padding-4;
+ line-height: $gl-padding;
+ }
+ }
+}
- &:nth-of-type(2) {
- z-index: 1;
- }
+.item-path-id {
+ font-size: $gl-font-size-xs;
+ white-space: nowrap;
- &:last-child {
- margin-right: 0;
- }
- }
+ .path-id-text {
+ font-weight: $gl-font-weight-bold;
+ max-width: $item-path-max-width;
+ }
- .avatar {
- height: $gl-padding;
- width: $gl-padding;
- margin-right: 0;
- vertical-align: bottom;
- }
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: block;
+ }
- .avatar-counter {
- height: $gl-padding;
- border: 1px solid transparent;
- background-color: $gl-text-color-tertiary;
- font-weight: $gl-font-weight-bold;
- padding: 0 $gl-padding-4;
- line-height: $gl-padding;
- }
+ @include media-breakpoint-down(sm) {
+ &:not(.mr-item-path) {
+ order: 1;
}
}
+}
- .btn-item-remove {
- position: absolute;
- right: 0;
- top: $gl-padding-4 / 2;
- padding: $gl-padding-4;
- margin-right: $gl-padding-4 / 2;
- line-height: 0;
- border-color: transparent;
- color: $gl-text-color-secondary;
+.btn-item-remove {
+ position: absolute;
+ right: 0;
+ top: $gl-padding-4 / 2;
+ padding: $gl-padding-4;
+ margin-right: $gl-padding-4 / 2;
+ line-height: 0;
+ border-color: transparent;
+ color: $gl-text-color-secondary;
- &:hover {
- color: $gl-text-color;
- }
+ &:hover {
+ color: $gl-text-color;
}
}
@@ -212,74 +179,52 @@ $item-weight-max-width: 48px;
}
@include media-breakpoint-up(sm) {
- .item-body {
- .item-contents .item-title {
- .mr-title-link,
- .sortable-link {
- max-width: 90%;
- }
- }
+ .sortable-link {
+ max-width: 90%;
}
}
/* Small devices (landscape phones, 768px and up) */
@include media-breakpoint-up(md) {
+ .sortable-link {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ max-width: 100%;
+ }
+
.item-body {
.item-contents {
min-width: 0;
+ }
- .item-title {
- flex-basis: unset;
- // 95% because we compensate
- // for remove button which is
- // positioned absolutely
- width: 95%;
- margin-bottom: $gl-padding-4;
-
- .mr-title-link,
- .sortable-link {
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- max-width: 100%;
- }
- }
-
- .item-meta {
- .item-path-id {
- order: 0;
- margin-top: 0;
- }
-
- .item-meta-child {
- flex-basis: unset;
- margin-left: auto;
- margin-right: $gl-padding-4;
-
- ~ .item-assignees {
- margin-left: $gl-padding-4;
- }
- }
-
- .item-assignees {
- margin-bottom: 0;
- margin-left: 0;
- order: 2;
- }
- }
+ .item-title {
+ flex-basis: unset;
+ // 95% because we compensate
+ // for remove button which is
+ // positioned absolutely
+ width: 95%;
}
.btn-item-remove {
order: 1;
}
}
+
+ .item-meta {
+ .item-meta-child {
+ flex-basis: unset;
+
+ ~ .item-assignees {
+ margin-left: $gl-padding-4;
+ }
+ }
+ }
}
/* Medium devices (desktops, 992px and up) */
@include media-breakpoint-up(lg) {
.item-body {
- padding: $gl-padding;
-
.item-title {
font-size: $gl-font-size;
}
@@ -287,107 +232,60 @@ $item-weight-max-width: 48px;
.item-meta .item-path-id {
font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small`
}
-
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- margin-right: $gl-padding-4;
- }
}
}
/* Large devices (large desktops, 1200px and up) */
@include media-breakpoint-up(xl) {
.item-body {
- padding: $gl-padding-8;
- padding-left: $gl-padding;
+ .item-title {
+ min-width: 0;
+ width: auto;
+ flex-basis: unset;
+ font-weight: $gl-font-weight-normal;
- .item-contents {
- flex-wrap: nowrap;
- overflow: hidden;
-
- .item-title {
- display: flex;
- margin-bottom: 0;
- min-width: 0;
- width: auto;
- flex-basis: unset;
- font-weight: $gl-font-weight-normal;
-
- .mr-title-link,
- .sortable-link {
- display: block;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- line-height: 1.3;
- }
-
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: block;
- margin-right: $gl-padding-8;
- }
-
- .confidential-icon {
- align-self: auto;
- margin-top: 0;
- }
+ .issue-token-state-icon-open,
+ .issue-token-state-icon-closed {
+ display: block;
+ margin-right: $gl-padding-8;
}
+ }
+ }
- .item-meta {
- margin-top: 0;
- justify-content: flex-end;
- flex: 1;
- flex-wrap: nowrap;
-
- .item-path-id {
- order: 0;
- margin-top: 0;
- margin-left: $gl-padding-8;
- margin-right: auto;
-
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: none;
- }
- }
-
- .item-meta-child {
- margin-left: $gl-padding-8;
- flex-wrap: nowrap;
- }
-
- .item-assignees {
- flex-grow: 0;
- margin-top: 0;
- margin-right: $gl-padding-4;
-
- .avatar {
- height: $gl-padding-24;
- width: $gl-padding-24;
- }
-
- .avatar-counter {
- height: $gl-padding-24;
- min-width: $gl-padding-24;
- line-height: $gl-padding-24;
- border-radius: $gl-padding-24;
- }
- }
- }
+ .item-contents {
+ overflow: hidden;
+ }
+
+ .item-meta {
+ flex: 1;
+ }
+
+ .item-assignees {
+ .avatar {
+ height: $gl-padding-24;
+ width: $gl-padding-24;
}
- .btn-item-remove {
- position: relative;
- align-self: center;
- top: initial;
- right: 0;
- margin-right: 0;
- padding: $btn-sm-side-margin;
+ .avatar-counter {
+ height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ line-height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ }
+ }
- &:hover {
- border-color: $border-color;
- }
+ .btn-item-remove {
+ position: relative;
+ top: initial;
+ right: 0;
+ padding: $btn-sm-side-margin;
+
+ &:hover {
+ border-color: $border-color;
}
}
+
+ .sortable-link {
+ line-height: 1.3;
+ }
}
diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss
new file mode 100644
index 00000000000..91d16c8e98d
--- /dev/null
+++ b/app/assets/stylesheets/components/toast.scss
@@ -0,0 +1,3 @@
+.toast-close {
+ font-size: $default-icon-size !important;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index be0073559b6..dffd5e70edb 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -200,12 +200,12 @@ li.note {
}
}
-.warning_message {
- border-left: 4px solid $orange-200;
- color: $orange-700;
+@mixin message($background-color, $border-color, $text-color) {
+ border-left: 4px solid $border-color;
+ color: $text-color;
padding: 10px;
margin-bottom: 10px;
- background: $orange-100;
+ background: $background-color;
padding-left: 20px;
&.centered {
@@ -213,6 +213,14 @@ li.note {
}
}
+.warning_message {
+ @include message($orange-100, $orange-200, $orange-700);
+}
+
+.danger_message {
+ @include message($red-100, $red-200, $red-900);
+}
+
.gitlab-promo {
a {
color: $gl-gray-350;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b90db135b4a..8fb4027bf97 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -351,6 +351,10 @@
// Expects up to 3 digits on the badge
margin-right: 40px;
}
+
+ .dropdown-menu-content {
+ padding: $dropdown-item-padding-y $dropdown-item-padding-x;
+ }
}
.droplab-dropdown {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index be544c0a814..d0f99c2df7e 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -206,6 +206,7 @@ label {
.form-text.text-muted {
margin-bottom: 0;
margin-top: #{$grid-size / 2};
+ font-size: $gl-font-size;
}
.gl-field-error,
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1e025b3a67d..a6179e2a96e 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -447,30 +447,29 @@
}
}
+.title-container,
.navbar-nav {
- li {
- .badge.badge-pill {
- position: inherit;
- font-weight: $gl-font-weight-normal;
- margin-left: -6px;
- font-size: 11px;
- color: $white-light;
- padding: 0 5px;
- line-height: 12px;
- border-radius: 7px;
- box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
-
- &.issues-count {
- background-color: $green-500;
- }
+ .badge.badge-pill {
+ position: inherit;
+ font-weight: $gl-font-weight-normal;
+ margin-left: -6px;
+ font-size: 11px;
+ color: $white-light;
+ padding: 0 5px;
+ line-height: 12px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba($gl-header-color, 0.2);
+
+ &.green-badge {
+ background-color: $green-500;
+ }
- &.merge-requests-count {
- background-color: $orange-600;
- }
+ &.merge-requests-count {
+ background-color: $orange-600;
+ }
- &.todos-count {
- background-color: $blue-500;
- }
+ &.todos-count {
+ background-color: $blue-500;
}
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 3ab61cc5c47..1be5ef276fd 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -20,8 +20,8 @@
}
.ci-status-icon-pending,
-.ci-status-icon-failed_with_warnings,
-.ci-status-icon-success_with_warnings {
+.ci-status-icon-failed-with-warnings,
+.ci-status-icon-success-with-warnings {
svg {
fill: $orange-500;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 53222a2bd4d..323a3dbecd5 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -34,10 +34,10 @@
.modal-body {
background-color: $modal-body-bg;
line-height: $line-height-base;
- min-height: $modal-body-height;
position: relative;
padding: #{3 * $grid-size} #{2 * $grid-size};
text-align: left;
+ white-space: normal;
.form-actions {
margin: #{2 * $grid-size} #{-2 * $grid-size} #{-2 * $grid-size};
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 244b414d334..7c152efd9c7 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -473,3 +473,7 @@ textarea {
/* stylelint-enable */
.lh-100 { line-height: 1; }
+
+wbr {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index fc1c1bd9962..09ff518bbdf 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -1,7 +1,3 @@
-[v-cloak] {
- display: none;
-}
-
.user-can-drag {
cursor: grab;
}
@@ -11,6 +7,9 @@
opacity: 1 !important;
* {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
user-select: none;
// !important to make sure no style can override this when dragging
cursor: grabbing !important;
@@ -40,35 +39,15 @@
margin: 0 8px 10px;
padding-bottom: 10px;
border-bottom: 1px solid $dropdown-divider-bg;
-
- > p {
- margin: 0;
- font-size: 14px;
- }
}
.issue-boards-page {
.content-wrapper {
padding-bottom: 0;
}
-
- .issues-details-filters {
- display: flex;
- }
-
- .filter-form {
- width: 100%;
- }
-}
-
-.board-extra-actions {
- font-size: 0;
- white-space: nowrap;
}
.boards-app {
- position: relative;
-
@include media-breakpoint-up(sm) {
transition: width $sidebar-transition-duration;
width: 100%;
@@ -79,17 +58,9 @@
}
}
-.boards-app-loading {
- width: 100%;
- font-size: 34px;
-}
-
.boards-list {
height: calc(100vh - #{$issue-board-list-difference-xs});
- width: 100%;
- padding: $gl-padding ($gl-padding / 2);
overflow-x: scroll;
- white-space: nowrap;
min-height: 200px;
@include media-breakpoint-only(sm) {
@@ -114,13 +85,7 @@
}
.board {
- display: inline-block;
width: calc(85vw - 15px);
- height: 100%;
- padding-right: ($gl-padding / 2);
- padding-left: ($gl-padding / 2);
- white-space: normal;
- vertical-align: top;
@include media-breakpoint-up(sm) {
width: 400px;
@@ -135,23 +100,7 @@
&.is-collapsed {
width: 50px;
- .board-header {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
-
- button {
- display: none;
- }
- }
-
.board-title {
- padding: 0;
- border-bottom: 0;
- justify-content: center;
-
> span {
width: 100%;
margin-top: -12px;
@@ -167,34 +116,16 @@
left: 50%;
margin-left: -10px;
}
-
- .board-list-component,
- .issue-count-badge {
- display: none;
- }
- }
-
- &:not(.is-collapsed) {
- .board-list-component {
- display: flex;
- flex-direction: column;
- }
}
}
.board-inner {
- position: relative;
- height: 100%;
font-size: $issue-boards-font-size;
background: $gray-light;
border: 1px solid $border-color;
- border-radius: $border-radius-default;
- flex: 1;
}
.board-header {
- position: relative;
-
&.has-border::before {
border-top: 3px solid;
border-color: inherit;
@@ -219,18 +150,9 @@
}
}
-.board-inner-container {
- border-bottom: 1px solid $border-color;
- padding: $gl-padding;
-}
-
.board-title {
- margin: 0;
- padding: $gl-padding-8 $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
- display: flex;
- align-items: center;
}
.board-title-text {
@@ -239,10 +161,8 @@
.board-delete {
margin-right: 10px;
- padding: 0;
color: $gray-darkest;
background-color: transparent;
- border: 0;
outline: 0;
&:hover {
@@ -252,7 +172,6 @@
.board-blank-state,
.board-promotion-state {
- padding: $gl-padding;
background-color: $white-light;
flex: 1;
overflow-y: auto;
@@ -260,35 +179,23 @@
}
.board-blank-state-list {
- list-style: none;
-
> li:not(:last-child) {
margin-bottom: 8px;
}
.label-color {
- position: relative;
top: 2px;
- display: inline-block;
width: 16px;
height: 16px;
margin-right: 3px;
- border-radius: $border-radius-default;
}
}
.board-list-component {
- position: relative;
- flex: 1;
min-height: 0; // firefox fix
}
.board-list {
- height: 100%;
- width: 100%;
- margin-bottom: 0;
- padding: $gl-padding-4;
- list-style: none;
overflow-y: auto;
overflow-x: hidden;
}
@@ -299,14 +206,11 @@
}
.board-card {
- position: relative;
- padding: $gl-padding;
background: $white-light;
- border-radius: $border-radius-default;
border: 1px solid $gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
- list-style: none;
line-height: $gl-padding;
+ list-style: none;
&:not(:last-child) {
margin-bottom: $gl-padding-8;
@@ -333,10 +237,6 @@
}
}
- svg {
- vertical-align: top;
- }
-
.confidential-icon {
color: $orange-600;
cursor: help;
@@ -361,11 +261,10 @@
}
.board-card-header {
- display: flex;
+ text-align: initial;
}
.board-card-assignee {
- display: flex;
margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4;
@@ -425,34 +324,16 @@
.board-card-number {
font-size: $gl-font-size-xs;
color: $gl-text-color-secondary;
- overflow: hidden;
@include media-breakpoint-up(md) {
font-size: $label-font-size;
}
}
-.board-card-number-container {
- overflow: hidden;
-}
-
-.issue-boards-search {
- width: 395px;
-
- .form-control {
- display: inline-block;
- width: 210px;
- }
-}
-
.board-list-count {
padding: 10px 0;
color: $gl-text-color-secondary;
font-size: 13px;
-
- > .fa {
- margin-right: 5px;
- }
}
.board-new-issue-form {
@@ -460,16 +341,9 @@
margin: 5px;
}
-.page-with-contextual-sidebar.layout-page .issue-boards-sidebar {
- .issuable-sidebar-header {
- position: relative;
- }
-
+.issue-boards-sidebar {
.gutter-toggle {
- position: absolute;
- top: 0;
bottom: 15px;
- right: 0;
width: 22px;
color: $gray-darkest;
@@ -489,10 +363,6 @@
.issuable-header-text {
@include overflow-break-word();
padding-right: 35px;
-
- > strong {
- font-weight: $gl-font-weight-bold;
- }
}
}
@@ -511,44 +381,25 @@
}
.add-issues-modal {
- display: flex;
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
background-color: rgba($black, 0.3);
z-index: 9999;
}
.add-issues-container {
- display: flex;
- flex-direction: column;
width: 90vw;
height: 85vh;
max-width: 1100px;
min-height: 500px;
- margin: auto;
padding: 25px 15px 0;
background-color: $white-light;
- border-radius: $border-radius-default;
box-shadow: 0 2px 12px rgba($black, 0.5);
.empty-state {
- display: flex;
- flex: 1;
- margin-top: 0;
-
&.add-issues-empty-state-filter {
flex-direction: column;
justify-content: center;
}
- > .row {
- width: 100%;
- margin: auto 0;
- }
-
.svg-content {
margin-top: -40px;
}
@@ -557,25 +408,15 @@
.add-issues-header {
margin: -25px -15px -5px;
- border-top: 0;
border-bottom: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
> h2 {
- margin: 0;
font-size: 18px;
}
}
-.add-issues-search {
- display: flex;
-
- .issues-filters {
- flex: 1;
- }
-}
-
.add-issues-list-column {
width: 100%;
@@ -589,8 +430,6 @@
}
.add-issues-list {
- display: flex;
- flex: 1;
padding-top: 3px;
margin-left: -$gl-vert-padding;
margin-right: -$gl-vert-padding;
@@ -607,14 +446,6 @@
}
}
-.add-issues-list-loading {
- align-self: center;
- width: 100%;
- padding-left: $gl-vert-padding;
- padding-right: $gl-vert-padding;
- font-size: 35px;
-}
-
.add-issues-footer {
margin: auto -15px 0;
padding-left: 15px;
@@ -642,27 +473,6 @@
border-radius: 50%;
}
-.modal-filters {
- display: flex;
-
- > .dropdown {
- display: none;
- margin-right: 10px;
-
- @include media-breakpoint-up(sm) {
- display: block;
- }
- }
-
- .dropdown-menu-toggle {
- width: 100px;
-
- @include media-breakpoint-up(md) {
- width: 140px;
- }
- }
-}
-
.board-card-info {
color: $gl-text-color-secondary;
white-space: nowrap;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 670e320dbc2..66cd113db84 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -58,14 +58,6 @@
display: inline-block;
vertical-align: middle;
- .stage-cell .stage-container {
- margin: 0 3px 3px 0;
- }
-
- .stage-container:last-child {
- margin-right: 0;
- }
-
.dropdown-menu {
margin-top: 11px;
}
@@ -128,18 +120,9 @@
}
.commit-row-title {
- .notes_count {
- float: right;
- margin-right: 10px;
- }
-
.str-truncated {
max-width: 70%;
}
-
- .commit-row-message {
- color: $gl-text-color;
- }
}
.text-expander {
@@ -185,7 +168,7 @@
flex-grow: 1;
min-width: 0;
- .project_namespace {
+ .project-namespace {
color: $gl-text-color-secondary;
}
}
@@ -208,10 +191,6 @@
}
}
- .ci-status-link {
- display: inline-flex;
- }
-
.ci-status-icon svg {
vertical-align: text-bottom;
}
@@ -239,7 +218,6 @@
}
.label-monospace {
- @extend .monospace;
user-select: text;
color: $gl-text-color;
background-color: $gray-light;
@@ -266,7 +244,7 @@
}
.commit,
-.generic_commit_status {
+.generic-commit-status {
a,
button {
vertical-align: baseline;
@@ -278,37 +256,22 @@
&.autodevops-badge {
color: $white-light;
}
-
- &.autodevops-link {
- color: $blue-600;
- }
}
.commit-row-description {
@extend %commit-description-base;
display: none;
flex: 1;
-
- a {
- color: $gl-text-color;
- }
}
&.inline-commit {
.commit-row-title {
font-size: 13px;
}
-
- .committed_ago {
- @extend .cgray;
- float: right;
- }
}
}
.branch-commit {
- color: $gl-text-color;
-
.commit-icon {
text-align: center;
display: inline-block;
@@ -320,14 +283,15 @@
fill: $gl-text-color-secondary;
}
}
+}
+.commit,
+.generic-commit-status,
+.branch-commit {
+ .autodevops-link,
.commit-sha {
color: $blue-600;
}
-
- .commit-row-message {
- color: $gl-text-color;
- }
}
.gpg-status-box {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 5ea96392afa..b2b3720fdde 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -431,9 +431,9 @@ table.code {
&.diff-line-num {
user-select: none;
margin: 0;
- border: 0;
padding: 0 10px 0 5px;
- border-right: 1px solid;
+ border-right-width: 1px;
+ border-right-style: solid;
text-align: right;
width: 50px;
position: relative;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index fbd291f095a..04c66006027 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -60,6 +60,7 @@
overflow-wrap: break-word;
min-width: 0;
width: 100%;
+ text-align: initial;
}
.btn-edit {
@@ -107,6 +108,10 @@
}
.scoped-label-wrapper {
+ > a {
+ max-width: 100%;
+ }
+
.color-label {
padding-right: $gl-padding-24;
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 7084e8715e0..13288d8bad1 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -75,7 +75,7 @@
padding: 0;
margin-bottom: 0;
- > li:not(.empty-message):not(.is-not-draggable) {
+ > li:not(.empty-message):not(.no-border) {
background-color: $white-light;
margin-bottom: 5px;
display: flex;
@@ -92,7 +92,7 @@
opacity: 0.3;
}
- .prioritized-labels & {
+ .prioritized-labels:not(.is-not-draggable) & {
box-shadow: 0 1px 2px $issue-boards-card-shadow;
cursor: move;
cursor: grab;
@@ -353,7 +353,7 @@
@media (max-width: map-get($grid-breakpoints, md)-1) {
.manage-labels-list {
- > li:not(.empty-message):not(.is-not-draggable) {
+ > li:not(.empty-message):not(.no-border) {
flex-wrap: wrap;
}
@@ -408,12 +408,21 @@
}
.scoped-label-wrapper {
+ max-width: 100%;
+ vertical-align: top;
+
+ .badge {
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ }
+
&.label-link .color-label a {
color: inherit;
}
.color-label {
padding-right: $gl-padding-24;
+ max-width: 100%;
}
.scoped-label {
@@ -444,3 +453,15 @@
}
}
}
+
+// Don't hide the overflow in system messages
+.system-note-message,
+.issuable-detail,
+.md-preview-holder,
+.note-body {
+ .scoped-label-wrapper {
+ .badge {
+ overflow: initial;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index e0b84e0f92d..47ffdbae4b6 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -130,9 +130,6 @@
.members-ldap {
align-self: center;
- height: 100%;
- margin-right: 10px;
- margin-left: -49px;
}
.alert-member-ldap {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3e6aa43175e..12a3b8c88f3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -565,6 +565,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
+ line-height: 2.2em;
}
.build {
@@ -701,6 +702,11 @@
}
}
}
+
+ .stage-action svg {
+ left: 1px;
+ top: -2px;
+ }
}
// Triggers the dropdown in the big pipeline graph
@@ -785,7 +791,7 @@
}
&.ci-status-icon-pending,
- &.ci-status-icon-success_with_warnings {
+ &.ci-status-icon-success-with-warnings {
@include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700);
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 7778b4aab3d..151af843c95 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1446,3 +1446,86 @@ pre.light-well {
}
}
}
+
+.project-filters {
+ .btn svg {
+ color: $gl-gray-700;
+ }
+
+ .button-filter-group {
+ .btn {
+ width: 96px;
+ }
+
+ a {
+ color: $black;
+ }
+
+ .active {
+ background: $btn-active-gray;
+ }
+ }
+
+ .filtered-search-dropdown-label {
+ min-width: 68px;
+
+ @include media-breakpoint-down(xs) {
+ min-width: 60px;
+ }
+ }
+
+ .filtered-search {
+ min-width: 30%;
+ flex-basis: 0;
+
+ .project-filter-form .project-filter-form-field {
+ padding-right: $gl-padding-8;
+ }
+
+ .filtered-search,
+ .filtered-search-nav,
+ .filtered-search-dropdown {
+ flex-basis: 0;
+ }
+
+ @include media-breakpoint-down(lg) {
+ min-width: 15%;
+
+ .project-filter-form-field {
+ min-width: 150px;
+ }
+ }
+
+ @include media-breakpoint-down(md) {
+ min-width: 30%;
+ }
+ }
+
+ .filtered-search-box {
+ border-radius: 3px 0 0 3px;
+ }
+
+ .dropdown-menu-toggle {
+ margin-left: $gl-padding-8;
+ }
+
+ @include media-breakpoint-down(md) {
+ .extended-filtered-search-box {
+ min-width: 55%;
+ }
+
+ .filtered-search-dropdown {
+ width: 50%;
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .filtered-search-dropdown {
+ width: 100%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 20bdc6596e9..37071a57bb3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -75,6 +75,8 @@ input[type='checkbox']:hover {
}
.search-input-wrap {
+ width: 100%;
+
.search-icon,
.clear-icon {
position: absolute;
@@ -84,6 +86,9 @@ input[type='checkbox']:hover {
.search-icon {
transition: color $default-transition-duration;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
user-select: none;
}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index a59bb31bdcb..613f643af3a 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -38,8 +38,8 @@
}
&.ci-pending,
- &.ci-failed_with_warnings,
- &.ci-success_with_warnings {
+ &.ci-failed-with-warnings,
+ &.ci-success-with-warnings {
@include status-color($orange-100, $orange-500, $orange-700);
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 2a1e8345755..586365eb1ce 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -110,45 +110,38 @@
}
.todo-body {
- .todo-note {
- word-wrap: break-word;
-
- .md {
- color: $gl-grayish-blue;
- font-size: $gl-font-size;
-
- .badge.badge-pill {
- color: $gl-text-color;
- }
+ .badge.badge-pill,
+ p {
+ color: $gl-text-color;
+ }
- p {
- color: $gl-text-color;
- }
- }
+ .md {
+ color: $gl-grayish-blue;
+ font-size: $gl-font-size;
+ }
- code {
- white-space: pre-wrap;
- }
+ code {
+ white-space: pre-wrap;
+ }
- pre {
- border: 0;
- background: $gray-light;
- border-radius: 0;
- color: $gl-gray-500;
- margin: 0 20px;
- overflow: hidden;
- }
+ pre {
+ border: 0;
+ background: $gray-light;
+ border-radius: 0;
+ color: $gl-gray-500;
+ margin: 0 20px;
+ overflow: hidden;
+ }
- .note-image-attach {
- margin-top: 4px;
- margin-left: 0;
- max-width: 200px;
- float: none;
- }
+ .note-image-attach {
+ margin-top: 4px;
+ margin-left: 0;
+ max-width: 200px;
+ float: none;
+ }
- p:last-child {
- margin-bottom: 0;
- }
+ p:last-child {
+ margin-bottom: 0;
}
}
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 9c01a2f8bda..5a8940ffd6d 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -79,8 +79,12 @@
table {
color: $black;
- strong {
- color: $black;
+ td {
+ vertical-align: top;
+ }
+
+ .backtrace-row {
+ display: none;
}
}
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index ef182b981f1..b742b7e19cf 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -4,10 +4,7 @@
#
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
- before_action :authenticate_admin!
- layout 'admin'
+ include EnforcesAdminAuthentication
- def authenticate_admin!
- render_404 unless current_user.admin?
- end
+ layout 'admin'
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b681949ab36..d445be0eb19 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -127,6 +127,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
[
*::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes,
+ *lets_encrypt_visible_attributes,
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
@@ -134,4 +135,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
restricted_visibility_levels: []
]
end
+
+ def lets_encrypt_visible_attributes
+ return [] unless Feature.enabled?(:pages_auto_ssl)
+
+ [
+ :lets_encrypt_notification_email,
+ :lets_encrypt_terms_of_service_accepted
+ ]
+ end
end
diff --git a/app/controllers/admin/clusters/applications_controller.rb b/app/controllers/admin/clusters/applications_controller.rb
new file mode 100644
index 00000000000..7400cc16175
--- /dev/null
+++ b/app/controllers/admin/clusters/applications_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class Admin::Clusters::ApplicationsController < Clusters::ApplicationsController
+ include EnforcesAdminAuthentication
+
+ private
+
+ def clusterable
+ @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user)
+ end
+end
diff --git a/app/controllers/admin/clusters_controller.rb b/app/controllers/admin/clusters_controller.rb
new file mode 100644
index 00000000000..f54933de10f
--- /dev/null
+++ b/app/controllers/admin/clusters_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Admin::ClustersController < Clusters::ClustersController
+ include EnforcesAdminAuthentication
+
+ layout 'admin'
+
+ private
+
+ def clusterable
+ @clusterable ||= InstanceClusterablePresenter.fabricate(Clusters::Instance.new, current_user: current_user)
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d5f1e35a79b..4cbab6811bc 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -27,6 +27,7 @@ class ApplicationController < ActionController::Base
before_action :check_impersonation_availability
around_action :set_locale
+ around_action :set_session_storage
after_action :set_page_title_header, if: :json_request?
after_action :limit_unauthenticated_session_times
@@ -128,7 +129,7 @@ class ApplicationController < ActionController::Base
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
- payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
+ payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
logged_user = auth_user
@@ -434,6 +435,10 @@ class ApplicationController < ActionController::Base
Gitlab::I18n.with_user_locale(current_user, &block)
end
+ def set_session_storage(&block)
+ Gitlab::Session.with_session(session, &block)
+ end
+
def set_page_title_header
# Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 73c744efeba..16c2365f85d 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -4,6 +4,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
before_action :cluster
before_action :authorize_create_cluster!, only: [:create]
before_action :authorize_update_cluster!, only: [:update]
+ before_action :authorize_admin_cluster!, only: [:destroy]
def create
request_handler do
@@ -21,6 +22,14 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
end
+ def destroy
+ request_handler do
+ Clusters::Applications::DestroyService
+ .new(@cluster, current_user, cluster_application_destroy_params)
+ .execute(request)
+ end
+ end
+
private
def request_handler
@@ -40,4 +49,8 @@ class Clusters::ApplicationsController < Clusters::BaseController
def cluster_application_params
params.permit(:application, :hostname, :email)
end
+
+ def cluster_application_destroy_params
+ params.permit(:application)
+ end
end
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index e82756e4643..73ebd4e0e42 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -12,6 +12,9 @@ class Clusters::ClustersController < Clusters::BaseController
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: [:show] do
+ push_frontend_feature_flag(:metrics_time_window)
+ end
helper_method :token_in_session
@@ -153,6 +156,7 @@ class Clusters::ClustersController < Clusters::BaseController
:enabled,
:name,
:environment_scope,
+ :managed,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
@@ -171,6 +175,7 @@ class Clusters::ClustersController < Clusters::BaseController
:enabled,
:name,
:environment_scope,
+ :managed,
platform_kubernetes_attributes: [
:namespace,
:api_url,
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index d5c4712bd78..4926062f9ca 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -8,13 +8,6 @@
module AuthenticatesWithTwoFactor
extend ActiveSupport::Concern
- included do
- # This action comes from DeviseController, but because we call `sign_in`
- # manually, not skipping this action would cause a "You are already signed
- # in." error message to be shown upon successful login.
- skip_before_action :require_no_authentication, only: [:create], raise: false
- end
-
# Store the user's ID in the session for later retrieval and render the
# two factor code prompt
#
diff --git a/app/controllers/concerns/enforces_admin_authentication.rb b/app/controllers/concerns/enforces_admin_authentication.rb
new file mode 100644
index 00000000000..3ef92730df6
--- /dev/null
+++ b/app/controllers/concerns/enforces_admin_authentication.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# == EnforcesAdminAuthentication
+#
+# Controller concern to enforce that users are authenticated as admins
+#
+# Upon inclusion, adds `authenticate_admin!` as a before_action
+#
+module EnforcesAdminAuthentication
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authenticate_admin!
+ end
+
+ def authenticate_admin!
+ render_404 unless current_user.admin?
+ end
+end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index c0c0160a827..cfff154c3dd 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -31,7 +31,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: @milestone.labels # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ labels: @milestone.labels.map { |label| label.present(issuable_subject: @milestone.parent) } # rubocop:disable Gitlab/ModuleWithInstanceVariables
})
end
end
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index b44e3b0fff4..11e3cfb01e4 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -41,7 +41,7 @@ module Groups
end
def variable_params_attributes
- %i[id key secret_value protected masked _destroy]
+ %i[id variable_type key secret_value protected masked _destroy]
end
def authorize_admin_build!
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 10cdce98437..837c26c630a 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -7,7 +7,7 @@ class HelpController < ApplicationController
# Taken from Jekyll
# https://github.com/jekyll/jekyll/blob/3.5-stable/lib/jekyll/document.rb#L13
- YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
+ YAML_FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m.freeze
def index
# Remove YAML frontmatter so that it doesn't look weird
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 643a3bfed1f..f71ea8642cd 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -15,8 +15,8 @@ class Import::BitbucketServerController < Import::BaseController
# (https://community.atlassian.com/t5/Answers-Developer-Questions/stash-repository-names/qaq-p/499054)
#
# Bitbucket Server starts personal project names with a tilde.
- VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/
- VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/
+ VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/.freeze
+ VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/.freeze
def new
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 49d21456f8e..5ecf4f114cf 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -39,9 +39,9 @@ class JwtController < ApplicationController
render json: {
errors: [
{ code: 'UNAUTHORIZED',
- message: "HTTP Basic: Access denied\n" \
- "You must use a personal access token with 'api' scope for Git over HTTP.\n" \
- "You can generate one at #{profile_personal_access_tokens_url}" }
+ message: _('HTTP Basic: Access denied\n' \
+ 'You must use a personal access token with \'api\' scope for Git over HTTP.\n' \
+ 'You can generate one at %{profile_personal_access_tokens_url}') % { profile_personal_access_tokens_url: profile_personal_access_tokens_url } }
]
}, status: :unauthorized
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index 5e872804448..9a5a45939e0 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -26,7 +26,7 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
override :fail_login
def fail_login(user)
- flash[:alert] = 'Access denied for your LDAP account.'
+ flash[:alert] = _('Access denied for your LDAP account.')
redirect_to new_user_session_path
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index e14abbf7c78..fc708400657 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -100,14 +100,14 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
- flash_type = result[:status] == :error ? :alert : :notice
- flash[flash_type] = result[:message]
+ flash_type = result.error? ? :alert : :notice
+ flash[flash_type] = result.message
redirect_to project_branches_path(@project), status: :see_other
end
- format.js { head result[:return_code] }
- format.json { render json: { message: result[:message] }, status: result[:return_code] }
+ format.js { head result.http_status }
+ format.json { render json: { message: result.message }, status: result.http_status }
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e35f34be23c..5a4adea497b 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,8 +10,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
- before_action only: [:metrics, :additional_metrics] do
+ before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:metrics_time_window)
+ push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
+ push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
+ push_frontend_feature_flag(:grafana_dashboard_link)
end
def index
@@ -134,13 +137,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics
- # Currently, this acts as a hint to load the metrics details into the cache
- # if they aren't there already
- @metrics = environment.metrics || {}
-
respond_to do |format|
format.html
format.json do
+ # Currently, this acts as a hint to load the metrics details into the cache
+ # if they aren't there already
+ @metrics = environment.metrics || {}
+
render json: @metrics, status: @metrics.any? ? :ok : :no_content
end
end
@@ -156,6 +159,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def metrics_dashboard
+ return render_403 unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project)
+
+ if Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
+ result = dashboard_finder.find(project, current_user, environment, params[:dashboard])
+
+ result[:all_dashboards] = project.repository.metrics_dashboard_paths
+ else
+ result = dashboard_finder.find(project, current_user, environment)
+ end
+
+ respond_to do |format|
+ if result[:status] == :success
+ format.json do
+ render status: :ok, json: result.slice(:all_dashboards, :dashboard, :status)
+ end
+ else
+ format.json do
+ render(
+ status: result[:http_status],
+ json: result.slice(:all_dashboards, :message, :status)
+ )
+ end
+ end
+ end
+ end
+
def search
respond_to do |format|
format.json do
@@ -196,6 +226,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
params.require([:start, :end])
end
+ def dashboard_finder
+ Gitlab::Metrics::Dashboard::Finder
+ end
+
def search_environment_names
return [] unless params[:query]
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3d16a368f23..b4d89db20c5 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,11 +10,11 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
- def self.issue_except_actions
+ def issue_except_actions
%i[index calendar new create bulk_update import_csv]
end
- def self.set_issuables_index_only_actions
+ def set_issuables_index_only_actions
%i[index calendar]
end
@@ -25,9 +25,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, except: issue_except_actions
+ before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
- before_action :set_issuables_index, only: set_issuables_index_only_actions
+ before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) }
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -132,18 +132,6 @@ class Projects::IssuesController < Projects::ApplicationController
render_conflict_response
end
- def referenced_merge_requests
- @merge_requests, @closed_by_merge_requests = ::Issues::ReferencedMergeRequestsService.new(project, current_user).execute(issue)
-
- respond_to do |format|
- format.json do
- render json: {
- html: view_to_html_string('projects/issues/_merge_requests')
- }
- end
- end
- end
-
def related_branches
@related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 39ba2a651d4..8f177895b08 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -98,20 +98,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def test_reports
- result = @merge_request.compare_test_reports
-
- case result[:status]
- when :parsing
- Gitlab::PollingInterval.set_header(response, interval: 3000)
-
- render json: '', status: :no_content
- when :parsed
- render json: result[:data].to_json, status: :ok
- when :error
- render json: { status_reason: result[:status_reason] }, status: :bad_request
- else
- render json: { status_reason: 'Unknown error' }, status: :internal_server_error
- end
+ reports_response(@merge_request.compare_test_reports)
end
def edit
@@ -353,4 +340,19 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
end
+
+ def reports_response(report_comparison)
+ case report_comparison[:status]
+ when :parsing
+ ::Gitlab::PollingInterval.set_header(response, interval: 3000)
+
+ render json: '', status: :no_content
+ when :parsed
+ render json: report_comparison[:data].to_json, status: :ok
+ when :error
+ render json: { status_reason: report_comparison[:status_reason] }, status: :bad_request
+ else
+ render json: { status_reason: 'Unknown error' }, status: :internal_server_error
+ end
+ end
end
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index ef330ae00f4..6c6adc233b7 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -81,6 +81,7 @@ class Projects::MirrorsController < Projects::ApplicationController
password
ssh_known_hosts
regenerate_ssh_private_key
+ _destroy
]
]
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index 6b721c8fdf7..72e939a3310 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -98,7 +98,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def schedule_params
params.require(:schedule)
.permit(:description, :cron, :cron_timezone, :ref, :active,
- variables_attributes: [:id, :key, :secret_value, :_destroy] )
+ variables_attributes: [:id, :variable_type, :key, :secret_value, :_destroy] )
end
def authorize_play_pipeline_schedule!
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 22c4b8eef1f..db3b7c8b177 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -169,7 +169,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create_params
- params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value])
+ params.require(:pipeline).permit(:ref, variables_attributes: %i[key variable_type secret_value])
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index 4eeaeb860ee..3b4215b766e 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -23,7 +23,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
append_sha = false if @filename == shortname
end
- send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
+ send_git_archive @repository, ref: @ref, path: params[:path], format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
git_not_found!
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index d1c5cef76fa..c4dff95a4b9 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -19,7 +19,7 @@ module Projects
redirect_to project_settings_ci_cd_path(@project)
else
- render 'show'
+ redirect_to project_settings_ci_cd_path(@project), alert: result[:message]
end
end
end
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 1fafc33e917..b5c77e5bbf4 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -5,6 +5,10 @@ module Projects
class OperationsController < Projects::ApplicationController
before_action :authorize_update_environment!
+ before_action do
+ push_frontend_feature_flag(:grafana_dashboard_link)
+ end
+
helper_method :error_tracking_setting
def show
@@ -56,6 +60,8 @@ module Projects
# overridden in EE
def permitted_project_params
{
+ metrics_setting_attributes: [:external_dashboard_url],
+
error_tracking_setting_attributes: [
:enabled,
:api_host,
diff --git a/app/controllers/projects/stages_controller.rb b/app/controllers/projects/stages_controller.rb
new file mode 100644
index 00000000000..c8db5b1277f
--- /dev/null
+++ b/app/controllers/projects/stages_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Projects::StagesController < Projects::PipelinesController
+ before_action :authorize_update_pipeline!
+
+ def play_manual
+ ::Ci::PlayManualStageService
+ .new(@project, current_user, pipeline: pipeline)
+ .execute(stage)
+
+ respond_to do |format|
+ format.json do
+ render json: StageSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent(stage)
+ end
+ end
+ end
+
+ private
+
+ def stage
+ @pipeline_stage ||= pipeline.find_stage_by_name!(params[:stage_name])
+ end
+end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 05a79d59ffd..646728e8167 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -38,6 +38,6 @@ class Projects::VariablesController < Projects::ApplicationController
end
def variable_params_attributes
- %i[id key secret_value protected masked _destroy]
+ %i[id variable_type key secret_value protected masked _destroy]
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 88910c91763..fa5bdbc7d49 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -17,7 +17,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages
@wiki_pages = Kaminari.paginate_array(
- @project_wiki.pages(sort: params[:sort], direction: params[:direction])
+ @project_wiki.list_pages(sort: params[:sort], direction: params[:direction])
).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
@@ -118,7 +118,7 @@ class Projects::WikisController < Projects::ApplicationController
@sidebar_page = @project_wiki.find_sidebar(params[:version_id])
unless @sidebar_page # Fallback to default sidebar
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages(limit: 15))
+ @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.list_pages(limit: 15))
end
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.")
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 48f4d7a586d..e88c46144ef 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -36,10 +36,10 @@ class ProjectsController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def new
- namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
- return access_denied! if namespace && !can?(current_user, :create_projects, namespace)
+ @namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
+ return access_denied! if @namespace && !can?(current_user, :create_projects, @namespace)
- @project = Project.new(namespace_id: namespace&.id)
+ @project = Project.new(namespace_id: @namespace&.id)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 6943795e8ac..6fea61cf45d 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -8,6 +8,8 @@ class SessionsController < Devise::SessionsController
include Recaptcha::Verify
skip_before_action :check_two_factor_requirement, only: [:destroy]
+ # replaced with :require_no_authentication_without_flash
+ skip_before_action :require_no_authentication, only: [:new, :create]
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
@@ -15,6 +17,8 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
+ prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
+
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
@@ -54,6 +58,14 @@ class SessionsController < Devise::SessionsController
private
+ def require_no_authentication_without_flash
+ require_no_authentication
+
+ if flash[:alert] == I18n.t('devise.failure.already_authenticated')
+ flash[:alert] = nil
+ end
+ end
+
def captcha_enabled?
request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 568c6e2a852..060b09f015c 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -56,8 +56,9 @@ class UploadsController < ApplicationController
def authorize_create_access!
return unless model
- # for now we support only personal snippets comments
- authorized = can?(current_user, :comment_personal_snippet, model)
+ # for now we support only personal snippets comments. Only personal_snippet
+ # is allowed as a model to #create through routing.
+ authorized = can?(current_user, :create_note, model)
render_unauthorized unless authorized
end
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
index 45955783be9..ce7d0b8699c 100644
--- a/app/finders/autocomplete/users_finder.rb
+++ b/app/finders/autocomplete/users_finder.rb
@@ -2,6 +2,8 @@
module Autocomplete
class UsersFinder
+ include Gitlab::Utils::StrongMemoize
+
# The number of users to display in the results is hardcoded to 20, and
# pagination is not supported. This ensures that performance remains
# consistent and removes the need for implementing keyset pagination to
@@ -31,7 +33,7 @@ module Autocomplete
# Include current user if available to filter by "Me"
items.unshift(current_user) if prepend_current_user?
- if prepend_author? && (author = User.find_by_id(author_id))
+ if prepend_author? && author&.active?
items.unshift(author)
end
end
@@ -41,6 +43,12 @@ module Autocomplete
private
+ def author
+ strong_memoize(:author) do
+ User.find_by_id(author_id)
+ end
+ end
+
# Returns the users based on the input parameters, as an Array.
#
# This method is separate so it is easier to extend in EE.
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 88ec77426d5..52b6e828cfa 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -29,6 +29,7 @@
# updated_after: datetime
# updated_before: datetime
# attempt_group_search_optimizations: boolean
+# attempt_project_search_optimizations: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -53,6 +54,7 @@ class IssuableFinder
assignee_username
author_id
author_username
+ label_name
milestone_title
my_reaction_emoji
search
@@ -183,7 +185,6 @@ class IssuableFinder
@project = project
end
- # rubocop: disable CodeReuse/ActiveRecord
def projects
return @projects if defined?(@projects)
@@ -191,17 +192,25 @@ class IssuableFinder
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
- current_user.authorized_projects
+ current_user.authorized_projects(min_access_level)
elsif group
- finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
- GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
+ find_group_projects
else
- ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder
+ Project.public_or_visible_to_user(current_user, min_access_level)
end
- @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
+ @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def find_group_projects
+ return Project.none unless group
+
+ if params[:include_subgroups]
+ Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ group.projects
+ end.public_or_visible_to_user(current_user, min_access_level)
end
- # rubocop: enable CodeReuse/ActiveRecord
def search
params[:search].presence
@@ -569,4 +578,8 @@ class IssuableFinder
scope = params[:scope]
scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
end
+
+ def min_access_level
+ ProjectFeature.required_minimum_access_level(klass)
+ end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index e6a82f55856..58a01d598ba 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
- OR issues.project_id IN(:project_ids)))',
+ OR EXISTS (:authorizations)))',
user_id: current_user.id,
- project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
+ authorizations: current_user.authorizations_for_projects(min_access_level: CONFIDENTIAL_ACCESS_LEVEL, related_project_column: "issues.project_id"))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 93d3c991846..23b731b1aed 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder
collection = by_personal(collection)
collection = by_starred(collection)
collection = by_trending(collection)
- collection = by_visibilty_level(collection)
+ collection = by_visibility_level(collection)
collection = by_tags(collection)
collection = by_search(collection)
collection = by_archived(collection)
@@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder
collection
end
- # rubocop: disable CodeReuse/ActiveRecord
def collection_with_user
if owned_projects?
current_user.owned_projects
elsif min_access_level?
- current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level])
+ current_user.authorized_projects(params[:min_access_level])
else
if private_only?
current_user.authorized_projects
@@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user.
def collection_without_user
@@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder
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/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 1afe000c5f8..a12568d5d31 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -11,7 +11,7 @@ class GitlabSchema < GraphQL::Schema
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
- use Gitlab::Graphql::Tracing
+ use Gitlab::Graphql::GenericTracing
query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 063def75d38..31850c2cadb 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -9,5 +9,24 @@ module Resolvers
end
end
end
+
+ def self.resolver_complexity(args)
+ complexity = 1
+ complexity += 1 if args[:sort]
+ complexity += 5 if args[:search]
+
+ complexity
+ end
+
+ def self.complexity_multiplier(args)
+ # When fetching many items, additional complexity is added to the field
+ # depending on how many items is fetched. For each item we add 1% of the
+ # original complexity - this means that loading 100 items (our default
+ # maxp_age_size limit) doubles the original complexity.
+ #
+ # Complexity is not increased when searching by specific ID(s), because
+ # complexity difference is minimal in this case.
+ [args[:iid], args[:iids]].any? ? 0 : 0.01
+ end
end
end
diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb
index 8fd26d85994..a166211fc18 100644
--- a/app/graphql/resolvers/concerns/resolves_pipelines.rb
+++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb
@@ -19,6 +19,16 @@ module ResolvesPipelines
description: "Filter pipelines by the sha of the commit they are run for"
end
+ class_methods do
+ def resolver_complexity(args)
+ complexity = super
+ complexity += 2 if args[:sha]
+ complexity += 2 if args[:ref]
+
+ complexity
+ end
+ end
+
def resolve_pipelines(project, params = {})
PipelinesFinder.new(project, context[:current_user], params).execute
end
diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb
index 0f1a64b6c58..972f318c806 100644
--- a/app/graphql/resolvers/full_path_resolver.rb
+++ b/app/graphql/resolvers/full_path_resolver.rb
@@ -7,14 +7,14 @@ module Resolvers
prepended do
argument :full_path, GraphQL::ID_TYPE,
required: true,
- description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"'
+ description: 'The full path of the project, group or namespace, e.g., "gitlab-org/gitlab-ce"'
end
def model_by_full_path(model, full_path)
BatchLoader.for(full_path).batch(key: model) do |full_paths, loader, args|
# `with_route` avoids an N+1 calculating full_path
- args[:key].where_full_path_in(full_paths).with_route.each do |project|
- loader.call(project.full_path, project)
+ args[:key].where_full_path_in(full_paths).with_route.each do |model_instance|
+ loader.call(model_instance.full_path, model_instance)
end
end
end
diff --git a/app/graphql/resolvers/group_resolver.rb b/app/graphql/resolvers/group_resolver.rb
new file mode 100644
index 00000000000..4260e18829e
--- /dev/null
+++ b/app/graphql/resolvers/group_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupResolver < BaseResolver
+ prepend FullPathResolver
+
+ type Types::GroupType, null: true
+
+ def resolve(full_path:)
+ model_by_full_path(Group, full_path)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index b98d8bd1fff..1c3c24ad6dc 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -44,6 +44,12 @@ module Resolvers
alias_method :project, :object
def resolve(**args)
+ # The project could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` of the project to query for issues, so
+ # make sure it's loaded and not `nil` before continueing.
+ project.sync if project.respond_to?(:sync)
+ return Issue.none if project.nil?
+
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54520
args[:project_id] = project.id
@@ -51,5 +57,12 @@ module Resolvers
IssuesFinder.new(context[:current_user], args).execute
end
+
+ def self.resolver_complexity(args)
+ complexity = super
+ complexity += 2 if args[:labelName]
+
+ complexity
+ end
end
end
diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb
index ac7c9b0ce2e..2132447da5e 100644
--- a/app/graphql/resolvers/project_resolver.rb
+++ b/app/graphql/resolvers/project_resolver.rb
@@ -9,5 +9,9 @@ module Resolvers
def resolve(full_path:)
model_by_full_path(Project, full_path)
end
+
+ def self.complexity_multiplier(args)
+ 0
+ end
end
end
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 8c8b8a82d3e..15331129134 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -7,10 +7,40 @@ module Types
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
- # complexity is already defaulted to 1, but let's make it explicit
- kwargs[:complexity] ||= DEFAULT_COMPLEXITY
+ kwargs[:complexity] ||= field_complexity(kwargs[:resolver_class])
super(*args, **kwargs, &block)
end
+
+ private
+
+ def field_complexity(resolver_class)
+ if resolver_class
+ field_resolver_complexity
+ else
+ DEFAULT_COMPLEXITY
+ end
+ end
+
+ def field_resolver_complexity
+ # Complexity can be either integer or proc. If proc is used then it's
+ # called when computing a query complexity and context and query
+ # arguments are available for computing complexity. For resolvers we use
+ # proc because we set complexity depending on arguments and number of
+ # items which can be loaded.
+ proc do |ctx, args, child_complexity|
+ page_size = @max_page_size || ctx.schema.default_max_page_size
+ limit_value = [args[:first], args[:last], page_size].compact.min
+
+ # Resolvers may add extra complexity depending on used arguments
+ complexity = child_complexity + self.resolver&.try(:resolver_complexity, args).to_i
+
+ # Resolvers may add extra complexity depending on number of items being loaded.
+ multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
+ complexity += complexity * limit_value * multiplier
+
+ complexity.to_i
+ end
+ end
end
end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
new file mode 100644
index 00000000000..a2d615ee732
--- /dev/null
+++ b/app/graphql/types/group_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ class GroupType < NamespaceType
+ graphql_name 'Group'
+
+ authorize :read_group
+
+ expose_permissions Types::PermissionTypes::Group
+
+ field :web_url, GraphQL::STRING_TYPE, null: true
+
+ field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (group, args, ctx) do
+ group.avatar_url(only_path: false)
+ end
+
+ if ::Group.supports_nested_objects?
+ field :parent, GroupType, null: true
+ end
+ end
+end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index adb137dfee3..b21a226d07f 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -19,9 +19,11 @@ module Types
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
- field :assignees, Types::UserType.connection_type, null: true
+ # Remove complexity when BatchLoader is used
+ field :assignees, Types::UserType.connection_type, null: true, complexity: 5
- field :labels, Types::LabelType.connection_type, null: true
+ # Remove complexity when BatchLoader is used
+ field :labels, Types::LabelType.connection_type, null: true, complexity: 5
field :milestone, Types::MilestoneType,
null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb
new file mode 100644
index 00000000000..36d8ee8c878
--- /dev/null
+++ b/app/graphql/types/namespace_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ class NamespaceType < BaseObject
+ graphql_name 'Namespace'
+
+ field :id, GraphQL::ID_TYPE, null: false
+
+ field :name, GraphQL::STRING_TYPE, null: false
+ field :path, GraphQL::STRING_TYPE, null: false
+ field :full_name, GraphQL::STRING_TYPE, null: false
+ field :full_path, GraphQL::ID_TYPE, null: false
+
+ field :description, GraphQL::STRING_TYPE, null: true
+ field :visibility, GraphQL::STRING_TYPE, null: true
+ field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?
+ field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ end
+end
diff --git a/app/graphql/types/permission_types/group.rb b/app/graphql/types/permission_types/group.rb
new file mode 100644
index 00000000000..29833993ce6
--- /dev/null
+++ b/app/graphql/types/permission_types/group.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Group < BasePermissionType
+ graphql_name 'GroupPermissions'
+
+ abilities :read_group
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index fbb4eddd13c..baea6658e05 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -66,6 +66,9 @@ module Types
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
+ field :namespace, Types::NamespaceType, null: false
+ field :group, Types::GroupType, null: true
+
field :merge_requests,
Types::MergeRequestType.connection_type,
null: true,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 0f655ab9d03..40d7de1a49a 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -9,6 +9,11 @@ module Types
resolver: Resolvers::ProjectResolver,
description: "Find a project"
+ field :group, Types::GroupType,
+ null: true,
+ resolver: Resolvers::GroupResolver,
+ description: "Find a group"
+
field :metadata, Types::MetadataType,
null: true,
resolver: Resolvers::MetadataResolver,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 5995ef57e26..971d1052824 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -286,4 +286,8 @@ module ApplicationSettingsHelper
def expanded_by_default?
Rails.env.test?
end
+
+ def instance_clusters_enabled?
+ can?(current_user, :read_cluster, Clusters::Instance.new)
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index b4ee648361c..076976175a9 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -2,7 +2,7 @@
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
- LDAP_PROVIDER = /\Aldap/
+ LDAP_PROVIDER = /\Aldap/.freeze
def ldap_enabled?
Gitlab::Auth::LDAP::Config.enabled?
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 289cb44f1e8..495c29d3e24 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -4,7 +4,7 @@ module BroadcastMessagesHelper
def broadcast_message(message)
return unless message.present?
- content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
+ content_tag :div, dir: 'auto', class: 'broadcast-message', style: broadcast_message_style(message) do
icon('bullhorn') << ' ' << render_broadcast_message(message)
end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 355b91a8661..f2b5b82b013 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -16,7 +16,7 @@ module CiStatusHelper
label = case status
when 'success'
'passed'
- when 'success_with_warnings'
+ when 'success-with-warnings'
'passed with warnings'
when 'manual'
'waiting for manual action'
@@ -37,7 +37,7 @@ module CiStatusHelper
case status
when 'success'
s_('CiStatusText|passed')
- when 'success_with_warnings'
+ when 'success-with-warnings'
s_('CiStatusText|passed')
when 'manual'
s_('CiStatusText|blocked')
@@ -71,7 +71,7 @@ module CiStatusHelper
case status
when 'success'
'status_success'
- when 'success_with_warnings'
+ when 'success-with-warnings'
'status_warning'
when 'failed'
'status_failed'
@@ -113,7 +113,7 @@ module CiStatusHelper
end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
- klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
+ klass = "ci-status-link ci-status-icon-#{status.dasherize} d-inline-flex #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
diff --git a/app/helpers/ci_variables_helper.rb b/app/helpers/ci_variables_helper.rb
index 88ce311a1d4..5bfdeb9e33c 100644
--- a/app/helpers/ci_variables_helper.rb
+++ b/app/helpers/ci_variables_helper.rb
@@ -20,4 +20,11 @@ module CiVariablesHelper
true
end
end
+
+ def ci_variable_type_options
+ [
+ %w(Variable env_var),
+ %w(File file)
+ ]
+ end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index 30d8a19ecce..769f75f57c4 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -20,9 +20,4 @@ module ClustersHelper
!cluster.provider.legacy_abac?
end
-
- # EE overrides this
- def show_cluster_health_graphs?(cluster)
- false
- end
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e91e8f85515..76300e791e6 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -13,9 +13,7 @@ module LabelsHelper
# Link to a Label
#
- # label - Label object to link to
- # subject - Project/Group object which will be used as the context for the
- # label's link. If omitted, defaults to the label's own group/project.
+ # label - LabelPresenter object to link to
# type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
@@ -40,8 +38,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
- def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
- link = label_filter_path(subject || label.subject, label, type: type)
+ def link_to_label(label, type: :issue, tooltip: true, css_class: nil, &block)
+ link = label.filter_path(type: type)
if block_given?
link_to link, class: css_class, &block
@@ -50,34 +48,6 @@ module LabelsHelper
end
end
- def label_filter_path(subject, label, type: :issue)
- case subject
- when Group
- send("#{type.to_s.pluralize}_group_path", # rubocop:disable GitlabSecurity/PublicSend
- subject,
- label_name: [label.name])
- when Project
- send("namespace_project_#{type.to_s.pluralize}_path", # rubocop:disable GitlabSecurity/PublicSend
- subject.namespace,
- subject,
- label_name: [label.name])
- end
- end
-
- def edit_label_path(label)
- case label
- when GroupLabel then edit_group_label_path(label.group, label)
- when ProjectLabel then edit_project_label_path(label.project, label)
- end
- end
-
- def destroy_label_path(label)
- case label
- when GroupLabel then group_label_path(label.group, label)
- when ProjectLabel then project_label_path(label.project, label)
- end
- end
-
def render_label(label, tooltip: true, link: nil, css: nil)
# if scoped label is used then EE wraps label tag with scoped label
# doc link
@@ -89,7 +59,7 @@ module LabelsHelper
def render_colored_label(label, label_suffix: '', tooltip: true, title: nil)
text_color = text_color_for_bg(label.color)
- title ||= tooltip ? label_tooltip_title(label) : ''
+ title ||= tooltip ? label_tooltip_title(label) : label.name
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
@@ -168,10 +138,6 @@ module LabelsHelper
end
end
- def can_subscribe_to_label_in_different_levels?(label)
- defined?(@project) && label.is_a?(GroupLabel)
- end
-
def label_subscription_status(label, project)
return 'group-level' if label.subscribed?(current_user)
return 'project-level' if label.subscribed?(current_user, project)
@@ -241,8 +207,8 @@ module LabelsHelper
"#{action} at #{level} level"
end
- def labels_sorted_by_title(labels)
- labels.sort_by(&:title)
+ def presented_labels_sorted_by_title(labels, subject)
+ labels.sort_by(&:title).map { |label| label.present(issuable_subject: subject) }
end
def label_dropdown_data(project, opts = {})
@@ -276,6 +242,10 @@ module LabelsHelper
klass.new(hash.slice(:color, :description, :title, :group_id, :project_id))
end
+ def issuable_types
+ ['issues', 'merge requests']
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once, :label_tooltip_title
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index be4fc2531ae..dce4168ad7b 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -4,7 +4,7 @@ require 'nokogiri'
module MarkupHelper
include ActionView::Helpers::TagHelper
- include ActionView::Context
+ include ::Gitlab::ActionViewOutput::Context
def plain?(filename)
Gitlab::MarkupHelper.plain?(filename)
@@ -83,7 +83,8 @@ module MarkupHelper
text = sanitize(
text,
tags: tags,
- attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes +
+ %w(style data-src data-name data-unicode-version data-iid data-project-path data-mr-title)
)
# since <img> tags are stripped, this can leave empty <a> tags hanging around
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 65c7cd82832..921c79ab771 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -7,4 +7,8 @@ module MirrorHelper
project_mirror_endpoint: project_mirror_path(@project, :json)
}
end
+
+ def mirror_lfs_sync_message
+ _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index a50137bea3d..2e31a5e2ed4 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -128,15 +128,9 @@ module NotesHelper
end
def can_create_note?
- issuable = @issue || @merge_request
+ noteable = @issue || @merge_request || @snippet || @project
- if @snippet.is_a?(PersonalSnippet)
- can?(current_user, :comment_personal_snippet, @snippet)
- elsif issuable
- can?(current_user, :create_note, issuable)
- else
- can?(current_user, :create_note, @project)
- end
+ can?(current_user, :create_note, noteable)
end
def initial_notes_data(autocomplete)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 5318ab4ddef..a7ce7667916 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -93,4 +93,11 @@ module NotificationsHelper
s_(event.to_s.humanize)
end
end
+
+ def notification_setting_icon(notification_setting)
+ sprite_icon(
+ notification_setting.disabled? ? "notifications-off" : "notifications",
+ css_class: "icon notifications-icon js-notifications-icon"
+ )
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7da51da8473..2c43b1a2067 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -239,8 +239,10 @@ module ProjectsHelper
end
# rubocop: enable CodeReuse/ActiveRecord
+ # TODO: Remove this method when removing the feature flag
+ # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234863
def show_projects?(projects, params)
- !!(params[:personal] || params[:name] || any_projects?(projects))
+ Feature.enabled?(:project_list_filter_bar) || !!(params[:personal] || params[:name] || any_projects?(projects))
end
def push_to_create_project_command(user = current_user)
@@ -299,6 +301,10 @@ module ProjectsHelper
}.to_json
end
+ def directory?
+ @path.present?
+ end
+
def external_classification_label_help_message
default_label = ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
@@ -314,8 +320,9 @@ module ProjectsHelper
def get_project_nav_tabs(project, current_user)
nav_tabs = [:home]
- if !project.empty_repo? && can?(current_user, :download_code, project)
- nav_tabs << [:files, :commits, :network, :graphs, :forks, :releases]
+ unless project.empty_repo?
+ nav_tabs << [:files, :commits, :network, :graphs, :forks] if can?(current_user, :download_code, project)
+ nav_tabs << :releases if can?(current_user, :read_release, project)
end
if project.repo_exists? && can?(current_user, :read_merge_request, project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a62c00df60b..4594f5a31b9 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -128,7 +128,7 @@ module SearchHelper
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.order_id_desc.search_by_title(term)
- .sorted_by_stars.non_archived.limit(limit).map do |p|
+ .sorted_by_stars_desc.non_archived.limit(limit).map do |p|
{
category: "Projects",
id: p.id,
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index 32bf3526571..6326d98461e 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -8,7 +8,7 @@ module SidekiqHelper
(?<state>[DIEKNRSTVWXZNLpsl\+<>/\d]+)\s+
(?<start>.+?)\s+
(?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
- \z}x
+ \z}x.freeze
def parse_sidekiq_ps(line)
match = line.strip.match(SIDEKIQ_PS_REGEXP)
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 07ec129dea3..f2d814e6930 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -30,13 +30,20 @@ module SortingHelper
end
def projects_sort_options_hash
+ Feature.enabled?(:project_list_filter_bar) && !current_controller?('admin/projects') ? projects_sort_common_options_hash : old_projects_sort_options_hash
+ end
+
+ # TODO: Simplify these sorting options
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/60798
+ # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/11209#note_162234858
+ def old_projects_sort_options_hash
options = {
sort_value_latest_activity => sort_title_latest_activity,
sort_value_name => sort_title_name,
sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_created => sort_title_recently_created,
- sort_value_most_stars => sort_title_most_stars
+ sort_value_stars_desc => sort_title_most_stars
}
if current_controller?('admin/projects')
@@ -46,6 +53,41 @@ module SortingHelper
options
end
+ def projects_sort_common_options_hash
+ {
+ sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_recently_created => sort_title_created_date,
+ sort_value_name => sort_title_name,
+ sort_value_stars_desc => sort_title_stars
+ }
+ end
+
+ def projects_sort_option_titles
+ {
+ sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_recently_created => sort_title_created_date,
+ sort_value_name => sort_title_name,
+ sort_value_stars_desc => sort_title_stars,
+ sort_value_oldest_activity => sort_title_latest_activity,
+ sort_value_oldest_created => sort_title_created_date,
+ sort_value_name_desc => sort_title_name,
+ sort_value_stars_asc => sort_title_stars
+ }
+ end
+
+ def projects_reverse_sort_options_hash
+ {
+ sort_value_latest_activity => sort_value_oldest_activity,
+ sort_value_recently_created => sort_value_oldest_created,
+ sort_value_name => sort_value_name_desc,
+ sort_value_stars_desc => sort_value_stars_asc,
+ sort_value_oldest_activity => sort_value_latest_activity,
+ sort_value_oldest_created => sort_value_recently_created,
+ sort_value_name_desc => sort_value_name,
+ sort_value_stars_asc => sort_value_stars_desc
+ }
+ end
+
def groups_sort_options_hash
{
sort_value_name => sort_title_name,
@@ -59,7 +101,7 @@ module SortingHelper
def subgroups_sort_options_hash
groups_sort_options_hash.merge(
- sort_value_most_stars => sort_title_most_stars
+ sort_value_stars_desc => sort_title_most_stars
)
end
@@ -142,7 +184,9 @@ module SortingHelper
{
sort_value_oldest_created => sort_value_created_date,
sort_value_oldest_updated => sort_value_recently_updated,
- sort_value_milestone_later => sort_value_milestone
+ sort_value_milestone_later => sort_value_milestone,
+ sort_value_due_date_later => sort_value_due_date,
+ sort_value_least_popular => sort_value_popularity
}
end
@@ -151,7 +195,11 @@ module SortingHelper
sort_value_created_date => sort_value_oldest_created,
sort_value_recently_created => sort_value_oldest_created,
sort_value_recently_updated => sort_value_oldest_updated,
- sort_value_milestone => sort_value_milestone_later
+ sort_value_milestone => sort_value_milestone_later,
+ sort_value_due_date => sort_value_due_date_later,
+ sort_value_due_date_soon => sort_value_due_date_later,
+ sort_value_popularity => sort_value_least_popular,
+ sort_value_most_popular => sort_value_least_popular
}.merge(issuable_sort_option_overrides)
end
@@ -170,6 +218,8 @@ module SortingHelper
end
end
+ # TODO: dedupicate issuable and project sort direction
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/60798
def issuable_sort_direction_button(sort_value)
link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
reverse_sort = issuable_reverse_sort_order_hash[sort_value]
@@ -181,7 +231,23 @@ module SortingHelper
link_class += ' disabled'
end
- link_to(reverse_url, type: 'button', class: link_class, title: 'Sort direction') do
+ link_to(reverse_url, type: 'button', class: link_class, title: s_('SortOptions|Sort direction')) do
+ sprite_icon("sort-#{issuable_sort_icon_suffix(sort_value)}", size: 16)
+ end
+ end
+
+ def project_sort_direction_button(sort_value)
+ link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
+ reverse_sort = projects_reverse_sort_options_hash[sort_value]
+
+ if reverse_sort
+ reverse_url = filter_projects_path(sort: reverse_sort)
+ else
+ reverse_url = '#'
+ link_class += ' disabled'
+ end
+
+ link_to(reverse_url, type: 'button', class: link_class, title: s_('SortOptions|Sort direction')) do
sprite_icon("sort-#{issuable_sort_icon_suffix(sort_value)}", size: 16)
end
end
@@ -319,6 +385,10 @@ module SortingHelper
s_('SortOptions|Most stars')
end
+ def sort_title_stars
+ s_('SortOptions|Stars')
+ end
+
def sort_title_oldest_last_activity
s_('SortOptions|Oldest last activity')
end
@@ -420,6 +490,14 @@ module SortingHelper
'popularity'
end
+ def sort_value_most_popular
+ 'popularity_desc'
+ end
+
+ def sort_value_least_popular
+ 'popularity_asc'
+ end
+
def sort_value_priority
'priority'
end
@@ -452,10 +530,14 @@ module SortingHelper
'contacted_asc'
end
- def sort_value_most_stars
+ def sort_value_stars_desc
'stars_desc'
end
+ def sort_value_stars_asc
+ 'stars_asc'
+ end
+
def sort_value_oldest_last_activity
'last_activity_on_asc'
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index be8761db562..15041bd5805 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -6,4 +6,14 @@ module StorageHelper
number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
end
+
+ def storage_counters_details(statistics)
+ counters = {
+ counter_repositories: storage_counter(statistics.repository_size),
+ counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
+ counter_lfs_objects: storage_counter(statistics.lfs_objects_size)
+ }
+
+ _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
+ end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 9d71f250466..d1d01368972 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -17,6 +17,19 @@ class ApplicationRecord < ActiveRecord::Base
where(nil).pluck(self.primary_key)
end
+ def self.safe_ensure_unique(retries: 0)
+ transaction(requires_new: true) do
+ yield
+ end
+ rescue ActiveRecord::RecordNotUnique
+ if retries > 0
+ retries -= 1
+ retry
+ end
+
+ false
+ end
+
def self.safe_find_or_create_by!(*args)
safe_find_or_create_by(*args).tap do |record|
record.validate! unless record.persisted?
@@ -24,10 +37,8 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.safe_find_or_create_by(*args)
- transaction(requires_new: true) do
+ safe_ensure_unique(retries: 1) do
find_or_create_by(*args)
end
- rescue ActiveRecord::RecordNotUnique
- retry
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2f9b4c4eaa2..fb1e558e46c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -229,6 +229,16 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
+ validates :lets_encrypt_notification_email,
+ devise_email: true,
+ format: { without: /@example\.(com|org|net)\z/,
+ message: N_("Let's Encrypt does not accept emails on example.com") },
+ allow_blank: true
+
+ validates :lets_encrypt_notification_email,
+ presence: true,
+ if: :lets_encrypt_terms_of_service_accepted?
+
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index b413ffddb9d..e51619b0f9c 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -8,7 +8,7 @@ module ApplicationSettingImplementation
\s # any whitespace character
| # or
[\r\n] # any number of newline characters
- }x
+ }x.freeze
# Setting a key restriction to `-1` means that all keys of this type are
# forbidden.
@@ -183,6 +183,22 @@ module ApplicationSettingImplementation
clientside_sentry_dsn.strip! if clientside_sentry_dsn.present?
end
+ def sentry_enabled
+ Gitlab.config.sentry.enabled || read_attribute(:sentry_enabled)
+ end
+
+ def sentry_dsn
+ Gitlab.config.sentry.dsn || read_attribute(:sentry_dsn)
+ end
+
+ def clientside_sentry_enabled
+ Gitlab.config.sentry.enabled || read_attribute(:clientside_sentry_enabled)
+ end
+
+ def clientside_sentry_dsn
+ Gitlab.config.sentry.clientside_dsn || read_attribute(:clientside_sentry_dsn)
+ end
+
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 0d8d7d95791..644716ba8e7 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -4,6 +4,7 @@ module Ci
class Bridge < CommitStatus
include Ci::Processable
include Ci::Contextable
+ include Ci::PipelineDelegator
include Importable
include AfterCommitQueue
include HasRef
@@ -13,8 +14,6 @@ module Ci
belongs_to :trigger_request
validates :ref, presence: true
- delegate :merge_request_event?, to: :pipeline
-
def self.retry(bridge, current_user)
raise NotImplementedError
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b8a76e662b0..d2f5ff13408 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,7 @@ module Ci
include Ci::Processable
include Ci::Metadatable
include Ci::Contextable
+ include Ci::PipelineDelegator
include TokenAuthenticatable
include AfterCommitQueue
include ObjectStorage::BackgroundMove
@@ -15,6 +16,7 @@ module Ci
include Gitlab::Utils::StrongMemoize
include Deployable
include HasRef
+ include UpdateProjectStatistics
BuildArchivedError = Class.new(StandardError)
@@ -48,8 +50,6 @@ module Ci
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
- delegate :merge_request_event?, :merge_request_ref?,
- :legacy_detached_merge_request_pipeline?, to: :pipeline
##
# Since Gitlab 11.5, deployments records started being created right after
@@ -104,8 +104,8 @@ module Ci
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
- scope :with_test_reports, ->() do
- with_existing_job_artifacts(Ci::JobArtifact.test_reports)
+ scope :with_reports, ->(reports_scope) do
+ with_existing_job_artifacts(reports_scope)
.eager_load_job_artifacts
end
@@ -157,8 +157,7 @@ module Ci
run_after_commit { BuildHooksWorker.perform_async(build.id) }
end
- after_save :update_project_statistics_after_save, if: :artifacts_size_changed?
- after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
+ update_project_statistics stat: :build_artifacts_size, attribute: :artifacts_size
class << self
# This is needed for url_for to work,
@@ -353,7 +352,7 @@ module Ci
end
def retryable?
- !archived? && (success? || failed? || canceled?)
+ !archived? && (success? || failed?)
end
def retries_count
@@ -847,21 +846,5 @@ module Ci
pipeline.config_processor.build_attributes(name)
end
-
- def update_project_statistics_after_save
- update_project_statistics(read_attribute(:artifacts_size).to_i - artifacts_size_was.to_i)
- end
-
- def update_project_statistics_after_destroy
- update_project_statistics(-artifacts_size)
- end
-
- def update_project_statistics(difference)
- ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
- end
-
- def project_destroyed?
- project.pending_delete?
- end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 99512a7c1dd..f9cf398556d 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -4,6 +4,7 @@ module Ci
class JobArtifact < ApplicationRecord
include AfterCommitQueue
include ObjectStorage::BackgroundMove
+ include UpdateProjectStatistics
extend Gitlab::Ci::Model
NotSupportedAdapterError = Class.new(StandardError)
@@ -21,7 +22,8 @@ module Ci
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
license_management: 'gl-license-management-report.json',
- performance: 'performance.json'
+ performance: 'performance.json',
+ metrics: 'metrics.txt'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
@@ -29,6 +31,7 @@ module Ci
metadata: :gzip,
trace: :raw,
junit: :gzip,
+ metrics: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
@@ -50,10 +53,10 @@ module Ci
validates :file_format, presence: true, unless: :trace?, on: :create
validate :valid_file_format?, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
- after_save :update_project_statistics_after_save, if: :size_changed?
- after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
- after_save :update_file_store, if: :file_changed?
+ update_project_statistics stat: :build_artifacts_size
+
+ after_save :update_file_store, if: :saved_change_to_file?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
@@ -88,7 +91,8 @@ module Ci
dast: 8, ## EE-specific
codequality: 9, ## EE-specific
license_management: 10, ## EE-specific
- performance: 11 ## EE-specific
+ performance: 11, ## EE-specific
+ metrics: 12 ## EE-specific
}
enum file_format: {
@@ -173,18 +177,6 @@ module Ci
self.size = file.size
end
- def update_project_statistics_after_save
- update_project_statistics(size.to_i - size_was.to_i)
- end
-
- def update_project_statistics_after_destroy
- update_project_statistics(-self.size.to_i)
- end
-
- def update_project_statistics(difference)
- ProjectStatistics.increment_statistic(project_id, :build_artifacts_size, difference)
- end
-
def project_destroyed?
# Use job.project to avoid extra DB query for project
job.project.pending_delete?
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 96dbc7b6895..930c8a71453 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -58,5 +58,9 @@ module Ci
statuses.latest.failed_but_allowed.any?
end
end
+
+ def manual_playable?
+ %[manual scheduled skipped].include?(status.to_s)
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b81a3cf8362..80401ca0a1e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -210,6 +210,10 @@ module Ci
where(source: branch_pipeline_sources).where(ref: ref, tag: false)
end
+ scope :with_reports, -> (reports_scope) do
+ where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
+ end
+
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
@@ -638,6 +642,7 @@ module Ci
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)
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
+ variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
if merge_request_event? && merge_request
variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
@@ -686,16 +691,16 @@ module Ci
# We purposely cast the builds to an Array here. Because we always use the
# rows if there are more than 0 this prevents us from having to run two
# queries: one to get the count and one to get the rows.
- @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
end
- def has_test_reports?
- complete? && builds.latest.with_test_reports.any?
+ def has_reports?(reports_scope)
+ complete? && builds.latest.with_reports(reports_scope).exists?
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
- builds.latest.with_test_reports.each do |build|
+ builds.latest.with_reports(Ci::JobArtifact.test_reports).each do |build|
build.collect_test_reports!(test_reports)
end
end
@@ -754,6 +759,22 @@ module Ci
user == current_user
end
+ def source_ref
+ if triggered_by_merge_request?
+ merge_request.source_branch
+ else
+ ref
+ end
+ end
+
+ def source_ref_slug
+ Gitlab::Utils.slugify(source_ref.to_s)
+ end
+
+ def find_stage_by_name!(name)
+ stages.find_by!(name: name)
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 1454b2dfb39..c0a0ca9acf6 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -5,6 +5,7 @@ module Ci
extend Gitlab::Ci::Model
include Importable
include IgnorableColumn
+ include StripAttribute
ignore_column :deleted_at
@@ -22,6 +23,8 @@ module Ci
before_save :set_next_run_at
+ strip_attributes :cron
+
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index b25b0369666..d90339d90dc 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -120,5 +120,9 @@ module Ci
.new(self, current_user)
.fabricate!
end
+
+ def manual_playable?
+ blocked? || skipped?
+ end
end
end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index ac0e7eb03bc..d6a7d1d2bdd 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -24,6 +24,12 @@ module Clusters
'stable/cert-manager'
end
+ # We will implement this in future MRs.
+ # Need to reverse postinstall step
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: 'certmanager',
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 71aff00077d..a83d06c4b00 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -29,6 +29,13 @@ module Clusters
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
+ # We will implement this in future MRs.
+ # Basically we need to check all other applications are not installed
+ # first.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 376d54aab2c..a1023f44049 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -35,6 +35,13 @@ module Clusters
'stable/nginx-ingress'
end
+ # We will implement this in future MRs.
+ # Basically we need to check all dependent applications are not installed
+ # first.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index f86ff3551a1..987c057ad6d 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -38,6 +38,12 @@ module Clusters
content_values.to_yaml
end
+ # Will be addressed in future MRs
+ # We need to investigate and document what will be permenantly deleted.
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index f7e54833296..9fbf5d8af04 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Knative < ApplicationRecord
- VERSION = '0.3.0'.freeze
+ VERSION = '0.5.0'.freeze
REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'.freeze
FETCH_IP_ADDRESS_DELAY = 30.seconds
@@ -51,6 +51,12 @@ module Clusters
{ "domain" => hostname }.to_yaml
end
+ # Handled in a new issue:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/59369
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 954c29da196..a6b7617b830 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -16,10 +16,12 @@ module Clusters
default_value_for :version, VERSION
+ after_destroy :disable_prometheus_integration
+
state_machine :status do
after_transition any => [:installed] do |application|
application.cluster.projects.each do |project|
- project.find_or_initialize_service('prometheus').update(active: true)
+ project.find_or_initialize_service('prometheus').update!(active: true)
end
end
end
@@ -47,6 +49,14 @@ module Clusters
)
end
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: name,
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files
+ )
+ end
+
def upgrade_command(values)
::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
@@ -82,6 +92,12 @@ module Clusters
private
+ def disable_prometheus_integration
+ cluster.projects.each do |project|
+ project.prometheus_service&.update!(active: false)
+ end
+ end
+
def kube_client
cluster&.kubeclient&.core_client
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 8cb81bfcbe4..ceecd931bba 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.3.0'.freeze
+ VERSION = '0.4.1'.freeze
self.table_name = 'clusters_applications_runners'
@@ -29,6 +29,13 @@ module Clusters
content_values.to_yaml
end
+ # Need to investigate if pipelines run by this runner will stop upon the
+ # executor pod stopping
+ # I.e.run a pipeline, and uninstall runner while pipeline is running
+ def allowed_to_uninstall?
+ false
+ end
+
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,
@@ -62,10 +69,12 @@ module Clusters
}
if cluster.group_type?
- attributes.merge(groups: [group])
+ attributes[:groups] = [group]
elsif cluster.project_type?
- attributes.merge(projects: [project])
+ attributes[:projects] = [project]
end
+
+ attributes
end
def gitlab_url
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 4262c03498d..9299e61dad3 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -10,14 +10,14 @@ module Clusters
PROJECT_ONLY_APPLICATIONS = {
Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative,
- Applications::Prometheus.application_name => Applications::Prometheus
+ Applications::Knative.application_name => Applications::Knative
}.freeze
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
Applications::CertManager.application_name => Applications::CertManager,
- Applications::Runner.application_name => Applications::Runner
+ Applications::Runner.application_name => Applications::Runner,
+ Applications::Prometheus.application_name => Applications::Prometheus
}.merge(PROJECT_ONLY_APPLICATIONS).freeze
DEFAULT_ENVIRONMENT = '*'.freeze
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'.freeze
@@ -94,6 +94,7 @@ module Clusters
scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
+ scope :managed, -> { where(managed: true) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
@@ -114,10 +115,12 @@ module Clusters
}
def self.ancestor_clusters_for_clusterable(clusterable, hierarchy_order: :asc)
+ return [] if clusterable.is_a?(Instance)
+
hierarchy_groups = clusterable.ancestors_upto(hierarchy_order: hierarchy_order).eager_load(:clusters)
hierarchy_groups = hierarchy_groups.merge(current_scope) if current_scope
- hierarchy_groups.flat_map(&:clusters)
+ hierarchy_groups.flat_map(&:clusters) + Instance.new.clusters
end
def status_name
@@ -176,6 +179,10 @@ module Clusters
end
alias_method :group, :first_group
+ def instance
+ Instance.new if instance_type?
+ end
+
def kubeclient
platform_kubernetes.kubeclient if kubernetes?
end
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index ee964fb7c93..4514498b84b 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -18,6 +18,16 @@ module Clusters
self.status = 'installable' if cluster&.application_helm_available?
end
+ def can_uninstall?
+ allowed_to_uninstall?
+ end
+
+ # All new applications should uninstall by default
+ # Override if there's dependencies that needs to be uninstalled first
+ def allowed_to_uninstall?
+ true
+ end
+
def self.application_name
self.to_s.demodulize.underscore
end
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
index 52498f123ff..a48ee340fac 100644
--- a/app/models/clusters/concerns/application_data.rb
+++ b/app/models/clusters/concerns/application_data.rb
@@ -6,6 +6,14 @@ module Clusters
extend ActiveSupport::Concern
included do
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: name,
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files
+ )
+ end
+
def repository
nil
end
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 1273ed83abe..54a3dda6d75 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -25,9 +25,11 @@ module Clusters
state :updating, value: 4
state :updated, value: 5
state :update_errored, value: 6
+ state :uninstalling, value: 7
+ state :uninstall_errored, value: 8
event :make_scheduled do
- transition [:installable, :errored, :installed, :updated, :update_errored] => :scheduled
+ transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled
end
event :make_installing do
@@ -40,8 +42,9 @@ module Clusters
end
event :make_errored do
- transition any - [:updating] => :errored
+ transition any - [:updating, :uninstalling] => :errored
transition [:updating] => :update_errored
+ transition [:uninstalling] => :uninstall_errored
end
event :make_updating do
@@ -52,6 +55,10 @@ module Clusters
transition any => :update_errored
end
+ event :make_uninstalling do
+ transition [:scheduled] => :uninstalling
+ end
+
before_transition any => [:scheduled] do |app_status, _|
app_status.status_reason = nil
end
@@ -65,7 +72,7 @@ module Clusters
app_status.status_reason = nil
end
- before_transition any => [:update_errored] do |app_status, transition|
+ before_transition any => [:update_errored, :uninstall_errored] do |app_status, transition|
status_reason = transition.args.first
app_status.status_reason = status_reason if status_reason
end
diff --git a/app/models/clusters/instance.rb b/app/models/clusters/instance.rb
new file mode 100644
index 00000000000..d8a888d53ba
--- /dev/null
+++ b/app/models/clusters/instance.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Clusters
+ class Instance
+ def clusters
+ Clusters::Cluster.instance_type
+ end
+
+ def feature_available?(feature)
+ ::Feature.enabled?(feature, default_enabled: true)
+ end
+
+ def self.enabled?
+ ::Feature.enabled?(:instance_clusters, default_enabled: true)
+ end
+ end
+end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 2ae141190a8..3b7b93e7631 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -76,6 +76,10 @@ module Clusters
end
end
+ def namespace_for(project)
+ cluster.find_or_initialize_kubernetes_namespace_for_project(project).namespace
+ end
+
def predefined_variables(project:)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'KUBE_URL', value: api_url)
@@ -88,11 +92,12 @@ module Clusters
if kubernetes_namespace = cluster.kubernetes_namespaces.has_service_account_token.find_by(project: project)
variables.concat(kubernetes_namespace.predefined_variables)
- elsif cluster.project_type?
- # From 11.5, every Clusters::Project should have at least one
- # Clusters::KubernetesNamespace, so once migration has been completed,
- # this 'else' branch will be removed. For more information, please see
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22433
+ elsif cluster.project_type? || !cluster.managed?
+ # As of 11.11 a user can create a cluster that they manage themselves,
+ # which replicates the existing project-level cluster behaviour.
+ # Once we have marked all project-level clusters that make use of this
+ # behaviour as "unmanaged", we can remove the `cluster.project_type?`
+ # check here.
variables
.append(key: 'KUBE_URL', value: api_url)
.append(key: 'KUBE_TOKEN', value: token, public: false, masked: true)
@@ -230,7 +235,7 @@ module Clusters
end
def update_kubernetes_namespace
- return unless namespace_changed?
+ return unless saved_change_to_namespace?
run_after_commit do
ClusterConfigureWorker.perform_async(cluster_id)
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 920b1d092dd..08ca86bc902 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -28,12 +28,12 @@ class CommitRange
# The beginning and ending refs can be named or SHAs, and
# the range notation can be double- or triple-dot.
- REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/
- PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
+ REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/.freeze
+ PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/.freeze
# In text references, the beginning and ending refs can only be SHAs
# between 7 and 40 hex characters.
- STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/
+ STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/.freeze
def self.reference_prefix
'@'
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index f97dc38dab7..be6f3e9c5b0 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -140,7 +140,7 @@ class CommitStatus < ApplicationRecord
end
def locking_enabled?
- status_changed?
+ will_save_change_to_status?
end
def before_sha
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index ab3d9e923c0..dc1735a7e48 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -53,6 +53,20 @@ module AtomicInternalId
value
end
+
+ define_method("reset_#{scope}_#{column}") do
+ if value = read_attribute(column)
+ scope_value = association(scope).reader
+ scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value }
+ usage = self.class.table_name.to_sym
+
+ if InternalId.reset(self, scope_attrs, usage, value)
+ write_attribute(column, nil)
+ end
+ end
+
+ read_attribute(column)
+ end
end
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 4687ec7d166..80278e07e65 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -91,7 +91,8 @@ module Avatarable
private
def retrieve_upload_from_batch(identifier)
- BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args|
+ BatchLoader.for(identifier: identifier, model: self)
+ .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args|
model_class = args[:key]
paths = upload_params.flat_map do |params|
params[:model].upload_paths(params[:identifier])
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 28ea51d6769..f90cd1ea690 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -15,7 +15,7 @@ module CacheMarkdownField
# Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION_START = 10
- CACHE_COMMONMARK_VERSION = 15
+ CACHE_COMMONMARK_VERSION = 16
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 4986a42dbd2..e1d5ce7f7d4 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -70,8 +70,8 @@ module Ci
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
- variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -85,8 +85,8 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
- variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
- variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
variables.append(key: 'CI_BUILD_NAME', value: name)
variables.append(key: 'CI_BUILD_STAGE', value: stage)
variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb
new file mode 100644
index 00000000000..dbc5ed1bc9a
--- /dev/null
+++ b/app/models/concerns/ci/pipeline_delegator.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+##
+# This module is mainly used by child associations of `Ci::Pipeline` that needs to look up
+# single source of truth. For example, `Ci::Build` has `git_ref` method, which behaves
+# slightly different from `Ci::Pipeline`'s `git_ref`. This is very confusing as
+# the system could behave differently time to time.
+# We should have a single interface in `Ci::Pipeline` and access the method always.
+module Ci
+ module PipelineDelegator
+ extend ActiveSupport::Concern
+
+ included do
+ delegate :merge_request_event?,
+ :merge_request_ref?,
+ :source_ref,
+ :source_ref_slug,
+ :legacy_detached_merge_request_pipeline?, to: :pipeline
+ end
+ end
+end
diff --git a/app/models/concerns/deployment_platform.rb b/app/models/concerns/deployment_platform.rb
index 0107af5f8ec..9ac0d612db3 100644
--- a/app/models/concerns/deployment_platform.rb
+++ b/app/models/concerns/deployment_platform.rb
@@ -14,6 +14,7 @@ module DeploymentPlatform
def find_deployment_platform(environment)
find_cluster_platform_kubernetes(environment: environment) ||
find_group_cluster_platform_kubernetes_with_feature_guard(environment: environment) ||
+ find_instance_cluster_platform_kubernetes_with_feature_guard(environment: environment) ||
find_kubernetes_service_integration ||
build_cluster_and_deployment_platform
end
@@ -36,6 +37,18 @@ module DeploymentPlatform
.first&.platform_kubernetes
end
+ def find_instance_cluster_platform_kubernetes_with_feature_guard(environment: nil)
+ return unless Clusters::Instance.enabled?
+
+ find_instance_cluster_platform_kubernetes(environment: environment)
+ end
+
+ # EE would override this and utilize environment argument
+ def find_instance_cluster_platform_kubernetes(environment: nil)
+ Clusters::Instance.new.clusters.enabled.default_environment
+ .first&.platform_kubernetes
+ end
+
def find_kubernetes_service_integration
services.deployment.reorder(nil).find_by(active: true)
end
diff --git a/app/models/concerns/has_ref.rb b/app/models/concerns/has_ref.rb
index 413cd36dcaa..fa0cf5ddfd2 100644
--- a/app/models/concerns/has_ref.rb
+++ b/app/models/concerns/has_ref.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+##
+# We will disable `ref` and `sha` attributes in `Ci::Build` in the future
+# and remove this module in favor of Ci::PipelineDelegator.
module HasRef
extend ActiveSupport::Concern
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 8882f48c281..78bcce2f592 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -66,6 +66,10 @@ module HasStatus
def all_state_names
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
end
+
+ def completed_statuses
+ COMPLETED_STATUSES.map(&:to_sym)
+ end
end
included do
diff --git a/app/models/concerns/has_variable.rb b/app/models/concerns/has_variable.rb
index 2ec42a1029b..b4e99569071 100644
--- a/app/models/concerns/has_variable.rb
+++ b/app/models/concerns/has_variable.rb
@@ -4,6 +4,11 @@ module HasVariable
extend ActiveSupport::Concern
included do
+ enum variable_type: {
+ env_var: 1,
+ file: 2
+ }
+
validates :key,
presence: true,
length: { maximum: 255 },
@@ -24,6 +29,6 @@ module HasVariable
end
def to_runner_variable
- { key: key, value: value, public: false }
+ { key: key, value: value, public: false, file: file? }
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3232c51bfbd..127430cc68f 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -117,7 +117,7 @@ module Issuable
# 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?
- title_changed? || description_changed?
+ will_save_change_to_title? || will_save_change_to_description?
end
def allows_multiple_assignees?
@@ -128,10 +128,6 @@ module Issuable
assignees.count > 1
end
- def milestone_available?
- project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
- end
-
private
def milestone_is_valid
@@ -277,6 +273,10 @@ module Issuable
end
end
+ def milestone_available?
+ project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
+ end
+
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
diff --git a/app/models/concerns/maskable.rb b/app/models/concerns/maskable.rb
index 8793f0ec965..2943872ffab 100644
--- a/app/models/concerns/maskable.rb
+++ b/app/models/concerns/maskable.rb
@@ -9,7 +9,7 @@ module Maskable
# * No spaces
# * Minimal length of 8 characters
# * Absolutely no fun is allowed
- REGEX = /\A\w{8,}\z/
+ REGEX = /\A\w{8,}\z/.freeze
included do
validates :masked, inclusion: { in: [true, false] }
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index 423ce7e7db1..bfd0c36942b 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -13,7 +13,7 @@ module Noteable
end
end
- # The timestamp of the note (e.g. the :updated_at attribute if provided via
+ # The timestamp of the note (e.g. the :created_at or :updated_at attribute if provided via
# API call)
def system_note_timestamp
@system_note_timestamp || Time.now # rubocop:disable Gitlab/ModuleWithInstanceVariables
diff --git a/app/models/concerns/redactable.rb b/app/models/concerns/redactable.rb
index 5ad96d6cc46..53ae300ee2d 100644
--- a/app/models/concerns/redactable.rb
+++ b/app/models/concerns/redactable.rb
@@ -10,7 +10,7 @@
module Redactable
extend ActiveSupport::Concern
- UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}
+ UNSUBSCRIBE_PATTERN = %r{/sent_notifications/\h{32}/unsubscribe}.freeze
class_methods do
def redact_field(field)
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 498996f4f80..a15dc19e07a 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -13,20 +13,20 @@ module Storage
raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry")
end
- parent_was = if parent_changed? && parent_id_was.present?
- Namespace.find(parent_id_was) # raise NotFound early if needed
+ parent_was = if saved_change_to_parent? && parent_id_before_last_save.present?
+ Namespace.find(parent_id_before_last_save) # raise NotFound early if needed
end
move_repositories
- if parent_changed?
+ if saved_change_to_parent?
former_parent_full_path = parent_was&.full_path
parent_full_path = parent&.full_path
Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
else
- Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
- Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
+ Gitlab::UploadsTransfer.new.rename_namespace(full_path_before_last_save, full_path)
+ Gitlab::PagesTransfer.new.rename_namespace(full_path_before_last_save, full_path)
end
# If repositories moved successfully we need to
@@ -38,7 +38,7 @@ module Storage
write_projects_repository_config
rescue => e
# Raise if development/test environment, else just notify Sentry
- Gitlab::Sentry.track_exception(e, extra: { full_path_was: full_path_was, full_path: full_path, action: 'move_dir' })
+ Gitlab::Sentry.track_exception(e, extra: { full_path_before_last_save: full_path_before_last_save, full_path: full_path, action: 'move_dir' })
end
true # false would cancel later callbacks but not rollback
@@ -57,14 +57,14 @@ module Storage
# Move the namespace directory in all storages used by member projects
repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage, full_path_was)
+ 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
- unless gitlab_shell.mv_namespace(repository_storage, full_path_was, 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_was} to #{full_path}"
+ Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_before_last_save} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
@@ -104,11 +104,5 @@ module Storage
end
end
end
-
- def remove_legacy_exports!
- legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was)
-
- FileUtils.rm_rf(legacy_export_path)
- end
end
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index f147ce8ad6b..2f0e078c807 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -19,7 +19,7 @@ module Taskable
\s+ # whitespace prefix has to be always presented for a list item
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
- }x
+ }x.freeze
def self.get_tasks(content)
content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
new file mode 100644
index 00000000000..67e1f0ec930
--- /dev/null
+++ b/app/models/concerns/update_project_statistics.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# This module is providing helpers for updating `ProjectStatistics` with `after_save` and `before_destroy` hooks.
+#
+# It deals with `ProjectStatistics.increment_statistic` making sure not to update statistics on a cascade delete from the
+# project, and keeping track of value deltas on each save. It updates the DB only when a change is needed.
+#
+# How to use
+# - Invoke `update_project_statistics stat: :a_project_statistics_column, attribute: :an_attr_to_track` in a model class body.
+#
+# Expectation
+# - `attribute` must be an ActiveRecord attribute
+# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
+module UpdateProjectStatistics
+ extend ActiveSupport::Concern
+
+ class_methods do
+ attr_reader :statistic_name, :statistic_attribute
+
+ # Configure the model to update +stat+ on ProjectStatistics when +attribute+ changes
+ #
+ # +stat+:: a column of ProjectStatistics to update
+ # +attribute+:: an attribute of the current model, default to +:size+
+ def update_project_statistics(stat:, attribute: :size)
+ @statistic_name = stat
+ @statistic_attribute = attribute
+
+ after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?)
+ after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?)
+ end
+ private :update_project_statistics
+ end
+
+ included do
+ private
+
+ def project_destroyed?
+ project.pending_delete?
+ end
+
+ def update_project_statistics_attribute_changed?
+ saved_change_to_attribute?(self.class.statistic_attribute)
+ end
+
+ def update_project_statistics_after_save
+ attr = self.class.statistic_attribute
+ delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
+
+ update_project_statistics(delta)
+ end
+
+ def update_project_statistics_after_destroy
+ update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
+ end
+
+ def update_project_statistics(delta)
+ ProjectStatistics.increment_statistic(project_id, self.class.statistic_name, delta)
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index d847a0a11e4..92c7311014a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -47,6 +47,12 @@ class Deployment < ApplicationRecord
Deployments::SuccessWorker.perform_async(id)
end
end
+
+ after_transition any => [:success, :failed, :canceled] do |deployment|
+ deployment.run_after_commit do
+ Deployments::FinishedWorker.perform_async(id)
+ end
+ end
end
enum status: {
@@ -79,7 +85,16 @@ class Deployment < ApplicationRecord
end
def cluster
- project.deployment_platform(environment: environment.name)&.cluster
+ platform = project.deployment_platform(environment: environment.name)
+
+ if platform.present? && platform.respond_to?(:cluster)
+ platform.cluster
+ end
+ end
+
+ def execute_hooks
+ deployment_data = Gitlab::DataBuilder::Deployment.build(self)
+ project.execute_services(deployment_data, :deployment_hooks)
end
def last?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 69224635e34..0eda7a2513f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -3,8 +3,8 @@
class Environment < ApplicationRecord
include Gitlab::Utils::StrongMemoize
# Used to generate random suffixes for the slug
- LETTERS = 'a'..'z'
- NUMBERS = '0'..'9'
+ LETTERS = ('a'..'z').freeze
+ NUMBERS = ('0'..'9').freeze
SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
belongs_to :project, required: true
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 72270ee8b4f..0b4fef5eac1 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -16,7 +16,7 @@ module ErrorTracking
(?<project>[^/]+)/*
)?
\z
- }x
+ }x.freeze
self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index 572cb12b26a..7c020dd3b3d 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GlobalLabel
+ include Presentable
+
attr_accessor :title, :labels
alias_attribute :name, :title
@@ -23,4 +25,8 @@ class GlobalLabel
@labels = labels
@first_label = labels.find { |lbl| lbl.description.present? } || labels.first
end
+
+ def present(attributes)
+ super(attributes.merge(presenter_class: ::LabelPresenter))
+ end
end
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index fd17745b035..59f5a7703e2 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -8,7 +8,9 @@ class GlobalMilestone
attr_reader :milestone
alias_attribute :name, :title
- delegate :title, :state, :due_date, :start_date, :participants, :project, :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title, :milestoneish_id, to: :milestone
+ delegate :title, :state, :due_date, :start_date, :participants, :project,
+ :group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
+ :milestoneish_id, :parent, to: :milestone
def to_hash
{
diff --git a/app/models/group.rb b/app/models/group.rb
index 8bc9b75f0a9..53331a19776 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -61,7 +61,7 @@ class Group < Namespace
after_create :post_create_hook
after_destroy :post_destroy_hook
after_save :update_two_factor_requirement
- after_update :path_changed_hook, if: :path_changed?
+ after_update :path_changed_hook, if: :saved_change_to_path?
class << self
def sort_by_attribute(method)
@@ -411,7 +411,7 @@ class Group < Namespace
private
def update_two_factor_requirement
- return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed?
+ return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
users.find_each(&:update_two_factor_requirement)
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 3f2d368a3f2..237401899db 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -55,7 +55,8 @@ class InternalId < ApplicationRecord
def track_greatest(subject, scope, usage, new_value, init)
return new_value unless available?
- InternalIdGenerator.new(subject, scope, usage, init).track_greatest(new_value)
+ InternalIdGenerator.new(subject, scope, usage)
+ .track_greatest(init, new_value)
end
def generate_next(subject, scope, usage, init)
@@ -63,7 +64,15 @@ class InternalId < ApplicationRecord
# This can be the case in other (unrelated) migration specs
return (init.call(subject) || 0) + 1 unless available?
- InternalIdGenerator.new(subject, scope, usage, init).generate
+ InternalIdGenerator.new(subject, scope, usage)
+ .generate(init)
+ end
+
+ def reset(subject, scope, usage, value)
+ return false unless available?
+
+ InternalIdGenerator.new(subject, scope, usage)
+ .reset(value)
end
# Flushing records is generally safe in a sense that those
@@ -78,12 +87,16 @@ class InternalId < ApplicationRecord
end
def available?
- @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
+ return true unless Rails.env.test?
+
+ Gitlab::SafeRequestStore.fetch(:internal_ids_available_flag) do
+ ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION
+ end
end
# Flushes cached information about schema
def reset_column_information
- @available_flag = nil
+ Gitlab::SafeRequestStore[:internal_ids_available_flag] = nil
super
end
end
@@ -103,14 +116,11 @@ class InternalId < ApplicationRecord
# subject: The instance we're generating an internal id for. Gets passed to init if called.
# scope: Attributes that define the scope for id generation.
# usage: Symbol to define the usage of the internal id, see InternalId.usages
- # init: Block that gets called to initialize InternalId record if not present
- # Make sure to not throw exceptions in the absence of records (if this is expected).
- attr_reader :subject, :scope, :init, :scope_attrs, :usage
+ attr_reader :subject, :scope, :scope_attrs, :usage
- def initialize(subject, scope, usage, init)
+ def initialize(subject, scope, usage)
@subject = subject
@scope = scope
- @init = init
@usage = usage
raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
@@ -121,23 +131,40 @@ class InternalId < ApplicationRecord
end
# Generates next internal id and returns it
- def generate
+ # init: Block that gets called to initialize InternalId record if not present
+ # Make sure to not throw exceptions in the absence of records (if this is expected).
+ def generate(init)
subject.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
- (lookup || create_record).increment_and_save!
+ (lookup || create_record(init)).increment_and_save!
end
end
+ # Reset tries to rewind to `value-1`. This will only succeed,
+ # if `value` stored in database is equal to `last_value`.
+ # value: The expected last_value to decrement
+ def reset(value)
+ return false unless value
+
+ updated =
+ InternalId
+ .where(**scope, usage: usage_value)
+ .where(last_value: value)
+ .update_all('last_value = last_value - 1')
+
+ updated > 0
+ end
+
# Create a record in internal_ids if one does not yet exist
# and set its new_value if it is higher than the current last_value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
- def track_greatest(new_value)
+ def track_greatest(init, new_value)
subject.transaction do
- (lookup || create_record).track_greatest_and_save!(new_value)
+ (lookup || create_record(init)).track_greatest_and_save!(new_value)
end
end
@@ -158,7 +185,7 @@ class InternalId < ApplicationRecord
# was faster in doing this, we'll realize once we hit the unique key constraint
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
- def create_record
+ def create_record(init)
subject.transaction(requires_new: true) do
InternalId.create!(
**scope,
diff --git a/app/models/label.rb b/app/models/label.rb
index c7fff0d393e..e9085e8bd25 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -8,6 +8,7 @@ class Label < ApplicationRecord
include OptionallySearch
include Sortable
include FromUnion
+ include Presentable
cache_markdown_field :description, pipeline: :single_line
@@ -233,6 +234,10 @@ class Label < ApplicationRecord
attributes
end
+ def present(attributes)
+ super(attributes.merge(presenter_class: ::LabelPresenter))
+ end
+
private
def issues_count(user, params = {})
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index e1aac691a64..5245dbc8d15 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -13,7 +13,7 @@ class LfsObject < ApplicationRecord
mount_uploader :file, LfsObjectUploader
- after_save :update_file_store, if: :file_changed?
+ after_save :update_file_store, if: :saved_change_to_file?
def update_file_store
# The file.object_store is set during `uploader.store!`
diff --git a/app/models/member.rb b/app/models/member.rb
index 8a06bff51b5..83b4f5b29c4 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -446,10 +446,10 @@ class Member < ApplicationRecord
end
def higher_access_level_than_group
- if highest_group_member && highest_group_member.access_level >= access_level
+ if highest_group_member && highest_group_member.access_level > access_level
error_parameters = { access: highest_group_member.human_access, group_name: highest_group_member.group.name }
- errors.add(:access_level, s_("should be higher than %{access} inherited membership from group %{group_name}") % error_parameters)
+ errors.add(:access_level, s_("should be greater than or equal to %{access} inherited membership from group %{group_name}") % error_parameters)
end
end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 510f856087d..b266c61f002 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -55,7 +55,7 @@ class GroupMember < Member
end
def post_update_hook
- if access_level_changed?
+ if saved_change_to_access_level?
run_after_commit { notification_service.update_group_member(self) }
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 5372c6084f4..c64e2669b6a 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -111,7 +111,7 @@ class ProjectMember < Member
end
def post_update_hook
- if access_level_changed?
+ if saved_change_to_access_level?
run_after_commit { notification_service.update_project_member(self) }
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 0a39a720766..df162e4844c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -66,7 +66,7 @@ class MergeRequest < ApplicationRecord
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue
- has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
+ has_many :pipelines_for_merge_request, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline'
has_many :suggestions, through: :notes
has_many :merge_request_assignees
@@ -698,7 +698,7 @@ class MergeRequest < ApplicationRecord
end
def reload_diff_if_branch_changed
- if (source_branch_changed? || target_branch_changed?) &&
+ if (saved_change_to_source_branch? || saved_change_to_target_branch?) &&
(source_branch_head && target_branch_head)
reload_diff
end
@@ -1054,6 +1054,16 @@ class MergeRequest < ApplicationRecord
@environments[current_user]
end
+ ##
+ # This method is for looking for active environments which created via pipelines for merge requests.
+ # Since deployments run on a merge request ref (e.g. `refs/merge-requests/:iid/head`),
+ # we cannot look up environments with source branch name.
+ def environments
+ return Environment.none unless actual_head_pipeline&.triggered_by_merge_request?
+
+ actual_head_pipeline.environments
+ end
+
def state_human_name
if merged?
"Merged"
@@ -1147,12 +1157,8 @@ class MergeRequest < ApplicationRecord
end
end
- def merge_request_pipeline_exists?
- merge_request_pipelines.exists?(sha: diff_head_sha)
- end
-
def has_test_reports?
- actual_head_pipeline&.has_test_reports?
+ actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports)
end
def predefined_variables
@@ -1369,12 +1375,12 @@ class MergeRequest < ApplicationRecord
source_project.repository.squash_in_progress?(id)
end
- private
-
def find_actual_head_pipeline
all_pipelines.for_sha_or_source_sha(diff_head_sha).first
end
+ private
+
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 0b787217410..f45bd0e03de 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -51,6 +51,10 @@ class MergeRequestDiff < ApplicationRecord
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
end
+ scope :by_project_id, -> (project_id) do
+ joins(:merge_request).where(merge_requests: { target_project_id: project_id })
+ end
+
scope :recent, -> { order(id: :desc).limit(100) }
scope :files_in_database, -> { where(stored_externally: [false, nil]) }
@@ -134,7 +138,7 @@ class MergeRequestDiff < ApplicationRecord
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
- after_save :update_external_diff_store, if: -> { !importing? && external_diff_changed? }
+ after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? }
def self.find_by_diff_refs(diff_refs)
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
@@ -154,7 +158,14 @@ class MergeRequestDiff < ApplicationRecord
ensure_commit_shas
save_commits
save_diffs
+
+ # Another set of `after_save` hooks will be called here when we update the record
save
+ # We need to reset so that dirty tracking is reset when running the original set
+ # of `after_save` hooks that come after this `after_create` hook. Otherwise, the
+ # hooks that run when an attribute was changed are run twice.
+ reset
+
keep_around_commits
end
@@ -348,7 +359,7 @@ class MergeRequestDiff < ApplicationRecord
has_attribute?(:external_diff_store)
end
- def external_diff_changed?
+ def saved_change_to_external_diff?
super if has_attribute?(:external_diff)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index dea34e812ca..7393ef4b05c 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -50,17 +50,20 @@ class Namespace < ApplicationRecord
validate :nesting_level_allowed
+ validates_associated :runners
+
delegate :name, to: :owner, allow_nil: true, prefix: true
+ delegate :avatar_url, to: :owner, allow_nil: true
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
before_create :sync_share_with_group_lock_with_parent
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
- after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
+ after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
# Legacy Storage specific hooks
- after_update :move_dir, if: :path_or_parent_changed?
+ after_update :move_dir, if: :saved_change_to_path_or_parent?
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
@@ -74,7 +77,8 @@ class Namespace < ApplicationRecord
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
- 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size'
+ 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
+ 'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
)
end
@@ -141,7 +145,7 @@ class Namespace < ApplicationRecord
def send_update_instructions
projects.each do |project|
- project.send_move_instructions("#{full_path_was}/#{project.path}")
+ project.send_move_instructions("#{full_path_before_last_save}/#{project.path}")
end
end
@@ -149,6 +153,10 @@ class Namespace < ApplicationRecord
type == 'Group' ? 'group' : 'user'
end
+ def user?
+ kind == 'user'
+ end
+
def find_fork_of(project)
return unless project.fork_network
@@ -222,10 +230,6 @@ class Namespace < ApplicationRecord
[owner_id]
end
- def parent_changed?
- parent_id_changed?
- end
-
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
@@ -255,12 +259,12 @@ class Namespace < ApplicationRecord
false
end
- def full_path_was
- if parent_id_was.nil?
- path_was
+ def full_path_before_last_save
+ if parent_id_before_last_save.nil?
+ path_before_last_save
else
- previous_parent = Group.find_by(id: parent_id_was)
- previous_parent.full_path + '/' + path_was
+ previous_parent = Group.find_by(id: parent_id_before_last_save)
+ previous_parent.full_path + '/' + path_before_last_save
end
end
@@ -286,8 +290,16 @@ class Namespace < ApplicationRecord
private
- def path_or_parent_changed?
- path_changed? || parent_changed?
+ def parent_changed?
+ parent_id_changed?
+ end
+
+ def saved_change_to_parent?
+ saved_change_to_parent_id?
+ end
+
+ def saved_change_to_path_or_parent?
+ saved_change_to_path? || saved_change_to_parent_id?
end
def refresh_access_of_projects_invited_groups
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
index 9afb94c869a..fcc9e2b3fd8 100644
--- a/app/models/note_diff_file.rb
+++ b/app/models/note_diff_file.rb
@@ -7,6 +7,10 @@ class NoteDiffFile < ApplicationRecord
joins(:diff_note).where("resolved_at IS NULL OR noteable_type = 'Commit'")
end
+ scope :referencing_sha, -> (oids, project_id:) do
+ joins(:diff_note).where(notes: { project_id: project_id, commit_id: oids })
+ end
+
delegate :original_position, :project, to: :diff_note
belongs_to :diff_note, inverse_of: :note_diff_file
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 793cce191fa..6889e0d776b 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -47,14 +47,14 @@ class NotificationRecipient
def suitable_notification_level?
case notification_level
- when :disabled, nil
- false
- when :custom
- custom_enabled? || %i[participating mention].include?(@type)
- when :watch, :participating
- !action_excluded?
when :mention
@type == :mention
+ when :participating
+ !excluded_participating_action? && %i[participating mention watch].include?(@type)
+ when :custom
+ custom_enabled? || %i[participating mention].include?(@type)
+ when :watch
+ !excluded_watcher_action?
else
false
end
@@ -100,18 +100,14 @@ class NotificationRecipient
end
end
- def action_excluded?
- excluded_watcher_action? || excluded_participating_action?
- end
-
def excluded_watcher_action?
- return false unless @custom_action && notification_level == :watch
+ return false unless @custom_action
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
def excluded_participating_action?
- return false unless @custom_action && notification_level == :participating
+ return false unless @custom_action
NotificationSetting::EXCLUDED_PARTICIPATING_EVENTS.include?(@custom_action)
end
@@ -123,15 +119,19 @@ class NotificationRecipient
return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability =
- case @target
- when Issuable
- :"read_#{@target.to_ability_name}"
- when Ci::Pipeline
+ if @target.is_a?(Ci::Pipeline)
:read_build # We have build trace in pipeline emails
- when ActiveRecord::Base
- :"read_#{@target.class.model_name.name.underscore}"
- else
- nil
+ elsif default_ability_for_target
+ :"read_#{default_ability_for_target}"
+ end
+ end
+
+ def default_ability_for_target
+ @default_ability_for_target ||=
+ if @target.respond_to?(:to_ability_name)
+ @target.to_ability_name
+ elsif @target.class.respond_to?(:model_name)
+ @target.class.model_name.name.underscore
end
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 82901ceec01..407d85b1520 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -26,7 +26,7 @@ class PagesDomain < ApplicationRecord
after_initialize :set_verification_code
after_create :update_daemon
- after_update :update_daemon, if: :pages_config_changed?
+ after_update :update_daemon, if: :saved_change_to_pages_config?
after_destroy :update_daemon
scope :enabled, -> { where('enabled_until >= ?', Time.now ) }
@@ -38,6 +38,8 @@ class PagesDomain < ApplicationRecord
where(verified_at.eq(nil).or(enabled_until.eq(nil).or(enabled_until.lt(threshold))))
end
+ scope :for_removal, -> { where("remove_at < ?", Time.now) }
+
def verified?
!!verified_at
end
@@ -146,21 +148,21 @@ class PagesDomain < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def pages_config_changed?
- project_id_changed? ||
- domain_changed? ||
- certificate_changed? ||
- key_changed? ||
+ def saved_change_to_pages_config?
+ saved_change_to_project_id? ||
+ saved_change_to_domain? ||
+ saved_change_to_certificate? ||
+ saved_change_to_key? ||
became_enabled? ||
became_disabled?
end
def became_enabled?
- enabled_until.present? && !enabled_until_was.present?
+ enabled_until.present? && !enabled_until_before_last_save.present?
end
def became_disabled?
- !enabled_until.present? && enabled_until_was.present?
+ !enabled_until.present? && enabled_until_before_last_save.present?
end
def validate_matching_key
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 7934118761e..50eed7344bd 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -7,7 +7,7 @@ class PoolRepository < ApplicationRecord
include Shardable
include AfterCommitQueue
- has_one :source_project, class_name: 'Project'
+ belongs_to :source_project, class_name: 'Project'
validates :source_project, presence: true
has_many :member_projects, class_name: 'Project'
@@ -99,7 +99,8 @@ class PoolRepository < ApplicationRecord
end
def inspect
- "#<#{self.class.name} id:#{id} state:#{state} disk_path:#{disk_path} source_project: #{source_project.full_path}>"
+ source = source_project ? source_project.full_path : 'nil'
+ "#<#{self.class.name} id:#{id} state:#{state} disk_path:#{disk_path} source_project: #{source}>"
end
private
diff --git a/app/models/project.rb b/app/models/project.rb
index 2fb6f5cb6a7..61d245478ca 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -56,6 +56,7 @@ class Project < ApplicationRecord
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
ignore_column :import_status, :import_jid, :import_error
+ ignore_column :ci_id
cache_markdown_field :description, pipeline: :description
@@ -90,7 +91,7 @@ class Project < ApplicationRecord
before_save :ensure_runners_token
- after_save :update_project_statistics, if: :namespace_id_changed?
+ after_save :update_project_statistics, if: :saved_change_to_namespace_id?
after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
@@ -116,7 +117,7 @@ class Project < ApplicationRecord
after_initialize :use_hashed_storage
after_create :check_repository_absence!
after_create :ensure_storage_path_exists
- after_save :ensure_storage_path_exists, if: :namespace_id_changed?
+ after_save :ensure_storage_path_exists, if: :saved_change_to_namespace_id?
acts_as_ordered_taggable
@@ -188,6 +189,7 @@ class Project < ApplicationRecord
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :project_repository, inverse_of: :project
has_one :error_tracking_setting, inverse_of: :project, class_name: 'ErrorTracking::ProjectErrorTrackingSetting'
+ has_one :metrics_setting, inverse_of: :project, class_name: 'ProjectMetricsSetting'
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -297,6 +299,7 @@ class Project < ApplicationRecord
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
accepts_nested_attributes_for :error_tracking_setting, update_only: true
+ accepts_nested_attributes_for :metrics_setting, update_only: true, allow_destroy: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -354,7 +357,8 @@ class Project < ApplicationRecord
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") }
- scope :sorted_by_stars, -> { reorder(star_count: :desc) }
+ scope :sorted_by_stars_desc, -> { reorder(star_count: :desc) }
+ scope :sorted_by_stars_asc, -> { reorder(star_count: :asc) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -460,10 +464,12 @@ class Project < ApplicationRecord
# Returns a collection of projects that is either public or visible to the
# logged in user.
- def self.public_or_visible_to_user(user = nil)
+ def self.public_or_visible_to_user(user = nil, min_access_level = nil)
+ min_access_level = nil if user&.admin?
+
if user
where('EXISTS (?) OR projects.visibility_level IN (?)',
- user.authorizations_for_projects,
+ user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user))
else
public_to_user
@@ -473,30 +479,32 @@ class Project < ApplicationRecord
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
# they are only available to team members. This scope returns projects where
# the feature is either public, enabled, or internal with permission for the user.
+ # Note: this scope doesn't enforce that the user has access to the projects, it just checks
+ # that the user has access to the feature. It's important to use this scope with others
+ # that checks project authorizations first.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
- min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin?
with_feature_enabled(feature)
elsif user
+ min_access_level = ProjectFeature.required_minimum_access_level(feature)
column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature
- .where(
- "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
- " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
- {
- private: Gitlab::VisibilityLevel::PRIVATE,
- public_visible: ProjectFeature::ENABLED,
- private_visible: ProjectFeature::PRIVATE,
- authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
- })
+ .where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
+ {
+ public_visible: visible,
+ private_visible: ProjectFeature::PRIVATE,
+ authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
+ })
else
+ # This has to be added to include features whose value is nil in the db
+ visible << nil
with_feature_access_level(feature, visible)
end
end
@@ -541,7 +549,9 @@ class Project < ApplicationRecord
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
when 'stars_desc'
- sorted_by_stars
+ sorted_by_stars_desc
+ when 'stars_asc'
+ sorted_by_stars_asc
else
order_by(method)
end
@@ -1430,7 +1440,7 @@ class Project < ApplicationRecord
# update visibility_level of forks
def update_forks_visibility_level
- return unless visibility_level < visibility_level_was
+ return unless visibility_level < visibility_level_before_last_save
forks.each do |forked_project|
if forked_project.visibility_level > visibility_level
@@ -1910,8 +1920,8 @@ class Project < ApplicationRecord
false
end
- def full_path_was
- File.join(namespace.full_path, previous_changes['path'].first)
+ def full_path_before_last_save
+ File.join(namespace.full_path, path_before_last_save)
end
alias_method :name_with_namespace, :full_name
@@ -2131,13 +2141,11 @@ class Project < ApplicationRecord
end
def create_new_pool_repository
- pool = begin
- create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
- rescue ActiveRecord::RecordNotUnique
- pool_repository(true)
- end
+ pool = PoolRepository.safe_find_or_create_by!(shard: Shard.by_name(repository_storage), source_project: self)
+ update!(pool_repository: pool)
pool.schedule unless pool.scheduled?
+
pool
end
diff --git a/app/models/project_metrics_setting.rb b/app/models/project_metrics_setting.rb
new file mode 100644
index 00000000000..a2a7dc571a4
--- /dev/null
+++ b/app/models/project_metrics_setting.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ProjectMetricsSetting < ApplicationRecord
+ belongs_to :project
+
+ validates :external_dashboard_url,
+ length: { maximum: 255 },
+ addressable_url: { enforce_sanitization: true, ascii_only: true }
+end
diff --git a/app/models/project_services/chat_message/deployment_message.rb b/app/models/project_services/chat_message/deployment_message.rb
new file mode 100644
index 00000000000..dae3a56116e
--- /dev/null
+++ b/app/models/project_services/chat_message/deployment_message.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module ChatMessage
+ class DeploymentMessage < BaseMessage
+ attr_reader :commit_title
+ attr_reader :commit_url
+ attr_reader :deployable_id
+ attr_reader :deployable_url
+ attr_reader :environment
+ attr_reader :short_sha
+ attr_reader :status
+ attr_reader :user_url
+
+ def initialize(data)
+ super
+
+ @commit_title = data[:commit_title]
+ @commit_url = data[:commit_url]
+ @deployable_id = data[:deployable_id]
+ @deployable_url = data[:deployable_url]
+ @environment = data[:environment]
+ @short_sha = data[:short_sha]
+ @status = data[:status]
+ @user_url = data[:user_url]
+ end
+
+ def attachments
+ [{
+ text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
+ color: color
+ }]
+ end
+
+ def activity
+ {}
+ end
+
+ private
+
+ def message
+ "Deploy to #{environment} #{humanized_status}"
+ end
+
+ def color
+ case status
+ when 'success'
+ 'good'
+ when 'canceled'
+ 'warning'
+ when 'failed'
+ 'danger'
+ else
+ '#334455'
+ end
+ end
+
+ def project_link
+ link(project_name, project_url)
+ end
+
+ def deployment_link
+ link("##{deployable_id}", deployable_url)
+ end
+
+ def user_link
+ link(user_combined_name, user_url)
+ end
+
+ def commit_link
+ link(short_sha, commit_url)
+ end
+
+ def humanized_status
+ status == 'success' ? 'succeeded' : status
+ end
+ end
+end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index c10ee07ccf4..7c9ecc6b821 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -33,7 +33,7 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
- pipeline wiki_page]
+ pipeline wiki_page deployment]
end
def fields
@@ -122,6 +122,8 @@ class ChatNotificationService < Service
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
ChatMessage::WikiPageMessage.new(data)
+ when "deployment"
+ ChatMessage::DeploymentMessage.new(data)
end
end
diff --git a/app/models/project_services/discord_service.rb b/app/models/project_services/discord_service.rb
index 405676792de..4385834ed0a 100644
--- a/app/models/project_services/discord_service.rb
+++ b/app/models/project_services/discord_service.rb
@@ -33,6 +33,11 @@ class DiscordService < ChatNotificationService
# No-op.
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: "text", name: "webhook", placeholder: "e.g. https://discordapp.com/api/webhooks/…" },
diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb
index 272cd0f4e47..699cf1659d1 100644
--- a/app/models/project_services/hangouts_chat_service.rb
+++ b/app/models/project_services/hangouts_chat_service.rb
@@ -35,6 +35,11 @@ class HangoutsChatService < ChatNotificationService
'https://chat.googleapis.com/v1/spaces…'
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f650dbd3726..fc8afa9bead 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -94,6 +94,10 @@ class KubernetesService < DeploymentService
end
end
+ def namespace_for(project)
+ actual_namespace
+ end
+
# Check we can connect to the Kubernetes API
def test(*args)
kubeclient = build_kube_client!
diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb
index c34078f13c1..c22a6dc26f6 100644
--- a/app/models/project_services/microsoft_teams_service.rb
+++ b/app/models/project_services/microsoft_teams_service.rb
@@ -33,6 +33,11 @@ class MicrosoftTeamsService < ChatNotificationService
def default_channel_placeholder
end
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index d8bba58dcbf..c5e5f4f6400 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -2,7 +2,7 @@
# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
class MockCiService < CiService
- ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
+ ALLOWED_STATES = %w[failed canceled running pending success success-with-warnings skipped not_found].freeze
prop_accessor :mock_service_url
validates :mock_service_url, presence: true, public_url: true, if: :activated?
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index c020e72908c..6fe8cb40d25 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -7,7 +7,7 @@ class ProjectStatistics < ApplicationRecord
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
- INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size] }.freeze
+ INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
def total_repository_size
repository_size + lfs_objects_size
@@ -36,8 +36,13 @@ class ProjectStatistics < ApplicationRecord
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
+ # older migrations fail due to non-existent attribute without this
+ def packages_size
+ has_attribute?(:packages_size) ? super.to_i : 0
+ end
+
def update_storage_size
- self.storage_size = repository_size + lfs_objects_size + build_artifacts_size
+ self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size
end
# Since this incremental update method does not call update_storage_size above,
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 23ddd708396..c91add6439f 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -82,17 +82,25 @@ class ProjectWiki
end
def empty?
- pages(limit: 1).empty?
+ list_pages(limit: 1).empty?
end
+ # Lists wiki pages of the repository.
+ #
+ # limit - max number of pages returned by the method.
+ # sort - criterion by which the pages are sorted.
+ # direction - order of the sorted pages.
+ # load_content - option, which specifies whether the content inside the page
+ # will be loaded.
+ #
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def pages(limit: 0, sort: nil, direction: DIRECTION_ASC)
- sort ||= TITLE_ORDER
- direction_desc = direction == DIRECTION_DESC
-
- wiki.pages(
- limit: limit, sort: sort, direction_desc: direction_desc
+ def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
+ wiki.list_pages(
+ limit: limit,
+ sort: sort,
+ direction_desc: direction == DIRECTION_DESC,
+ load_content: load_content
).map do |page|
WikiPage.new(self, page, true)
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 0f9e94373c7..7bbeb3c9976 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -31,8 +31,11 @@ class Release < ApplicationRecord
actual_tag.nil?
end
- def assets_count
- links.count + sources.count
+ def assets_count(except: [])
+ links_count = links.count
+ sources_count = except.include?(:sources) ? 0 : sources.count
+
+ links_count + sources_count
end
def sources
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index b2fd5394a03..af705b29f7a 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -22,8 +22,8 @@ class RemoteMirror < ApplicationRecord
before_save :set_new_remote_name, if: :mirror_url_changed?
after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
- after_save :refresh_remote, if: :mirror_url_changed?
- after_update :reset_fields, if: :mirror_url_changed?
+ after_save :refresh_remote, if: :saved_change_to_mirror_url?
+ after_update :reset_fields, if: :saved_change_to_mirror_url?
after_commit :remove_remote, on: :destroy
@@ -133,6 +133,10 @@ class RemoteMirror < ApplicationRecord
end
alias_method :enabled?, :enabled
+ def disabled?
+ !enabled?
+ end
+
def updated_since?(timestamp)
last_update_started_at && last_update_started_at > timestamp && !update_failed?
end
@@ -248,7 +252,7 @@ class RemoteMirror < ApplicationRecord
# Before adding a new remote we have to delete the data from
# the previous remote name
- prev_remote_name = remote_name_was || fallback_remote_name
+ prev_remote_name = remote_name_before_last_save || fallback_remote_name
run_after_commit do
project.repository.async_remove_remote(prev_remote_name)
end
@@ -265,4 +269,8 @@ class RemoteMirror < ApplicationRecord
def mirror_url_changed?
url_changed? || credentials_changed?
end
+
+ def saved_change_to_mirror_url?
+ saved_change_to_url? || saved_change_to_credentials?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 574ce12b309..be17b54ff12 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -39,7 +39,8 @@ class Repository
changelog license_blob license_key gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref has_visible_content?
- issue_template_names merge_request_template_names xcode_project?).freeze
+ issue_template_names merge_request_template_names
+ metrics_dashboard_paths xcode_project?).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license).freeze
@@ -57,6 +58,7 @@ class Repository
avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names,
+ metrics_dashboard: :metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -299,13 +301,14 @@ class Repository
end
end
- def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:)
+ def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:, path: nil)
raw_repository.archive_metadata(
ref,
storage_path,
project.path,
format,
- append_sha: append_sha
+ append_sha: append_sha,
+ path: path
)
end
@@ -601,6 +604,11 @@ class Repository
end
cache_method :merge_request_template_names, fallback: []
+ def metrics_dashboard_paths
+ Gitlab::Metrics::Dashboard::Finder.find_all_paths_from_source(project)
+ end
+ cache_method :metrics_dashboard_paths
+
def readme
head_tree&.readme
end
@@ -1029,11 +1037,41 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end
+ # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
+ def rebase_deprecated(user, merge_request)
+ rebase_sha = raw.rebase_deprecated(
+ user,
+ merge_request.id,
+ branch: merge_request.source_branch,
+ branch_sha: merge_request.source_branch_sha,
+ remote_repository: merge_request.target_project.repository.raw,
+ remote_branch: merge_request.target_branch
+ )
+
+ # To support the full deprecated behaviour, set the
+ # `rebase_commit_sha` for the merge_request here and return the value
+ merge_request.update(rebase_commit_sha: rebase_sha)
+
+ rebase_sha
+ end
+
def rebase(user, merge_request)
- raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
- branch_sha: merge_request.source_branch_sha,
- remote_repository: merge_request.target_project.repository.raw,
- remote_branch: merge_request.target_branch)
+ if Feature.disabled?(:two_step_rebase, default_enabled: true)
+ return rebase_deprecated(user, merge_request)
+ end
+
+ MergeRequest.transaction do
+ raw.rebase(
+ user,
+ merge_request.id,
+ branch: merge_request.source_branch,
+ branch_sha: merge_request.source_branch_sha,
+ remote_repository: merge_request.target_project.repository.raw,
+ remote_branch: merge_request.target_branch
+ ) do |commit_id|
+ merge_request.update!(rebase_commit_sha: commit_id)
+ end
+ end
end
def squash(user, merge_request, message)
@@ -1064,6 +1102,19 @@ class Repository
blob.data
end
+ def create_if_not_exists
+ return if exists?
+
+ raw.create_repository
+ after_create
+ end
+
+ def blobs_metadata(paths, ref = 'HEAD')
+ references = Array.wrap(paths).map { |path| [ref, path] }
+
+ Gitlab::Git::Blob.batch_metadata(raw, references).map { |raw_blob| Blob.decorate(raw_blob) }
+ end
+
private
# TODO Generice finder, later split this on finders by Ref or Oid
diff --git a/app/models/route.rb b/app/models/route.rb
index 7e3db54d4fe..91ea2966013 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -14,26 +14,26 @@ class Route < ApplicationRecord
before_validation :delete_conflicting_orphaned_routes
after_create :delete_conflicting_redirects
- after_update :delete_conflicting_redirects, if: :path_changed?
+ after_update :delete_conflicting_redirects, if: :saved_change_to_path?
after_update :create_redirect_for_old_path
after_update :rename_descendants
scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") }
def rename_descendants
- return unless path_changed? || name_changed?
+ return unless saved_change_to_path? || saved_change_to_name?
- descendant_routes = self.class.inside_path(path_was)
+ descendant_routes = self.class.inside_path(path_before_last_save)
descendant_routes.each do |route|
attributes = {}
- if path_changed? && route.path.present?
- attributes[:path] = route.path.sub(path_was, path)
+ if saved_change_to_path? && route.path.present?
+ attributes[:path] = route.path.sub(path_before_last_save, path)
end
- if name_changed? && name_was.present? && route.name.present?
- attributes[:name] = route.name.sub(name_was, name)
+ if saved_change_to_name? && name_before_last_save.present? && route.name.present?
+ attributes[:name] = route.name.sub(name_before_last_save, name)
end
if attributes.present?
@@ -65,7 +65,7 @@ class Route < ApplicationRecord
private
def create_redirect_for_old_path
- create_redirect(path_was) if path_changed?
+ create_redirect(path_before_last_save) if saved_change_to_path?
end
def delete_conflicting_orphaned_routes
diff --git a/app/models/service.rb b/app/models/service.rb
index de549becf71..9896aa12e90 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -50,6 +50,7 @@ class Service < ApplicationRecord
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
+ scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
@@ -335,6 +336,8 @@ class Service < ApplicationRecord
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
+ when "deployment"
+ "Event will be triggered when a deployment finishes"
end
end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
index 76ac5c13c18..b483c677be9 100644
--- a/app/models/storage/legacy_project.rb
+++ b/app/models/storage/legacy_project.rb
@@ -30,7 +30,7 @@ module Storage
end
def rename_repo(old_full_path: nil, new_full_path: nil)
- old_full_path ||= project.full_path_was
+ old_full_path ||= project.full_path_before_last_save
new_full_path ||= project.build_full_path
if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path)
diff --git a/app/models/user.rb b/app/models/user.rb
index 551eb58a4de..4a1bf5514fe 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -194,7 +194,7 @@ class User < ApplicationRecord
before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
after_validation :set_username_errors
- after_update :username_changed_hook, if: :username_changed?
+ after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
after_commit(on: :update) do
@@ -757,11 +757,15 @@ class User < ApplicationRecord
# Typically used in conjunction with projects table to get projects
# a user has been given access to.
+ # The param `related_project_column` is the column to compare to the
+ # project_authorizations. By default is projects.id
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
- def authorizations_for_projects(min_access_level: nil)
- authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
+ authorizations = project_authorizations
+ .select(1)
+ .where("project_authorizations.project_id = #{related_project_column}")
return authorizations unless min_access_level.present?
diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb
index d6d590687e2..316bd39f7a3 100644
--- a/app/policies/clusters/cluster_policy.rb
+++ b/app/policies/clusters/cluster_policy.rb
@@ -6,5 +6,6 @@ module Clusters
delegate { cluster.group }
delegate { cluster.project }
+ delegate { cluster.instance }
end
end
diff --git a/app/policies/clusters/instance_policy.rb b/app/policies/clusters/instance_policy.rb
new file mode 100644
index 00000000000..e1045c85e6d
--- /dev/null
+++ b/app/policies/clusters/instance_policy.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Clusters
+ class InstancePolicy < BasePolicy
+ include ClusterableActions
+
+ condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
+ condition(:can_have_multiple_clusters) { multiple_clusters_available? }
+ condition(:instance_clusters_enabled) { Instance.enabled? }
+
+ rule { admin & instance_clusters_enabled }.policy do
+ enable :read_cluster
+ enable :add_cluster
+ enable :create_cluster
+ enable :update_cluster
+ enable :admin_cluster
+ end
+
+ rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index d412a591fdc..e85397422e6 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -44,7 +44,6 @@ class GlobalPolicy < BasePolicy
prevent :access_api
prevent :access_git
prevent :receive_notifications
- prevent :use_quick_actions
end
rule { required_terms_not_accepted }.policy do
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 2b5cca76c20..40dd49b4afd 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -7,7 +7,7 @@ class PersonalSnippetPolicy < BasePolicy
rule { public_snippet }.policy do
enable :read_personal_snippet
- enable :comment_personal_snippet
+ enable :create_note
end
rule { is_author }.policy do
@@ -15,7 +15,7 @@ class PersonalSnippetPolicy < BasePolicy
enable :update_personal_snippet
enable :destroy_personal_snippet
enable :admin_personal_snippet
- enable :comment_personal_snippet
+ enable :create_note
end
rule { ~anonymous }.enable :create_personal_snippet
@@ -23,15 +23,12 @@ class PersonalSnippetPolicy < BasePolicy
rule { internal_snippet & ~external_user }.policy do
enable :read_personal_snippet
- enable :comment_personal_snippet
+ enable :create_note
end
- rule { anonymous }.prevent :comment_personal_snippet
+ rule { anonymous }.prevent :create_note
- rule { can?(:comment_personal_snippet) }.policy do
- enable :create_note
- enable :award_emoji
- end
+ rule { can?(:create_note) }.enable :award_emoji
rule { full_private_access }.enable :read_personal_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index ba38af9c529..76544249688 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -186,6 +186,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :award_emoji
enable :read_pages_content
+ enable :read_release
end
# These abilities are not allowed to admins that are not members of the project,
@@ -212,7 +213,6 @@ class ProjectPolicy < BasePolicy
enable :read_deployment
enable :read_merge_request
enable :read_sentry_issue
- enable :read_release
enable :read_prometheus
end
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 1c1347c5a57..944895904fe 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -63,19 +63,11 @@ module Ci
end
def link_to_merge_request_source_branch
- return unless merge_request_presenter
-
- link_to(merge_request_presenter.source_branch,
- merge_request_presenter.source_branch_commits_path,
- class: 'ref-name')
+ merge_request_presenter&.source_branch_link
end
def link_to_merge_request_target_branch
- return unless merge_request_presenter
-
- link_to(merge_request_presenter.target_branch,
- merge_request_presenter.target_branch_commits_path,
- class: 'ref-name')
+ merge_request_presenter&.target_branch_link
end
private
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 81994bbce7d..33b217c8498 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -35,6 +35,8 @@ module Clusters
s_("ClusterIntegration|Project cluster")
elsif cluster.group_type?
s_("ClusterIntegration|Group cluster")
+ elsif cluster.instance_type?
+ s_("ClusterIntegration|Instance cluster")
end
end
@@ -43,6 +45,8 @@ module Clusters
project_cluster_path(project, cluster)
elsif cluster.group_type?
group_cluster_path(group, cluster)
+ elsif cluster.instance_type?
+ admin_cluster_path(cluster)
else
raise NotImplementedError
end
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
new file mode 100644
index 00000000000..f8bbe5216f1
--- /dev/null
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class InstanceClusterablePresenter < ClusterablePresenter
+ extend ::Gitlab::Utils::Override
+ include ActionView::Helpers::UrlHelper
+
+ def self.fabricate(clusterable, **attributes)
+ attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter)
+
+ Gitlab::View::Presenter::Factory
+ .new(clusterable, attributes_with_presenter_class)
+ .fabricate!
+ end
+
+ override :index_path
+ def index_path
+ admin_clusters_path
+ end
+
+ override :new_path
+ def new_path
+ new_admin_cluster_path
+ end
+
+ override :cluster_status_cluster_path
+ def cluster_status_cluster_path(cluster, params = {})
+ cluster_status_admin_cluster_path(cluster, params)
+ end
+
+ override :install_applications_cluster_path
+ def install_applications_cluster_path(cluster, application)
+ install_applications_admin_cluster_path(cluster, application)
+ end
+
+ override :update_applications_cluster_path
+ def update_applications_cluster_path(cluster, application)
+ update_applications_admin_cluster_path(cluster, application)
+ end
+
+ override :cluster_path
+ def cluster_path(cluster, params = {})
+ admin_cluster_path(cluster, params)
+ end
+
+ override :create_user_clusters_path
+ def create_user_clusters_path
+ create_user_admin_clusters_path
+ end
+
+ override :create_gcp_clusters_path
+ def create_gcp_clusters_path
+ create_gcp_admin_clusters_path
+ end
+
+ override :empty_state_help_text
+ def empty_state_help_text
+ s_('ClusterIntegration|Adding an integration will share the cluster across all projects.')
+ end
+
+ override :sidebar_text
+ def sidebar_text
+ s_('ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster.')
+ end
+
+ override :learn_more_link
+ def learn_more_link
+ link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ end
+end
diff --git a/app/presenters/label_presenter.rb b/app/presenters/label_presenter.rb
new file mode 100644
index 00000000000..5227ef353c3
--- /dev/null
+++ b/app/presenters/label_presenter.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class LabelPresenter < Gitlab::View::Presenter::Delegated
+ presents :label
+
+ def edit_path
+ case label
+ when GroupLabel then edit_group_label_path(label.group, label)
+ when ProjectLabel then edit_project_label_path(label.project, label)
+ end
+ end
+
+ def destroy_path
+ case label
+ when GroupLabel then group_label_path(label.group, label)
+ when ProjectLabel then project_label_path(label.project, label)
+ end
+ end
+
+ def filter_path(type: :issue)
+ case context_subject
+ when Group
+ send("#{type.to_s.pluralize}_group_path", # rubocop:disable GitlabSecurity/PublicSend
+ context_subject,
+ label_name: [label.name])
+ when Project
+ send("namespace_project_#{type.to_s.pluralize}_path", # rubocop:disable GitlabSecurity/PublicSend
+ context_subject.namespace,
+ context_subject,
+ label_name: [label.name])
+ end
+ end
+
+ def can_subscribe_to_label_in_different_levels?
+ issuable_subject.is_a?(Project) && label.is_a?(GroupLabel)
+ end
+
+ private
+
+ def context_subject
+ issuable_subject || label.try(:subject)
+ end
+end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index 284b1ad9b55..ba0711ca867 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -13,7 +13,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
def ci_status
if pipeline
status = pipeline.status
- status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
+ status = "success-with-warnings" if pipeline.success? && pipeline.has_warnings?
status || "preparing"
else
@@ -50,7 +50,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
if user_can_fork_project? && cached_can_be_reverted?
continue_params = {
to: merge_request_path(merge_request),
- notice: "#{edit_in_new_fork_notice} Try to cherry-pick this commit again.",
+ notice: _('%{edit_in_new_fork_notice} Try to cherry-pick this commit again.') % { edit_in_new_fork_notice: edit_in_new_fork_notice },
notice_now: edit_in_new_fork_notice_now
}
@@ -64,7 +64,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
if user_can_fork_project? && can_be_cherry_picked?
continue_params = {
to: merge_request_path(merge_request),
- notice: "#{edit_in_new_fork_notice} Try to revert this commit again.",
+ notice: _('%{edit_in_new_fork_notice} Try to revert this commit again.') % { edit_in_new_fork_notice: edit_in_new_fork_notice },
notice_now: edit_in_new_fork_notice_now
}
@@ -156,8 +156,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
).assignable_issues
path = assign_related_issues_project_merge_request_path(project, merge_request)
if issues.present?
- pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
- link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
+ if issues.count > 1
+ link_to _('Assign yourself to these issues'), path, method: :post
+ else
+ link_to _('Assign yourself to this issue'), path, method: :post
+ end
end
# rubocop: enable CodeReuse/ServiceClass
end
@@ -209,6 +212,26 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
help_page_path('user/project/merge_requests/resolve_conflicts.md')
end
+ def merge_request_pipelines_docs_path
+ help_page_path('ci/merge_request_pipelines/index.md')
+ end
+
+ def source_branch_link
+ if source_branch_exists?
+ link_to(source_branch, source_branch_commits_path, class: 'ref-name')
+ else
+ content_tag(:span, source_branch, class: 'ref-name')
+ end
+ end
+
+ def target_branch_link
+ if target_branch_exists?
+ link_to(target_branch, target_branch_commits_path, class: 'ref-name')
+ else
+ content_tag(:span, target_branch, class: 'ref-name')
+ end
+ end
+
private
def cached_can_be_reverted?
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 161eebcfb3f..9afbaf035c7 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -37,7 +37,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout),
kubernetes_cluster_anchor_data,
gitlab_ci_anchor_data
- ].compact.reject(&:is_link)
+ ].compact.reject(&:is_link).sort_by.with_index { |item, idx| [item.class_modifier ? 0 : 1, idx] }
end
def empty_repo_statistics_anchors
@@ -259,7 +259,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
if auto_devops_enabled?
AnchorData.new(false,
- statistic_icon('doc-text') + _('Auto DevOps enabled'),
+ statistic_icon('settings') + _('Auto DevOps enabled'),
project_settings_ci_cd_path(project, anchor: 'autodevops-settings'),
'default')
else
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index a4a2c015c4e..2a916b13f52 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -10,4 +10,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
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/concerns/user_status_tooltip.rb b/app/serializers/concerns/user_status_tooltip.rb
index 633b117d392..a81e377691e 100644
--- a/app/serializers/concerns/user_status_tooltip.rb
+++ b/app/serializers/concerns/user_status_tooltip.rb
@@ -3,7 +3,7 @@
module UserStatusTooltip
extend ActiveSupport::Concern
include ActionView::Helpers::TagHelper
- include ActionView::Context
+ include ::Gitlab::ActionViewOutput::Context
include EmojiHelper
include UsersHelper
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 4831eb32c96..b130f447cce 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -256,6 +256,10 @@ class MergeRequestWidgetEntity < IssuableEntity
presenter(merge_request).conflicts_docs_path
end
+ expose :merge_request_pipelines_docs_path do |merge_request|
+ presenter(merge_request).merge_request_pipelines_docs_path
+ end
+
private
delegate :current_user, to: :request
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 41dee4e5641..c17712355af 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -55,6 +55,10 @@ module Ci
end
end
+ # If pipeline is not persisted, try to recover IID
+ pipeline.reset_project_iid unless pipeline.persisted? ||
+ Feature.disabled?(:ci_pipeline_rewind_iid, project, default_enabled: true)
+
pipeline
end
@@ -100,17 +104,11 @@ module Ci
end
def schedule_head_pipeline_update
- related_merge_requests.each do |merge_request|
+ pipeline.all_merge_requests.opened.each do |merge_request|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
- # rubocop: disable CodeReuse/ActiveRecord
- def related_merge_requests
- pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def extra_options(options = {})
# In Ruby 2.4, even when options is empty, f(**options) doesn't work when f
# doesn't have any parameters. We reproduce the Ruby 2.5 behavior by
diff --git a/app/services/ci/play_manual_stage_service.rb b/app/services/ci/play_manual_stage_service.rb
new file mode 100644
index 00000000000..2497fc52e6b
--- /dev/null
+++ b/app/services/ci/play_manual_stage_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ci
+ class PlayManualStageService < BaseService
+ def initialize(project, current_user, params)
+ super
+
+ @pipeline = params[:pipeline]
+ end
+
+ def execute(stage)
+ stage.builds.manual.each do |build|
+ next unless build.playable?
+
+ build.play(current_user)
+ rescue Gitlab::Access::AccessDeniedError
+ logger.error(message: 'Unable to play manual action', build_id: build.id)
+ end
+ end
+
+ private
+
+ attr_reader :pipeline, :current_user
+
+ def logger
+ Gitlab::AppLogger
+ end
+ end
+end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index 973ae5ce5aa..d9a800791f2 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -9,12 +9,11 @@ module Ci
return unless @ref.present?
- environments.each do |environment|
- next unless environment.stop_action_available?
- next unless can?(current_user, :stop_environment, environment)
+ environments.each { |environment| stop(environment) }
+ end
- environment.stop_with_action!(current_user)
- end
+ def execute_for_merge_request(merge_request)
+ merge_request.environments.each { |environment| stop(environment) }
end
private
@@ -24,5 +23,12 @@ module Ci
.new(project, current_user, ref: @ref, recently_updated: true)
.execute
end
+
+ def stop(environment)
+ return unless environment.stop_action_available?
+ return unless can?(current_user, :stop_environment, environment)
+
+ environment.stop_with_action!(current_user)
+ end
end
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index c592d608b89..3c6803d24e6 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -37,7 +37,7 @@ module Clusters
end
def check_timeout
- if timeouted?
+ if timed_out?
begin
app.make_errored!("Operation timed out. Check pod logs for #{pod_name} for more details.")
end
@@ -51,8 +51,8 @@ module Clusters
install_command.pod_name
end
- def timeouted?
- Time.now.utc - app.updated_at.to_time.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
+ def timed_out?
+ Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb
new file mode 100644
index 00000000000..8786d295d6a
--- /dev/null
+++ b/app/services/clusters/applications/check_uninstall_progress_service.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class CheckUninstallProgressService < BaseHelmService
+ def execute
+ return unless app.uninstalling?
+
+ case installation_phase
+ when Gitlab::Kubernetes::Pod::SUCCEEDED
+ on_success
+ when Gitlab::Kubernetes::Pod::FAILED
+ on_failed
+ else
+ check_timeout
+ end
+ rescue Kubeclient::HttpError => e
+ log_error(e)
+
+ app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
+ end
+
+ private
+
+ def on_success
+ app.destroy!
+ rescue StandardError => e
+ app.make_errored!(_('Application uninstalled but failed to destroy: %{error_message}') % { error_message: e.message })
+ ensure
+ remove_installation_pod
+ end
+
+ def on_failed
+ app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
+ end
+
+ def check_timeout
+ if timed_out?
+ app.make_errored!(_('Operation timed out. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
+ else
+ WaitForUninstallAppWorker.perform_in(WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
+ end
+ end
+
+ def pod_name
+ app.uninstall_command.pod_name
+ end
+
+ def timed_out?
+ Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
+ end
+
+ def remove_installation_pod
+ helm_api.delete_pod!(pod_name)
+ end
+
+ def installation_phase
+ helm_api.status(pod_name)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index ae36da7b3dd..f723c42c049 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -10,8 +10,8 @@ module Clusters
end
def builder
- cluster.method("application_#{application_name}").call ||
- cluster.method("build_application_#{application_name}").call
+ cluster.public_send(:"application_#{application_name}") || # rubocop:disable GitlabSecurity/PublicSend
+ cluster.public_send(:"build_application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb
new file mode 100644
index 00000000000..f3a4c4f754a
--- /dev/null
+++ b/app/services/clusters/applications/destroy_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class DestroyService < ::Clusters::Applications::BaseService
+ def execute(_request)
+ instantiate_application.tap do |application|
+ break unless application.can_uninstall?
+
+ application.make_scheduled!
+
+ Clusters::Applications::UninstallWorker.perform_async(application.name, application.id)
+ end
+ end
+
+ private
+
+ def builder
+ cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index b37ac510ff4..dffb4ce65ab 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -22,10 +22,10 @@ module Clusters
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
- app.make_errored!("Kubernetes error: #{e.error_code}")
+ app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
rescue StandardError => e
log_error(e)
- app.make_errored!('Failed to install.')
+ app.make_errored!(_('Failed to install.'))
end
end
end
diff --git a/app/services/clusters/applications/patch_service.rb b/app/services/clusters/applications/patch_service.rb
index 977a5e91041..fbea18bae6b 100644
--- a/app/services/clusters/applications/patch_service.rb
+++ b/app/services/clusters/applications/patch_service.rb
@@ -22,10 +22,10 @@ module Clusters
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
- app.make_errored!("Kubernetes error: #{e.error_code}")
+ app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
rescue StandardError => e
log_error(e)
- app.make_errored!('Failed to update.')
+ app.make_errored!(_('Failed to update.'))
end
end
end
diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb
new file mode 100644
index 00000000000..50c8d806c14
--- /dev/null
+++ b/app/services/clusters/applications/uninstall_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class UninstallService < BaseHelmService
+ def execute
+ return unless app.scheduled?
+
+ app.make_uninstalling!
+ uninstall
+ end
+
+ private
+
+ def uninstall
+ helm_api.uninstall(app.uninstall_command)
+
+ Clusters::Applications::WaitForUninstallAppWorker.perform_in(
+ Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
+ rescue Kubeclient::HttpError => e
+ log_error(e)
+ app.make_errored!("Kubernetes error: #{e.error_code}")
+ rescue StandardError => e
+ log_error(e)
+ app.make_errored!('Failed to uninstall.')
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/applications/update_service.rb b/app/services/clusters/applications/update_service.rb
index 5071c31839c..0fa937da865 100644
--- a/app/services/clusters/applications/update_service.rb
+++ b/app/services/clusters/applications/update_service.rb
@@ -10,7 +10,7 @@ module Clusters
end
def builder
- cluster.method("application_#{application_name}").call
+ cluster.public_send(:"application_#{application_name}") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb
index 813a9c4d071..ac68e64af38 100644
--- a/app/services/clusters/applications/upgrade_service.rb
+++ b/app/services/clusters/applications/upgrade_service.rb
@@ -24,10 +24,10 @@ module Clusters
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
log_error(e)
- app.make_errored!("Kubernetes error: #{e.error_code}")
+ app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
rescue StandardError => e
log_error(e)
- app.make_errored!('Failed to upgrade.')
+ app.make_errored!(_('Failed to upgrade.'))
end
end
end
diff --git a/app/services/clusters/build_service.rb b/app/services/clusters/build_service.rb
index 8de73831164..b1ac5549e30 100644
--- a/app/services/clusters/build_service.rb
+++ b/app/services/clusters/build_service.rb
@@ -12,6 +12,8 @@ module Clusters
cluster.cluster_type = :project_type
when ::Group
cluster.cluster_type = :group_type
+ when Instance
+ cluster.cluster_type = :instance_type
else
raise NotImplementedError
end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 5a9da053780..886e484caaf 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -38,6 +38,8 @@ module Clusters
{ cluster_type: :project_type, projects: [clusterable] }
when ::Group
{ cluster_type: :group_type, groups: [clusterable] }
+ when Instance
+ { cluster_type: :instance_type }
else
raise NotImplementedError
end
diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb
index 7c82b98a33f..b02bb9c0247 100644
--- a/app/services/clusters/refresh_service.rb
+++ b/app/services/clusters/refresh_service.rb
@@ -21,7 +21,11 @@ module Clusters
private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster
def self.clusters_with_missing_kubernetes_namespaces_for_project(project)
- project.all_clusters.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ if Feature.enabled?(:ci_preparing_state, default_enabled: true)
+ project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ else
+ project.all_clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces)
+ end
end
private_class_method :clusters_with_missing_kubernetes_namespaces_for_project
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 3adf8a0c1a1..3f0aedfbfb2 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@
require 'securerandom'
# Compare 2 refs for one repo or between repositories
-# and return Gitlab::Git::Compare object that responds to commits and diffs
+# and return Compare object that responds to commits and diffs
class CompareService
attr_reader :start_project, :start_ref_name
@@ -15,7 +15,7 @@ class CompareService
def execute(target_project, target_ref, base_sha: nil, straight: false)
raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight)
- return unless raw_compare
+ return unless raw_compare && raw_compare.base && raw_compare.head
Compare.new(raw_compare,
target_project,
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 4c3ac19f754..fd41ce54486 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -6,27 +6,25 @@ class DeleteBranchService < BaseService
branch = repository.find_branch(branch_name)
unless current_user.can?(:push_code, project)
- return error('You dont have push access to repo', 405)
+ return ServiceResponse.error(
+ message: 'You dont have push access to repo',
+ http_status: 405)
end
unless branch
- return error('No such branch', 404)
+ return ServiceResponse.error(
+ message: 'No such branch',
+ http_status: 404)
end
if repository.rm_branch(current_user, branch_name)
- success('Branch was deleted')
+ ServiceResponse.success(message: 'Branch was deleted')
else
- error('Failed to remove branch')
+ ServiceResponse.error(
+ message: 'Failed to remove branch',
+ http_status: 400)
end
rescue Gitlab::Git::PreReceiveError => ex
- error(ex.message)
- end
-
- def error(message, return_code = 400)
- super(message).merge(return_code: return_code)
- end
-
- def success(message)
- super().merge(message: message)
+ ServiceResponse.error(message: ex.message, http_status: 400)
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 2b3e96e6c53..54ab07da680 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -19,7 +19,7 @@ module Files
super
if file_has_changed?(@file_path, @last_commit_sha)
- raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
+ raise FileChangedError, _('You are attempting to update a file that has changed since you started editing it.')
end
end
end
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index a8478e3a904..9d371e234ee 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -73,13 +73,13 @@ module Git
def push_data
@push_data ||= Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- params[:oldrev],
- params[:newrev],
- params[:ref],
- limited_commits,
- event_message,
+ project: project,
+ user: current_user,
+ oldrev: params[:oldrev],
+ newrev: params[:newrev],
+ ref: params[:ref],
+ commits: limited_commits,
+ message: event_message,
commits_count: commits_count,
push_options: params[:push_options] || {}
)
diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb
index 6ecf583cb6a..5239fe1b6e3 100644
--- a/app/services/lfs/file_transformer.rb
+++ b/app/services/lfs/file_transformer.rb
@@ -24,7 +24,7 @@ module Lfs
def new_file(file_path, file_content, encoding: nil)
if project.lfs_enabled? && lfs_file?(file_path)
- file_content = Base64.decode64(file_content) if encoding == 'base64'
+ file_content = parse_file_content(file_content, encoding: encoding)
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
@@ -66,5 +66,12 @@ module Lfs
def link_lfs_object!(lfs_object)
project.lfs_objects << lfs_object
end
+
+ def parse_file_content(file_content, encoding: nil)
+ return file_content.read if file_content.respond_to?(:read)
+ return Base64.decode64(file_content) if encoding == 'base64'
+
+ file_content
+ end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index b8334a87f6d..bb9062e9b40 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -24,6 +24,11 @@ module MergeRequests
end
end
+ def cleanup_environments(merge_request)
+ Ci::StopEnvironmentsService.new(merge_request.source_project, current_user)
+ .execute_for_merge_request(merge_request)
+ end
+
private
def handle_wip_event(merge_request)
@@ -76,7 +81,7 @@ module MergeRequests
##
# UpdateMergeRequestsWorker could be retried by an exception.
# pipelines for merge request should not be recreated in such case.
- return false if merge_request.merge_request_pipeline_exists?
+ return false if merge_request.find_actual_head_pipeline&.triggered_by_merge_request?
return false if merge_request.has_no_commits?
true
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 04527bb9713..e77051bb1c9 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -17,6 +17,7 @@ module MergeRequests
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
+ cleanup_environments(merge_request)
end
merge_request
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index f26e3bee06f..c13f7dd5088 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -18,6 +18,7 @@ module MergeRequests
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
+ cleanup_environments(merge_request)
end
private
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 31b3ebf311e..4b9921c28ba 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -20,17 +20,7 @@ module MergeRequests
return false
end
- log_prefix = "#{self.class.name} info (#{merge_request.to_reference(full: true)}):"
-
- Gitlab::GitLogger.info("#{log_prefix} rebase started")
-
- rebase_sha = repository.rebase(current_user, merge_request)
-
- Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}")
-
- merge_request.update(rebase_commit_sha: rebase_sha)
-
- Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}")
+ repository.rebase(current_user, merge_request)
true
rescue => e
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 760962346fb..ca3f0b73096 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -135,7 +135,7 @@ module NotificationRecipientService
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
- add_recipients(user_scope.where(id: user_ids), :watch, nil)
+ add_recipients(user_scope.where(id: user_ids), :custom, nil)
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -391,7 +391,7 @@ module NotificationRecipientService
def build!
return [] unless project
- add_recipients(project.team.maintainers, :watch, nil)
+ add_recipients(project.team.maintainers, :mention, nil)
end
def acting_user
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 12103ea34b5..5972bfd4071 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -18,9 +18,6 @@ module Projects
# per rewritten object, with the old and new SHAs space-separated. It can be
# used to update or remove content that references the objects that BFG has
# altered
- #
- # Currently, only the project repository is modified by this service, but we
- # may wish to modify other data sources in the future.
def execute
apply_bfg_object_map!
@@ -41,10 +38,52 @@ module Projects
raise NoUploadError unless project.bfg_object_map.exists?
project.bfg_object_map.open do |io|
- repository_cleaner.apply_bfg_object_map(io)
+ repository_cleaner.apply_bfg_object_map_stream(io) do |response|
+ cleanup_diffs(response)
+ end
+ end
+ end
+
+ def cleanup_diffs(response)
+ old_commit_shas = extract_old_commit_shas(response.entries)
+
+ ActiveRecord::Base.transaction do
+ cleanup_merge_request_diffs(old_commit_shas)
+ cleanup_note_diff_files(old_commit_shas)
end
end
+ def extract_old_commit_shas(batch)
+ batch.lazy.select { |entry| entry.type == :COMMIT }.map(&:old_oid).force
+ end
+
+ def cleanup_merge_request_diffs(old_commit_shas)
+ merge_request_diffs = MergeRequestDiff
+ .by_project_id(project.id)
+ .by_commit_sha(old_commit_shas)
+
+ # It's important to run the ActiveRecord callbacks here
+ merge_request_diffs.destroy_all # rubocop:disable Cop/DestroyAll
+
+ # TODO: ensure the highlight cache is removed immediately. It's too hard
+ # to calculate the Redis keys at present.
+ #
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/61115
+ end
+
+ def cleanup_note_diff_files(old_commit_shas)
+ # Pluck the IDs instead of running the query twice to ensure we clear the
+ # cache for exactly the note diffs we remove
+ ids = NoteDiffFile
+ .referencing_sha(old_commit_shas, project_id: project.id)
+ .pluck_primary_key
+
+ NoteDiffFile.id_in(ids).delete_all
+
+ # A highlighted version of the diff is stored in redis. Remove it now.
+ Gitlab::DiscussionsDiff::HighlightCache.clear_multiple(ids)
+ end
+
def repository_cleaner
@repository_cleaner ||= Gitlab::Git::RepositoryCleaner.new(repository.raw)
end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 10bd5363b51..9428575591e 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -11,6 +11,7 @@ module Projects
class HousekeepingService < BaseService
# Timeout set to 24h
LEASE_TIMEOUT = 86400
+ PACK_REFS_PERIOD = 6
class LeaseTaken < StandardError
def to_s
@@ -76,13 +77,15 @@ module Projects
:gc
elsif pushes_since_gc % full_repack_period == 0
:full_repack
- else
+ elsif pushes_since_gc % repack_period == 0
:incremental_repack
+ else
+ :pack_refs
end
end
def period_match?
- [gc_period, full_repack_period, repack_period].any? { |period| pushes_since_gc % period == 0 }
+ [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 }
end
def housekeeping_enabled?
diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb
index a0fc5149bb4..737b794484d 100644
--- a/app/services/projects/import_error_filter.rb
+++ b/app/services/projects/import_error_filter.rb
@@ -4,7 +4,7 @@ module Projects
# Used by project imports, it removes any potential paths
# included in an error message that could be stored in the DB
class ImportErrorFilter
- ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/
+ ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/.freeze
FILTER_MESSAGE = '[FILTERED]'
def self.filter_message(message)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 642465551c9..073c14040ce 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -94,16 +94,13 @@ module Projects
return unless project.lfs_enabled?
- lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute
+ result = Projects::LfsPointers::LfsImportService.new(project).execute
- lfs_objects_to_download.each do |lfs_download_object|
- Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object)
- .execute
+ if result[:status] == :error
+ # To avoid aborting the importing process, we silently fail
+ # if any exception raises.
+ Gitlab::AppLogger.error("The Lfs import process failed. #{result[:message]}")
end
- rescue => e
- # Right now, to avoid aborting the importing process, we silently fail
- # if any exception raises.
- Rails.logger.error("The Lfs import process failed. #{e.message}")
end
def import_data
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index a9570176e81..05974948505 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -21,9 +21,9 @@ module Projects
# This method accepts two parameters:
# - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
#
- # Returns a hash with the structure { lfs_file_oids => download_link }
+ # Returns an array of LfsDownloadObject
def execute(oids)
- return {} unless project&.lfs_enabled? && remote_uri && oids.present?
+ return [] unless project&.lfs_enabled? && remote_uri && oids.present?
get_download_links(oids)
end
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
index 9215fa0a7bf..2afcce7099b 100644
--- a/app/services/projects/lfs_pointers/lfs_import_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -1,95 +1,23 @@
# frozen_string_literal: true
-# This service manages the whole worflow of discovering the Lfs files in a
-# repository, linking them to the project and downloading (and linking) the non
-# existent ones.
+# This service is responsible of managing the retrieval of the lfs objects,
+# and call the service LfsDownloadService, which performs the download
+# for each of the retrieved lfs objects
module Projects
module LfsPointers
class LfsImportService < BaseService
- include Gitlab::Utils::StrongMemoize
-
- HEAD_REV = 'HEAD'.freeze
- LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
- LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
-
- LfsImportError = Class.new(StandardError)
-
def execute
- return {} unless project&.lfs_enabled?
+ return success unless project&.lfs_enabled?
- if external_lfs_endpoint?
- # If the endpoint host is different from the import_url it means
- # that the repo is using a third party service for storing the LFS files.
- # In this case, we have to disable lfs in the project
- disable_lfs!
+ lfs_objects_to_download = LfsObjectDownloadListService.new(project).execute
- return {}
+ lfs_objects_to_download.each do |lfs_download_object|
+ LfsDownloadService.new(project, lfs_download_object).execute
end
- get_download_links
- rescue LfsDownloadLinkListService::DownloadLinksError => e
- raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
- end
-
- private
-
- def external_lfs_endpoint?
- lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
- end
-
- def disable_lfs!
- project.update(lfs_enabled: false)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def get_download_links
- existent_lfs = LfsListService.new(project).execute
- linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys)
-
- # Retrieving those oids not linked and which we need to download
- not_linked_lfs = existent_lfs.except(*linked_oids)
-
- LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def lfsconfig_endpoint_uri
- strong_memoize(:lfsconfig_endpoint_uri) do
- # Retrieveing the blob data from the .lfsconfig file
- data = project.repository.lfsconfig_for(HEAD_REV)
- # Parsing the data to retrieve the url
- parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
-
- if parsed_data
- URI.parse(parsed_data[1]).tap do |endpoint|
- endpoint.user ||= import_uri.user
- endpoint.password ||= import_uri.password
- end
- end
- end
- rescue URI::InvalidURIError
- raise LfsImportError, 'Invalid URL in .lfsconfig file'
- end
-
- def import_uri
- @import_uri ||= URI.parse(project.import_url)
- rescue URI::InvalidURIError
- raise LfsImportError, 'Invalid project import URL'
- end
-
- def current_endpoint_uri
- (lfsconfig_endpoint_uri || default_endpoint_uri)
- end
-
- # The import url must end with '.git' here we ensure it is
- def default_endpoint_uri
- @default_endpoint_uri ||= begin
- import_uri.dup.tap do |uri|
- path = uri.path.gsub(%r(/$), '')
- path += '.git' unless path.ends_with?('.git')
- uri.path = path + LFS_BATCH_API_ENDPOINT
- end
- end
+ success
+ rescue => e
+ error(e.message)
end
end
end
diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb
index 8401f3d1d89..e3c956250f0 100644
--- a/app/services/projects/lfs_pointers/lfs_link_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_link_service.rb
@@ -6,9 +6,9 @@ module Projects
class LfsLinkService < BaseService
# Accept an array of oids to link
#
- # Returns a hash with the same structure with oids linked
+ # Returns an array with the oid of the existent lfs objects
def execute(oids)
- return {} unless project&.lfs_enabled?
+ return [] unless project&.lfs_enabled?
# Search and link existing LFS Object
link_existing_lfs_objects(oids)
diff --git a/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
new file mode 100644
index 00000000000..5ba0f50f2ff
--- /dev/null
+++ b/app/services/projects/lfs_pointers/lfs_object_download_list_service.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+# This service manages the whole worflow of discovering the Lfs files in a
+# repository, linking them to the project and downloading (and linking) the non
+# existent ones.
+module Projects
+ module LfsPointers
+ class LfsObjectDownloadListService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ HEAD_REV = 'HEAD'.freeze
+ LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze
+ LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze
+
+ LfsObjectDownloadListError = Class.new(StandardError)
+
+ def execute
+ return [] unless project&.lfs_enabled?
+
+ if external_lfs_endpoint?
+ # If the endpoint host is different from the import_url it means
+ # that the repo is using a third party service for storing the LFS files.
+ # In this case, we have to disable lfs in the project
+ disable_lfs!
+
+ return []
+ end
+
+ # Getting all Lfs pointers already in the database and linking them to the project
+ linked_oids = LfsLinkService.new(project).execute(lfs_pointers_in_repository.keys)
+ # Retrieving those oids not present in the database which we need to download
+ missing_oids = lfs_pointers_in_repository.except(*linked_oids) # rubocop: disable CodeReuse/ActiveRecord
+ # Downloading the required information and gathering it inside a LfsDownloadObject for each oid
+ LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(missing_oids)
+ rescue LfsDownloadLinkListService::DownloadLinksError => e
+ raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
+ end
+
+ private
+
+ def external_lfs_endpoint?
+ lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
+ end
+
+ def disable_lfs!
+ unless project.update(lfs_enabled: false)
+ raise LfsDownloadLinkListService::DownloadLinksError, "Invalid project state"
+ end
+ end
+
+ # Retrieves all lfs pointers in the repository
+ def lfs_pointers_in_repository
+ @lfs_pointers_in_repository ||= LfsListService.new(project).execute
+ end
+
+ def lfsconfig_endpoint_uri
+ strong_memoize(:lfsconfig_endpoint_uri) do
+ # Retrieveing the blob data from the .lfsconfig file
+ data = project.repository.lfsconfig_for(HEAD_REV)
+ # Parsing the data to retrieve the url
+ parsed_data = data&.match(LFS_ENDPOINT_PATTERN)
+
+ if parsed_data
+ URI.parse(parsed_data[1]).tap do |endpoint|
+ endpoint.user ||= import_uri.user
+ endpoint.password ||= import_uri.password
+ end
+ end
+ end
+ rescue URI::InvalidURIError
+ raise LfsObjectDownloadListError, 'Invalid URL in .lfsconfig file'
+ end
+
+ def import_uri
+ @import_uri ||= URI.parse(project.import_url)
+ rescue URI::InvalidURIError
+ raise LfsObjectDownloadListError, 'Invalid project import URL'
+ end
+
+ def current_endpoint_uri
+ (lfsconfig_endpoint_uri || default_endpoint_uri)
+ end
+
+ # The import url must end with '.git' here we ensure it is
+ def default_endpoint_uri
+ @default_endpoint_uri ||= begin
+ import_uri.dup.tap do |uri|
+ path = uri.path.gsub(%r(/$), '')
+ path += '.git' unless path.ends_with?('.git')
+ uri.path = path + LFS_BATCH_API_ENDPOINT
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/operations/update_service.rb b/app/services/projects/operations/update_service.rb
index aedf79c86d7..48eddb0e8d0 100644
--- a/app/services/projects/operations/update_service.rb
+++ b/app/services/projects/operations/update_service.rb
@@ -12,7 +12,16 @@ module Projects
private
def project_update_params
- error_tracking_params
+ error_tracking_params.merge(metrics_setting_params)
+ end
+
+ def metrics_setting_params
+ attribs = params[:metrics_setting_attributes]
+ return {} unless attribs
+
+ destroy = attribs[:external_dashboard_url].blank?
+
+ { metrics_setting_attributes: attribs.merge(_destroy: destroy) }
end
def error_tracking_params
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index 633a263af7b..a2f36d2bd1b 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -80,7 +80,7 @@ module Projects
value = value.is_a?(Hash) ? value.to_json : value
service_hash[ActiveRecord::Base.connection.quote_column_name(key)] =
- ActiveRecord::Base.sanitize(value)
+ ActiveRecord::Base.connection.quote(value)
end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 864ce4fa9f5..dfa7bd20254 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -79,10 +79,7 @@ module Projects
end
def after_rename_service(project)
- # The path slug the project was using, before the rename took place.
- path_before = project.previous_changes['path'].first
-
- AfterRenameService.new(project, path_before: path_before, full_path_before: project.full_path_was)
+ AfterRenameService.new(project, path_before: project.path_before_last_save, full_path_before: project.full_path_before_last_save)
end
def changing_pages_related_config?
diff --git a/app/services/service_response.rb b/app/services/service_response.rb
new file mode 100644
index 00000000000..1de30e68d87
--- /dev/null
+++ b/app/services/service_response.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class ServiceResponse
+ def self.success(message: nil)
+ new(status: :success, message: message)
+ end
+
+ def self.error(message:, http_status: nil)
+ new(status: :error, message: message, http_status: http_status)
+ end
+
+ attr_reader :status, :message, :http_status
+
+ def initialize(status:, message: nil, http_status: nil)
+ self.status = status
+ self.message = message
+ self.http_status = http_status
+ end
+
+ def success?
+ status == :success
+ end
+
+ def error?
+ status == :error
+ end
+
+ private
+
+ attr_writer :status, :message, :http_status
+end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index bd3907cdf8e..858e04f43b2 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -47,7 +47,7 @@ class SystemHooksService
case event
when :rename
- data[:old_username] = model.username_was
+ data[:old_username] = model.username_before_last_save
when :failed_login
data[:state] = model.state
end
@@ -58,8 +58,8 @@ class SystemHooksService
if event == :rename
data.merge!(
- old_path: model.path_was,
- old_full_path: model.full_path_was
+ old_path: model.path_before_last_save,
+ old_full_path: model.full_path_before_last_save
)
end
when GroupMember
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index cab507946b4..4f6ae07be7d 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -41,12 +41,11 @@ module Tags
def build_push_data(tag)
Gitlab::DataBuilder::Push.build(
- project,
- current_user,
- tag.dereferenced_target.sha,
- Gitlab::Git::BLANK_SHA,
- "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
- [])
+ project: project,
+ user: current_user,
+ oldrev: tag.dereferenced_target.sha,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}")
end
end
end
diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb
index 6607f5b2418..a71278e8b8b 100644
--- a/app/services/test_hooks/project_service.rb
+++ b/app/services/test_hooks/project_service.rb
@@ -56,7 +56,7 @@ module TestHooks
end
def wiki_page_events_data
- page = project.wiki.pages.first
+ page = project.wiki.list_pages(limit: 1).first
if !project.wiki_enabled? || page.blank?
throw(:validation_error, s_('TestHooks|Ensure the wiki is enabled and has pages.'))
end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
index ebfb20132d0..4743e9b02ce 100644
--- a/app/services/todos/destroy/entity_leave_service.rb
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -37,8 +37,8 @@ module Todos
private
def enqueue_private_features_worker
- project_ids.each do |project_id|
- TodosDestroyer::PrivateFeaturesWorker.perform_async(project_id, user.id)
+ projects.each do |project|
+ TodosDestroyer::PrivateFeaturesWorker.perform_async(project.id, user.id)
end
end
@@ -62,9 +62,8 @@ module Todos
end
# rubocop: enable CodeReuse/ActiveRecord
- override :project_ids
# rubocop: disable CodeReuse/ActiveRecord
- def project_ids
+ def projects
condition = case entity
when Project
{ id: entity.id }
@@ -72,13 +71,13 @@ module Todos
{ namespace_id: non_member_groups }
end
- Project.where(condition).select(:id)
+ Project.where(condition)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def non_authorized_projects
- project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id))
+ projects.where('id NOT IN (?)', user.authorized_projects.select(:id))
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -110,7 +109,7 @@ module Todos
authorized_reporter_projects = user
.authorized_projects(Gitlab::Access::REPORTER).select(:id)
- Issue.where(project_id: project_ids, confidential: true)
+ Issue.where(project_id: projects, confidential: true)
.where('project_id NOT IN(?)', authorized_reporter_projects)
.where('author_id != ?', user.id)
.where('id NOT IN (?)', assigned_ids)
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index e90599f2505..6dfe2bed0ba 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -14,8 +14,8 @@ class FileUploader < GitlabUploader
include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads
- MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
- DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}
+ MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}.freeze
+ DYNAMIC_PATH_PATTERN = %r{.*(?<secret>\h{32})/(?<identifier>.*)}.freeze
after :remove, :prune_store_dir
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
index 716922bc017..104d5d3b3dd 100644
--- a/app/uploaders/import_export_uploader.rb
+++ b/app/uploaders/import_export_uploader.rb
@@ -7,10 +7,6 @@ class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST
end
- def move_to_store
- true
- end
-
def move_to_cache
false
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 5f8b89f2a24..0a44d60778d 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -117,7 +117,7 @@ module ObjectStorage
next unless uploader
next unless uploader.exists?
- next unless send(:"#{mounted_as}_changed?") # rubocop:disable GitlabSecurity/PublicSend
+ next unless send(:"saved_change_to_#{mounted_as}?") # rubocop:disable GitlabSecurity/PublicSend
mount
end.keys
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
index 41b787515b5..1da5f6fccd6 100644
--- a/app/views/admin/application_settings/_logging.html.haml
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -1,6 +1,12 @@
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-logging-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
+ %p
+ %strong
+ NOTE:
+ These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml.
+ In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production.
+
%fieldset
.form-group
.form-check
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index ad5c8d4da22..64e01fa2d00 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -5,16 +5,33 @@
.form-group
= f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-bold'
= f.number_field :max_pages_size, class: 'form-control'
- .form-text.text-muted 0 for unlimited
+ .form-text.text-muted
+ = _("0 for unlimited")
.form-group
.form-check
= f.check_box :pages_domain_verification_enabled, class: 'form-check-input'
= f.label :pages_domain_verification_enabled, class: 'form-check-label' do
- Require users to prove ownership of custom domains
+ = _("Require users to prove ownership of custom domains")
.form-text.text-muted
- Domain verification is an essential security measure for public GitLab
- sites. Users are required to demonstrate they control a domain before
- it is enabled
+ = _("Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled")
= link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')
+ - if Feature.enabled?(:pages_auto_ssl)
+ %h5
+ = _("Configure Let's Encrypt")
+ %p
+ - lets_encrypt_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: "https://letsencrypt.org/" }
+ = _("%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites.").html_safe % { lets_encrypt_link_start: lets_encrypt_link_start, lets_encrypt_link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :lets_encrypt_notification_email, _("Email"), class: 'label-bold'
+ = f.text_field :lets_encrypt_notification_email, class: 'form-control'
+ .form-text.text-muted
+ = _("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.")
+ .form-group
+ .form-check
+ = f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
+ = f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
+ // Terms of Service should actually be a link, but the best way to get the url is using API
+ // So it will be done in later MR
+ = _("I have read and agree to the Let's Encrypt Terms of 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/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 44ac8d94764..f992d531ea5 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -5,10 +5,10 @@
.form-group
.form-check
= f.check_box :performance_bar_enabled, class: 'form-check-input'
- = f.label :performance_bar_enabled, class: 'form-check-label' do
+ = f.label :performance_bar_enabled, class: 'form-check-label qa-enable-performance-bar-checkbox' do
Enable the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
- = f.submit 'Save changes', class: "btn btn-success"
+ = f.submit 'Save changes', class: 'btn btn-success qa-save-changes-button'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index f50aca32bdf..d5ba6abe7af 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -24,7 +24,7 @@
.settings-content
= render 'prometheus'
-%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.qa-performance-bar-settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Profiling - Performance bar')
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index df3eeba907c..180066723f1 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -11,7 +11,7 @@
%td
.clipboard-group
.input-group
- %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
+ %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
@@ -20,7 +20,7 @@
%td
.clipboard-group
.input-group
- %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
+ %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append
= clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index c465d9f51d6..46beca0465e 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -14,6 +14,7 @@
.col-sm-10
= f.text_area :message, class: "form-control js-autosize",
required: true,
+ dir: 'auto',
data: { preview_path: preview_admin_broadcast_messages_path }
.form-group.row.js-toggle-colors-container
.col-sm-10.offset-sm-2
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 00d255846f9..f524d35d79e 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -44,12 +44,10 @@
%li
%span.light= _('Storage:')
- - counter_storage = storage_counter(@group.storage_size)
- - counter_repositories = storage_counter(@group.repository_size)
- - counter_build_artifacts = storage_counter(@group.build_artifacts_size)
- - counter_lfs_objects = storage_counter(@group.lfs_objects_size)
- %strong
- = _("%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)") % { counter_storage: counter_storage, counter_repositories: counter_repositories, counter_build_artifacts: counter_build_artifacts, counter_lfs_objects: counter_lfs_objects }
+ %strong= storage_counter(@group.storage_size)
+ (
+ = storage_counters_details(@group)
+ )
%li
%span.light= _('Group Git LFS status:')
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index dbb7224f5f9..6d934654c5d 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,5 +1,5 @@
%li.label-list-item{ id: dom_id(label) }
- = render "shared/label_row", label: label
+ = render "shared/label_row", label: label.present(issuable_subject: nil)
.label-actions-list
= link_to edit_admin_label_path(label), class: 'btn btn-transparent label-action has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
= sprite_icon('pencil')
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 46bb57c78a8..b88b760536d 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.prepend-top-default
.search-holder
- = render 'shared/projects/search_form', autofocus: true, icon: true
+ = render 'shared/projects/search_form', autofocus: true, icon: true, admin_view: true
.dropdown
- toggle_text = 'Namespace'
- if params[:namespace_id].present?
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 03cce4745aa..bc34af88928 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -73,15 +73,10 @@
= @project.repository.relative_path
%li
- %span.light Storage used:
+ %span.light= _('Storage:')
%strong= storage_counter(@project.statistics.storage_size)
(
- = storage_counter(@project.statistics.repository_size)
- repository,
- = storage_counter(@project.statistics.build_artifacts_size)
- build artifacts,
- = storage_counter(@project.statistics.lfs_objects_size)
- LFS
+ = storage_counters_details(@project.statistics)
)
%li
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index aecfdea10d9..37257b3aa1c 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -3,6 +3,7 @@
- only_key_value = local_assigns.fetch(:only_key_value, false)
- id = variable&.id
+- variable_type = variable&.variable_type
- key = variable&.key
- value = variable&.value
- is_protected_default = ci_variable_protected_by_default?
@@ -12,6 +13,7 @@
- id_input_name = "#{form_field}[variables_attributes][][id]"
- 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]"
- protected_input_name = "#{form_field}[variables_attributes][][protected]"
@@ -21,6 +23,8 @@
.ci-variable-row-body
%input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id }
%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{ 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{ type: "text",
name: key_input_name,
value: key,
@@ -33,7 +37,9 @@
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
- %p.masking-validation-error.gl-field-error.hide= s_("CiVariables|This variable will not be masked")
+ %p.masking-validation-error.gl-field-error.hide
+ = s_("CiVariables|Cannot use Masked Variable with current value")
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'masked-variables'), target: '_blank', rel: 'noopener noreferrer'
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item
.append-right-default
diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml
index 3e0f8955081..70e2eaeaf3b 100644
--- a/app/views/clusters/clusters/gcp/_form.html.haml
+++ b/app/views/clusters/clusters/gcp/_form.html.haml
@@ -74,6 +74,13 @@
= link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
- .form-group
- = field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
- class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
+ .form-group
+ = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
+ label_class: 'label-bold' }
+ .form-text.text-muted
+ = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank'
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
+ class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index e38a16e7a1a..deb6b21e2be 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -4,7 +4,7 @@
- page_title _('Kubernetes Cluster')
- manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- status_path = clusterable.cluster_status_cluster_path(@cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster)
.edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path,
@@ -34,7 +34,7 @@
= render 'banner'
= render 'form'
- = render_if_exists 'projects/clusters/prometheus_graphs' if show_cluster_health_graphs?(@cluster)
+ = render_if_exists 'projects/clusters/prometheus_graphs'
.cluster-applications-table#js-cluster-applications
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 27b11e8469f..f2fc5ac93fb 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -1,39 +1,55 @@
+- more_info_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
+ anchor: 'adding-an-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'
+
+- 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.')
+- token_help_text = s_('ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges.').html_safe % { code: '<code>'.html_safe, end_code: '</code>'.html_safe }
+- rbac_help_text = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + ' '
+- rbac_help_text << s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+
= bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' },
url: clusterable.create_user_clusters_path, as: :cluster do |field|
= field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- if has_multiple_clusters?
- = field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
- class: 'label-bold' } do
- = field.text_field :environment_scope, required: true,
- title: 'Environment scope is required.', wrapper: false
- .form-text.text-muted
- = s_("ClusterIntegration|Choose which of your environments will use this cluster.")
+ = field.text_field :environment_scope, required: true, title: 'Environment scope is required.',
+ label: s_('ClusterIntegration|Environment scope'), label_class: 'label-bold',
+ help: s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
= platform_kubernetes_field.url_field :api_url, required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- label: s_('ClusterIntegration|API URL'), label_class: 'label-bold'
+ label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
+ help: '%{help_text} %{help_link}'.html_safe % { help_text: api_url_help_text, help_link: more_info_link }
+
= platform_kubernetes_field.text_area :ca_cert,
placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold'
+ label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
+ help: '%{help_text} %{help_link}'.html_safe % { help_text: ca_cert_help_text, help_link: more_info_link }
+
= platform_kubernetes_field.text_field :token, required: true,
title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'),
- autocomplete: 'off', label_class: 'label-bold'
+ autocomplete: 'off', label_class: 'label-bold',
+ help: '%{help_text} %{help_link}'.html_safe % { help_text: token_help_text, help_link: more_info_link }
- if @user_cluster.allow_user_defined_namespace?
= platform_kubernetes_field.text_field :namespace,
label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold'
- = platform_kubernetes_field.form_group :authorization_type do
+ = platform_kubernetes_field.form_group :authorization_type,
+ { help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do
= platform_kubernetes_field.check_box :authorization_type,
{ class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
- .form-text.text-muted
- = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
- = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
- = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
-
- .form-group
- = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
+
+ .form-group
+ = field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),
+ label_class: 'label-bold' }
+ .form-text.text-muted
+ = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank'
+
+ .form-group
+ = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml
index f9f8097cb38..8caa25a7b5e 100644
--- a/app/views/clusters/platforms/kubernetes/_form.html.haml
+++ b/app/views/clusters/platforms/kubernetes/_form.html.haml
@@ -47,5 +47,12 @@
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
+ .form-group
+ = field.check_box :managed, { disabled: true, label: s_('ClusterIntegration|GitLab-managed cluster'),
+ label_class: 'label-bold' }
+ .form-text.text-muted
+ = s_('ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster.')
+ = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'gitlab-managed-clusters'), target: '_blank'
+
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index ca2822e2b29..97a446dbeec 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,3 +1,6 @@
+- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
+- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
+
= content_for :flash_message do
= render 'shared/project_limit'
@@ -6,24 +9,27 @@
- if current_user.can_create_project?
.page-title-controls
- = link_to "New project", new_project_path, class: "btn btn-success"
+ = link_to _("New project"), new_project_path, class: "btn btn-success"
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
- %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs
+ %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs{ class: ('border-0' if feature_project_list_filter_bar) }
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- Your projects
+ = _("Your projects")
%span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count)
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, data: {placement: 'right'} do
- Starred projects
+ = _("Starred projects")
%span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count)
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, data: {placement: 'right'} do
- Explore projects
-
- .nav-controls
- = render 'shared/projects/search_form'
- = render 'shared/projects/dropdown'
+ = _("Explore projects")
+ - unless feature_project_list_filter_bar
+ .nav-controls
+ = render 'shared/projects/search_form'
+ = render 'shared/projects/dropdown'
+- if feature_project_list_filter_bar
+ .project-filters
+ = render 'shared/projects/search_bar', project_tab_filter: project_tab_filter
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index da3cf5807b0..f9b61bf1f3e 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -1,6 +1,21 @@
-.nav-block
- %ul.nav-links.mobile-separator.nav.nav-tabs
- = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
- = link_to s_('DashboardProjects|All'), dashboard_projects_path
- = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
- = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
+- inactive_class = 'btn p-2'
+- active_class = 'btn p-2 active'
+- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
+- is_explore_trending = project_tab_filter == :explore_trending
+- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
+
+.nav-block{ class: ("w-100" if feature_project_list_filter_bar) }
+ - if feature_project_list_filter_bar
+ .btn-group.button-filter-group.d-flex.m-0.p-0
+ - if project_tab_filter == :explore || is_explore_trending
+ = link_to s_('DashboardProjects|Trending'), trending_explore_projects_path, class: is_explore_trending ? active_class : inactive_class
+ = link_to s_('DashboardProjects|All'), explore_projects_path, class: is_explore_trending ? inactive_class : active_class
+ - else
+ = link_to s_('DashboardProjects|All'), dashboard_projects_path, class: params[:personal].present? ? inactive_class : active_class
+ = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), class: params[:personal].present? ? active_class : inactive_class
+ - else
+ %ul.nav-links.mobile-separator.nav.nav-tabs
+ = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
+ = link_to s_('DashboardProjects|All'), dashboard_projects_path
+ = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
+ = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index dc9468b3368..0298f539b4b 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -13,7 +13,7 @@
= render "projects/last_push"
- if show_projects?(@projects, params)
= render 'dashboard/projects_head'
- = render 'nav'
+ = render 'nav' unless Feature.enabled?(:project_list_filter_bar)
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index a0d85446e5f..0fcc6894b68 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -8,7 +8,7 @@
%div{ class: container_class }
= render "projects/last_push"
- = render 'dashboard/projects_head'
+ = render 'dashboard/projects_head', project_tab_filter: :starred
- if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index efe1fb99efc..db6e40a6fd0 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -34,7 +34,7 @@
= todo_due_date(todo)
.todo-body
- .todo-note
+ .todo-note.break-word
.md
= first_line_in_markdown(todo, :body, 150, project: todo.project)
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index cac00f9c854..6750732ab67 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -14,7 +14,7 @@
%td
.clipboard-group
.input-group
- %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
+ %input.label.label-monospace.monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true }
.input-group-append
= clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default")
%tr
@@ -23,7 +23,7 @@
%td
.clipboard-group
.input-group
- %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
+ %input.label.label-monospace.monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true }
.input-group-append
= clipboard_button(target: '#secret', title: _("Copy secret to clipboard"), class: "btn btn btn-default")
%tr
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 96d6553a2ac..b02fdb4b638 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -11,7 +11,8 @@
= link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do
= event.target.reference_link_text
- unless event.milestone?
- %span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe
+ %span.event-target-title.append-right-4{ dir: "auto" }
+ = "&quot;".html_safe + event.target.title + "&quot".html_safe
- else
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event_action_name(event)
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 90ed8e41d32..7e2103287f7 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -7,7 +7,8 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
= event_note_title_html(event)
- %span.event-target-title.append-right-4= "&quot;".html_safe + event.target.title + "&quot".html_safe
+ %span.event-target-title.append-right-4{ dir: "auto" }
+ = "&quot;".html_safe + event.target.title + "&quot".html_safe
= render "events/event_scope", event: event
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index f518205f14c..d00a3d266d8 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,8 +1,12 @@
+- has_label = local_assigns.fetch(:has_label, false)
+- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
+
- if current_user
- .dropdown
+ .dropdown.js-project-filter-dropdown-wrap{ class: ('d-flex flex-grow-1 flex-shrink-1' if feature_project_list_filter_bar) }
%button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
- = icon('globe', class: 'mt-1')
- %span.light.ml-3= _("Visibility:")
+ - unless has_label
+ = icon('globe', class: 'mt-1')
+ %span.light.ml-3= _("Visibility:")
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index dd2bf6a5ef8..341ad681c7c 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -5,9 +5,9 @@
= render_dashboard_gold_trial(current_user)
- if current_user
- = render 'dashboard/projects_head'
+ = render 'dashboard/projects_head', project_tab_filter: :explore
- else
= render 'explore/head'
-= render 'explore/projects/nav'
+= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index dd2bf6a5ef8..ec92852ddde 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -5,9 +5,9 @@
= render_dashboard_gold_trial(current_user)
- if current_user
- = render 'dashboard/projects_head'
+ = render 'dashboard/projects_head', project_tab_filter: :starred
- else
= render 'explore/head'
-= render 'explore/projects/nav'
+= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index dd2bf6a5ef8..ed508fa2506 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -5,9 +5,9 @@
= render_dashboard_gold_trial(current_user)
- if current_user
- = render 'dashboard/projects_head'
+ = render 'dashboard/projects_head', project_tab_filter: :explore_trending
- else
= render 'explore/head'
-= render 'explore/projects/nav'
+= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
= render 'projects', projects: @projects
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2f635757902..0c8f86c2822 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 2af3e861587..09cc713e3af 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -22,7 +22,7 @@
%span.flex-project-title
Members with access to
%strong= @group.name
- %span.badge= @members.total_count
+ %span.badge.badge-pill= @members.total_count
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
.position-relative.append-right-8
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 5cf3193bc62..a8358704b03 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
-- page_title "Labels"
+- page_title 'Labels'
- can_admin_label = can?(current_user, :admin_label, @group)
-- issuables = ['issues', 'merge requests']
- search = params[:search]
- subscribed = params[:subscribed]
- labels_or_filters = @labels.exists? || search.present? || subscribed.present?
@@ -14,11 +13,11 @@
.labels-container.prepend-top-5
- if @labels.any?
.text-muted
- = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuable_types.to_sentence }
.other-labels
%h5= _('Labels')
%ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = render partial: 'shared/label', collection: @labels, as: :label, locals: { use_label_priority: false, subject: @group }
= paginate @labels, theme: 'gitlab'
- elsif search.present?
.nothing-here-block
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index d0f5cd94002..d21496ee0aa 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title "CI / CD Settings"
- page_title "CI / CD"
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings#ci-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 916f98a62d1..75e4dc46c9b 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,6 +1,7 @@
%div
- if Gitlab::CurrentSettings.help_page_text.present?
- = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
+ .prepend-top-default.md
+ = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text)
%hr
%h1
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index a88b04eccbb..c4670869c93 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -2,18 +2,18 @@
- header_title _("Projects"), root_path
%h3.page-title
- = custom_icon('go_logo')
+ = custom_icon('gitea_logo')
= _('Import Projects from Gitea')
%p
- - link_to_personal_token = link_to(_('Personal Access Token'), 'https://github.com/gogits/go-gogs-client/wiki#access-token')
+ - link_to_personal_token = link_to(_('Personal Access Token'), 'https://docs.gitea.io/en-us/api-usage/#authentication-via-the-api')
= _('To get started, please enter your Gitea Host URL and a %{link_to_personal_token}.').html_safe % { link_to_personal_token: link_to_personal_token }
= form_tag personal_access_token_import_gitea_path do
.form-group.row
= label_tag :gitea_host_url, _('Gitea Host URL'), class: 'col-form-label col-sm-2'
.col-sm-4
- = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control'
+ = text_field_tag :gitea_host_url, nil, placeholder: 'https://gitea.com', class: 'form-control'
.form-group.row
= label_tag :personal_access_token, _('Personal Access Token'), class: 'col-form-label col-sm-2'
.col-sm-4
diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml
index 88244fde16b..ef0693e73c3 100644
--- a/app/views/import/gitea/status.html.haml
+++ b/app/views/import/gitea/status.html.haml
@@ -1,7 +1,7 @@
- page_title _("Gitea Import")
- header_title _("Projects"), root_path
%h3.page-title
- = custom_icon('go_logo')
+ = custom_icon('gitea_logo')
= _('Import Projects from Gitea')
= render 'import/githubish_status', provider: 'gitea'
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a9b85889846..724c9976954 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -17,6 +17,9 @@
- if logo_text.present?
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
+ - if Gitlab.com?
+ %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
+ = _('Next')
- if current_user
= render "layouts/nav/dashboard"
@@ -38,7 +41,7 @@
= link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('issues', size: 16)
- issues_count = assigned_issuables_count(:issues)
- %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index cd9128c452b..fbec62b02f8 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -2,8 +2,12 @@
- if current_user_menu?(:help)
%li
= link_to _("Help"), help_path
+ = render_if_exists "shared/learn_gitlab_menu_item"
%li.divider
%li
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
+ - if Gitlab.com?
+ %li.js-canary-link
+ = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index bb2d206ba91..04d67e024ba 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -132,6 +132,19 @@
= _('Abuse Reports')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+ - if instance_clusters_enabled?
+ = nav_link(controller: :clusters) do
+ = link_to admin_clusters_path do
+ .nav-icon-container
+ = sprite_icon('cloud-gear')
+ %span.nav-item-name
+ = _('Kubernetes')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_clusters_path do
+ %strong.fly-out-top-item-name
+ = _('Kubernetes')
+
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
= link_to admin_spam_logs_path do
@@ -232,7 +245,7 @@
%span
= _('Reporting')
= nav_link(path: 'application_settings#metrics_and_profiling') do
- = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling') do
+ = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), class: 'qa-admin-settings-metrics-and-profiling-item' do
%span
= _('Metrics and profiling')
= nav_link(path: 'application_settings#network') do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index b950e53639a..c2116ec63dd 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -46,6 +46,7 @@
= _('Contribution Analytics')
= render_if_exists 'layouts/nav/group_insights_link'
+ = render_if_exists 'groups/sidebar/dependency_proxy' # EE-specific
= render_if_exists "layouts/nav/ee/epic_link", group: @group
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
index 773ae8174e9..afb02f97e5a 100644
--- a/app/views/notify/links/ci/builds/_build.text.erb
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -1 +1 @@
-Job #<%= build.id %> ( <%= pipeline_job_url(pipeline, build) %> )
+Job #<%= build.id %> ( <%= raw_project_job_url(pipeline.project, build) %> )
diff --git a/app/views/notify/member_access_granted_email.html.haml b/app/views/notify/member_access_granted_email.html.haml
index 18dec806539..1c50dba9c97 100644
--- a/app/views/notify/member_access_granted_email.html.haml
+++ b/app/views/notify/member_access_granted_email.html.haml
@@ -1,3 +1,10 @@
+- 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')
+
%p
- You have been granted #{member.human_access} access to the
- #{link_to member_source.human_name, member_source.web_url} #{member_source.model_name.singular}.
+ = _('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_granted_email.text.erb b/app/views/notify/member_access_granted_email.text.erb
index a9fb3a589a5..445009bb413 100644
--- a/app/views/notify/member_access_granted_email.text.erb
+++ b/app/views/notify/member_access_granted_email.text.erb
@@ -1,3 +1,8 @@
-You have been granted <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>.
+<% source_type = member_source.model_name.singular %>
+<%= _('You have been granted %{access_level} access to the %{source_name} %{source_type}.') % { access_level: member.human_access, source_name: member_source.human_name, source_type: source_type } %>
<%= member_source.web_url %>
+
+<%= _('If this was a mistake you can leave the %{source_type}.') % { source_type: source_type } %>
+
+<%= polymorphic_url([member_source], leave: 1) %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 77d2e65d285..9ab648e2a64 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -3,7 +3,7 @@
#{link_to @merge_request.author_name, user_url(@merge_request.author)} created a merge request:
%p.details
- != merge_path_description(@merge_request, '&rarr;')
+ = merge_path_description(@merge_request, '→')
- if @merge_request.assignees.any?
%p
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 50adc19f524..3ca4abddbb8 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -80,6 +80,8 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ = render_if_exists "projects/home_mirror"
+
- if @project.badges.present?
.project-badges.mb-2
- @project.badges.each do |badge|
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 2b425f18389..9c854369c93 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -50,7 +50,7 @@
- if gitea_import_enabled?
%div
= link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do
- = custom_icon('go_logo')
+ = custom_icon('gitea_logo')
Gitea
- if git_import_enabled?
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
new file mode 100644
index 00000000000..1ab467a3710
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -0,0 +1,19 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge checks')
+ %p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged')
+ .form-check.mb-2.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
+ = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
+ = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
+ = s_('ProjectSettings|Pipelines must succeed')
+ .descr.text-secondary
+ = s_('ProjectSettings|Pipelines need to be configured to enable this feature.')
+ = link_to icon('question-circle'),
+ help_page_path('ci/merge_request_pipelines/index.md',
+ anchor: 'pipelines-for-merge-requests'),
+ target: '_blank'
+ .form-check.mb-2
+ = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
+ = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
+ = s_('ProjectSettings|All discussions must be resolved')
diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml
index 9082bfc409d..47c311f42d0 100644
--- a/app/views/projects/_merge_request_merge_method_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_method_settings.html.haml
@@ -1,30 +1,33 @@
- form = local_assigns.fetch(:form)
.form-group
- = label_tag :merge_method_merge, class: 'label-bold' do
- = _('Merge method')
- .form-check
+ %b= s_('ProjectSettings|Merge method')
+ %p.text-secondary= s_('ProjectSettings|This will dictate the commit history when you merge a merge request')
+ .form-check.mb-2
= form.radio_button :merge_method, :merge, class: "js-merge-method-radio form-check-input"
= label_tag :project_merge_method_merge, class: 'form-check-label' do
- .mb-3
- = _('Merge commit')
- .text-secondary
- = _('A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.')
+ = s_('ProjectSettings|Merge commit')
+ .descr.text-secondary
+ = s_('ProjectSettings|Every merge creates a merge commit')
-.form-check
- = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
- = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
- .mb-3
- = _('Merge commit with semi-linear history')
- .text-secondary
- = _('A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build.')
- .text-secondary
- = _('When fast-forward merge is not possible, the user is given the option to rebase.')
+ .form-check.mb-2
+ = form.radio_button :merge_method, :rebase_merge, class: "js-merge-method-radio form-check-input"
+ = label_tag :project_merge_method_rebase_merge, class: 'form-check-label' do
+ = s_('ProjectSettings|Merge commit with semi-linear history')
+ .descr.text-secondary
+ = s_('ProjectSettings|Every merge creates a merge commit')
+ %br
+ = s_('ProjectSettings|Fast-forward merges only')
+ %br
+ = s_('ProjectSettings|When conflicts arise the user is given the option to rebase')
-.form-check
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input"
- = label_tag :project_merge_method_ff, class: 'form-check-label' do
- .mb-3
- = _('Fast-forward merge')
- .text-secondary
- = _('No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase.')
+ .form-check.mb-2
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff form-check-input"
+ = label_tag :project_merge_method_ff, class: 'form-check-label' do
+ = s_('ProjectSettings|Fast-forward merge')
+ .descr.text-secondary
+ = s_('ProjectSettings|No merge commits are created')
+ %br
+ = s_('ProjectSettings|Fast-forward merges only')
+ %br
+ = s_('ProjectSettings|When conflicts arise the user is given the option to rebase')
diff --git a/app/views/projects/_merge_request_merge_options_settings.html.haml b/app/views/projects/_merge_request_merge_options_settings.html.haml
new file mode 100644
index 00000000000..5ab475822de
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_options_settings.html.haml
@@ -0,0 +1,14 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge options')
+ %p.text-secondary= s_('ProjectSettings|Additional merge request capabilities that influence how and when merges will be performed')
+ = render_if_exists 'projects/merge_pipelines_settings', form: form
+ .form-check.mb-2
+ = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input'
+ = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do
+ = s_('ProjectSettings|Automatically resolve merge request diff discussions when they become outdated')
+ .form-check.mb-2
+ = 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')
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
deleted file mode 100644
index 3a9f7ca42db..00000000000
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- form = local_assigns.fetch(:form)
-
-.form-group
- .form-check.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
- = form.check_box :only_allow_merge_if_pipeline_succeeds, class: 'form-check-input'
- = form.label :only_allow_merge_if_pipeline_succeeds, class: 'form-check-label' do
- .mb-3
- = _('Only allow merge requests to be merged if the pipeline succeeds')
- .text-secondary
- = _('Pipelines need to be configured to enable this feature.')
- = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank'
- = render_if_exists 'projects/merge_pipelines_settings', form: form
- .form-check
- = form.check_box :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-input'
- = form.label :only_allow_merge_if_all_discussions_are_resolved, class: 'form-check-label' do
- %p= _('Only allow merge requests to be merged if all discussions are resolved')
- .form-check
- = form.check_box :resolve_outdated_diff_discussions, class: 'form-check-input'
- = form.label :resolve_outdated_diff_discussions, class: 'form-check-label' do
- %p= _('Automatically resolve merge request diff discussions when they become outdated')
- .form-check
- = form.check_box :printing_merge_request_link_enabled, class: 'form-check-input'
- = form.label :printing_merge_request_link_enabled, class: 'form-check-label' do
- %p= _('Show link to create/view merge request when pushing from the command line')
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index c80e831dd33..f2ba38387a3 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -2,4 +2,6 @@
= render 'projects/merge_request_merge_method_settings', project: @project, form: form
-= render 'projects/merge_request_merge_settings', form: form
+= render 'projects/merge_request_merge_options_settings', project: @project, form: form
+
+= render 'projects/merge_request_merge_checks_settings', project: @project, form: form
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index afc40ca4eab..c502b392384 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -8,6 +8,7 @@
= f.text_area attr,
class: classes,
placeholder: placeholder,
+ dir: 'auto',
data: { supports_quick_actions: supports_quick_actions,
supports_autocomplete: supports_autocomplete }
- else
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index 7892019bb15..e33e9509e3a 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,9 +1,9 @@
-.branch-commit
+.branch-commit.cgray
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha"
&middot;
%span.str-truncated
- = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
+ = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message cgray"
&middot;
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 4eb53faa6ff..4762045ee96 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -7,31 +7,22 @@
= sprite_icon('download')
%span.sr-only= _('Select Archive Format')
= sprite_icon("arrow-down")
- %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %li.dropdown-header
- #{ _('Source code') }
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'zip'), rel: 'nofollow', download: '' do
- %span= _('Download zip')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.gz'), rel: 'nofollow', download: '' do
- %span= _('Download tar.gz')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar.bz2'), rel: 'nofollow', download: '' do
- %span= _('Download tar.bz2')
- %li
- = link_to project_archive_path(project, id: tree_join(ref, archive_prefix), format: 'tar'), rel: 'nofollow', download: '' do
- %span= _('Download tar')
-
+ .dropdown-menu.dropdown-menu-right{ role: 'menu' }
+ %section
+ %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 pipeline && pipeline.latest_builds_with_artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - pipeline.latest_builds_with_artifacts.each do |job|
- %li
- = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %span
- #{s_('DownloadArtifacts|Download')} '#{job.name}'
+ %section.border-top.pt-1.mt-1
+ %h5.m-0.dropdown-bold-header= _('Download artifacts')
+ - unless pipeline.latest?
+ %span.unclickable= ci_status_for_statuseable(project.pipeline_for(ref))
+ %h6.m-0.dropdown-header= _('Previous Artifacts')
+ %ul
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ %li= link_to job.name, latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: ''
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
new file mode 100644
index 00000000000..7f2cd8e109e
--- /dev/null
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -0,0 +1,5 @@
+- formats = [['zip', 'btn-primary'], ['tar.gz'], ['tar.bz2'], ['tar']]
+
+.d-flex.justify-content-between
+ - formats.each do |(fmt, extra_class)|
+ = link_to fmt, project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt), rel: 'nofollow', download: '', class: "btn btn-xs #{extra_class}"
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 6d48475d505..f4560404c03 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -12,7 +12,7 @@
%td.status
= render "ci/status/badge", status: job.detailed_status(current_user), title: job.status_title
- %td.branch-commit
+ %td.branch-commit.cgray
- if can?(current_user, :read_build, job)
= link_to project_job_path(job.project, job) do
%span.build-link ##{job.id}
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index 888be4ee282..ed3c9890efd 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#cleanup{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index ce55dd78747..e2d078855d9 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -49,7 +49,7 @@
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
.commit-sha-group.d-none.d-sm-flex
- .label.label-monospace
+ .label.label-monospace.monospace
= commit.short_id
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"), class: "btn btn-default", container: "body")
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml
index caaff082cc3..56bebeca581 100644
--- a/app/views/projects/commits/_inline_commit.html.haml
+++ b/app/views/projects/commits/_inline_commit.html.haml
@@ -3,6 +3,6 @@
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha"
&nbsp;
%span.str-truncated
- = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message")
+ = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message cgray")
.float-right
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index ff6a9d49a61..59efcde5825 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 24d665761cc..fcf27351a21 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 9774b797928..743aa60b3ba 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -1,5 +1,5 @@
.table-mobile-content
- .branch-commit
+ .branch-commit.cgray
- if deployment.ref
%span.icon-container
= deployment.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
@@ -12,6 +12,6 @@
%span.flex-truncate-child
- if commit_title = deployment.commit_title
= author_avatar(deployment.commit, size: 20)
- = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
+ = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message cgray"
- else
= _("Can't find HEAD commit for this branch")
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 1a3e4a5d608..c15b84d0aac 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("General Settings")
- page_title _("General")
- @content_class = "limit-container-width" unless fluid_layout
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.general-settings.no-animate.expanded#js-general-settings
.settings-header
@@ -27,7 +27,7 @@
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }= expanded ? _('Collapse') : _('Expand')
- %p= _('Choose your merge method, set up a default merge request description template.')
+ %p= _('Choose your merge method, options, checks, and set up a default merge request description template.')
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index e8a89b8c6fc..b37dba8b35d 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -1,24 +1,20 @@
-- page_title "Fork project"
+- page_title _("Fork project")
- if @forked_project && !@forked_project.saved?
.alert.alert-danger.alert-block
%h4
= sprite_icon('fork', size: 16)
- Fork Error!
+ = _("Fork Error!")
%p
- You tried to fork
- = link_to_project @project
- but it failed for the following reason:
-
+ = _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
- if @forked_project && @forked_project.errors.any?
%p
&ndash;
- error = @forked_project.errors.full_messages.first
- if error.include?("already been taken")
- Name has already been taken
+ = _("Name has already been taken")
- else
= error
%p
- = link_to new_project_fork_path(@project), title: "Fork", class: "btn" do
- Try to fork again
+ = link_to _("Try to fork again"), new_project_fork_path(@project), title: _("Fork"), class: "btn"
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index c63c34c4ebb..0397a7034c7 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -5,12 +5,12 @@
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
- = search_field_tag :filter_projects, nil, placeholder: 'Search forks', class: 'projects-list-filter project-filter-form-field form-control input-short',
+ = search_field_tag :filter_projects, nil, placeholder: _('Search forks'), class: 'projects-list-filter project-filter-form-field form-control input-short',
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light sort:
+ %span.light= _("sort:")
- if @sort.present?
= sort_options_hash[@sort]
- else
@@ -30,13 +30,12 @@
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-success' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn btn-success' do
= sprite_icon('fork', size: 12)
- %span Fork
+ %span= _('Fork')
- else
- = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-success' do
+ = link_to new_project_fork_path(@project), title: _("Fork project"), class: 'btn btn-success' do
= sprite_icon('fork', size: 12)
- %span Fork
-
+ %span= _('Fork')
= render 'projects', projects: @forks
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index a603b1024eb..bf03353a565 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -1,13 +1,11 @@
-- page_title "Fork project"
+- page_title _("Fork project")
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
- Fork project
+ = _("Fork project")
%p
- A fork is a copy of a project.
- %br
- Forking a repository allows you to make changes without affecting the original project.
+ = _("A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project.").html_safe
.col-lg-9
- if @namespaces.present?
.fork-thumbnail-container.js-fork-content
@@ -17,13 +15,13 @@
= render 'fork_button', namespace: namespace
- else
%strong
- No available namespaces to fork the project.
+ = _("No available namespaces to fork the project.")
%p.prepend-top-default
- You must have permission to create a project in a namespace before forking.
+ = _("You must have permission to create a project in a namespace before forking.")
.save-project-loader.hide.js-fork-content
%h2.text-center
= icon('spinner spin')
- Forking repository
+ = _("Forking repository")
%p.text-center
- Please wait a moment, this page will automatically refresh when ready.
+ = _("Please wait a moment, this page will automatically refresh when ready.")
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 7614d40ba1f..1118b44d7a2 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -5,11 +5,11 @@
- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
-%tr.generic_commit_status{ class: ('retried' if retried) }
+%tr.generic-commit-status{ class: ('retried' if retried) }
%td.status
= render 'ci/status/badge', status: generic_commit_status.detailed_status(current_user)
- %td.generic_commit_status-link
+ %td.generic-commit-status-link
- if can?(current_user, :read_commit_status, generic_commit_status) && generic_commit_status.target_url
= link_to generic_commit_status.target_url do
%span.build-link ##{generic_commit_status.id}
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 377b2a6d8d9..9293aa1b309 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,7 +6,7 @@
.issuable-info-container
.issuable-main-info
.issue-title.title
- %span.issue-title-text
+ %span.issue-title-text{ dir: "auto" }
- if issue.confidential?
%span.has-tooltip{ title: _('Confidential') }
= confidential_icon(issue)
@@ -36,8 +36,10 @@
= issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- - labels_sorted_by_title(issue.labels).each do |label|
- = link_to_label(label, subject: issue.project, css_class: 'label-link')
+ - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
+ = link_to_label(label, css_class: 'label-link')
+
+ = render_if_exists "projects/issues/issue_weight", issue: issue
.issuable-meta
%ul.controls
diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml
deleted file mode 100644
index 90838a75214..00000000000
--- a/app/views/projects/issues/_merge_requests_status.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- time_format = '%b %e, %Y %l:%M%P %Z%z'
-
-- if merge_request.merged?
- - mr_status_date = merge_request.merged_at
- - mr_status_title = _('Merged')
- - mr_status_icon = 'merge'
- - mr_status_class = 'merged'
-- elsif merge_request.closed?
- - mr_status_date = merge_request.closed_event&.created_at
- - mr_status_title = _('Closed')
- - mr_status_icon = 'issue-close'
- - mr_status_class = 'closed'
-- else
- - mr_status_date = merge_request.created_at
- - mr_status_title = mr_status_date ? _('Opened') : _('Open')
- - mr_status_icon = 'issue-open-m'
- - mr_status_class = 'open'
-
-- if mr_status_date
- - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>"
-- else
- - mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span></div>"
-
-%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } }
- = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}")
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 9a081a42b6f..d1601d7fd10 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,9 +1,8 @@
-- add_to_breadcrumbs "Issues", project_issues_path(@project)
-- breadcrumb_title "New"
-- page_title "New Issue"
+- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
+- breadcrumb_title _("New")
+- page_title _("New Issue")
-%h3.page-title
- New Issue
+%h3.page-title= _("New Issue")
%hr
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2c95ac6dbb3..d55afee4523 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- add_to_breadcrumbs "Issues", project_issues_path(@project)
+- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
-- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
+- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- page_description @issue.description
- page_card_attributes @issue.card_attributes
@@ -15,7 +15,7 @@
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) }
= sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
.d-none.d-sm-block
- - if @issue.moved?
+ - if @issue.moved? && can?(current_user, :read_issue, @issue.moved_to)
- moved_link_start = "<a href=\"#{issue_path(@issue.moved_to)}\" class=\"text-white text-underline\">".html_safe
- moved_link_end = '</a>'.html_safe
= s_('IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})').html_safe % {moved_link_start: moved_link_start,
@@ -77,7 +77,9 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- #js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } }
+ = render_if_exists 'projects/issues/related_issues'
+
+ #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index bb7c297ba1f..511d7a82d1b 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -11,31 +11,30 @@
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
.labels-container.prepend-top-10
- - if can_admin_label
- - if search.blank?
- %p.text-muted
- = _('Labels can be applied to issues and merge requests.')
- %br
- = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
- -# Only show it in the first page
- - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- .prioritized-labels{ class: ('hide' if hide) }
- %h5.prepend-top-10= _('Prioritized Labels')
- .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
- #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
- = render 'shared/empty_states/priority_labels'
- - if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- - elsif search.present?
- .nothing-here-block
- = _('No prioritised labels with such name or description')
+ - if can_admin_label && search.blank?
+ %p.text-muted
+ = _('Labels can be applied to issues and merge requests.')
+ %br
+ = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
+
+ -# Only show it in the first page
+ - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
+ .prioritized-labels{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
+ %h5.prepend-top-10= _('Prioritized Labels')
+ .content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
+ #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
+ = render 'shared/empty_states/priority_labels'
+ - if @prioritized_labels.present?
+ = 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')
- if @labels.present?
.other-labels
- - if can_admin_label
- %h5{ class: ('hide' if hide) }= _('Other Labels')
+ %h5{ class: ('hide' if hide) }= _('Other Labels')
.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
+ = render partial: 'shared/label', collection: @labels, as: :label, locals: { subject: @project }
= paginate @labels, theme: 'gitlab'
- elsif search.present?
.other-labels
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 47c8e3d73f5..67e5e4ca62d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -34,8 +34,8 @@
= merge_request.target_branch
- if merge_request.labels.any?
&nbsp;
- - labels_sorted_by_title(merge_request.labels).each do |label|
- = link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
+ - presented_labels_sorted_by_title(merge_request.labels, merge_request.project).each do |label|
+ = link_to_label(label, type: :merge_request, css_class: 'label-link')
.issuable-meta
%ul.controls
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 79c586eef73..05aeb5d972b 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -37,21 +37,21 @@
%ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs
%li.notes-tab.qa-notes-tab
= tab_link_for @merge_request, :show, force_link: @commit.present? do
- Discussion
+ = _("Discussion")
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
= tab_link_for @merge_request, :commits do
- Commits
+ = _("Commits")
%span.badge.badge-pill= @commits_count
- if @pipelines.any?
%li.pipelines-tab
= tab_link_for @merge_request, :pipelines do
- Pipelines
+ = _("Pipelines")
%span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab.qa-diffs-tab
= tab_link_for @merge_request, :diffs do
- Changes
+ = _("Changes")
%span.badge.badge-pill= @merge_request.diff_size
.d-inline-flex.flex-wrap
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index ef6db07a1bb..ee82d68d398 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -1,8 +1,5 @@
- mirror = f.object
-- is_push = local_assigns.fetch(:is_push, false)
- auth_options = [[_('Password'), 'password'], [_('SSH public key'), 'ssh_public_key']]
-- regen_data = { auth_method: 'ssh_public_key', regenerate_ssh_private_key: true }
-- ssh_public_key_present = mirror.ssh_public_key.present?
.form-group
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
@@ -17,21 +14,3 @@
.well-password-auth.collapse.js-well-password-auth
= f.label :password, _("Password"), class: "label-bold"
= f.password_field :password, value: mirror.password, class: 'form-control qa-password', autocomplete: 'new-password'
- - unless is_push
- .well-ssh-auth.collapse.js-well-ssh-auth
- %p.js-ssh-public-key-present{ class: ('collapse' unless ssh_public_key_present) }
- = _('Here is the public SSH key that needs to be added to the remote server. For more information, please refer to the documentation.')
- %p.js-ssh-public-key-pending{ class: ('collapse' if ssh_public_key_present) }
- = _('An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation.')
-
- .clearfix.js-ssh-public-key-wrap{ class: ('collapse' unless ssh_public_key_present) }
- %code.prepend-top-10.ssh-public-key
- = mirror.ssh_public_key
- = clipboard_button(text: mirror.ssh_public_key, title: _("Copy SSH public key to clipboard"), class: 'prepend-top-10 btn-copy-ssh-public-key')
-
- = button_tag type: 'button',
- data: { endpoint: project_mirror_path(@project), project_data: { import_data_attributes: regen_data } },
- class: "btn btn-inverted btn-warning prepend-top-10 js-btn-regenerate-ssh-key#{ ' collapse' unless ssh_public_key_present }" do
- = icon('spinner spin', class: 'js-spinner d-none')
- = _('Regenerate key')
- = render 'projects/mirrors/regenerate_public_ssh_key_confirm_modal'
diff --git a/app/views/projects/mirrors/_disabled_mirror_badge.html.haml b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml
new file mode 100644
index 00000000000..356cb43f07f
--- /dev/null
+++ b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml
@@ -0,0 +1 @@
+.badge.badge-warning.qa-disabled-mirror-badge{ data: { toggle: 'tooltip', html: 'true' }, title: _('Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them.') }= _('Disabled')
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 35a6885318a..33e5a6e67c3 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -7,7 +7,7 @@
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
- %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ %li= mirror_lfs_sync_message
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
like new branches being created or new commits being pushed to existing branches.')
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index c031815200b..73e2a4ffb8b 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
%section.settings.project-mirror-settings.js-mirror-settings.no-animate.qa-mirroring-repositories-settings#js-push-remote-settings{ class: ('expanded' if expanded) }
@@ -11,7 +11,7 @@
= link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
.settings-content
- = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f|
+ = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'new-password', data: mirrors_form_data_attributes } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title= _('Mirror a repository')
@@ -20,7 +20,7 @@
.form-group.has-feedback
= label_tag :url, _('Git repository URL'), class: 'label-light'
- = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+"
+ = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url qa-mirror-repository-url-input', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+", autocomplete: 'new-password'
= render 'projects/mirrors/instructions'
@@ -49,17 +49,19 @@
%tbody.js-mirrors-table-body
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- - if mirror.enabled
- %tr.qa-mirrored-repository-row
- %td.qa-mirror-repository-url= mirror.safe_url
- %td= _('Push')
- %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
- %td
- - if mirror.last_error.present?
- .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
- %td
- .btn-group.mirror-actions-group.pull-right{ role: 'group' }
- - if mirror.ssh_key_auth?
- = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'))
- = render 'shared/remote_mirror_update_button', remote_mirror: mirror
- %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
+ - next if mirror.new_record?
+ %tr.qa-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) }
+ %td.qa-mirror-repository-url= mirror.safe_url
+ %td= _('Push')
+ %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td
+ - if mirror.disabled?
+ = render 'projects/mirrors/disabled_mirror_badge'
+ - if mirror.last_error.present?
+ .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
+ %td
+ .btn-group.mirror-actions-group.pull-right{ role: 'group' }
+ - if mirror.ssh_key_auth?
+ = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'))
+ = render 'shared/remote_mirror_update_button', remote_mirror: mirror
+ %button.js-delete-mirror.qa-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 1d9c83653fe..b7c885b4a63 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -5,4 +5,4 @@
= rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden'
= render partial: 'projects/mirrors/ssh_host_keys', locals: { f: rm_f }
- = render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f, is_push: true }
+ = render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f }
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index f61aa6ecd11..7762fb4b844 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -3,7 +3,7 @@
- verified_at = mirror.ssh_known_hosts_verified_at
.form-group.js-ssh-host-keys-section{ class: ('collapse' unless mirror.ssh_mirror_url?) }
- %button.btn.btn-inverted.btn-success.inline.js-detect-host-keys.append-right-10{ type: 'button' }
+ %button.btn.btn-inverted.btn-secondary.inline.js-detect-host-keys.append-right-10{ type: 'button' }
= icon('spinner spin', class: 'js-spinner d-none')
= _('Detect host keys')
.fingerprint-ssh-info.js-fingerprint-ssh-info.prepend-top-10.append-bottom-10{ class: ('collapse' unless mirror.ssh_mirror_url?) }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index ff7c36c2d5b..d7e16dbd40c 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,6 +16,7 @@
= _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ = render_if_exists 'projects/new_ci_cd_banner_external_repo'
%p
- pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank'
= _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
@@ -42,13 +43,19 @@
%a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' }
%span.d-none.d-sm-block Import project
%span.d-block.d-sm-none Import
+ = render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
.tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
- .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' }
+ #create-from-template-pane.tab-pane.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
+ .card-slim.m-4.p-4
+ %div
+ - contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: contributing_templates_url }
+ = _('Learn how to %{link_start}contribute to the built-in templates%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
@@ -63,6 +70,8 @@
%h4 No import options available
%p Contact an administrator to enable options for importing your project.
+ = render_if_exists 'projects/new_ci_cd_only_project_pane', active_tab: active_tab
+
.save-project-loader.d-none
.center
%h2
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 539b184e5c2..63748d8d85f 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.qa-protected-branches-settings.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 9a50a51e4be..b0c87ac8c17 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -1,4 +1,4 @@
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
%section.settings.no-animate#js-protected-tags-settings{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 548b7c06867..5e3e1076c2c 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -2,7 +2,7 @@
- page_title _("CI / CD Settings")
- page_title _("CI / CD")
-- expanded = Rails.env.test?
+- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
new file mode 100644
index 00000000000..2fbb9195a04
--- /dev/null
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -0,0 +1,2 @@
+.js-operation-settings{ data: { external_dashboard: { path: '',
+ help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 6f777305a54..edc2c58a8ed 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -4,4 +4,5 @@
= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking', expanded: true
+= render 'projects/settings/operations/external_dashboard'
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 5b25a67bc87..8ae2807729b 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -21,10 +21,9 @@
- if @scope == 'projects'
.term
= render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- - elsif %w[blobs wiki_blobs].include?(@scope)
- = render partial: 'search/results/blob', collection: @search_objects, locals: { projects: blob_projects(@search_objects) }
- else
- = render partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope)
+ = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals
- if @scope != 'projects'
= paginate_collection(@search_objects)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index b351ecd4edf..5847751b268 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,5 +1,5 @@
- project = find_project_for_result_blob(projects, wiki_blob)
- wiki_blob = parse_search_result(wiki_blob)
-- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
+- wiki_blob_link = project_wiki_path(project, Pathname.new(wiki_blob.filename).sub_ext(''))
= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: wiki_blob.filename, blob_link: wiki_blob_link }
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index dbd3bbb43af..6bd8cadd7d9 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -9,13 +9,13 @@
.modal-body
%p
%strong= label.name
- %span will be permanently deleted from #{label.is_a?(ProjectLabel)? label.project.name : label.group.name}. This cannot be undone.
+ %span will be permanently deleted from #{label.subject.name}. This cannot be undone.
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
= link_to 'Delete label',
- destroy_label_path(label),
+ label.destroy_path,
title: 'Delete',
method: :delete,
class: 'btn btn-remove'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 21ea188d7b3..2b4a24a001f 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,13 +1,13 @@
+- label = label.present(issuable_subject: local_assigns[:subject])
- label_css_id = dom_id(label)
- status = label_subscription_status(label, @project).inquiry if current_user
-- subject = local_assigns[:subject]
- use_label_priority = local_assigns.fetch(:use_label_priority, false)
- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false)
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- tooltip_title = label_status_tooltip(label, status) if status
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label, subject: subject, force_priority: force_priority
+ = render "shared/label_row", label: label, force_priority: force_priority
%ul.label-actions-list
- if @project
%li.inline
@@ -21,7 +21,7 @@
= sprite_icon('star')
- if can?(current_user, :admin_label, label)
%li.inline
- = link_to edit_label_path(label), class: 'btn btn-transparent label-action edit has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
+ = link_to label.edit_path, class: 'btn btn-transparent label-action edit has-tooltip', title: _('Edit'), data: { placement: 'bottom' }, aria_label: _('Edit') do
= sprite_icon('pencil')
- if can?(current_user, :admin_label, label)
%li.inline
@@ -48,7 +48,7 @@
%button.text-danger.remove-row{ type: 'button' }= _('Delete')
- if current_user
%li.inline.label-subscription
- - if can_subscribe_to_label_in_different_levels?(label)
+ - if label.can_subscribe_to_label_in_different_levels?
%button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 6651f12f6de..a1aab2e6a08 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,13 +1,10 @@
-- subject = local_assigns[:subject]
- force_priority = local_assigns.fetch(:force_priority, false)
-- show_label_issues_link = defined?(@project) && show_label_issuables_link?(label, :issues, project: @project)
-- show_label_merge_requests_link = defined?(@project) && show_label_issuables_link?(label, :merge_requests, project: @project)
+- subject_or_group_defined = defined?(@project) || defined?(@group)
+- show_label_issues_link = subject_or_group_defined && show_label_issuables_link?(label, :issues, project: @project)
+- show_label_merge_requests_link = subject_or_group_defined && show_label_issuables_link?(label, :merge_requests, project: @project)
.label-name
- - if defined?(@project)
- = link_to_label(label, subject: @project, tooltip: false)
- - else
- = render_label(label, tooltip: false)
+ = render_label(label, tooltip: false)
.label-description
.append-right-default.prepend-left-default
- if label.description.present?
@@ -16,11 +13,12 @@
%ul.label-links
- if show_label_issues_link
%li.label-link-item.inline
- = link_to_label(label, subject: subject) { 'Issues' }
+ = link_to_label(label) { 'Issues' }
- if show_label_merge_requests_link
&middot;
%li.label-link-item.inline
- = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') }
+ = link_to_label(label, type: :merge_request) { _('Merge requests') }
- if force_priority
+ &middot;
%li.label-link-item.priority-badge.js-priority-badge.inline.prepend-left-10
.label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index 8607f87ce0b..b46479d9f1a 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -4,7 +4,7 @@
- detailed_status = stage.detailed_status(current_user)
- icon_status = "#{detailed_status.icon}_borderless"
- .stage-container.dropdown{ class: klass }
+ .stage-container.mt-0.ml-1.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } }
= sprite_icon(icon_status)
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index 721a2af8069..8da2ae5111a 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -1,6 +1,6 @@
- if remote_mirror.update_in_progress?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') }
= icon("refresh spin")
-- else
+- elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index 2530db986e0..d90a6d43761 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,8 +1,8 @@
%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('angle-double-left', css_class: 'icon-angle-double-left')
= sprite_icon('angle-double-right', css_class: 'icon-angle-double-right')
- %span.collapse-text Collapse sidebar
+ %span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
= sprite_icon('close', size: 16)
- %span.collapse-text Close sidebar
+ %span.collapse-text= _("Close sidebar")
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index f0d1dd162df..813fccd217b 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -13,14 +13,14 @@
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal, show_sorting_dropdown: false
%script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
-#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
+#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
.d-none.d-sm-none.d-md-block
= render 'shared/issuable/search_bar', type: :boards, board: board
- .boards-list
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- %board{ "v-cloak" => true,
+ .boards-list.w-100.py-3.px-2.text-nowrap
+ .boards-app-loading.w-100.text-center{ "v-if" => "loading" }
+ = icon("spinner spin 2x")
+ %board{ "v-cloak" => "true",
"v-for" => "list in state.lists",
"ref" => "board",
":list" => "list",
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 307a0919a4c..f9cfcabc015 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,8 +1,8 @@
-.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
+.board.d-inline-block.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
- .board-inner.d-flex.flex-column
- %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
- %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
+ .board-inner.d-flex.flex-column.position-relative.h-100.rounded
+ %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color, "position-relative": list.isExpanded, "position-absolute position-top-0 position-left-0 w-100 h-100": !list.isExpanded }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
+ %h3.board-title.m-0.d-flex.align-items-center.py-2.px-3.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset), "p-0 border-bottom-0 justify-content-center": !list.isExpanded }' }
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
@@ -31,9 +31,9 @@
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.float-right{ type: "button", title: _("Delete list"), "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ %button.board-delete.p-0.border-0.has-tooltip.float-right{ type: "button", title: _("Delete list"), ":class": "{ 'd-none': !list.isExpanded }", "aria-label" => _("Delete list"), data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
- .issue-count-badge.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", "v-tooltip": true, data: { placement: "top" } }
+ .issue-count-badge.text-secondary{ "v-if" => 'list.type !== "blank" && list.type !== "promotion"', ":title": "counterTooltip", ":class": "{ 'd-none': !list.isExpanded }", "v-tooltip": true, data: { placement: "top" } }
%span.issue-count-badge-count
%icon.mr-1{ name: "issues" }
{{ list.issuesSize }}
@@ -42,6 +42,7 @@
%button.issue-count-badge-add-button.btn.btn-sm.btn-default.ml-1.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => "isNewIssueShown",
+ ":class": "{ 'd-none': !list.isExpanded }",
"aria-label" => _("New issue"),
"title" => _("New issue"),
data: { placement: "top", container: "body" } }
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index 30e51ecc261..b4f75967a67 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -2,16 +2,16 @@
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
- .block.issuable-sidebar-header
+ .block.issuable-sidebar-header.position-relative
%span.issuable-header-text.hide-collapsed.float-left
- %strong
+ %strong.bold
{{ issue.title }}
%br/
%span
= render_if_exists "shared/boards/components/sidebar/issue_project_path"
= precede "#" do
{{ issue.iid }}
- %a.gutter-toggle.float-right{ role: "button",
+ %a.gutter-toggle.position-absolute.position-top-0.position-right-0{ role: "button",
href: "#",
"@click.prevent" => "closeSidebar",
"aria-label" => "Toggle sidebar" }
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index 5630375f428..117d56b30f5 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -7,7 +7,7 @@
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
- = _("No due date")
+ = _("None")
%span.bold{ "v-if" => "issue.dueDate" }
{{ issue.dueDate | due-date }}
- if can_admin_issue?
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 1ae6d1f5ee3..f4915440cb2 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -24,10 +24,10 @@
%li.divider
%li.js-filter-archived-projects
= link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
- Hide archived projects
+ = _("Hide archived projects")
%li.js-filter-archived-projects
= link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
- Show archived projects
+ = _("Show archived projects")
%li.js-filter-archived-projects
= link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
- Show archived projects only
+ = _("Show archived projects only")
diff --git a/app/views/shared/icons/_gitea_logo.svg.erb b/app/views/shared/icons/_gitea_logo.svg.erb
new file mode 100644
index 00000000000..c8ddbc5535e
--- /dev/null
+++ b/app/views/shared/icons/_gitea_logo.svg.erb
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?><!-- Created with Inkscape (http://www.inkscape.org/) --><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="<%= size %>" height="<%= size %>" viewBox="0 0 135.46667 135.46667" version="1.1" id="svg8" sodipodi:docname="logo.svg" inkscape:version="0.92.1 r15371" inkscape:export-filename="" inkscape:export-xdpi="48.000004" inkscape:export-ydpi="48.000004" style="zoom: 1;"><defs id="defs2"></defs><sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:zoom="0.70710678" inkscape:cx="418.13805" inkscape:cy="177.57445" inkscape:document-units="mm" inkscape:current-layer="layer2" showgrid="false" units="px" width="256px" showguides="false" inkscape:window-width="1920" inkscape:window-height="1137" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:pagecheckerboard="false" inkscape:measure-start="283.373,243.952" inkscape:measure-end="290.267,236.527"><sodipodi:guide position="0,0" orientation="0,512" id="guide3699" inkscape:locked="false"></sodipodi:guide><sodipodi:guide position="135.46667,0" orientation="-512,0" id="guide3701" inkscape:locked="false"></sodipodi:guide><sodipodi:guide position="135.46667,135.46667" orientation="0,-512" id="guide3703" inkscape:locked="false"></sodipodi:guide><sodipodi:guide position="0,135.46667" orientation="512,0" id="guide3705" inkscape:locked="false"></sodipodi:guide></sodipodi:namedview><metadata id="metadata5"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"></dc:type><dc:title></dc:title></cc:Work></rdf:RDF></metadata><g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(0,-161.53334)" style="display:inline"><path d="M27.709937,195.15095 c-9.546573,-0.0272 -22.3392732,6.79805 -21.6317552,23.90397 c1.105534,26.72889 25.4565952,29.20839 35.1916502,29.42301 c1.068023,5.01357 12.521798,22.30563 21.001818,23.21667 h37.15277 c22.27763,-1.66785 38.9607,-75.75671 26.59321,-76.03825 c-46.781583,2.47691 -49.995146,2.13838 -88.599758,0 c-2.495053,-0.0266 -5.972321,-0.49474 -9.707935,-0.5054 z m2.491319,9.45886 c1.351378,13.69267 3.555849,21.70359 8.018216,33.94345 c-11.382872,-1.50473 -21.069822,-5.22443 -22.851515,-19.10984 c-0.950962,-7.4112 2.390428,-15.16769 14.833299,-14.83361 z " id="path3722" sodipodi:nodetypes="sscccccsccsc" inkscape:connector-curvature="0" style="fill:#000;fill-opacity:1;stroke:#000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"></path></g><g inkscape:groupmode="layer" id="layer2" inkscape:label="Layer 2" style="display:inline"><rect style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.24757317;stroke-opacity:1" id="rect4599" width="34.762054" height="34.762054" x="87.508659" y="18.291576" transform="rotate(25.914715)" ry="5.4825778"></rect><path style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26644793px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 79.804947,57.359056 3.241146,1.609954 V 35.255731 h -3.262698 z" id="path4525" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccc"></path></g><g inkscape:groupmode="layer" id="layer3" inkscape:label="Layer 3" style="display:inline"><g style="display:inline" id="g4539"><circle style="fill:#000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1" id="path4606" cy="90.077766" r="3.4745038" cx="49.064713" transform="rotate(-19.796137)"></circle><circle style="fill:#000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1" id="path4606-3" cy="102.1049" r="3.4745038" cx="36.810425" transform="rotate(-19.796137)"></circle><circle style="fill:#000;fill-opacity:1;stroke:none;stroke-width:0.26458332;stroke-opacity:1" id="path4606-1" cy="111.43928" r="3.4745038" cx="46.484283" transform="rotate(-19.796137)"></circle><rect height="27.261492" style="fill:#000;fill-opacity:1;stroke:none;stroke-width:0.27444693;stroke-opacity:1" x="97.333458" y="18.061695" id="rect4629-8" width="2.6726954" transform="rotate(26.024158)"></rect><path d="M76.558096,68.116343 c12.97589,6.395378 13.012989,4.101862 4.890858,20.907244 " id="path4514" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" style="fill:none;stroke:#000;stroke-width:2.68000007;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"></path></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index aa4a5f0e0d3..a0fb5229fc3 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -11,7 +11,7 @@
= dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
- %p
+ %p.m-0
= content_title
= dropdown_filter(filter_placeholder)
= dropdown_content
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 2bcfcb6fa7c..3d6c5d29d44 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -3,11 +3,11 @@
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
-.issues-filters
- .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
+.issues-filters{ class: ("w-100" if type == :boards_modal) }
+ .issues-details-filters.filtered-search-block.d-flex{ class: block_css_class, "v-pre" => type == :boards_modal }
- if type == :boards
= render_if_exists "shared/boards/switcher", board: board
- = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form' do
+ = 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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0798b1da4b7..2c185549b24 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -73,7 +73,7 @@
%span.bold= issuable_sidebar[:due_date].to_s(:medium)
- else
%span.no-value
- = _('No due date')
+ = _('None')
- if can_edit_issuable
%span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) }
\-
@@ -106,7 +106,7 @@
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
- = render_label(label_from_hash(label_hash), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
+ = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title]))
- else
%span.no-value
= _('None')
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 56c4b021eab..75e9ab547ce 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -6,7 +6,7 @@
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title')
+ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title'), dir: 'auto'
- if issuable.respond_to?(:work_in_progress?)
.form-text.text-muted
diff --git a/app/views/shared/members/_access_request_links.html.haml b/app/views/shared/members/_access_request_links.html.haml
index f7227b9101e..eac743b5206 100644
--- a/app/views/shared/members/_access_request_links.html.haml
+++ b/app/views/shared/members/_access_request_links.html.haml
@@ -5,7 +5,7 @@
= link_to link_text, polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
- class: 'access-request-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]),
method: :delete,
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2db1f67a793..2e5747121b6 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -53,7 +53,7 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
- .controls.member-controls
+ .controls.member-controls.row
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 5863f52aa78..ae3ab2adfd0 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -21,7 +21,7 @@
%span.issuable-number= issuable.to_reference
- labels.each do |label|
- = render_label(label, link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }))
+ = render_label(label.present(issuable_subject: project), link: polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }))
%span.assignee-icon
- assignees.each do |assignee|
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 6b0640bd8cb..ecab037e378 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -2,7 +2,7 @@
- labels.each do |label|
- options = { milestone_title: @milestone.title, label_name: label.title }
- %li.is-not-draggable
+ %li.no-border
%span.label-row
%span.label-name
= render_label(label, tooltip: false, link: milestones_label_path(options))
diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
index 6d26dbebbc8..af8ab992f0e 100644
--- a/app/views/shared/notifications/_new_button.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -10,14 +10,14 @@
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
- = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ = notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" }
= sprite_icon("arrow-down", css_class: "icon mr-0")
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
- = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon")
+ = notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("arrow-down", css_class: "icon")
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 98b258d9275..88ac03bf9e3 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,10 +1,9 @@
- @sort ||= sort_value_latest_activity
.dropdown.js-project-filter-dropdown-wrap
- - toggle_text = projects_sort_options_hash[@sort]
- = dropdown_toggle(toggle_text, { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' })
+ = dropdown_toggle(projects_sort_options_hash[@sort], { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
- Sort by
+ = _("Sort by")
- projects_sort_options_hash.each do |value, title|
%li
= link_to filter_projects_path(sort: value), class: ("is-active" if @sort == value) do
@@ -13,29 +12,29 @@
%li.divider
%li
= link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
- Hide archived projects
+ = _("Hide archived projects")
%li
= link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
- Show archived projects
+ = _("Show archived projects")
%li
= link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
- Show archived projects only
+ = _("Show archived projects only")
- if current_user
%li.divider
%li
= link_to filter_projects_path(personal: nil), class: ("is-active" unless params[:personal].present?) do
- Owned by anyone
+ = _("Owned by anyone")
%li
= link_to filter_projects_path(personal: true), class: ("is-active" if params[:personal].present?) do
- Owned by me
+ = _("Owned by me")
- if @group && @group.shared_projects.present?
%li.divider
%li
= link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
- All projects
+ = _("All projects")
%li
= link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
- Hide shared projects
+ = _("Hide shared projects")
%li
= link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
- Hide group projects
+ = _("Hide group projects")
diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml
new file mode 100644
index 00000000000..c1f2eaba284
--- /dev/null
+++ b/app/views/shared/projects/_search_bar.html.haml
@@ -0,0 +1,28 @@
+- @sort ||= sort_value_latest_activity
+- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
+- flex_grow_and_shrink_xs = 'd-flex flex-xs-grow-1 flex-xs-shrink-1 flex-grow-0 flex-shrink-0'
+
+.filtered-search-block.row-content-block.bt-0
+ .filtered-search-wrapper.d-flex.flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
+ - unless project_tab_filter == :starred
+ .filtered-search-nav.mb-2.mb-lg-0{ class: flex_grow_and_shrink_xs }
+ = render 'dashboard/projects/nav', project_tab_filter: project_tab_filter
+ .filtered-search.d-flex.flex-grow-1.flex-shrink-1.w-100.mb-2.mb-lg-0.ml-0{ class: project_tab_filter == :starred ? "extended-filtered-search-box mb-2 mb-lg-0" : "ml-sm-3" }
+ .btn-group.w-100{ role: "group" }
+ .btn-group.w-100{ role: "group" }
+ .filtered-search-box.m-0
+ .filtered-search-box-input-container.pl-2
+ = render 'shared/projects/search_form', admin_view: false, search_form_placeholder: _("Search projects...")
+ %button.btn.btn-secondary{ type: 'submit', form: 'project-filter-form' }
+ = sprite_icon('search', size: 16, css_class: 'search-icon ')
+ .filtered-search-dropdown.flex-row.align-items-center.mb-2.m-sm-0#filtered-search-visibility-dropdown{ class: flex_grow_and_shrink_xs }
+ .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
+ %span
+ = _("Visibility")
+ = render 'explore/projects/filter', has_label: true
+ .filtered-search-dropdown.flex-row.align-items-center.m-sm-0#filtered-search-sorting-dropdown{ class: flex_grow_and_shrink_xs }
+ .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
+ %span
+ = _("Sort by")
+ = render 'shared/projects/sort_dropdown'
+
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index 3b5c13ed93a..7c7c0a363ac 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -1,7 +1,10 @@
+- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : ''
+- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...'
+
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
- placeholder: 'Filter by name...',
- class: 'project-filter-form-field form-control input-short js-projects-list-filter',
+ placeholder: placeholder,
+ class: "project-filter-form-field form-control #{form_field_classes}",
spellcheck: false,
id: 'project-filter-form-field',
tabindex: "2",
diff --git a/app/views/shared/projects/_sort_dropdown.html.haml b/app/views/shared/projects/_sort_dropdown.html.haml
new file mode 100644
index 00000000000..f5f940db189
--- /dev/null
+++ b/app/views/shared/projects/_sort_dropdown.html.haml
@@ -0,0 +1,39 @@
+- @sort ||= sort_value_latest_activity
+- toggle_text = projects_sort_option_titles[@sort]
+
+.btn-group.w-100{ role: "group" }
+ .btn-group.w-100.dropdown.js-project-filter-dropdown-wrap{ role: "group" }
+ %button#sort-projects-dropdown.btn.btn-default.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
+ = toggle_text
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
+ %li.dropdown-header
+ = _("Sort by")
+ - projects_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, filter_projects_path(sort: value), class: ("is-active" if toggle_text == title)
+
+ %li.divider
+ %li
+ = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
+ = _("Hide archived projects")
+ %li
+ = link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
+ = _("Show archived projects")
+ %li
+ = link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
+ = _("Show archived projects only")
+
+ - if current_user && @group && @group.shared_projects.present?
+ %li.divider
+ %li
+ = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
+ = _("All projects")
+ %li
+ = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
+ = _("Hide shared projects")
+ %li
+ = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
+ = _("Hide group projects")
+
+ = project_sort_direction_button(@sort)
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index f9b2e698fc9..e4e85de93da 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -6,6 +6,7 @@
- cronjob:gitlab_usage_ping
- cronjob:import_export_project_cleanup
- cronjob:pages_domain_verification_cron
+- cronjob:pages_domain_removal_cron
- cronjob:pipeline_schedule
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
@@ -32,6 +33,8 @@
- gcp_cluster:cluster_wait_for_ingress_ip_address
- gcp_cluster:cluster_configure
- gcp_cluster:cluster_project_configure
+- gcp_cluster:clusters_applications_wait_for_uninstall_app
+- gcp_cluster:clusters_applications_uninstall
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -83,6 +86,7 @@
- pipeline_processing:ci_build_schedule
- deployment:deployments_success
+- deployment:deployments_finished
- repository_check:repository_check_clear
- repository_check:repository_check_batch
diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb
index b984dee5b21..37ea7dde7a1 100644
--- a/app/workers/cluster_configure_worker.rb
+++ b/app/workers/cluster_configure_worker.rb
@@ -5,10 +5,10 @@ class ClusterConfigureWorker
include ClusterQueue
def perform(cluster_id)
- return if Feature.enabled?(:ci_preparing_state, default_enabled: true)
-
- Clusters::Cluster.find_by_id(cluster_id).try do |cluster|
- Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
+ Clusters::Cluster.managed.find_by_id(cluster_id).try do |cluster|
+ if cluster.project_type? || Feature.disabled?(:ci_preparing_state, default_enabled: true)
+ Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
+ end
end
end
end
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
index d7bea69a01c..497e57c0d0b 100644
--- a/app/workers/cluster_project_configure_worker.rb
+++ b/app/workers/cluster_project_configure_worker.rb
@@ -5,8 +5,6 @@ class ClusterProjectConfigureWorker
include ClusterQueue
def perform(project_id)
- return if Feature.enabled?(:ci_preparing_state, default_enabled: true)
-
project = Project.find(project_id)
::Clusters::RefreshService.create_or_update_namespaces_for_project(project)
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
new file mode 100644
index 00000000000..85e8ecc4ad5
--- /dev/null
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class UninstallWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::UninstallService.new(app).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
new file mode 100644
index 00000000000..163c99d3c3c
--- /dev/null
+++ b/app/workers/clusters/applications/wait_for_uninstall_app_worker.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class WaitForUninstallAppWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ INTERVAL = 10.seconds
+ TIMEOUT = 20.minutes
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckUninstallProgressService.new(app).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/deployments/finished_worker.rb b/app/workers/deployments/finished_worker.rb
new file mode 100644
index 00000000000..c9d448d5d18
--- /dev/null
+++ b/app/workers/deployments/finished_worker.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Deployments
+ class FinishedWorker
+ include ApplicationWorker
+
+ queue_namespace :deployment
+
+ def perform(deployment_id)
+ Deployment.find_by_id(deployment_id).try(:execute_hooks)
+ end
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index b33e9b1f718..d4a6f53dae5 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -29,7 +29,7 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
- project.repository.expire_statistics_caches
+ project.repository.expire_statistics_caches if task != :pack_refs
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
@@ -58,7 +58,12 @@ class GitGarbageCollectWorker
## `repository` has to be a Gitlab::Git::Repository
def gitaly_call(task, repository)
- client = Gitlab::GitalyClient::RepositoryService.new(repository)
+ client = if task == :pack_refs
+ Gitlab::GitalyClient::RefService.new(repository)
+ else
+ Gitlab::GitalyClient::RepositoryService.new(repository)
+ end
+
case task
when :gc
client.garbage_collect(bitmaps_enabled?)
@@ -66,6 +71,8 @@ class GitGarbageCollectWorker
client.repack_full(bitmaps_enabled?)
when :incremental_repack
client.repack_incremental
+ when :pack_refs
+ client.pack_refs
end
rescue GRPC::NotFound => e
Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found")
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
new file mode 100644
index 00000000000..3aca123e5ac
--- /dev/null
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class PagesDomainRemovalCronWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ def perform
+ return unless Feature.enabled?(:remove_disabled_domains)
+
+ PagesDomain.for_removal.find_each do |domain|
+ domain.destroy!
+ rescue => e
+ Raven.capture_exception(e)
+ end
+ end
+end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 02a69ea3b54..8a9ee7808e4 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -3,20 +3,26 @@
class PipelineScheduleWorker
include ApplicationWorker
include CronjobQueue
+ include ::Gitlab::ExclusiveLeaseHelpers
+
+ EXCLUSIVE_LOCK_KEY = 'pipeline_schedules:run:lock'
+ LOCK_TIMEOUT = 50.minutes
# rubocop: disable CodeReuse/ActiveRecord
def perform
- Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
- .preload(:owner, :project).find_each do |schedule|
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
- rescue => e
- error(schedule, e)
- ensure
- schedule.schedule_next_run!
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
+ .preload(:owner, :project).find_each do |schedule|
+
+ schedule.schedule_next_run!
+
+ Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
+ rescue => e
+ error(schedule, e)
+ end
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 337efa7919b..9a9c0c9d803 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -21,8 +21,10 @@ class PostReceive
if repo_type.wiki?
process_wiki_changes(post_received)
- else
+ elsif repo_type.project?
process_project_changes(post_received)
+ else
+ # Other repos don't have hooks for now
end
end
diff --git a/babel.config.js b/babel.config.js
index 78d14095b0b..df30892731d 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -38,8 +38,9 @@ if (BABEL_ENV === 'karma' || BABEL_ENV === 'coverage') {
plugins.push('babel-plugin-rewire');
}
-// Jest is running in node environment
-if (BABEL_ENV === 'jest') {
+// Jest is running in node environment, so we need additional plugins
+const isJest = !!process.env.JEST_WORKER_ID;
+if (isJest) {
plugins.push('@babel/plugin-transform-modules-commonjs');
/*
without the following, babel-plugin-istanbul throws an error:
diff --git a/changelogs/unreleased/10012-move-ee-diff-for-boards-issue-card-inner.yml b/changelogs/unreleased/10012-move-ee-diff-for-boards-issue-card-inner.yml
new file mode 100644
index 00000000000..f15375e83f4
--- /dev/null
+++ b/changelogs/unreleased/10012-move-ee-diff-for-boards-issue-card-inner.yml
@@ -0,0 +1,5 @@
+---
+title: Move ee-specific code from boards/components/issue_card_inner.vue
+merge_request: 27394
+author: Roman Rodionov
+type: other
diff --git a/changelogs/unreleased/10029-env-item.yml b/changelogs/unreleased/10029-env-item.yml
deleted file mode 100644
index f4e742d3e17..00000000000
--- a/changelogs/unreleased/10029-env-item.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes EE differences for environment_item.vue
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/10081-env-table.yml b/changelogs/unreleased/10081-env-table.yml
deleted file mode 100644
index b27a1be8cca..00000000000
--- a/changelogs/unreleased/10081-env-table.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes EE differences for environments_table.vue
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/10083-dropdown-ce-ee-difference.yml b/changelogs/unreleased/10083-dropdown-ce-ee-difference.yml
new file mode 100644
index 00000000000..e8594edb76a
--- /dev/null
+++ b/changelogs/unreleased/10083-dropdown-ce-ee-difference.yml
@@ -0,0 +1,5 @@
+---
+title: Moved EE/CE differences for dropdown_value_collapsed into CE
+merge_request: 27367
+author:
+type: other
diff --git a/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
new file mode 100644
index 00000000000..89d2fced6e1
--- /dev/null
+++ b/changelogs/unreleased/10327-enable-reliable-fetcher-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Sidekiq Reliable Fetcher for background jobs by default
+merge_request: 27530
+author:
+type: added
diff --git a/changelogs/unreleased/10735-geo-gitlab-revision-can-return-not-consistent-results.yml b/changelogs/unreleased/10735-geo-gitlab-revision-can-return-not-consistent-results.yml
deleted file mode 100644
index 2360295b022..00000000000
--- a/changelogs/unreleased/10735-geo-gitlab-revision-can-return-not-consistent-results.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use a fixed git abbrev parameter when we fetch a git revision
-merge_request: 26707
-author:
-type: fixed
diff --git a/changelogs/unreleased/11124-update-deployment-service-fails-if-project-is-nil.yml b/changelogs/unreleased/11124-update-deployment-service-fails-if-project-is-nil.yml
deleted file mode 100644
index 355743f4cb0..00000000000
--- a/changelogs/unreleased/11124-update-deployment-service-fails-if-project-is-nil.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Always return the deployment in the UpdateDeploymentService#execute method
-merge_request: 27322
-author:
-type: fixed
diff --git a/changelogs/unreleased/11254-overflow-ce.yml b/changelogs/unreleased/11254-overflow-ce.yml
new file mode 100644
index 00000000000..dcac46000ac
--- /dev/null
+++ b/changelogs/unreleased/11254-overflow-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Hide ScopedBadge overflow notes
+merge_request: 27651
+author:
+type: fixed
diff --git a/changelogs/unreleased/13784-validate-variables-for-masking.yml b/changelogs/unreleased/13784-validate-variables-for-masking.yml
deleted file mode 100644
index e8e97fac3d2..00000000000
--- a/changelogs/unreleased/13784-validate-variables-for-masking.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add control for masking variable values in runner logs
-merge_request: 26751
-author:
-type: added
diff --git a/changelogs/unreleased/18432-switch-to-sassc-rails.yml b/changelogs/unreleased/18432-switch-to-sassc-rails.yml
new file mode 100644
index 00000000000..1c9d515c52f
--- /dev/null
+++ b/changelogs/unreleased/18432-switch-to-sassc-rails.yml
@@ -0,0 +1,5 @@
+---
+title: Switch to sassc-rails for faster stylesheet compilation
+merge_request: 26224
+author:
+type: changed
diff --git a/changelogs/unreleased/24704-download-repository-path.yml b/changelogs/unreleased/24704-download-repository-path.yml
new file mode 100644
index 00000000000..ff3082bec45
--- /dev/null
+++ b/changelogs/unreleased/24704-download-repository-path.yml
@@ -0,0 +1,5 @@
+---
+title: Download a folder from repository
+merge_request: 26532
+author: kiameisomabes
+type: added
diff --git a/changelogs/unreleased/24936-remove-type-from-review-app-name.yml b/changelogs/unreleased/24936-remove-type-from-review-app-name.yml
deleted file mode 100644
index 639333264f6..00000000000
--- a/changelogs/unreleased/24936-remove-type-from-review-app-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove unnecessary folder prefix from environment name
-merge_request: 25600
-author:
-type: changed
diff --git a/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml b/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml
deleted file mode 100644
index 04dbc3a1d5a..00000000000
--- a/changelogs/unreleased/24971-align-emailvalidator-to-validate_email-gem-implementation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Align EmailValidator to validate_email gem implementation
-merge_request: 24971
-author: Horatiu Eugen Vlad
-type: fixed
diff --git a/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml b/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml
new file mode 100644
index 00000000000..ef9172aaf3b
--- /dev/null
+++ b/changelogs/unreleased/25604-add-dotnet-core-yaml-template.yml
@@ -0,0 +1,5 @@
+---
+title: Add .NET Core YAML template
+merge_request: 25604
+author: Piotr Wosiek
+type: added
diff --git a/changelogs/unreleased/25942-remove-fake-repository-path-response.yml b/changelogs/unreleased/25942-remove-fake-repository-path-response.yml
deleted file mode 100644
index e1da28ab03c..00000000000
--- a/changelogs/unreleased/25942-remove-fake-repository-path-response.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove fake repository_path response
-merge_request: 25942
-author: Fabio Papa
-type: other
diff --git a/changelogs/unreleased/27383-added_omniauth_openid_connect_startegy.yml b/changelogs/unreleased/27383-added_omniauth_openid_connect_startegy.yml
new file mode 100644
index 00000000000..c49b201f0de
--- /dev/null
+++ b/changelogs/unreleased/27383-added_omniauth_openid_connect_startegy.yml
@@ -0,0 +1,5 @@
+---
+title: Added OmniAuth OpenID Connect strategy
+merge_request: 27383
+author: Horatiu Eugen Vlad
+type: added
diff --git a/changelogs/unreleased/27424-tklk-gitea-logo.yml b/changelogs/unreleased/27424-tklk-gitea-logo.yml
new file mode 100644
index 00000000000..0d41bb39aad
--- /dev/null
+++ b/changelogs/unreleased/27424-tklk-gitea-logo.yml
@@ -0,0 +1,5 @@
+---
+title: "Use official Gitea logo in importer"
+merge_request: 27424
+author: Matti Ranta (@techknowlogick)
+type: added \ No newline at end of file
diff --git a/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml
new file mode 100644
index 00000000000..2fbacbcb011
--- /dev/null
+++ b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the note in the docs that multi-line suggestions are not yet available
+merge_request: 28119
+author: hardysim
+type: other
diff --git a/changelogs/unreleased/28741-play-all-manual-jobs.yml b/changelogs/unreleased/28741-play-all-manual-jobs.yml
new file mode 100644
index 00000000000..30b26e3c0ed
--- /dev/null
+++ b/changelogs/unreleased/28741-play-all-manual-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Play all manual jobs in a stage
+merge_request: 27188
+author:
+type: added
diff --git a/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml b/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml
deleted file mode 100644
index 5942860a20f..00000000000
--- a/changelogs/unreleased/29249-show-download-diff-even-when-merge-request-is-closed.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show download diff links for closed MRs
-merge_request: 26772
-author:
-type: changed
diff --git a/changelogs/unreleased/30093-apply-bfg-object-map-to-database.yml b/changelogs/unreleased/30093-apply-bfg-object-map-to-database.yml
new file mode 100644
index 00000000000..ec851dfcacc
--- /dev/null
+++ b/changelogs/unreleased/30093-apply-bfg-object-map-to-database.yml
@@ -0,0 +1,5 @@
+---
+title: Remove cleaned up OIDs from database and cache
+merge_request: 26555
+author:
+type: added
diff --git a/changelogs/unreleased/38564-cant-leave-subgroup.yml b/changelogs/unreleased/38564-cant-leave-subgroup.yml
deleted file mode 100644
index a6397062343..00000000000
--- a/changelogs/unreleased/38564-cant-leave-subgroup.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow removing last owner from subgroup if parent group has owners
-merge_request: 26718
-author:
-type: changed
diff --git a/changelogs/unreleased/43263-git-push-option-to-create-mr.yml b/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
deleted file mode 100644
index d50c33da162..00000000000
--- a/changelogs/unreleased/43263-git-push-option-to-create-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow merge requests to be created via git push options
-merge_request: 26752
-author:
-type: added
diff --git a/changelogs/unreleased/43297-authorized-application-count.yml b/changelogs/unreleased/43297-authorized-application-count.yml
deleted file mode 100644
index d22e155fb14..00000000000
--- a/changelogs/unreleased/43297-authorized-application-count.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix authorized application count
-merge_request: 25715
-author: moyuru
-type: fixed
diff --git a/changelogs/unreleased/46048-canary-next.yml b/changelogs/unreleased/46048-canary-next.yml
new file mode 100644
index 00000000000..1a702cccff9
--- /dev/null
+++ b/changelogs/unreleased/46048-canary-next.yml
@@ -0,0 +1,5 @@
+---
+title: Adds badge for Canary environment and help link
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/46787-create-project-label-window-is-cut-off-at-the-bottom.yml b/changelogs/unreleased/46787-create-project-label-window-is-cut-off-at-the-bottom.yml
deleted file mode 100644
index dca1d57d14e..00000000000
--- a/changelogs/unreleased/46787-create-project-label-window-is-cut-off-at-the-bottom.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed - Create project label window is cut off at the bottom
-merge_request: 26049
-author:
-type: fixed
diff --git a/changelogs/unreleased/46806-typed-ci-variables.yml b/changelogs/unreleased/46806-typed-ci-variables.yml
new file mode 100644
index 00000000000..aa15c31bca1
--- /dev/null
+++ b/changelogs/unreleased/46806-typed-ci-variables.yml
@@ -0,0 +1,5 @@
+---
+title: CI variables of type file
+merge_request: 27112
+author:
+type: added
diff --git a/changelogs/unreleased/47234-composable-auto-devops.yml b/changelogs/unreleased/47234-composable-auto-devops.yml
deleted file mode 100644
index 9403c5ae6d3..00000000000
--- a/changelogs/unreleased/47234-composable-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Split Auto-DevOps.gitlab-ci.yml into reusable templates
-merge_request: 26520
-author:
-type: changed
diff --git a/changelogs/unreleased/47584-label-text-color.yml b/changelogs/unreleased/47584-label-text-color.yml
new file mode 100644
index 00000000000..7d5eaa62793
--- /dev/null
+++ b/changelogs/unreleased/47584-label-text-color.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve issue where list labels did not have the correct text color on creation
+merge_request: 26794
+author: Tucker Chapman
+type: fixed
diff --git a/changelogs/unreleased/47771-highlighting-in-diff.yml b/changelogs/unreleased/47771-highlighting-in-diff.yml
deleted file mode 100644
index a8e8cbf0174..00000000000
--- a/changelogs/unreleased/47771-highlighting-in-diff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enabled text selection highlighting in diffs in Web IDE
-merge_request: 26721
-author: Isaac Smith
-type: fixed
diff --git a/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml b/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml
deleted file mode 100644
index e588fa79619..00000000000
--- a/changelogs/unreleased/48090-filter-sensitive-metric-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove `path` and `branch` labels from metrics
-merge_request: 26744
-author:
-type: fixed
diff --git a/changelogs/unreleased/48132-display-output-from-pre-receive-scripts.yml b/changelogs/unreleased/48132-display-output-from-pre-receive-scripts.yml
deleted file mode 100644
index e06a4d5ee75..00000000000
--- a/changelogs/unreleased/48132-display-output-from-pre-receive-scripts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Allow failed custom hook script errors to safely appear in GitLab UI by filtering error messages by the prefix GL-HOOK-ERR:"
-merge_request: 25625
-author:
-type: changed
diff --git a/changelogs/unreleased/48297-fix-code-selection.yml b/changelogs/unreleased/48297-fix-code-selection.yml
deleted file mode 100644
index 14841b00969..00000000000
--- a/changelogs/unreleased/48297-fix-code-selection.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Resolve Code in other column of side-by-side diff is highlighted when selecting
- code on one side
-merge_request: 26423
-author:
-type: fixed
diff --git a/changelogs/unreleased/48479-auto-direction-for-issue-title.yml b/changelogs/unreleased/48479-auto-direction-for-issue-title.yml
new file mode 100644
index 00000000000..0571f58ab4d
--- /dev/null
+++ b/changelogs/unreleased/48479-auto-direction-for-issue-title.yml
@@ -0,0 +1,5 @@
+---
+title: Add auto direction for issue title
+merge_request: 27378
+author: Ahmad Haghighi
+type: fixed
diff --git a/changelogs/unreleased/49856-upgrade-bootstrap-form-gem.yml b/changelogs/unreleased/49856-upgrade-bootstrap-form-gem.yml
deleted file mode 100644
index 1dc8d5b4179..00000000000
--- a/changelogs/unreleased/49856-upgrade-bootstrap-form-gem.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade bootstrap_form Gem
-merge_request: 26568
-author:
-type: other
diff --git a/changelogs/unreleased/49863-ingress-ip-loading-state.yml b/changelogs/unreleased/49863-ingress-ip-loading-state.yml
deleted file mode 100644
index 51bb27d3153..00000000000
--- a/changelogs/unreleased/49863-ingress-ip-loading-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show loading spinner while Ingress/Knative IP is being assigned
-merge_request: 25912
-author:
-type: changed
diff --git a/changelogs/unreleased/49910-reopening-a-closed-milestone-from-the-closed-milestones-page-fails2.yml b/changelogs/unreleased/49910-reopening-a-closed-milestone-from-the-closed-milestones-page-fails2.yml
deleted file mode 100644
index 68d38cd56c5..00000000000
--- a/changelogs/unreleased/49910-reopening-a-closed-milestone-from-the-closed-milestones-page-fails2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug when reopening milestone from index page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/50926-sort-by-due-date-and-popularity.yml b/changelogs/unreleased/50926-sort-by-due-date-and-popularity.yml
new file mode 100644
index 00000000000..7efc800be1e
--- /dev/null
+++ b/changelogs/unreleased/50926-sort-by-due-date-and-popularity.yml
@@ -0,0 +1,5 @@
+---
+title: Sort by due date and popularity in both directions for Issues and Merge requests
+merge_request: 25502
+author: Nermin Vehabovic
+type: changed
diff --git a/changelogs/unreleased/51963-support-prometheus-for-group-level-clusters.yml b/changelogs/unreleased/51963-support-prometheus-for-group-level-clusters.yml
new file mode 100644
index 00000000000..ede2e242156
--- /dev/null
+++ b/changelogs/unreleased/51963-support-prometheus-for-group-level-clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Support prometheus for group level clusters
+merge_request: 27280
+author:
+type: changed
diff --git a/changelogs/unreleased/51988-install-group-runner-on-group-cluster.yml b/changelogs/unreleased/51988-install-group-runner-on-group-cluster.yml
deleted file mode 100644
index 86f08dd1798..00000000000
--- a/changelogs/unreleased/51988-install-group-runner-on-group-cluster.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support installing Group runner on group-level cluster
-merge_request: 26260
-author:
-type: added
diff --git a/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml b/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml
deleted file mode 100644
index 5dd25d0ffc1..00000000000
--- a/changelogs/unreleased/52258-labels-with-long-names-overflow-on-metrics-dashboard.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix long label overflow on metrics dashboard
-merge_request: 26775
-author:
-type: fixed
diff --git a/changelogs/unreleased/52366-improved-group-lists-ui-spinners.yml b/changelogs/unreleased/52366-improved-group-lists-ui-spinners.yml
deleted file mode 100644
index ab09272eaf4..00000000000
--- a/changelogs/unreleased/52366-improved-group-lists-ui-spinners.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update spinners in group list component
-merge_request: 26572
-author:
-type: changed
diff --git a/changelogs/unreleased/52447-auto-devops-at-group-level.yml b/changelogs/unreleased/52447-auto-devops-at-group-level.yml
deleted file mode 100644
index 0a21c6a2b7b..00000000000
--- a/changelogs/unreleased/52447-auto-devops-at-group-level.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable/disable Auto DevOps at the Group level
-merge_request: 25533
-author:
-type: added
diff --git a/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml b/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml
deleted file mode 100644
index b8d58d6bd30..00000000000
--- a/changelogs/unreleased/52560-fix-duplicate-tag-system-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only execute system hooks once when pushing tags
-merge_request: 26888
-author:
-type: fixed
diff --git a/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml b/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml
new file mode 100644
index 00000000000..48f0a668982
--- /dev/null
+++ b/changelogs/unreleased/53064-bypassing-pipeline-jobs-by-canceling-the-pipeline-and-manually-running-later-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Make canceled jobs not retryable
+merge_request: 27503
+author:
+type: changed
diff --git a/changelogs/unreleased/53138-add-metrics-usage-ping.yml b/changelogs/unreleased/53138-add-metrics-usage-ping.yml
new file mode 100644
index 00000000000..db6af77a647
--- /dev/null
+++ b/changelogs/unreleased/53138-add-metrics-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Adds if InfluxDB and Prometheus metrics are enabled to usage ping data
+merge_request: 27238
+author:
+type: added
diff --git a/changelogs/unreleased/53139-hide-tree-single-file.yml b/changelogs/unreleased/53139-hide-tree-single-file.yml
deleted file mode 100644
index 17fe957e42e..00000000000
--- a/changelogs/unreleased/53139-hide-tree-single-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: collapse file tree by default if the merge request changes only one file
-merge_request:
-author: Riccardo Padovani <riccardo@rpadovani.com>
-type: changed
diff --git a/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml b/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
deleted file mode 100644
index 6fefd05049c..00000000000
--- a/changelogs/unreleased/53198-git-push-option-merge-when-pipeline-succeeds.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Allow merge requests to be set to merge when pipeline succeeds via git push
- options
-merge_request: 26842
-author:
-type: added
diff --git a/changelogs/unreleased/53210-add-uniq-constraints-on-issues-and-mrs-labels.yml b/changelogs/unreleased/53210-add-uniq-constraints-on-issues-and-mrs-labels.yml
deleted file mode 100644
index f9cd8716b92..00000000000
--- a/changelogs/unreleased/53210-add-uniq-constraints-on-issues-and-mrs-labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: add a uniq constraints on issues and mrs labels
-merge_request: 25435
-author: Antoine Huret
-type: fixed
diff --git a/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml b/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml
deleted file mode 100644
index 30d8c0e95d7..00000000000
--- a/changelogs/unreleased/53459-quick-action-adds-multiple-labels-to-issue-if-middle-words-overlap-with-existing-label.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix quick actions add label name middle word overlaps
-merge_request: 26602
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml
new file mode 100644
index 00000000000..862ce623d8c
--- /dev/null
+++ b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve discussion when apply suggestion
+merge_request: 28160
+author:
+type: changed
diff --git a/changelogs/unreleased/54417-graphql-type-authorization.yml b/changelogs/unreleased/54417-graphql-type-authorization.yml
deleted file mode 100644
index 528b58a858a..00000000000
--- a/changelogs/unreleased/54417-graphql-type-authorization.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: GraphQL Types can be made to always authorize access to resources of that Type
-merge_request: 25724
-author:
-type: added
diff --git a/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml b/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml
deleted file mode 100644
index 3e3784d5413..00000000000
--- a/changelogs/unreleased/54506-show-error-when-namespace-svc-missing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show error when namespace/svc account missing
-merge_request: 26362
-author:
-type: added
diff --git a/changelogs/unreleased/54670-external-diffs-when-outdated.yml b/changelogs/unreleased/54670-external-diffs-when-outdated.yml
deleted file mode 100644
index 2a0b9e75cb4..00000000000
--- a/changelogs/unreleased/54670-external-diffs-when-outdated.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Allow external diffs to be used conditionally'
-merge_request: 25432
-author:
-type: added
diff --git a/changelogs/unreleased/54916-extended-tooltip-for-merge-request-links.yml b/changelogs/unreleased/54916-extended-tooltip-for-merge-request-links.yml
deleted file mode 100644
index 7fd0bcd1c00..00000000000
--- a/changelogs/unreleased/54916-extended-tooltip-for-merge-request-links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add extended merge request tooltip
-merge_request: !25221
-author:
-type: added
diff --git a/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml
new file mode 100644
index 00000000000..ac3bb596842
--- /dev/null
+++ b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Wait for pipeline creation to complete before accepting a MR via API
+merge_request: 27978
+author: kerrizor
+type: fixed
diff --git a/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml b/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml
deleted file mode 100644
index 7af4739136b..00000000000
--- a/changelogs/unreleased/55268-exclude-system-notes-from-commits-in-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Exclude system notes from commits in merge request discussions
-merge_request: 26396
-author:
-type: fixed
diff --git a/changelogs/unreleased/55948-help-text-formatting-wiki.yml b/changelogs/unreleased/55948-help-text-formatting-wiki.yml
new file mode 100644
index 00000000000..e1e0475a117
--- /dev/null
+++ b/changelogs/unreleased/55948-help-text-formatting-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Format extra help page text like wiki
+merge_request: 26782
+author: Bastian Blank
+type: fixed
diff --git a/changelogs/unreleased/55964-fix-email-encoding.yml b/changelogs/unreleased/55964-fix-email-encoding.yml
deleted file mode 100644
index 2195a853702..00000000000
--- a/changelogs/unreleased/55964-fix-email-encoding.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix notfication emails having wrong encoding
-merge_request: 26931
-author:
-type: fixed
diff --git a/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml b/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml
deleted file mode 100644
index 4c16b635297..00000000000
--- a/changelogs/unreleased/55980-remove-add-issue-on-blank-list.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove non-functional add issue button on welcome list
-merge_request: !26742
-author:
-type: fixed
diff --git a/changelogs/unreleased/56015-remove-remote-timeout.yml b/changelogs/unreleased/56015-remove-remote-timeout.yml
deleted file mode 100644
index 9b40ada5291..00000000000
--- a/changelogs/unreleased/56015-remove-remote-timeout.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix removing remote mirror failure which leaves unnecessary refs behind
-merge_request: 26213
-author:
-type: fixed
diff --git a/changelogs/unreleased/56089-merge-gitlab-keys.yml b/changelogs/unreleased/56089-merge-gitlab-keys.yml
deleted file mode 100644
index 5e2cafd3254..00000000000
--- a/changelogs/unreleased/56089-merge-gitlab-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Merge the gitlab-shell "gitlab-keys" functionality into GitLab CE
-merge_request: 25598
-author:
-type: other
diff --git a/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml b/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml
new file mode 100644
index 00000000000..6521eb9d1c0
--- /dev/null
+++ b/changelogs/unreleased/56557-disable-kubernetes-namespace-service-account-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Disables kubernetes resources creation if a cluster is not managed
+merge_request: 26565
+author:
+type: added
diff --git a/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml b/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml
deleted file mode 100644
index 18bd51711d9..00000000000
--- a/changelogs/unreleased/56762-fix-commit-swipe-view-26968.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Fix image diff swipe view on commit and compare pages"
-merge_request: 26968
-author: ftab
-type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml b/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml
deleted file mode 100644
index 19cf3d69db1..00000000000
--- a/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Project: Improve empty repository state UI'
-merge_request: 26024
-author:
-type: other
diff --git a/changelogs/unreleased/56838-allow-guest-access-to-releases.yml b/changelogs/unreleased/56838-allow-guest-access-to-releases.yml
new file mode 100644
index 00000000000..701a015b9ac
--- /dev/null
+++ b/changelogs/unreleased/56838-allow-guest-access-to-releases.yml
@@ -0,0 +1,5 @@
+---
+title: Allow guests users to access project releases
+merge_request: 27247
+author:
+type: changed
diff --git a/changelogs/unreleased/56864-reopen-locked-mr.yml b/changelogs/unreleased/56864-reopen-locked-mr.yml
deleted file mode 100644
index d1d71531ac8..00000000000
--- a/changelogs/unreleased/56864-reopen-locked-mr.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disallow reopening of a locked merge request
-merge_request: 24882
-author: Jan Beckmann
-type: fixed
diff --git a/changelogs/unreleased/56954-improve-knative-after-installing-tiller.yml b/changelogs/unreleased/56954-improve-knative-after-installing-tiller.yml
deleted file mode 100644
index b9fb27c3218..00000000000
--- a/changelogs/unreleased/56954-improve-knative-after-installing-tiller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve the Knative installation on Clusters
-merge_request: 26339
-author:
-type: added
diff --git a/changelogs/unreleased/57017-add-toast-success-message.yml b/changelogs/unreleased/57017-add-toast-success-message.yml
new file mode 100644
index 00000000000..931e7755591
--- /dev/null
+++ b/changelogs/unreleased/57017-add-toast-success-message.yml
@@ -0,0 +1,5 @@
+---
+title: Display a toast message when the Kubernetes runner has successfully upgraded.
+merge_request: 27206
+author:
+type: changed
diff --git a/changelogs/unreleased/57077-add-salesforce-omniauth.yml b/changelogs/unreleased/57077-add-salesforce-omniauth.yml
new file mode 100644
index 00000000000..ebd0637ddac
--- /dev/null
+++ b/changelogs/unreleased/57077-add-salesforce-omniauth.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Salesforce.com omniauth support
+merge_request: 27834
+author:
+type: added
diff --git a/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml b/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml
deleted file mode 100644
index 2141c75ec72..00000000000
--- a/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create Kubernetes resources for projects when their deployment jobs run.
-merge_request: 25586
-author:
-type: changed
diff --git a/changelogs/unreleased/57131-external_auth_to_core.yml b/changelogs/unreleased/57131-external_auth_to_core.yml
deleted file mode 100644
index aacd3916c82..00000000000
--- a/changelogs/unreleased/57131-external_auth_to_core.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move "Authorize project access with external service" to Core
-merge_request: 26823
-author:
-type: changed
diff --git a/changelogs/unreleased/57171-add-dashboard-settings.yml b/changelogs/unreleased/57171-add-dashboard-settings.yml
new file mode 100644
index 00000000000..f235872b35c
--- /dev/null
+++ b/changelogs/unreleased/57171-add-dashboard-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add backend support for a External Dashboard URL setting
+merge_request: 27550
+author:
+type: added
diff --git a/changelogs/unreleased/57247-show-prioritized-labels-to-guests.yml b/changelogs/unreleased/57247-show-prioritized-labels-to-guests.yml
new file mode 100644
index 00000000000..5210ff0ccef
--- /dev/null
+++ b/changelogs/unreleased/57247-show-prioritized-labels-to-guests.yml
@@ -0,0 +1,5 @@
+---
+title: Show prioritized labels to guests
+merge_request: 27307
+author:
+type: fixed
diff --git a/changelogs/unreleased/57293-fix-image-rename.yml b/changelogs/unreleased/57293-fix-image-rename.yml
new file mode 100644
index 00000000000..50dddbdf114
--- /dev/null
+++ b/changelogs/unreleased/57293-fix-image-rename.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Renaming an image via Web IDE corrupts it
+merge_request: 27486
+author:
+type: fixed
diff --git a/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml b/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml
deleted file mode 100644
index a6953a68a76..00000000000
--- a/changelogs/unreleased/57319-hide-kubernetes-cluster-warning-if-project-has-cluster-related.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolve "Hide Kubernetes cluster warning if project has cluster related"
-merge_request: 26749
-author:
-type: fixed
diff --git a/changelogs/unreleased/57357-automate-base-domain-help-text.yml b/changelogs/unreleased/57357-automate-base-domain-help-text.yml
deleted file mode 100644
index fa1831b66ea..00000000000
--- a/changelogs/unreleased/57357-automate-base-domain-help-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automate base domain help text on Clusters page
-merge_request: 26124
-author:
-type: changed
diff --git a/changelogs/unreleased/57364-improve-diff-nav-header.yml b/changelogs/unreleased/57364-improve-diff-nav-header.yml
deleted file mode 100644
index 95d119b949c..00000000000
--- a/changelogs/unreleased/57364-improve-diff-nav-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make stylistic improvements to diff nav header
-merge_request: 26557
-author:
-type: fixed
diff --git a/changelogs/unreleased/57409-loading-button-transition.yml b/changelogs/unreleased/57409-loading-button-transition.yml
deleted file mode 100644
index 3cf169d79de..00000000000
--- a/changelogs/unreleased/57409-loading-button-transition.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent fade out transition on loading-button component.
-merge_request: 26428
-author:
-type: fixed
diff --git a/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml b/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml
deleted file mode 100644
index c188d59fe94..00000000000
--- a/changelogs/unreleased/57482-shortcut-to-create-merge-request-from-web-ide.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create a shortcut for a new MR in the Web IDE
-merge_request: 26792
-author:
-type: added
diff --git a/changelogs/unreleased/57493-add-limit-to-user-name.yml b/changelogs/unreleased/57493-add-limit-to-user-name.yml
deleted file mode 100644
index e6c78572d23..00000000000
--- a/changelogs/unreleased/57493-add-limit-to-user-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set user.name limit to 128 characters
-merge_request: 26146
-author:
-type: changed
diff --git a/changelogs/unreleased/57540-filename-trailing-space.yml b/changelogs/unreleased/57540-filename-trailing-space.yml
deleted file mode 100644
index db85fb350db..00000000000
--- a/changelogs/unreleased/57540-filename-trailing-space.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implemented whitespace-trimming for file names in Web IDE
-merge_request: 26270
-author:
-type: fixed
diff --git a/changelogs/unreleased/57602-create-cluster-validations.yml b/changelogs/unreleased/57602-create-cluster-validations.yml
deleted file mode 100644
index 35349c1e9f4..00000000000
--- a/changelogs/unreleased/57602-create-cluster-validations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display cluster form validation error messages inline
-merge_request: 26502
-author:
-type: changed
diff --git a/changelogs/unreleased/57648-make-emoji-picker-full-width-on-mobile.yml b/changelogs/unreleased/57648-make-emoji-picker-full-width-on-mobile.yml
deleted file mode 100644
index d92fd2a762e..00000000000
--- a/changelogs/unreleased/57648-make-emoji-picker-full-width-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Makes emoji picker full width on mobile.
-merge_request: 25883
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/57655-fix-markdown-tables-border.yml b/changelogs/unreleased/57655-fix-markdown-tables-border.yml
deleted file mode 100644
index 6a8ba8c4353..00000000000
--- a/changelogs/unreleased/57655-fix-markdown-tables-border.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix markdown table header and table content borders
-merge_request: 25666
-author:
-type: fixed
diff --git a/changelogs/unreleased/57668-create-file-from-url.yml b/changelogs/unreleased/57668-create-file-from-url.yml
deleted file mode 100644
index b6033fa24ca..00000000000
--- a/changelogs/unreleased/57668-create-file-from-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implemented support for creation of new files from URL in Web IDE
-merge_request: 26622
-author:
-type: added
diff --git a/changelogs/unreleased/57669-fix-bug-clicking-file-header-refreshes-page.yml b/changelogs/unreleased/57669-fix-bug-clicking-file-header-refreshes-page.yml
deleted file mode 100644
index c6161870096..00000000000
--- a/changelogs/unreleased/57669-fix-bug-clicking-file-header-refreshes-page.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Scroll to diff file content when clicking on file header name and it is not
- a link to other page
-merge_request: !26422
-author:
-type: fixed
diff --git a/changelogs/unreleased/57815.yml b/changelogs/unreleased/57815.yml
new file mode 100644
index 00000000000..ccf76c99f1e
--- /dev/null
+++ b/changelogs/unreleased/57815.yml
@@ -0,0 +1,5 @@
+---
+title: Added Omniauth UltraAuth strategy to GitLab
+merge_request:
+author: Kartikey Tanna
+type: added
diff --git a/changelogs/unreleased/57894-buttons-on-group-page-are-misaligned.yml b/changelogs/unreleased/57894-buttons-on-group-page-are-misaligned.yml
deleted file mode 100644
index ca0f529df6c..00000000000
--- a/changelogs/unreleased/57894-buttons-on-group-page-are-misaligned.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix misalignment of group overview page buttons
-merge_request: 26292
-author:
-type: fixed
diff --git a/changelogs/unreleased/57984-store-branch-name.yml b/changelogs/unreleased/57984-store-branch-name.yml
deleted file mode 100644
index 26dfdb7a5d6..00000000000
--- a/changelogs/unreleased/57984-store-branch-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Resolves Branch name is lost if I change commit mode in Web IDE
-merge_request: 26180
-author:
-type: fixed
diff --git a/changelogs/unreleased/58105-pipeline-author-and-commit-author-too-close-together-in-pipeline-list.yml b/changelogs/unreleased/58105-pipeline-author-and-commit-author-too-close-together-in-pipeline-list.yml
new file mode 100644
index 00000000000..aef0a5ad53e
--- /dev/null
+++ b/changelogs/unreleased/58105-pipeline-author-and-commit-author-too-close-together-in-pipeline-list.yml
@@ -0,0 +1,5 @@
+---
+title: Improve pipelines table spacing, add triggerer column
+merge_request: 26136
+author:
+type: changed
diff --git a/changelogs/unreleased/58208-explicitly-set-masterauth.yml b/changelogs/unreleased/58208-explicitly-set-masterauth.yml
deleted file mode 100644
index e3512d11113..00000000000
--- a/changelogs/unreleased/58208-explicitly-set-masterauth.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Explicitly set master_auth setting to enable basic auth and client certificate
- for new GKE clusters
-merge_request: 26018
-author:
-type: other
diff --git a/changelogs/unreleased/58252-web-ide-dropdown-duplicates.yml b/changelogs/unreleased/58252-web-ide-dropdown-duplicates.yml
new file mode 100644
index 00000000000..48b03994586
--- /dev/null
+++ b/changelogs/unreleased/58252-web-ide-dropdown-duplicates.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Web IDE template dropdown showing duplicates
+merge_request: 27237
+author:
+type: fixed
diff --git a/changelogs/unreleased/58293-extract-discussion-actions.yml b/changelogs/unreleased/58293-extract-discussion-actions.yml
new file mode 100644
index 00000000000..2ca4716a6de
--- /dev/null
+++ b/changelogs/unreleased/58293-extract-discussion-actions.yml
@@ -0,0 +1,5 @@
+---
+title: Extract DiscussionActions component from NoteableDiscussion
+merge_request: 27227
+author:
+type: other
diff --git a/changelogs/unreleased/58294-discussion-notes-component.yml b/changelogs/unreleased/58294-discussion-notes-component.yml
new file mode 100644
index 00000000000..fbe08360a9a
--- /dev/null
+++ b/changelogs/unreleased/58294-discussion-notes-component.yml
@@ -0,0 +1,5 @@
+---
+title: Extract DiscussionNotes component from NoteableDiscussion
+merge_request: 27066
+author:
+type: other
diff --git a/changelogs/unreleased/58361-issue-create-system-note-timestamp.yml b/changelogs/unreleased/58361-issue-create-system-note-timestamp.yml
new file mode 100644
index 00000000000..d8fe3e4aa48
--- /dev/null
+++ b/changelogs/unreleased/58361-issue-create-system-note-timestamp.yml
@@ -0,0 +1,5 @@
+---
+title: Fix system notes timestamp when creating issue in the past
+merge_request: 27406
+author:
+type: fixed
diff --git a/changelogs/unreleased/58375-api-controller.yml b/changelogs/unreleased/58375-api-controller.yml
deleted file mode 100644
index 60f21b37ae7..00000000000
--- a/changelogs/unreleased/58375-api-controller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a Prometheus API per environment
-merge_request: 26841
-author:
-type: added
diff --git a/changelogs/unreleased/58375-reactive-caching-changes.yml b/changelogs/unreleased/58375-reactive-caching-changes.yml
deleted file mode 100644
index cf73736b8ef..00000000000
--- a/changelogs/unreleased/58375-reactive-caching-changes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow reactive caching to be used in services
-merge_request: 26839
-author:
-type: added
diff --git a/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml b/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml
deleted file mode 100644
index 058a120b500..00000000000
--- a/changelogs/unreleased/58405-basic-limiting-complexity-of-graphql-queries.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add initial complexity limits to GraphQL queries
-merge_request: 26629
-author:
-type: performance
diff --git a/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml b/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml
deleted file mode 100644
index 3e494847e75..00000000000
--- a/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reduce height of instance system header and footer
-merge_request: 25752
-author:
-type: changed
diff --git a/changelogs/unreleased/58482-update-airminc-clair-local-scan-to-2-0-6.yml b/changelogs/unreleased/58482-update-airminc-clair-local-scan-to-2-0-6.yml
deleted file mode 100644
index be9c38aba1e..00000000000
--- a/changelogs/unreleased/58482-update-airminc-clair-local-scan-to-2-0-6.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update clair-local-scan to 2.0.6
-merge_request: 25743
-author: Takuya Noguchi
-type: added
diff --git a/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml b/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml
deleted file mode 100644
index 9f5881966c7..00000000000
--- a/changelogs/unreleased/58717-checkbox-cannot-be-checked-if-a-blockquote-is-above.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow task lists that follow a blockquote to work correctly
-merge_request: 26937
-author:
-type: fixed
diff --git a/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml b/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml
deleted file mode 100644
index 765a991bb6a..00000000000
--- a/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Hashed Storage: Prevent a migration and rollback running at the same time'
-merge_request: 25976
-author:
-type: changed
diff --git a/changelogs/unreleased/58748-update-nodejs-to-10-15-3.yml b/changelogs/unreleased/58748-update-nodejs-to-10-15-3.yml
new file mode 100644
index 00000000000..4c7e4b5c604
--- /dev/null
+++ b/changelogs/unreleased/58748-update-nodejs-to-10-15-3.yml
@@ -0,0 +1,5 @@
+---
+title: Update node.js to 10.15.3 in CI template for Hexo
+merge_request: 25943
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/58751-fix-project-name-members-page.yml b/changelogs/unreleased/58751-fix-project-name-members-page.yml
deleted file mode 100644
index a295afc5c48..00000000000
--- a/changelogs/unreleased/58751-fix-project-name-members-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Fix unwanted character replacement on project members page caused by usage of sanitize function"
-merge_request: 25946
-author: Elias Werberich
-type: fixed
diff --git a/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml b/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml
deleted file mode 100644
index e45db8eafc3..00000000000
--- a/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use curl silent/show-error options on Auto DevOps
-merge_request: 25954
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml b/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml
deleted file mode 100644
index ebfb7aeaa1f..00000000000
--- a/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Keep inline as much as possible in system notes on issuable
-merge_request: 25968
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/58793-fix-nav-links-archived-project.yml b/changelogs/unreleased/58793-fix-nav-links-archived-project.yml
deleted file mode 100644
index a8250804c34..00000000000
--- a/changelogs/unreleased/58793-fix-nav-links-archived-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Disable inaccessible navigation links upon archiving a project"
-merge_request: 26020
-author: Elias Werberich
-type: fixed
diff --git a/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml b/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml
deleted file mode 100644
index e30f48ed1a8..00000000000
--- a/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI for closed MR when source project is removed
-merge_request: 25967
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/58805-allow-incomplete-commit-data-to-be-fetched-from-collection.yml b/changelogs/unreleased/58805-allow-incomplete-commit-data-to-be-fetched-from-collection.yml
deleted file mode 100644
index 4377ebfdbdf..00000000000
--- a/changelogs/unreleased/58805-allow-incomplete-commit-data-to-be-fetched-from-collection.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix merge commits being used as default squash commit messages
-merge_request: 26445
-author:
-type: fixed
diff --git a/changelogs/unreleased/58835-button-run-pipeline.yml b/changelogs/unreleased/58835-button-run-pipeline.yml
deleted file mode 100644
index 39407a60780..00000000000
--- a/changelogs/unreleased/58835-button-run-pipeline.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Changed button label at /pipelines/new
-merge_request: 26893
-author: antfobe,leonardofl
-type: other
diff --git a/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml b/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml
deleted file mode 100644
index 2c6edf45ae2..00000000000
--- a/changelogs/unreleased/58839-automatically-set-prometheus-step-interval.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Automatically set Prometheus step interval
-merge_request: 26441
-author:
-type: changed
diff --git a/changelogs/unreleased/58850-fix-misplaced-swipe-view-26969.yml b/changelogs/unreleased/58850-fix-misplaced-swipe-view-26969.yml
new file mode 100644
index 00000000000..fa3e81df4e0
--- /dev/null
+++ b/changelogs/unreleased/58850-fix-misplaced-swipe-view-26969.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix misaligned image diff swipe view"
+merge_request: 26969
+author: ftab
+type: fixed
diff --git a/changelogs/unreleased/58883-fix-fetching-comments.yml b/changelogs/unreleased/58883-fix-fetching-comments.yml
deleted file mode 100644
index 14c0f1687f2..00000000000
--- a/changelogs/unreleased/58883-fix-fetching-comments.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix error shown when loading links to specific comments
-merge_request: 26092
-author:
-type: fixed
diff --git a/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml b/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml
deleted file mode 100644
index ec357d9a832..00000000000
--- a/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix continuous bitbucket import loading spinner
-merge_request: 26175
-author:
-type: fixed
diff --git a/changelogs/unreleased/58933-broken-ui-on-commits-on-mobile.yml b/changelogs/unreleased/58933-broken-ui-on-commits-on-mobile.yml
deleted file mode 100644
index ca9f9dd21c9..00000000000
--- a/changelogs/unreleased/58933-broken-ui-on-commits-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI layout on Commits on mobile
-merge_request: 26133
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/58971-sentry-api-keyerror.yml b/changelogs/unreleased/58971-sentry-api-keyerror.yml
deleted file mode 100644
index 0f195c4b4f7..00000000000
--- a/changelogs/unreleased/58971-sentry-api-keyerror.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Handle missing keys in sentry api response
-merge_request: 26264
-author:
-type: fixed
diff --git a/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml b/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml
deleted file mode 100644
index 3df13dbb960..00000000000
--- a/changelogs/unreleased/58981-migrate-clusters-tests-to-jest.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Migrate clusters tests to jest
-merge_request: 27013
-author:
-type: other
diff --git a/changelogs/unreleased/59034-external-link-button.yml b/changelogs/unreleased/59034-external-link-button.yml
new file mode 100644
index 00000000000..3d8e9e82836
--- /dev/null
+++ b/changelogs/unreleased/59034-external-link-button.yml
@@ -0,0 +1,5 @@
+---
+title: Replaced icon for external URL with doc-text icon
+merge_request: 27365
+author:
+type: fixed
diff --git a/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml b/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml
deleted file mode 100644
index febbbce2139..00000000000
--- a/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve mobile UI on User Profile page
-merge_request: 26240
-author: Takuya Noguchi
-type: other
diff --git a/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml b/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml
deleted file mode 100644
index 265a7e36841..00000000000
--- a/changelogs/unreleased/59062-update-gitlab-markup-python-3.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update gitlab-markup to 1.7.0 which requies python3
-merge_request: 26246
-author:
-type: changed
diff --git a/changelogs/unreleased/59079-fix-jupyter-render-loop.yml b/changelogs/unreleased/59079-fix-jupyter-render-loop.yml
deleted file mode 100644
index 29264b33dfa..00000000000
--- a/changelogs/unreleased/59079-fix-jupyter-render-loop.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix jupyter rendering bug that ended in an infinite loop
-merge_request: 26656
-author: ROSPARS Benoit
-type: fixed
diff --git a/changelogs/unreleased/59117-inconsistent-hover-behavior-on-navbar-items.yml b/changelogs/unreleased/59117-inconsistent-hover-behavior-on-navbar-items.yml
deleted file mode 100644
index eb9dcef4a89..00000000000
--- a/changelogs/unreleased/59117-inconsistent-hover-behavior-on-navbar-items.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix hover animation consistency in top navbar items
-merge_request: 26345
-author:
-type: fixed
diff --git a/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml b/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml
deleted file mode 100644
index 688190f4458..00000000000
--- a/changelogs/unreleased/59131-set-the-size-of-instance-system-message-text-12px.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update system message banner font size to 12px
-merge_request: 26293
-author:
-type: changed
diff --git a/changelogs/unreleased/59189-long-names-in-project-path-namespace-dropdown-breaks-past-container.yml b/changelogs/unreleased/59189-long-names-in-project-path-namespace-dropdown-breaks-past-container.yml
deleted file mode 100644
index bed7fcf2651..00000000000
--- a/changelogs/unreleased/59189-long-names-in-project-path-namespace-dropdown-breaks-past-container.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent namespace dropdown in new project form from exceeding container
-merge_request: 26343
-author:
-type: fixed
diff --git a/changelogs/unreleased/59273-update-fugit.yml b/changelogs/unreleased/59273-update-fugit.yml
deleted file mode 100644
index 3a1c64d87ef..00000000000
--- a/changelogs/unreleased/59273-update-fugit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update fugit which fixes a potential infinite loop
-merge_request: 26579
-author:
-type: fixed
diff --git a/changelogs/unreleased/59275-cluster-form-hints.yml b/changelogs/unreleased/59275-cluster-form-hints.yml
new file mode 100644
index 00000000000..0031b9557f4
--- /dev/null
+++ b/changelogs/unreleased/59275-cluster-form-hints.yml
@@ -0,0 +1,5 @@
+---
+title: Add help texts to K8 form fields
+merge_request: 27274
+author:
+type: changed
diff --git a/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml b/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml
deleted file mode 100644
index 440b24a548c..00000000000
--- a/changelogs/unreleased/59296-add-filter-by-title-milestones-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add select by title to milestones API
-merge_request: 26573
-author:
-type: added
diff --git a/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml b/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml
deleted file mode 100644
index 9ab8d2b8596..00000000000
--- a/changelogs/unreleased/59324-queries-which-return-multiple-series-are-not-working-correctly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix multiple series queries on metrics dashboard
-merge_request: 26514
-author:
-type: fixed
diff --git a/changelogs/unreleased/59352-fix-mr-discussion-expansion.yml b/changelogs/unreleased/59352-fix-mr-discussion-expansion.yml
deleted file mode 100644
index ab9ad53835c..00000000000
--- a/changelogs/unreleased/59352-fix-mr-discussion-expansion.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expand resolved discussion when linking to a comment in the discussion
-merge_request: 26483
-author:
-type: fixed
diff --git a/changelogs/unreleased/59441-add-base-domain-to-cluster-api.yml b/changelogs/unreleased/59441-add-base-domain-to-cluster-api.yml
deleted file mode 100644
index cb5cbba4e92..00000000000
--- a/changelogs/unreleased/59441-add-base-domain-to-cluster-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add cluster domain to Project Cluster API
-merge_request: 26735
-author:
-type: other
diff --git a/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml b/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml
deleted file mode 100644
index da65c3bc870..00000000000
--- a/changelogs/unreleased/59502-fix-breadcrumb-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes job link in artifacts page breadcrumb
-merge_request: 26592
-author:
-type: fixed
diff --git a/changelogs/unreleased/59514-uploading-images-base64.yml b/changelogs/unreleased/59514-uploading-images-base64.yml
new file mode 100644
index 00000000000..905b00db06a
--- /dev/null
+++ b/changelogs/unreleased/59514-uploading-images-base64.yml
@@ -0,0 +1,5 @@
+---
+title: Show proper preview for uploaded images in Web IDE
+merge_request: 27471
+author:
+type: fixed
diff --git a/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml b/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml
deleted file mode 100644
index 8f0ce5d57c5..00000000000
--- a/changelogs/unreleased/59546-fix-error-handling-for-missing-domain.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Auto DevOps missing domain error handling
-merge_request: 26627
-author:
-type: fixed
diff --git a/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml b/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml
deleted file mode 100644
index 8b82d757303..00000000000
--- a/changelogs/unreleased/59621-order-labels-alphabetically-in-issue-boards.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Order labels alphabetically in issue boards
-merge_request: 26927
-author:
-type: changed
diff --git a/changelogs/unreleased/5966-rebase-with-block.yml b/changelogs/unreleased/5966-rebase-with-block.yml
new file mode 100644
index 00000000000..9272a02977f
--- /dev/null
+++ b/changelogs/unreleased/5966-rebase-with-block.yml
@@ -0,0 +1,5 @@
+---
+title: Fix approvals sometimes being reset after a merge request is rebased
+merge_request: 27446
+author:
+type: fixed
diff --git a/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml b/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml
deleted file mode 100644
index bfea3ac52af..00000000000
--- a/changelogs/unreleased/60006-add-touch-events-to-image-diff-26971.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Make touch events work on image diff swipe view and onion skin"
-merge_request: 26971
-author: ftab
-type: added
diff --git a/changelogs/unreleased/60026-group-member-count-bg.yml b/changelogs/unreleased/60026-group-member-count-bg.yml
new file mode 100644
index 00000000000..0a1f6eac2ea
--- /dev/null
+++ b/changelogs/unreleased/60026-group-member-count-bg.yml
@@ -0,0 +1,5 @@
+---
+title: Add badge-pill class on group member count
+merge_request: 27019
+author:
+type: fixed
diff --git a/changelogs/unreleased/60068-avoid-null-domain-help-text.yml b/changelogs/unreleased/60068-avoid-null-domain-help-text.yml
deleted file mode 100644
index 5305b8584a8..00000000000
--- a/changelogs/unreleased/60068-avoid-null-domain-help-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Do not display Ingress IP help text when there isn’t an Ingress IP assigned
-merge_request: 27057
-author:
-type: fixed
diff --git a/changelogs/unreleased/60116-fix-button-wrapping.yml b/changelogs/unreleased/60116-fix-button-wrapping.yml
deleted file mode 100644
index d6df920b51d..00000000000
--- a/changelogs/unreleased/60116-fix-button-wrapping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add to white-space nowrap to all buttons
-merge_request: 27069
-author:
-type: fixed
diff --git a/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml b/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml
deleted file mode 100644
index 8c3a47cf62c..00000000000
--- a/changelogs/unreleased/60149-nameerror-uninitialized-constant-sentry-client-sentryerror.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Handle possible HTTP exception for Sentry client
-merge_request: 27080
-author:
-type: fixed
diff --git a/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml b/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml
deleted file mode 100644
index ce942777dca..00000000000
--- a/changelogs/unreleased/60241-merge-request-popover-doesn-t-go-away-on-mouse-leave.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix bug where MR popover doesn't go away on mouse leave
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml b/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml
deleted file mode 100644
index ec5e9e4703b..00000000000
--- a/changelogs/unreleased/60304-long-file-names-in-mr-diffs-cause-horizontal-scrolling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix long file header names bug in diffs
-merge_request: 27233
-author:
-type: fixed
diff --git a/changelogs/unreleased/60387-use-icons-in-user-popovers.yml b/changelogs/unreleased/60387-use-icons-in-user-popovers.yml
new file mode 100644
index 00000000000..100d33690b3
--- /dev/null
+++ b/changelogs/unreleased/60387-use-icons-in-user-popovers.yml
@@ -0,0 +1,5 @@
+---
+title: Show category icons in user popover
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/60462-empty-pipeline-section.yml b/changelogs/unreleased/60462-empty-pipeline-section.yml
new file mode 100644
index 00000000000..7d90215e20c
--- /dev/null
+++ b/changelogs/unreleased/60462-empty-pipeline-section.yml
@@ -0,0 +1,5 @@
+---
+title: Fix empty block in MR widget when user doesn't have permission
+merge_request: 27462
+author:
+type: fixed
diff --git a/changelogs/unreleased/60552-period-dropdown.yml b/changelogs/unreleased/60552-period-dropdown.yml
new file mode 100644
index 00000000000..e1b4a098ab0
--- /dev/null
+++ b/changelogs/unreleased/60552-period-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix autocomplete dropdown for usernames starting with period
+merge_request: 27533
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/60777-uninstall-button.yml b/changelogs/unreleased/60777-uninstall-button.yml
new file mode 100644
index 00000000000..a2727b16ef1
--- /dev/null
+++ b/changelogs/unreleased/60777-uninstall-button.yml
@@ -0,0 +1,5 @@
+---
+title: Implement UI for uninstalling Cluster’s managed apps
+merge_request: 27559
+author:
+type: added
diff --git a/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
new file mode 100644
index 00000000000..b340f8408f3
--- /dev/null
+++ b/changelogs/unreleased/60808-only-show-target-branch-advanced-error-before-merge.yml
@@ -0,0 +1,6 @@
+---
+title: Only show the "target branch has advanced" message when the merge request is
+ open
+merge_request: 27588
+author:
+type: fixed
diff --git a/changelogs/unreleased/60874-fix-suggestion-misalignment.yml b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
new file mode 100644
index 00000000000..f5717ac19fd
--- /dev/null
+++ b/changelogs/unreleased/60874-fix-suggestion-misalignment.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Misalignment on suggested changes diff table
+merge_request: 27612
+author:
+type: fixed
diff --git a/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml
new file mode 100644
index 00000000000..32f0e023923
--- /dev/null
+++ b/changelogs/unreleased/61036-fix-ingress-base-domain-text.yml
@@ -0,0 +1,5 @@
+---
+title: Fix base domain help text update
+merge_request: 27746
+author:
+type: fixed
diff --git a/changelogs/unreleased/61203-fix-lfs-ui-upload.yml b/changelogs/unreleased/61203-fix-lfs-ui-upload.yml
new file mode 100644
index 00000000000..66afe9f0597
--- /dev/null
+++ b/changelogs/unreleased/61203-fix-lfs-ui-upload.yml
@@ -0,0 +1,5 @@
+---
+title: Fix uploading of LFS tracked file through UI
+merge_request: 28052
+author:
+type: fixed
diff --git a/changelogs/unreleased/61278-next.yml b/changelogs/unreleased/61278-next.yml
new file mode 100644
index 00000000000..829f37f75ba
--- /dev/null
+++ b/changelogs/unreleased/61278-next.yml
@@ -0,0 +1,5 @@
+---
+title: Render Next badge only for gitlab.com
+merge_request: 28056
+author:
+type: fixed
diff --git a/changelogs/unreleased/61469-align-play-icon.yml b/changelogs/unreleased/61469-align-play-icon.yml
new file mode 100644
index 00000000000..a118da29703
--- /dev/null
+++ b/changelogs/unreleased/61469-align-play-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Vertically aligns the play button for stages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/MaxWinterstein-master-patch-23232.yml b/changelogs/unreleased/MaxWinterstein-master-patch-23232.yml
deleted file mode 100644
index 8fb9f1057fe..00000000000
--- a/changelogs/unreleased/MaxWinterstein-master-patch-23232.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Unify behaviour of 'Copy commit SHA to clipboard' to use full commit SHA.
-merge_request: 25829
-author: Max Winterstein
-type: changed
diff --git a/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml b/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml
deleted file mode 100644
index b773eb2720c..00000000000
--- a/changelogs/unreleased/_acet-related-mrs-widget-rewrite.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Rewrite related MRs widget with Vue
-merge_request: 27027
-author:
-type: other
diff --git a/changelogs/unreleased/ac-package-storage-stats.yml b/changelogs/unreleased/ac-package-storage-stats.yml
new file mode 100644
index 00000000000..fedffb41597
--- /dev/null
+++ b/changelogs/unreleased/ac-package-storage-stats.yml
@@ -0,0 +1,5 @@
+---
+title: Add packages_size to ProjectStatistics
+merge_request: 27373
+author:
+type: added
diff --git a/changelogs/unreleased/add-ci-variable-protected-ref.yml b/changelogs/unreleased/add-ci-variable-protected-ref.yml
new file mode 100644
index 00000000000..150ddcc21ad
--- /dev/null
+++ b/changelogs/unreleased/add-ci-variable-protected-ref.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI_COMMIT_REF_PROTECTED CI variable
+merge_request: 26716
+author: Jason van den Hurk
+type: added
diff --git a/changelogs/unreleased/add-runner-access-level-registration.yml b/changelogs/unreleased/add-runner-access-level-registration.yml
new file mode 100644
index 00000000000..7ae95025abb
--- /dev/null
+++ b/changelogs/unreleased/add-runner-access-level-registration.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to set access_level of runners upon registration
+merge_request: 27490
+author: Zelin L
+type: added
diff --git a/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml b/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml
deleted file mode 100644
index 26b8ac4b1ef..00000000000
--- a/changelogs/unreleased/add_backtrace_to_kubernetes_log.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show error backtrace when logging errors to kubernetes.log
-merge_request: 25726
-author:
-type: other
diff --git a/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml b/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml
deleted file mode 100644
index 31165bbadb7..00000000000
--- a/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow filtering labels list by one or two characters
-merge_request: 26012
-author:
-type: changed
diff --git a/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml b/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml
deleted file mode 100644
index 61236b9b82b..00000000000
--- a/changelogs/unreleased/allow-ref-name-caching-projects-controller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable FindCommit caching for project and commits pages
-merge_request: 27048
-author:
-type: performance
diff --git a/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml b/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml
new file mode 100644
index 00000000000..b268b0689ad
--- /dev/null
+++ b/changelogs/unreleased/allow-replying-to-individual-notes-from-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow replying to individual notes from API
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/an-use-labkit.yml b/changelogs/unreleased/an-use-labkit.yml
new file mode 100644
index 00000000000..ab293c15787
--- /dev/null
+++ b/changelogs/unreleased/an-use-labkit.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate correlation and tracing code to LabKit
+merge_request: 25379
+author:
+type: other
diff --git a/changelogs/unreleased/autodevops_remote_private_helm_repository.yml b/changelogs/unreleased/autodevops_remote_private_helm_repository.yml
new file mode 100644
index 00000000000..5341abb1095
--- /dev/null
+++ b/changelogs/unreleased/autodevops_remote_private_helm_repository.yml
@@ -0,0 +1,6 @@
+---
+title: Allow linking to a private helm repository by providing credentials, and customisation
+ of repository name
+merge_request: 27123
+author: Stuart Moore @stjm-cc
+type: added
diff --git a/changelogs/unreleased/avoid_es_loading_project_ci_status.yml b/changelogs/unreleased/avoid_es_loading_project_ci_status.yml
deleted file mode 100644
index 514909c730d..00000000000
--- a/changelogs/unreleased/avoid_es_loading_project_ci_status.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid loading pipeline status in project search
-merge_request: 26342
-author:
-type: performance
diff --git a/changelogs/unreleased/bw-add-graphql-groups.yml b/changelogs/unreleased/bw-add-graphql-groups.yml
new file mode 100644
index 00000000000..f72ee1cf2b7
--- /dev/null
+++ b/changelogs/unreleased/bw-add-graphql-groups.yml
@@ -0,0 +1,5 @@
+---
+title: Add initial GraphQL query for Groups
+merge_request: 27492
+author:
+type: added
diff --git a/changelogs/unreleased/ce-10725-restructure-project-merge-request-settings-page.yml b/changelogs/unreleased/ce-10725-restructure-project-merge-request-settings-page.yml
new file mode 100644
index 00000000000..7aa9204fe4e
--- /dev/null
+++ b/changelogs/unreleased/ce-10725-restructure-project-merge-request-settings-page.yml
@@ -0,0 +1,5 @@
+---
+title: Reorganize project merge request settings
+merge_request: 26834
+author:
+type: changed
diff --git a/changelogs/unreleased/ce-11430-update_clair_local_scan.yml b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml
new file mode 100644
index 00000000000..04bb04c3919
--- /dev/null
+++ b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml
@@ -0,0 +1,5 @@
+---
+title: Update clair-local-scan to v2.0.8 for container scanning
+merge_request: 27977
+author:
+type: other
diff --git a/changelogs/unreleased/ce-4681-autosave.yml b/changelogs/unreleased/ce-4681-autosave.yml
new file mode 100644
index 00000000000..029954ec92b
--- /dev/null
+++ b/changelogs/unreleased/ce-4681-autosave.yml
@@ -0,0 +1,5 @@
+---
+title: Autosave description in epics
+merge_request: 27296
+author:
+type: added
diff --git a/changelogs/unreleased/ce-56153-error-tracking-counts.yml b/changelogs/unreleased/ce-56153-error-tracking-counts.yml
deleted file mode 100644
index fc3d8c01d7f..00000000000
--- a/changelogs/unreleased/ce-56153-error-tracking-counts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add usage counts for error tracking feature
-merge_request: 25472
-author:
-type: added
diff --git a/changelogs/unreleased/ce-its-simple-just-destroy-the-mirrors.yml b/changelogs/unreleased/ce-its-simple-just-destroy-the-mirrors.yml
new file mode 100644
index 00000000000..ac5fc27cf36
--- /dev/null
+++ b/changelogs/unreleased/ce-its-simple-just-destroy-the-mirrors.yml
@@ -0,0 +1,5 @@
+---
+title: Destroy project remote mirrors instead of disabling
+merge_request: 27087
+author:
+type: security
diff --git a/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml b/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml
deleted file mode 100644
index 10475824a75..00000000000
--- a/changelogs/unreleased/ce-proj-settings-ok-avatar-only.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Change project avatar remove button to a link
-merge_request: 26589
-author:
-type: other
diff --git a/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml b/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml
deleted file mode 100644
index 4bbbc706e62..00000000000
--- a/changelogs/unreleased/ce-proj-settings-ok-mr-settings-only.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve project merge request settings
-merge_request: 26495
-author:
-type: other
diff --git a/changelogs/unreleased/ce-remove-already-signed-in.yml b/changelogs/unreleased/ce-remove-already-signed-in.yml
new file mode 100644
index 00000000000..70bed136ced
--- /dev/null
+++ b/changelogs/unreleased/ce-remove-already-signed-in.yml
@@ -0,0 +1,5 @@
+---
+title: Remove "You are already signed in" banner
+merge_request: 27377
+author:
+type: other
diff --git a/changelogs/unreleased/check-mergeability-in-merge-to-ref-service.yml b/changelogs/unreleased/check-mergeability-in-merge-to-ref-service.yml
deleted file mode 100644
index 9f615bbb54a..00000000000
--- a/changelogs/unreleased/check-mergeability-in-merge-to-ref-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Check mergeability in MergeToRefService
-merge_request: 26757
-author:
-type: changed
diff --git a/changelogs/unreleased/ci-lint-ssl-error.yml b/changelogs/unreleased/ci-lint-ssl-error.yml
new file mode 100644
index 00000000000..d59b9204357
--- /dev/null
+++ b/changelogs/unreleased/ci-lint-ssl-error.yml
@@ -0,0 +1,6 @@
+---
+title: Catch and report OpenSSL exceptions while fetching external configuration files
+ in CI::Config
+merge_request: 26750
+author: Drew Cimino
+type: fixed
diff --git a/changelogs/unreleased/create-label-and-list-checkbox.yml b/changelogs/unreleased/create-label-and-list-checkbox.yml
deleted file mode 100644
index 330372df1be..00000000000
--- a/changelogs/unreleased/create-label-and-list-checkbox.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Added "Add List" checkbox to create label dropdown to make creation of list optional'
-merge_request: 25716
-author: Tucker Chapman
-type: fixed
diff --git a/changelogs/unreleased/da-sentry-client-side-settings.yml b/changelogs/unreleased/da-sentry-client-side-settings.yml
new file mode 100644
index 00000000000..e36ac7c354b
--- /dev/null
+++ b/changelogs/unreleased/da-sentry-client-side-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Sentry client-side DSN to be passed on gitlab.yml
+merge_request: 27967
+author:
+type: added
diff --git a/changelogs/unreleased/delete-release-when-delete-tag.yml b/changelogs/unreleased/delete-release-when-delete-tag.yml
deleted file mode 100644
index 58acd449bf1..00000000000
--- a/changelogs/unreleased/delete-release-when-delete-tag.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Releases will now be automatically deleted when deleting corresponding tag
-merge_request: 26530
-author:
-type: fixed
diff --git a/changelogs/unreleased/deploy-keys-ext.yml b/changelogs/unreleased/deploy-keys-ext.yml
deleted file mode 100644
index e1d2fe08425..00000000000
--- a/changelogs/unreleased/deploy-keys-ext.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Externalize admin deploy keys strings
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/disallow-guests-to-access-releases.yml b/changelogs/unreleased/disallow-guests-to-access-releases.yml
deleted file mode 100644
index f2d518108d2..00000000000
--- a/changelogs/unreleased/disallow-guests-to-access-releases.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disallow guest users from accessing Releases
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/do-not-force-2fa.yml b/changelogs/unreleased/do-not-force-2fa.yml
deleted file mode 100644
index f9be40e8f37..00000000000
--- a/changelogs/unreleased/do-not-force-2fa.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add link on two-factor authorization settings page to leave group that enforces
- two-factor authorization
-merge_request: 25731
-author:
-type: changed
diff --git a/changelogs/unreleased/downloading-expired-artifacts.yml b/changelogs/unreleased/downloading-expired-artifacts.yml
new file mode 100644
index 00000000000..2f4b79ca106
--- /dev/null
+++ b/changelogs/unreleased/downloading-expired-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: stop rendering download links for expired artifacts on the project tags page
+merge_request: 26753
+author: Drew Cimino
+type: fixed
diff --git a/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml b/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml
deleted file mode 100644
index d99187d8d41..00000000000
--- a/changelogs/unreleased/drop-usage-of-leagcy-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Drop legacy artifacts usage as there are no leftovers
-merge_request: 24294
-author:
-type: performance
diff --git a/changelogs/unreleased/duplicate-related-mrs.yml b/changelogs/unreleased/duplicate-related-mrs.yml
deleted file mode 100644
index 0f5f6ede9f8..00000000000
--- a/changelogs/unreleased/duplicate-related-mrs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove duplicates from issue related merge requests
-merge_request: 27067
-author:
-type: fixed
diff --git a/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml b/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml
deleted file mode 100644
index 42bc320a542..00000000000
--- a/changelogs/unreleased/ekigbo-extend-timezone-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Extend timezone dropdown
-merge_request: 26311
-author:
-type: changed
diff --git a/changelogs/unreleased/expand-diff-performance.yml b/changelogs/unreleased/expand-diff-performance.yml
new file mode 100644
index 00000000000..134ea4081e4
--- /dev/null
+++ b/changelogs/unreleased/expand-diff-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve expanding diff to full file performance
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/expose-group-id-on-home-panel.yml b/changelogs/unreleased/expose-group-id-on-home-panel.yml
deleted file mode 100644
index 1efe15a6e1a..00000000000
--- a/changelogs/unreleased/expose-group-id-on-home-panel.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expose group id on home panel
-merge_request: 25897
-author: Peter Marko
-type: added
diff --git a/changelogs/unreleased/extend-cte-optimisations-to-projects.yml b/changelogs/unreleased/extend-cte-optimisations-to-projects.yml
deleted file mode 100644
index e5407127b2f..00000000000
--- a/changelogs/unreleased/extend-cte-optimisations-to-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Speed up filtering issues in a project when searching
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/feat-sentry-environment.yml b/changelogs/unreleased/feat-sentry-environment.yml
new file mode 100644
index 00000000000..44ea19375f8
--- /dev/null
+++ b/changelogs/unreleased/feat-sentry-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Sentry configuration to be passed on gitlab.yml
+merge_request: 27091
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml b/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml
deleted file mode 100644
index 81cf5cb810d..00000000000
--- a/changelogs/unreleased/feature-gb-serverless-switch-to-gitlabktl.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use gitlabktl to build and deploy GitLab Serverless Functions
-merge_request: 26926
-author:
-type: added
diff --git a/changelogs/unreleased/feature-users-search-results.yml b/changelogs/unreleased/feature-users-search-results.yml
deleted file mode 100644
index 151d08bce12..00000000000
--- a/changelogs/unreleased/feature-users-search-results.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add users search results to global search
-merge_request: 21197
-author: Alexis Reigel
-type: added
diff --git a/changelogs/unreleased/feature-webide_escaping.yml b/changelogs/unreleased/feature-webide_escaping.yml
deleted file mode 100644
index 88fa1bd948e..00000000000
--- a/changelogs/unreleased/feature-webide_escaping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug with hashes in urls in WebIDE
-merge_request: 54376
-author: Kieran Andrews
-type: fixed
diff --git a/changelogs/unreleased/filter-merge-requests-by-target-branch.yml b/changelogs/unreleased/filter-merge-requests-by-target-branch.yml
deleted file mode 100644
index d0aba631c96..00000000000
--- a/changelogs/unreleased/filter-merge-requests-by-target-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add target branch filter to merge requests search bar
-merge_request: 24380
-author: Hiroyuki Sato
-type: added
diff --git a/changelogs/unreleased/fix-UI-links-to-route-map-info.yml b/changelogs/unreleased/fix-UI-links-to-route-map-info.yml
deleted file mode 100644
index bb506507080..00000000000
--- a/changelogs/unreleased/fix-UI-links-to-route-map-info.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix UI anchor links after docs refactor
-merge_request: 26890
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-api-ide-relative-url-root.yml b/changelogs/unreleased/fix-api-ide-relative-url-root.yml
new file mode 100644
index 00000000000..8c058645f3e
--- /dev/null
+++ b/changelogs/unreleased/fix-api-ide-relative-url-root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix FE API and IDE handling of '/' relative_url_root
+merge_request: 27635
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-container-scanning-on-k8s.yml b/changelogs/unreleased/fix-container-scanning-on-k8s.yml
deleted file mode 100644
index f4500370a0b..00000000000
--- a/changelogs/unreleased/fix-container-scanning-on-k8s.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Container Scanning in Kubernetes Runners
-merge_request: 26793
-author:
-type: changed
diff --git a/changelogs/unreleased/fix-expand-full-file-on-image.yml b/changelogs/unreleased/fix-expand-full-file-on-image.yml
deleted file mode 100644
index a88d30cfa38..00000000000
--- a/changelogs/unreleased/fix-expand-full-file-on-image.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed expand full file button showing on images
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-extra-emails-for-custom-notifications.yml b/changelogs/unreleased/fix-extra-emails-for-custom-notifications.yml
new file mode 100644
index 00000000000..6eb3225e4a1
--- /dev/null
+++ b/changelogs/unreleased/fix-extra-emails-for-custom-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Fix extra emails for custom notifications
+merge_request: 25607
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-hidden-statistics.yml b/changelogs/unreleased/fix-hidden-statistics.yml
deleted file mode 100644
index 4d99bd00136..00000000000
--- a/changelogs/unreleased/fix-hidden-statistics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show statistics also when repository is disabled
-merge_request: 26509
-author: Peter Marko
-type: fixed
diff --git a/changelogs/unreleased/fix-ide-relative-url-bug.yml b/changelogs/unreleased/fix-ide-relative-url-bug.yml
new file mode 100644
index 00000000000..183af722657
--- /dev/null
+++ b/changelogs/unreleased/fix-ide-relative-url-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix IDE get file data with '/' as relative root
+merge_request: 27911
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-ide-web-worker-relative-url.yml b/changelogs/unreleased/fix-ide-web-worker-relative-url.yml
deleted file mode 100644
index 2accad68c4e..00000000000
--- a/changelogs/unreleased/fix-ide-web-worker-relative-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed Web IDE web workers not working with relative URLs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-include-ci-yaml.yml b/changelogs/unreleased/fix-include-ci-yaml.yml
deleted file mode 100644
index 042413b89aa..00000000000
--- a/changelogs/unreleased/fix-include-ci-yaml.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix single string values for the 'include' keyword validation of gitlab-ci.yml.
-merge_request: 26998
-author: Paul Bonaud (@paulrbr)
-type: fixed
diff --git a/changelogs/unreleased/fix-issues-time-counter.yml b/changelogs/unreleased/fix-issues-time-counter.yml
deleted file mode 100644
index 76f17063db5..00000000000
--- a/changelogs/unreleased/fix-issues-time-counter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make time counters show 'just now' for everything under one minute
-merge_request: 25992
-author: Sergiu Marton
-type: changed
diff --git a/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml b/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml
new file mode 100644
index 00000000000..58f5a9c943c
--- /dev/null
+++ b/changelogs/unreleased/fix-lazy-blobs-requesting-all-previous-blobs.yml
@@ -0,0 +1,6 @@
+---
+title: Fix Blob.lazy always loading all previously-requested blobs when a new request
+ is made
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-merge-request-pipeline-exist-method.yml b/changelogs/unreleased/fix-merge-request-pipeline-exist-method.yml
new file mode 100644
index 00000000000..294a665ff3e
--- /dev/null
+++ b/changelogs/unreleased/fix-merge-request-pipeline-exist-method.yml
@@ -0,0 +1,5 @@
+---
+title: Fix duplicate merge request pipelines created by Sidekiq worker retry
+merge_request: 26643
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml b/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml
deleted file mode 100644
index 9ccc79109d8..00000000000
--- a/changelogs/unreleased/fix-merge-request-relations-with-pipeline-on-mwps.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix MWPS does not work for merge request pipelines
-merge_request: 26906
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml b/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml
deleted file mode 100644
index dadbd5c940f..00000000000
--- a/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed sticky headers in merge request creation diffs
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-pipeline-entity.yml b/changelogs/unreleased/fix-pipeline-entity.yml
deleted file mode 100644
index b429139402c..00000000000
--- a/changelogs/unreleased/fix-pipeline-entity.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add merge request pipeline flag to pipeline entity
-merge_request: 25846
-author:
-type: added
diff --git a/changelogs/unreleased/fix-review-app-env-url.yml b/changelogs/unreleased/fix-review-app-env-url.yml
deleted file mode 100644
index 963cd0c2992..00000000000
--- a/changelogs/unreleased/fix-review-app-env-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes long review app subdomains
-merge_request: 25990
-author: walkafwalka
-type: fixed
diff --git a/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml b/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml
deleted file mode 100644
index ae097e859d9..00000000000
--- a/changelogs/unreleased/fix-routes-n-plus-one-in-user-autocomplete.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix some N+1s in loading routes and counting members for groups in @-autocomplete
-merge_request: 26491
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml b/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml
new file mode 100644
index 00000000000..5e574ef686c
--- /dev/null
+++ b/changelogs/unreleased/fix-schedule-head-pipeline-update-method.yml
@@ -0,0 +1,5 @@
+---
+title: Fix update head pipeline process of Pipelines for merge requests
+merge_request: 28057
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-transfer-group-possibilities.yml b/changelogs/unreleased/fix-transfer-group-possibilities.yml
deleted file mode 100644
index ebefb47b3da..00000000000
--- a/changelogs/unreleased/fix-transfer-group-possibilities.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix group transfer selection possibilities
-merge_request: 26123
-author: Peter Marko
-type: fixed
diff --git a/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
new file mode 100644
index 00000000000..80936245f3e
--- /dev/null
+++ b/changelogs/unreleased/fix-webpack-assets-relative-url-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix webpack assets handling when relative url root is '/'
+merge_request: 27909
+author:
+type: fixed
diff --git a/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml b/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml
deleted file mode 100644
index 770186a64b0..00000000000
--- a/changelogs/unreleased/fixed-duplicated-large-text-on-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed duplicated diff too large error message
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fixed-web-ide-merge-request-review.yml b/changelogs/unreleased/fixed-web-ide-merge-request-review.yml
deleted file mode 100644
index 2799f5ee38a..00000000000
--- a/changelogs/unreleased/fixed-web-ide-merge-request-review.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed Web IDE not loading merge request files
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml b/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml
new file mode 100644
index 00000000000..49eaff52e5a
--- /dev/null
+++ b/changelogs/unreleased/fj-53523-add-option-avoid-loading-wiki-page-content.yml
@@ -0,0 +1,5 @@
+---
+title: Added list_pages method to avoid loading all wiki pages content
+merge_request: 22801
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml
new file mode 100644
index 00000000000..c513f3c3aeb
--- /dev/null
+++ b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Add improvements to global search of issues and merge requests
+merge_request: 27817
+author:
+type: performance
diff --git a/changelogs/unreleased/friendly-wrap-component.yml b/changelogs/unreleased/friendly-wrap-component.yml
new file mode 100644
index 00000000000..c16ca0af287
--- /dev/null
+++ b/changelogs/unreleased/friendly-wrap-component.yml
@@ -0,0 +1,5 @@
+---
+title: Add CSS fix for <wbr> elements on IE11
+merge_request: 27846
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-version-v1.29.0.yml b/changelogs/unreleased/gitaly-version-v1.29.0.yml
deleted file mode 100644
index b6ce14c33a2..00000000000
--- a/changelogs/unreleased/gitaly-version-v1.29.0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade to Gitaly v1.29.0
-merge_request: 26406
-author:
-type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.32.0.yml b/changelogs/unreleased/gitaly-version-v1.32.0.yml
deleted file mode 100644
index 8413f31278e..00000000000
--- a/changelogs/unreleased/gitaly-version-v1.32.0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade to Gitaly v1.32.0
-merge_request: 26989
-author:
-type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.33.0.yml b/changelogs/unreleased/gitaly-version-v1.33.0.yml
deleted file mode 100644
index d21e521a0bb..00000000000
--- a/changelogs/unreleased/gitaly-version-v1.33.0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade to Gitaly v1.33.0
-merge_request: 27065
-author:
-type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.36.0.yml b/changelogs/unreleased/gitaly-version-v1.36.0.yml
new file mode 100644
index 00000000000..22fdca8da80
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.36.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.36.0
+merge_request: 27831
+author:
+type: changed
diff --git a/changelogs/unreleased/gitaly-version-v1.42.0.yml b/changelogs/unreleased/gitaly-version-v1.42.0.yml
new file mode 100644
index 00000000000..38621fa071e
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.42.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.42.0
+merge_request: 28135
+author:
+type: changed
diff --git a/changelogs/unreleased/gitlab-issue-54894.yml b/changelogs/unreleased/gitlab-issue-54894.yml
new file mode 100644
index 00000000000..513c0163c0e
--- /dev/null
+++ b/changelogs/unreleased/gitlab-issue-54894.yml
@@ -0,0 +1,6 @@
+---
+title: Fix some label links not appearing on group labels page and label title being
+ a link on project labels page
+merge_request: 24060
+author: Tanya Pazitny
+type: fixed
diff --git a/changelogs/unreleased/graphql-prometheus.yml b/changelogs/unreleased/graphql-prometheus.yml
deleted file mode 100644
index 180577f3aec..00000000000
--- a/changelogs/unreleased/graphql-prometheus.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added prometheus monitoring to GraphQL
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/graphql-resolvers-complexity.yml b/changelogs/unreleased/graphql-resolvers-complexity.yml
new file mode 100644
index 00000000000..503ffbd97f2
--- /dev/null
+++ b/changelogs/unreleased/graphql-resolvers-complexity.yml
@@ -0,0 +1,6 @@
+---
+title: 'GraphQL: improve evaluation of query complexity based on arguments and query
+ limits.'
+merge_request: 28017
+author:
+type: added
diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-pipelines.yml b/changelogs/unreleased/gt-externalize-app-views-projects-pipelines.yml
deleted file mode 100644
index 094cd3ab751..00000000000
--- a/changelogs/unreleased/gt-externalize-app-views-projects-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Externalize strings from `/app/views/projects/pipelines`
-merge_request: 26035
-author: George Tsiolis
-type: other
diff --git a/changelogs/unreleased/id-51433-sort-wiki-by-date.yml b/changelogs/unreleased/id-51433-sort-wiki-by-date.yml
deleted file mode 100644
index 86fcf195fa7..00000000000
--- a/changelogs/unreleased/id-51433-sort-wiki-by-date.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to sort wiki pages by date and title
-merge_request: 25365
-author:
-type: added
diff --git a/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml b/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml
deleted file mode 100644
index 8f4f49896d7..00000000000
--- a/changelogs/unreleased/ide-fix-detect-mr-from-fork.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix IDE detection of MR from fork with same branch name
-merge_request: 26986
-author:
-type: fixed
diff --git a/changelogs/unreleased/instance_level_clusters.yml b/changelogs/unreleased/instance_level_clusters.yml
new file mode 100644
index 00000000000..afd06a4e05f
--- /dev/null
+++ b/changelogs/unreleased/instance_level_clusters.yml
@@ -0,0 +1,5 @@
+---
+title: Instance level kubernetes clusters
+merge_request: 27196
+author:
+type: added
diff --git a/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml b/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml
new file mode 100644
index 00000000000..3f0a96ad50e
--- /dev/null
+++ b/changelogs/unreleased/issue-42692-deployment-chat-notifications.yml
@@ -0,0 +1,5 @@
+---
+title: Add deployment events to chat notification services
+merge_request: 27338
+author:
+type: added
diff --git a/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml b/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml
new file mode 100644
index 00000000000..c85ddc7b91c
--- /dev/null
+++ b/changelogs/unreleased/issue-61038-deploy-chat-message-update.yml
@@ -0,0 +1,5 @@
+---
+title: Update deployment event chat notification message
+merge_request: 27972
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_57906_fix_github_import.yml b/changelogs/unreleased/issue_57906_fix_github_import.yml
new file mode 100644
index 00000000000..d28a78d5d11
--- /dev/null
+++ b/changelogs/unreleased/issue_57906_fix_github_import.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issuables state_id nil when importing projects from GitHub
+merge_request: 28027
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_58547.yml b/changelogs/unreleased/issue_58547.yml
deleted file mode 100644
index 553c752e72d..00000000000
--- a/changelogs/unreleased/issue_58547.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API access check to Graphql
-merge_request: 26570
-author:
-type: other
diff --git a/changelogs/unreleased/jc-client-gitaly-session-id.yml b/changelogs/unreleased/jc-client-gitaly-session-id.yml
new file mode 100644
index 00000000000..ae5b7144b98
--- /dev/null
+++ b/changelogs/unreleased/jc-client-gitaly-session-id.yml
@@ -0,0 +1,5 @@
+---
+title: Add gitaly session id & catfile-cache feature flag
+merge_request: 27472
+author:
+type: performance
diff --git a/changelogs/unreleased/jc-guard-against-empty-dereferenced_target.yml b/changelogs/unreleased/jc-guard-against-empty-dereferenced_target.yml
deleted file mode 100644
index 94e5b4a81b7..00000000000
--- a/changelogs/unreleased/jc-guard-against-empty-dereferenced_target.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Guard against nil dereferenced_target
-merge_request: 27192
-author:
-type: fixed
diff --git a/changelogs/unreleased/jc-update-list-last-commits.yml b/changelogs/unreleased/jc-update-list-last-commits.yml
new file mode 100644
index 00000000000..0e72c4255ae
--- /dev/null
+++ b/changelogs/unreleased/jc-update-list-last-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Client side changes for ListLastCommitsForTree response update
+merge_request: 26880
+author:
+type: fixed
diff --git a/changelogs/unreleased/jv-dedup-activerecord.yml b/changelogs/unreleased/jv-dedup-activerecord.yml
new file mode 100644
index 00000000000..7b440c7c0db
--- /dev/null
+++ b/changelogs/unreleased/jv-dedup-activerecord.yml
@@ -0,0 +1,6 @@
+---
+title: Fix wrong use of ActiveRecord in PoolRepository
+merge_request: 27464
+author:
+type: fixed
+
diff --git a/changelogs/unreleased/k8s_new_deployment_labels.yml b/changelogs/unreleased/k8s_new_deployment_labels.yml
deleted file mode 100644
index e9ef3ee0082..00000000000
--- a/changelogs/unreleased/k8s_new_deployment_labels.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update deploy boards to additionally select on "app.gitlab.com" annotations
-merge_request: 25623
-author:
-type: changed
diff --git a/changelogs/unreleased/knative-0-5.yml b/changelogs/unreleased/knative-0-5.yml
new file mode 100644
index 00000000000..c7112b957e9
--- /dev/null
+++ b/changelogs/unreleased/knative-0-5.yml
@@ -0,0 +1,5 @@
+---
+title: Knative version bump 0.3 -> 0.5
+merge_request:
+author: Chris Baumbauer <cab@cabnetworks.net>
+type: changed
diff --git a/changelogs/unreleased/knative-prometheus.yml b/changelogs/unreleased/knative-prometheus.yml
deleted file mode 100644
index e24f53b7225..00000000000
--- a/changelogs/unreleased/knative-prometheus.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Knative metrics to Prometheus
-merge_request: 24663
-author: Chris Baumbauer <cab@cabnetworks.net>
-type: added
diff --git a/changelogs/unreleased/knative-update.yml b/changelogs/unreleased/knative-update.yml
deleted file mode 100644
index e84940ae7e0..00000000000
--- a/changelogs/unreleased/knative-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Knative version bump 0.2.2 -> 0.3.0
-merge_request: 26459
-author: Chris Baumbauer
-type: changed
diff --git a/changelogs/unreleased/kube_helm_auto_devops_213.yml b/changelogs/unreleased/kube_helm_auto_devops_213.yml
deleted file mode 100644
index 3c718d4a22c..00000000000
--- a/changelogs/unreleased/kube_helm_auto_devops_213.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Bump kubectl to 1.11.9 and Helm to 2.13.1 in Auto-DevOps.gitlab-ci.yml
-merge_request: 26534
-author:
-type: other
diff --git a/changelogs/unreleased/limit-amount-of-created-pipelines.yml b/changelogs/unreleased/limit-amount-of-created-pipelines.yml
deleted file mode 100644
index 51fdbb4d7ff..00000000000
--- a/changelogs/unreleased/limit-amount-of-created-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Process at most 4 pipelines during push
-merge_request: 27205
-author:
-type: performance
diff --git a/changelogs/unreleased/localize-notification-dropdown.yml b/changelogs/unreleased/localize-notification-dropdown.yml
deleted file mode 100644
index 9599aaf344b..00000000000
--- a/changelogs/unreleased/localize-notification-dropdown.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Localize notifications dropdown
-merge_request: 26844
-author:
-type: changed
diff --git a/changelogs/unreleased/markdown-autocomplete-escaping.yml b/changelogs/unreleased/markdown-autocomplete-escaping.yml
new file mode 100644
index 00000000000..0ea034b14ee
--- /dev/null
+++ b/changelogs/unreleased/markdown-autocomplete-escaping.yml
@@ -0,0 +1,5 @@
+---
+title: Only escape Markdown emphasis characters in autocomplete when necessary
+merge_request: 27457
+author:
+type: changed
diff --git a/changelogs/unreleased/member-access-granted-leave-email-fe.yml b/changelogs/unreleased/member-access-granted-leave-email-fe.yml
new file mode 100644
index 00000000000..919a2464a4d
--- /dev/null
+++ b/changelogs/unreleased/member-access-granted-leave-email-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Leave project/group from access granted email
+merge_request: 27892
+author:
+type: added
diff --git a/changelogs/unreleased/minimized-multiple-queries-ce.yml b/changelogs/unreleased/minimized-multiple-queries-ce.yml
deleted file mode 100644
index d8c20d492d6..00000000000
--- a/changelogs/unreleased/minimized-multiple-queries-ce.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support multiple queries per chart on metrics dash
-merge_request: 25758
-author:
-type: added
diff --git a/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml b/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml
deleted file mode 100644
index 34fd0c1b787..00000000000
--- a/changelogs/unreleased/move-allow-developers-to-create-projects-in-groups-to-core.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move allow developers to create projects in groups to Core
-merge_request: 25975
-author:
-type: added
diff --git a/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml b/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml
deleted file mode 100644
index 8cde0958f7a..00000000000
--- a/changelogs/unreleased/nfriend-css-updates-for-gitlab-design-system-compliance.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update `border-radius` of form controls and remove extra space above page titles
-merge_request: 24497
-author:
-type: fixed
diff --git a/changelogs/unreleased/nfriend-update-job-detail-view-sidebar.yml b/changelogs/unreleased/nfriend-update-job-detail-view-sidebar.yml
deleted file mode 100644
index 5364d29710a..00000000000
--- a/changelogs/unreleased/nfriend-update-job-detail-view-sidebar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update job detail sidebar to accommodate post-merge pipeline information
-merge_request: 25777
-author:
-type: added
diff --git a/changelogs/unreleased/nfriend-update-merge-request-widget-pipeline-block.yml b/changelogs/unreleased/nfriend-update-merge-request-widget-pipeline-block.yml
deleted file mode 100644
index bd4120eb06f..00000000000
--- a/changelogs/unreleased/nfriend-update-merge-request-widget-pipeline-block.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Update pipeline block on merge request page to accommodate post-merge pipeline
- information
-merge_request: 25745
-author:
-type: added
diff --git a/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml b/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml
deleted file mode 100644
index a24325c4eb6..00000000000
--- a/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update pipeline detail view to accommodate post-merge pipelines
-merge_request: 25775
-author:
-type: added
diff --git a/changelogs/unreleased/nfriend-update-pipeline-list-view.yml b/changelogs/unreleased/nfriend-update-pipeline-list-view.yml
deleted file mode 100644
index 34e43162b5c..00000000000
--- a/changelogs/unreleased/nfriend-update-pipeline-list-view.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update pipeline list view to accommodate post-merge pipeline information
-merge_request: 25690
-author:
-type: added
diff --git a/changelogs/unreleased/only-counted-active-milestones-as-started.yml b/changelogs/unreleased/only-counted-active-milestones-as-started.yml
deleted file mode 100644
index 1a9c4b9023b..00000000000
--- a/changelogs/unreleased/only-counted-active-milestones-as-started.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only consider active milestones when using the special Started milestone filter
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml b/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml
deleted file mode 100644
index 01bd7ede270..00000000000
--- a/changelogs/unreleased/osw-multi-line-suggestions-creation-strategy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implements the creation strategy for multi-line suggestions
-merge_request: 26057
-author:
-type: changed
diff --git a/changelogs/unreleased/osw-multi-line-suggestions-parsing.yml b/changelogs/unreleased/osw-multi-line-suggestions-parsing.yml
deleted file mode 100644
index 985b01e9254..00000000000
--- a/changelogs/unreleased/osw-multi-line-suggestions-parsing.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prepare multi-line suggestions for rendering in Markdown
-merge_request: 26107
-author:
-type: other
diff --git a/changelogs/unreleased/osw-support-multi-line-suggestions.yml b/changelogs/unreleased/osw-support-multi-line-suggestions.yml
deleted file mode 100644
index 8c8206c3822..00000000000
--- a/changelogs/unreleased/osw-support-multi-line-suggestions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Support multi-line suggestions
-merge_request: 25211
-author:
-type: added
diff --git a/changelogs/unreleased/patch-49.yml b/changelogs/unreleased/patch-49.yml
new file mode 100644
index 00000000000..2c8af1e5c48
--- /dev/null
+++ b/changelogs/unreleased/patch-49.yml
@@ -0,0 +1,5 @@
+---
+title: Remove leading / trailing spaces from heading when generating header ids
+merge_request: 27025
+author: Willian Balmant
+type: fixed
diff --git a/changelogs/unreleased/persist-fulll-ref-path-for-mr-pipelines.yml b/changelogs/unreleased/persist-fulll-ref-path-for-mr-pipelines.yml
deleted file mode 100644
index ca42a26e8ff..00000000000
--- a/changelogs/unreleased/persist-fulll-ref-path-for-mr-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create MR pipelines with `refs/merge-requests/:iid/head`
-merge_request: 25504
-author:
-type: changed
diff --git a/changelogs/unreleased/pl-upgrade-letter_opener_web.yml b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml
new file mode 100644
index 00000000000..9891344215a
--- /dev/null
+++ b/changelogs/unreleased/pl-upgrade-letter_opener_web.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade letter_opener_web to support Rails 5.1
+merge_request: 27829
+author:
+type: fixed
diff --git a/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml b/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml
deleted file mode 100644
index 95379fb2ec1..00000000000
--- a/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply recaptcha API change in 4.0
-merge_request: 25921
-author: Praveen Arimbrathodiyil
-type: other
diff --git a/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml b/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
deleted file mode 100644
index d003ca55feb..00000000000
--- a/changelogs/unreleased/prevent-running-mr-pipelines-when-target-updated.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create pipelines for merge requests only when source branch is updated
-merge_request: 26921
-author:
-type: fixed
diff --git a/changelogs/unreleased/rails5-1.yml b/changelogs/unreleased/rails5-1.yml
new file mode 100644
index 00000000000..da16735bb0d
--- /dev/null
+++ b/changelogs/unreleased/rails5-1.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Rails 5.1
+merge_request: 27480
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
new file mode 100644
index 00000000000..03d94c39c10
--- /dev/null
+++ b/changelogs/unreleased/refactor-58827-migrate-issue-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'refactor(issue): Refactored issue tests from Karma to Jest'
+merge_request: 27673
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
new file mode 100644
index 00000000000..9a1886797da
--- /dev/null
+++ b/changelogs/unreleased/refactor-58829-migrate-notes-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'Refactored notes tests from Karma to Jest'
+merge_request: 27648
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml
new file mode 100644
index 00000000000..20a4be8c9ad
--- /dev/null
+++ b/changelogs/unreleased/refactor-58830-migrate-sidebar-spec-to-jest.yml
@@ -0,0 +1,5 @@
+---
+title: 'Refactored Karma spec files to Jest'
+merge_request: 27688
+author: Martin Hobert
+type: other
diff --git a/changelogs/unreleased/related-mr-link-cutoff.yml b/changelogs/unreleased/related-mr-link-cutoff.yml
deleted file mode 100644
index 8cf77b6231a..00000000000
--- a/changelogs/unreleased/related-mr-link-cutoff.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't cutoff letters in MR and Issue links
-merge_request: 25910
-author: gfyoung
-type: fixed
diff --git a/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml b/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml
deleted file mode 100644
index 0e090592101..00000000000
--- a/changelogs/unreleased/remove-ci-charts-undescriptive-header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removes the undescriptive CI Charts header
-merge_request: !26869
-author:
-type: changed
diff --git a/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml
new file mode 100644
index 00000000000..9b208cbaa0e
--- /dev/null
+++ b/changelogs/unreleased/remove-disabled-pages-domains-part-2.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pages domains if they weren't verified for 1 week
+merge_request: 26227
+author:
+type: added
diff --git a/changelogs/unreleased/rename_auto_deploy_app_links.yml b/changelogs/unreleased/rename_auto_deploy_app_links.yml
new file mode 100644
index 00000000000..c56b5fb5e5c
--- /dev/null
+++ b/changelogs/unreleased/rename_auto_deploy_app_links.yml
@@ -0,0 +1,5 @@
+---
+title: Move location of charts/auto-deploy-app -> gitlab-org/charts/auto-deploy-app
+merge_request: 27477
+author:
+type: changed
diff --git a/changelogs/unreleased/rewind-iid-on-pipelines.yml b/changelogs/unreleased/rewind-iid-on-pipelines.yml
new file mode 100644
index 00000000000..b5738860024
--- /dev/null
+++ b/changelogs/unreleased/rewind-iid-on-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Rewind IID on Ci::Pipelines
+merge_request: 26490
+author:
+type: fixed
diff --git a/changelogs/unreleased/security-55503-fix-pdf-js-vulnerability.yml b/changelogs/unreleased/security-55503-fix-pdf-js-vulnerability.yml
deleted file mode 100644
index e5d0cd4fee1..00000000000
--- a/changelogs/unreleased/security-55503-fix-pdf-js-vulnerability.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix PDF.js vulnerability
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-56224.yml b/changelogs/unreleased/security-56224.yml
deleted file mode 100644
index a4e274e6ca5..00000000000
--- a/changelogs/unreleased/security-56224.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide "related branches" when user does not have permission
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-56927-xss-resolve-conflicts-branch-name.yml b/changelogs/unreleased/security-56927-xss-resolve-conflicts-branch-name.yml
deleted file mode 100644
index f92d2c0dcb1..00000000000
--- a/changelogs/unreleased/security-56927-xss-resolve-conflicts-branch-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix XSS in resolve conflicts form
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-exif-migration.yml b/changelogs/unreleased/security-exif-migration.yml
deleted file mode 100644
index cc529099df5..00000000000
--- a/changelogs/unreleased/security-exif-migration.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added rake task for removing EXIF data from existing uploads.
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-id-potential-denial-languages.yml b/changelogs/unreleased/security-id-potential-denial-languages.yml
deleted file mode 100644
index 2194ecb97dc..00000000000
--- a/changelogs/unreleased/security-id-potential-denial-languages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Return cached languages if they've been detected before
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/security-mass-assignment-on-project-update.yml b/changelogs/unreleased/security-mass-assignment-on-project-update.yml
deleted file mode 100644
index 93561cd91b3..00000000000
--- a/changelogs/unreleased/security-mass-assignment-on-project-update.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disallow updating namespace when updating a project
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-add-gitaly-backtrace.yml b/changelogs/unreleased/sh-add-gitaly-backtrace.yml
new file mode 100644
index 00000000000..1515edd6db9
--- /dev/null
+++ b/changelogs/unreleased/sh-add-gitaly-backtrace.yml
@@ -0,0 +1,5 @@
+---
+title: Add backtrace to Gitaly performance bar
+merge_request: 27345
+author:
+type: other
diff --git a/changelogs/unreleased/sh-add-gitaly-duration-logs.yml b/changelogs/unreleased/sh-add-gitaly-duration-logs.yml
deleted file mode 100644
index eea50384278..00000000000
--- a/changelogs/unreleased/sh-add-gitaly-duration-logs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Log Gitaly RPC duration to api_json.log and production_json.log
-merge_request: 26652
-author:
-type: other
diff --git a/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml b/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml
deleted file mode 100644
index a051c1f70a8..00000000000
--- a/changelogs/unreleased/sh-add-gitaly-ref-name-caching-tree-controller.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable Gitaly FindCommit caching for TreeController
-merge_request: 27100
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml b/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml
new file mode 100644
index 00000000000..adbed52db81
--- /dev/null
+++ b/changelogs/unreleased/sh-allow-equal-level-in-subgroup-membership.yml
@@ -0,0 +1,5 @@
+---
+title: Allow a member to have an access level equal to parent group
+merge_request: 27913
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-avoid-fetching-temp-refs-within-project.yml b/changelogs/unreleased/sh-avoid-fetching-temp-refs-within-project.yml
new file mode 100644
index 00000000000..7511543f7f6
--- /dev/null
+++ b/changelogs/unreleased/sh-avoid-fetching-temp-refs-within-project.yml
@@ -0,0 +1,5 @@
+---
+title: Don't create a temp reference for branch comparisons within project
+merge_request: 24038
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-cache-pipeline-find-commits.yml b/changelogs/unreleased/sh-cache-pipeline-find-commits.yml
deleted file mode 100644
index 2acf180d6fa..00000000000
--- a/changelogs/unreleased/sh-cache-pipeline-find-commits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache FindCommit results in pipelines view
-merge_request: 26776
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml b/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml
deleted file mode 100644
index 106d070cc05..00000000000
--- a/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache Repository#root_ref within a request
-merge_request: 25903
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-cleanup-import-export.yml b/changelogs/unreleased/sh-cleanup-import-export.yml
new file mode 100644
index 00000000000..3d5d6f3c907
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up CarrierWave's import/export files
+merge_request: 27487
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-clear-pipeline-status-cache-upon-destroy.yml b/changelogs/unreleased/sh-clear-pipeline-status-cache-upon-destroy.yml
deleted file mode 100644
index 55779f0f9d3..00000000000
--- a/changelogs/unreleased/sh-clear-pipeline-status-cache-upon-destroy.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clear pipeline status cache after destruction of pipeline
-merge_request: 26575
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml
new file mode 100644
index 00000000000..00f897ac4b1
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-batch-load-replace-methods.yml
@@ -0,0 +1,5 @@
+---
+title: Disable method replacement in avatar loading
+merge_request: 27866
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-disable-internal-ids-available-check.yml b/changelogs/unreleased/sh-disable-internal-ids-available-check.yml
new file mode 100644
index 00000000000..069a9ba7d69
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-internal-ids-available-check.yml
@@ -0,0 +1,5 @@
+---
+title: Always use internal ID tables in development and production
+merge_request: 27544
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
new file mode 100644
index 00000000000..e855684bab1
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-autocomplete-mirror-repo.yml
@@ -0,0 +1,5 @@
+---
+title: Disable password autocomplete in mirror repository form
+merge_request: 27542
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-gitaly-find-commit-caching.yml b/changelogs/unreleased/sh-fix-gitaly-find-commit-caching.yml
deleted file mode 100644
index 16d349c407c..00000000000
--- a/changelogs/unreleased/sh-fix-gitaly-find-commit-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow ref name caching CommitService#find_commit
-merge_request: 26248
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-issue-59065.yml b/changelogs/unreleased/sh-fix-issue-59065.yml
deleted file mode 100644
index 41cd5ce0960..00000000000
--- a/changelogs/unreleased/sh-fix-issue-59065.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Error 500 when user commits Wiki page with no commit message
-merge_request: 26247
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-merge-requests-api-remove-branch-param.yml b/changelogs/unreleased/sh-fix-merge-requests-api-remove-branch-param.yml
deleted file mode 100644
index d13c972ccc9..00000000000
--- a/changelogs/unreleased/sh-fix-merge-requests-api-remove-branch-param.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix remove_source_branch merge request API handling
-merge_request: 27392
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml b/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml
deleted file mode 100644
index 3d1501cd667..00000000000
--- a/changelogs/unreleased/sh-fix-realtime-changes-with-reserved-words.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix real-time updates for projects that contain a reserved word
-merge_request: 27060
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-ref-name-caching.yml b/changelogs/unreleased/sh-fix-ref-name-caching.yml
deleted file mode 100644
index 6abd86688b4..00000000000
--- a/changelogs/unreleased/sh-fix-ref-name-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix and expand Gitaly FindCommit caching
-merge_request: 27018
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-fix-related-merge-requests-path.yml b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml
new file mode 100644
index 00000000000..4b4108feda4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml
@@ -0,0 +1,5 @@
+---
+title: Use a path for the related merge requests endpoint
+merge_request: 28171
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml b/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml
deleted file mode 100644
index 867d7e6b9df..00000000000
--- a/changelogs/unreleased/sh-git-gc-after-initial-fetch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'GitHub import: Run housekeeping after initial import'
-merge_request: 26600
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-improve-find-commit-caching.yml b/changelogs/unreleased/sh-improve-find-commit-caching.yml
deleted file mode 100644
index 1b38684d018..00000000000
--- a/changelogs/unreleased/sh-improve-find-commit-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Expand FindCommit caching to blob and refs
-merge_request: 27084
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-reject-info-refs-head-requests.yml b/changelogs/unreleased/sh-reject-info-refs-head-requests.yml
deleted file mode 100644
index 0dca18e2fd8..00000000000
--- a/changelogs/unreleased/sh-reject-info-refs-head-requests.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Reject HEAD requests to info/refs endpoint
-merge_request: 26334
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml b/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml
deleted file mode 100644
index 5bf0ccf3e9d..00000000000
--- a/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix counting of groups in admin dashboard
-merge_request: 26009
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-update-rails-5-0-7-2.yml b/changelogs/unreleased/sh-update-rails-5-0-7-2.yml
deleted file mode 100644
index b0bc08d4760..00000000000
--- a/changelogs/unreleased/sh-update-rails-5-0-7-2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update Rails to 5.0.7.2
-merge_request: 27022
-author:
-type: security
diff --git a/changelogs/unreleased/sh-upgrade-grpc-and-protobuf.yml b/changelogs/unreleased/sh-upgrade-grpc-and-protobuf.yml
new file mode 100644
index 00000000000..a43a59a4f8a
--- /dev/null
+++ b/changelogs/unreleased/sh-upgrade-grpc-and-protobuf.yml
@@ -0,0 +1,5 @@
+---
+title: Bump gRPC to 1.19.0 and protobuf to 3.7.1
+merge_request: 27086
+author:
+type: other
diff --git a/changelogs/unreleased/show-disabled-mirrors.yml b/changelogs/unreleased/show-disabled-mirrors.yml
new file mode 100644
index 00000000000..a401606b331
--- /dev/null
+++ b/changelogs/unreleased/show-disabled-mirrors.yml
@@ -0,0 +1,5 @@
+---
+title: Show disabled project repo mirrors in settings
+merge_request: 27326
+author:
+type: other
diff --git a/changelogs/unreleased/stop-signing-avatar-paths.yml b/changelogs/unreleased/stop-signing-avatar-paths.yml
deleted file mode 100644
index 2c2493f0f21..00000000000
--- a/changelogs/unreleased/stop-signing-avatar-paths.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Speed up generation of avatar URLs when using object storage
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/support-negative-matches.yml b/changelogs/unreleased/support-negative-matches.yml
new file mode 100644
index 00000000000..8d3f2d3cbae
--- /dev/null
+++ b/changelogs/unreleased/support-negative-matches.yml
@@ -0,0 +1,5 @@
+---
+title: Support negative matches
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/tc-raw-log-in-plaintext-mail.yml b/changelogs/unreleased/tc-raw-log-in-plaintext-mail.yml
new file mode 100644
index 00000000000..373c2847ef2
--- /dev/null
+++ b/changelogs/unreleased/tc-raw-log-in-plaintext-mail.yml
@@ -0,0 +1,5 @@
+---
+title: Include link to raw job log in plain-text emails
+merge_request: 27409
+author:
+type: changed
diff --git a/changelogs/unreleased/tortuetorche-gitlab-ce-patch-auto-deploy-extra-values.yml b/changelogs/unreleased/tortuetorche-gitlab-ce-patch-auto-deploy-extra-values.yml
new file mode 100644
index 00000000000..84ed4a8fccb
--- /dev/null
+++ b/changelogs/unreleased/tortuetorche-gitlab-ce-patch-auto-deploy-extra-values.yml
@@ -0,0 +1,5 @@
+---
+title: Allow extra arguments in helm commands when deploying the application in Auto-DevOps.gitlab-ci.yml
+merge_request: 26171
+author: tortuetorche
+type: changed
diff --git a/changelogs/unreleased/tpresa-add-highest-role-to-user.yml b/changelogs/unreleased/tpresa-add-highest-role-to-user.yml
deleted file mode 100644
index 9714d8dcc99..00000000000
--- a/changelogs/unreleased/tpresa-add-highest-role-to-user.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adding highest role property to admin's user details page
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-3-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-3-0.yml
deleted file mode 100644
index 2e1adb1e1e9..00000000000
--- a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-3-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GitLab Runner Helm Chart to 0.3.0/11.9.0
-merge_request: 26467
-author:
-type: other
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-0.yml
new file mode 100644
index 00000000000..7eb5bd58035
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.4.0
+merge_request: 27508
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml
new file mode 100644
index 00000000000..f36c1d0e77e
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-4-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.4.1
+merge_request: 27627
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-shell.yml b/changelogs/unreleased/update-gitlab-shell.yml
deleted file mode 100644
index 446a5fe1ea9..00000000000
--- a/changelogs/unreleased/update-gitlab-shell.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GitLab Shell to v9.0.0
-merge_request: 27002
-author:
-type: other
diff --git a/changelogs/unreleased/update-rack-oauth2.yml b/changelogs/unreleased/update-rack-oauth2.yml
deleted file mode 100644
index dc2e7017695..00000000000
--- a/changelogs/unreleased/update-rack-oauth2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update rack-oauth2 1.2.1 -> 1.9.3
-merge_request: 17868
-author:
-type: other
diff --git a/changelogs/unreleased/update-workhorse-8-5-1.yml b/changelogs/unreleased/update-workhorse-8-5-1.yml
deleted file mode 100644
index 18193701678..00000000000
--- a/changelogs/unreleased/update-workhorse-8-5-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update GitLab Workhorse to v8.5.1
-merge_request: 27217
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-workhorse-master.yml b/changelogs/unreleased/update-workhorse-master.yml
new file mode 100644
index 00000000000..97e2e891ab1
--- /dev/null
+++ b/changelogs/unreleased/update-workhorse-master.yml
@@ -0,0 +1,5 @@
+---
+title: Update Workhorse to v8.7.0
+merge_request: 27630
+author:
+type: fixed
diff --git a/changelogs/unreleased/use-only-all-pipelines.yml b/changelogs/unreleased/use-only-all-pipelines.yml
deleted file mode 100644
index 68364d2a923..00000000000
--- a/changelogs/unreleased/use-only-all-pipelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Refactor all_pipelines in Merge request
-merge_request: 25676
-author:
-type: other
diff --git a/changelogs/unreleased/use-pg-10-7.yml b/changelogs/unreleased/use-pg-10-7.yml
new file mode 100644
index 00000000000..aa57c3a6a17
--- /dev/null
+++ b/changelogs/unreleased/use-pg-10-7.yml
@@ -0,0 +1,5 @@
+---
+title: Use PostgreSQL 10.7 in tests
+merge_request: 28020
+author:
+type: other
diff --git a/changelogs/unreleased/use-untrusted-regexp.yml b/changelogs/unreleased/use-untrusted-regexp.yml
deleted file mode 100644
index dd7f1bcaca1..00000000000
--- a/changelogs/unreleased/use-untrusted-regexp.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use UntrustedRegexp for matching refs policy
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/weimeng-user-autocomplete-fix.yml b/changelogs/unreleased/weimeng-user-autocomplete-fix.yml
new file mode 100644
index 00000000000..aca9fc4be30
--- /dev/null
+++ b/changelogs/unreleased/weimeng-user-autocomplete-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Only show in autocomplete when author active
+merge_request: 27292
+author:
+type: fixed
diff --git a/changelogs/unreleased/wiki-search-results-fix.yml b/changelogs/unreleased/wiki-search-results-fix.yml
new file mode 100644
index 00000000000..693867eb385
--- /dev/null
+++ b/changelogs/unreleased/wiki-search-results-fix.yml
@@ -0,0 +1,5 @@
+---
+title: fix wiki search result links in titles
+merge_request: 27400
+author: khm
+type: fixed
diff --git a/changelogs/unreleased/winh-toggle-comment-draft.yml b/changelogs/unreleased/winh-toggle-comment-draft.yml
deleted file mode 100644
index 6b4aad55a05..00000000000
--- a/changelogs/unreleased/winh-toggle-comment-draft.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display draft when toggling replies
-merge_request: 25563
-author:
-type: fixed
diff --git a/changelogs/unreleased/xanf-gitlab-ce-add-template-link.yml b/changelogs/unreleased/xanf-gitlab-ce-add-template-link.yml
new file mode 100644
index 00000000000..b868758dcd2
--- /dev/null
+++ b/changelogs/unreleased/xanf-gitlab-ce-add-template-link.yml
@@ -0,0 +1,5 @@
+---
+title: Add instructions on how to contribute a Built-In template for project
+merge_request: 26976
+author:
+type: other
diff --git a/changelogs/unreleased/xanf-gitlab-ce-improve-project-overview.yml b/changelogs/unreleased/xanf-gitlab-ce-improve-project-overview.yml
new file mode 100644
index 00000000000..9755540953a
--- /dev/null
+++ b/changelogs/unreleased/xanf-gitlab-ce-improve-project-overview.yml
@@ -0,0 +1,5 @@
+---
+title: Improve icons and button order in project overview
+merge_request: 26796
+author:
+type: other
diff --git a/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml b/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml
deleted file mode 100644
index 124584c9bd4..00000000000
--- a/changelogs/unreleased/xanf-gitlab-ce-move-project-tags.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move project tags to separate line
-merge_request: 26797
-author:
-type: other
diff --git a/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml b/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml
deleted file mode 100644
index 57c9a1aaa48..00000000000
--- a/changelogs/unreleased/xanf-gitlab-ce-transfer-disables-js.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Group transfer now properly redirects to edit on failure
-merge_request: 26837
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-git-2-21-tests.yml b/changelogs/unreleased/zj-git-2-21-tests.yml
new file mode 100644
index 00000000000..56711379368
--- /dev/null
+++ b/changelogs/unreleased/zj-git-2-21-tests.yml
@@ -0,0 +1,5 @@
+---
+title: Test using Git 2.21
+merge_request: 27418
+author:
+type: added
diff --git a/config/application.rb b/config/application.rb
index cbcfef34e01..cddd91f267a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -164,9 +164,6 @@ module Gitlab
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
- # Can be removed once upgraded to Rails 5.1 or higher
- config.action_controller.raise_on_unfiltered_parameters = true
-
# Nokogiri is significantly faster and uses less memory than REXML
ActiveSupport::XmlMini.backend = 'Nokogiri'
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bdac5b2a6a1..bff809b7661 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -315,6 +315,14 @@ production: &base
# path: shared/registry
# issuer: gitlab-issuer
+
+ ## Error Reporting and Logging with Sentry
+ sentry:
+ # enabled: false
+ # dsn: https://<key>@sentry.io/<project>
+ # clientside_dsn: https://<key>@sentry.io/<project>
+ # environment: 'production' # e.g. development, staging, production
+
#
# 2. GitLab CI settings
# ==========================
@@ -932,6 +940,10 @@ test:
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: { scope: 'aq:name email~rs address aq:push' } }
+ - { name: 'salesforce',
+ app_id: 'YOUR_CLIENT_ID',
+ app_secret: 'YOUR_CLIENT_SECRET'
+ }
ldap:
enabled: false
servers:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 3c426cdb969..d56bd7654af 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -136,6 +136,8 @@ Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
Settings.gitlab['port'] ||= ENV['GITLAB_PORT'] || (Settings.gitlab.https ? 443 : 80)
Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || ''
+# / is not a valid relative URL root
+Settings.gitlab['relative_url_root'] = '' if Settings.gitlab['relative_url_root'] == '/'
Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
@@ -216,6 +218,15 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = Settings.absolute(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'))
#
+# Error Reporting and Logging with Sentry
+#
+Settings['sentry'] ||= Settingslogic.new({})
+Settings.sentry['enabled'] ||= false
+Settings.sentry['dsn'] ||= nil
+Settings.sentry['environment'] ||= nil
+Settings.sentry['clientside_dsn'] ||= nil
+
+#
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
@@ -338,6 +349,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
+Settings.cron_jobs['pages_domain_removal_cron_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *'
+Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker'
+
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
diff --git a/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb b/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
deleted file mode 100644
index 228ced32188..00000000000
--- a/config/initializers/active_record_avoid_type_casting_in_uniqueness_validator.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# This is a monkey patch which must be removed when migrating to Rails 5.1 from 5.0.
-#
-# In Rails 5.0 there was introduced a bug which casts types in the uniqueness validator.
-# https://github.com/rails/rails/pull/23523/commits/811a4fa8eb6ceea841e61e8ac05747ffb69595ae
-#
-# That causes to bugs like this:
-#
-# 1) API::Users POST /user/:id/gpg_keys/:key_id/revoke when authenticated revokes existing key
-# Failure/Error: let(:gpg_key) { create(:gpg_key, user: user) }
-#
-# TypeError:
-# can't cast Hash
-# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
-# # ./spec/requests/api/users_spec.rb:908:in `block (4 levels) in <top (required)>'
-# # ------------------
-# # --- Caused by: ---
-# # TypeError:
-# # TypeError
-# # ./spec/requests/api/users_spec.rb:7:in `block (2 levels) in <top (required)>'
-#
-# This bug was fixed in Rails 5.1 by https://github.com/rails/rails/pull/24745/commits/aa062318c451512035c10898a1af95943b1a3803
-
-if Rails.gem_version >= Gem::Version.new("5.1")
- raise "Remove this monkey patch: #{__FILE__}"
-end
-
-# Copy-paste from https://github.com/kamipo/rails/blob/aa062318c451512035c10898a1af95943b1a3803/activerecord/lib/active_record/validations/uniqueness.rb
-# including local fixes to make Rubocop happy again.
-module ActiveRecord
- module Validations
- class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
- def validate_each(record, attribute, value)
- finder_class = find_finder_class_for(record)
- table = finder_class.arel_table
- value = map_enum_attribute(finder_class, attribute, value)
-
- relation = build_relation(finder_class, table, attribute, value)
-
- if record.persisted?
- if finder_class.primary_key
- relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
- else
- raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
- end
- end
-
- relation = scope_relation(record, table, relation)
- relation = relation.merge(options[:conditions]) if options[:conditions]
-
- if relation.exists?
- error_options = options.except(:case_sensitive, :scope, :conditions)
- error_options[:value] = value
-
- record.errors.add(attribute, :taken, error_options)
- end
- rescue RangeError
- end
-
- protected
-
- def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass._reflect_on_association(attribute)
- attribute = reflection.foreign_key
- value = value.attributes[reflection.klass.primary_key] unless value.nil?
- end
-
- # the attribute may be an aliased attribute
- if klass.attribute_alias?(attribute)
- attribute = klass.attribute_alias(attribute)
- end
-
- attribute_name = attribute.to_s
-
- column = klass.columns_hash[attribute_name]
- cast_type = klass.type_for_attribute(attribute_name)
-
- comparison =
- if !options[:case_sensitive] && !value.nil?
- # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- klass.connection.case_insensitive_comparison(table, attribute, column, value)
- else
- klass.connection.case_sensitive_comparison(table, attribute, column, value)
- end
-
- if value.nil?
- klass.unscoped.where(comparison)
- else
- bind = Relation::QueryAttribute.new(attribute_name, value, cast_type)
- klass.unscoped.where(comparison, bind)
- end
- end
- end
- end
-end
diff --git a/config/initializers/active_record_build_select.rb b/config/initializers/active_record_build_select.rb
new file mode 100644
index 00000000000..ab5a872cac6
--- /dev/null
+++ b/config/initializers/active_record_build_select.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+# build_select only selects the required fields if the model has ignored_columns.
+# This is incompatible with some migrations or background migration specs because
+# rails keeps a statement cache in memory. So if a model with ignored_columns in a
+# migration is used, the query with select table.col1, table.col2 is stored in the
+# statement cache. If a different migration is then run and one of these columns is
+# removed in the meantime, the query is invalid.
+
+module ActiveRecord
+ module QueryMethods
+ private
+
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
+ else
+ arel.project(@klass.arel_table[Arel.star])
+ end
+ end
+ end
+end
diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb
deleted file mode 100644
index 1bd1a12e4b7..00000000000
--- a/config/initializers/active_record_locking.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-# rubocop:disable Lint/RescueException
-
-# Remove this monkey patch when we move to Rails 5.1, because the bug has been fixed in https://github.com/rails/rails/pull/26050.
-if Rails.gem_version >= Gem::Version.new("5.1")
- raise "Remove this monkey patch: #{__FILE__}"
-end
-
-module ActiveRecord
- module Locking
- module Optimistic
- # We overwrite this method because we don't want to have default value
- # for newly created records
- def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
- super
- end
-
- def _update_record(attribute_names = self.attribute_names) #:nodoc:
- return super unless locking_enabled?
- return 0 if attribute_names.empty?
-
- lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i
- increment_lock
-
- attribute_names += [lock_col]
- attribute_names.uniq!
-
- begin
- relation = self.class.unscoped
-
- affected_rows = relation.where(
- self.class.primary_key => id,
- # Patched because when `lock_version` is read as `0`, it may actually be `NULL` in the DB.
- lock_col => previous_lock_value == 0 ? [nil, 0] : previous_lock_value
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
- )
-
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
- end
-
- affected_rows
-
- # If something went wrong, revert the version.
- rescue Exception
- send(lock_col + '=', previous_lock_value)
- raise
- end
- end
-
- # This is patched because we need it to query `lock_version IS NULL`
- # rather than `lock_version = 0` whenever lock_version is NULL.
- def relation_for_destroy
- return super unless locking_enabled?
-
- column_name = self.class.locking_column
- super.where(self.class.arel_table[column_name].eq(self[column_name]))
- end
- end
-
- # This is patched because we want `lock_version` default to `NULL`
- # rather than `0`
- class LockingType
- def deserialize(value)
- super
- end
-
- def serialize(value)
- super
- end
- end
- end
-end
diff --git a/config/initializers/ar_native_database_types.rb b/config/initializers/ar_native_database_types.rb
index 3522b1db536..6d397661f75 100644
--- a/config/initializers/ar_native_database_types.rb
+++ b/config/initializers/ar_native_database_types.rb
@@ -4,7 +4,8 @@ module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter
NATIVE_DATABASE_TYPES.merge!(
- bigserial: { name: 'bigint(20) auto_increment PRIMARY KEY' }
+ bigserial: { name: 'bigint(20) auto_increment PRIMARY KEY' },
+ serial: { name: 'int auto_increment PRIMARY KEY' }
)
end
end
diff --git a/config/initializers/config_initializers_active_record_locking.rb b/config/initializers/config_initializers_active_record_locking.rb
new file mode 100644
index 00000000000..608d63223a3
--- /dev/null
+++ b/config/initializers/config_initializers_active_record_locking.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# ensure ActiveRecord's version has been required already
+require 'active_record/locking/optimistic'
+
+# rubocop:disable Lint/RescueException
+module ActiveRecord
+ module Locking
+ module Optimistic
+ private
+
+ def _update_row(attribute_names, attempted_action = "update")
+ return super unless locking_enabled?
+
+ begin
+ locking_column = self.class.locking_column
+ previous_lock_value = read_attribute_before_type_cast(locking_column)
+ attribute_names << locking_column
+
+ self[locking_column] += 1
+
+ # Patched because when `lock_version` is read as `0`, it may actually be `NULL` in the DB.
+ possible_previous_lock_value = previous_lock_value.to_i == 0 ? [nil, 0] : previous_lock_value
+
+ affected_rows = self.class.unscoped._update_record(
+ arel_attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database,
+ locking_column => possible_previous_lock_value
+ )
+
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
+ end
+
+ affected_rows
+
+ # If something went wrong, revert the locking_column value.
+ rescue Exception
+ self[locking_column] = previous_lock_value.to_i
+ raise
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index c1342f48ebd..714dd505824 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -2,7 +2,7 @@ require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
require 'json'
-VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/
+VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/.freeze
module GettextI18nRails
class HamlParser
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 5e790a9eccb..1ad93e14f7e 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -35,7 +35,7 @@ unless Sidekiq.server?
end
payload[:response] = event.payload[:response] if event.payload[:response]
- payload[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
+ payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
payload
end
diff --git a/config/initializers/peek.rb b/config/initializers/peek.rb
index e051e5c68c4..cb108416b10 100644
--- a/config/initializers/peek.rb
+++ b/config/initializers/peek.rb
@@ -10,6 +10,18 @@ elsif Gitlab::Database.postgresql?
require 'peek-pg'
PEEK_DB_CLIENT = ::PG::Connection
PEEK_DB_VIEW = Peek::Views::PG
+
+ # Remove once we have https://github.com/peek/peek-pg/pull/10
+ module ::Peek::PGInstrumented
+ def exec_params(*args)
+ start = Time.now
+ super(*args)
+ ensure
+ duration = (Time.now - start)
+ PEEK_DB_CLIENT.query_time.update { |value| value + duration }
+ PEEK_DB_CLIENT.query_count.update { |value| value + 1 }
+ end
+ end
else
raise "Unsupported database adapter for peek!"
end
@@ -19,7 +31,7 @@ Peek.into Peek::Views::Gitaly
Peek.into Peek::Views::Rblineprof
Peek.into Peek::Views::Redis
Peek.into Peek::Views::GC
-Peek.into Peek::Views::Tracing if Gitlab::Tracing.tracing_url_enabled?
+Peek.into Peek::Views::Tracing if Labkit::Tracing.tracing_url_enabled?
# rubocop:disable Naming/ClassAndModuleCamelCase
class PEEK_DB_CLIENT
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
index b066f3788ec..7e912180820 100644
--- a/config/initializers/postgresql_opclasses_support.rb
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -78,7 +78,7 @@ module ActiveRecord
if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
- if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ if data_source_exists?(table_name) && index_name_exists?(table_name, index_name)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 680cfa6f0ed..e5589ce0ad1 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -14,6 +14,7 @@ def configure_sentry
Raven.configure do |config|
config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
config.release = Gitlab.revision
+ config.current_environment = Gitlab.config.sentry.environment.presence
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2e4aa9c1053..7b69cf11288 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,5 +1,17 @@
require 'sidekiq/web'
+def enable_reliable_fetch?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_reliable_fetcher, default_enabled: true)
+end
+
+def enable_semi_reliable_fetch_mode?
+ return true unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+end
+
# Disable the Sidekiq Rack session since GitLab already has its own session store.
# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
Sidekiq::Web.set :sessions, false
@@ -45,9 +57,8 @@ Sidekiq.configure_server do |config|
ActiveRecord::Base.clear_all_connections!
end
- if Feature::FlipperFeature.table_exists? && Feature.enabled?(:gitlab_sidekiq_reliable_fetcher)
- # By default we're going to use Semi Reliable Fetch
- config.options[:semi_reliable_fetch] = Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
+ if enable_reliable_fetch?
+ config.options[:semi_reliable_fetch] = enable_semi_reliable_fetch_mode?
Sidekiq::ReliableFetch.setup_reliable_fetch!(config)
end
diff --git a/config/initializers/tracing.rb b/config/initializers/tracing.rb
index ddd91150c90..3c8779f238f 100644
--- a/config/initializers/tracing.rb
+++ b/config/initializers/tracing.rb
@@ -1,16 +1,14 @@
# frozen_string_literal: true
-if Gitlab::Tracing.enabled?
- require 'opentracing'
-
+if Labkit::Tracing.enabled?
Rails.application.configure do |config|
- config.middleware.insert_after Gitlab::Middleware::CorrelationId, ::Gitlab::Tracing::RackMiddleware
+ config.middleware.insert_after Gitlab::Middleware::CorrelationId, ::Labkit::Tracing::RackMiddleware
end
# Instrument the Sidekiq client
Sidekiq.configure_client do |config|
config.client_middleware do |chain|
- chain.add Gitlab::Tracing::Sidekiq::ClientMiddleware
+ chain.add Labkit::Tracing::Sidekiq::ClientMiddleware
end
end
@@ -18,20 +16,20 @@ if Gitlab::Tracing.enabled?
if Sidekiq.server?
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
- chain.add Gitlab::Tracing::Sidekiq::ServerMiddleware
+ chain.add Labkit::Tracing::Sidekiq::ServerMiddleware
end
end
end
# Instrument Rails
- Gitlab::Tracing::Rails::ActiveRecordSubscriber.instrument
- Gitlab::Tracing::Rails::ActionViewSubscriber.instrument
+ Labkit::Tracing::Rails::ActiveRecordSubscriber.instrument
+ Labkit::Tracing::Rails::ActionViewSubscriber.instrument
# In multi-processed clustered architectures (puma, unicorn) don't
# start tracing until the worker processes are spawned. This works
# around issues when the opentracing implementation spawns threads
Gitlab::Cluster::LifecycleEvents.on_worker_start do
- tracer = Gitlab::Tracing::Factory.create_tracer(Gitlab.process_name, Gitlab::Tracing.connection_string)
+ tracer = Labkit::Tracing::Factory.create_tracer(Gitlab.process_name, Labkit::Tracing.connection_string)
OpenTracing.global_tracer = tracer if tracer
end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 7e1e89f3c10..83ba46345f2 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -4,6 +4,7 @@ const chalk = require('chalk');
const webpack = require('webpack');
const argumentsParser = require('commander');
const webpackConfig = require('./webpack.config.js');
+const IS_EE = require('./helpers/is_ee_env');
const ROOT_PATH = path.resolve(__dirname, '..');
const SPECS_PATH = /^(?:\.[\\\/])?(ee[\\\/])?spec[\\\/]javascripts[\\\/]/;
@@ -90,6 +91,8 @@ if (specFilters.length) {
module.exports = function(config) {
process.env.TZ = 'Etc/UTC';
+ const fixturesPath = `${IS_EE ? 'ee/' : ''}spec/javascripts/fixtures`;
+
const karmaConfig = {
basePath: ROOT_PATH,
browsers: ['ChromeHeadlessCustom'],
@@ -110,7 +113,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],
files: [
{ pattern: 'spec/javascripts/test_bundle.js', watched: false },
- { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.png)', included: false },
+ { pattern: `${fixturesPath}/**/*@(.json|.html|.png|.bmpr|.pdf)`, included: false },
],
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
diff --git a/config/locales/en.yml b/config/locales/en.yml
index eb3b7771968..a3dceb2fb62 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -14,6 +14,8 @@ en:
token: "Auth Token"
project: "Project"
api_url: "Sentry API URL"
+ project/metrics_setting:
+ external_dashboard_url: "External dashboard URL"
errors:
messages:
label_already_exists_at_group_level: "already exists at group level for %{group}. Please choose another one."
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 884868c6336..3c67ca36791 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -1,42 +1,33 @@
- # NGINX Ingress metrics for pre-0.16.0 versions
+dashboard: 'Environment metrics'
+priority: 1
+panel_groups:
+# NGINX Ingress metrics for pre-0.16.0 versions
- group: Response metrics (NGINX Ingress VTS)
priority: 10
- metrics:
+ panels:
- title: "Throughput"
+ type: "area-chart"
y_label: "Requests / Sec"
- required_metrics:
- - nginx_upstream_responses_total
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_throughput_status_code
query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)'
unit: req / sec
label: Status Code
- series:
- - label: status_code
- when:
- - value: 2xx
- color: green
- - value: 4xx
- color: orange
- - value: 5xx
- color: red
- title: "Latency"
+ type: "area-chart"
y_label: "Latency (ms)"
- required_metrics:
- - nginx_upstream_response_msecs_avg
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_latency_pod_average
query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
label: Pod average
unit: ms
- title: "HTTP Error Rate"
+ type: "area-chart"
y_label: "HTTP Errors"
- required_metrics:
- - nginx_upstream_responses_total
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_http_error_rate
query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100'
label: 5xx Errors
@@ -44,228 +35,180 @@
# NGINX Ingress metrics for post-0.16.0 versions
- group: Response metrics (NGINX Ingress)
priority: 10
- metrics:
+ panels:
- title: "Throughput"
+ type: "area-chart"
y_label: "Requests / Sec"
- required_metrics:
- - nginx_ingress_controller_requests
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_16_throughput_status_code
query_range: 'sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code)'
unit: req / sec
label: Status Code
- series:
- - label: status_code
- when:
- - value: 2xx
- color: green
- - value: 3xx
- color: blue
- - value: 4xx
- color: orange
- - value: 5xx
- color: red
- title: "Latency"
+ type: "area-chart"
y_label: "Latency (ms)"
- required_metrics:
- - nginx_ingress_controller_ingress_upstream_latency_seconds_sum
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_16_latency_pod_average
query_range: 'sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000'
label: Pod average
unit: ms
- title: "HTTP Error Rate"
+ type: "area-chart"
y_label: "HTTP Errors"
- required_metrics:
- - nginx_ingress_controller_requests
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_ingress_16_http_error_rate
query_range: 'sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100'
label: 5xx Errors
unit: "%"
- group: Response metrics (HA Proxy)
priority: 10
- metrics:
+ panels:
- title: "Throughput"
+ type: "area-chart"
y_label: "Requests / Sec"
- required_metrics:
- - haproxy_frontend_http_requests_total
weight: 1
- queries:
+ metrics:
- id: response_metrics_ha_proxy_throughput_status_code
query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)'
unit: req / sec
label: Status Code
- series:
- - label: status_code
- when:
- - value: 2xx
- color: green
- - value: 4xx
- color: yellow
- - value: 5xx
- color: red
- title: "HTTP Error Rate"
+ type: "area-chart"
y_label: "Error Rate (%)"
- required_metrics:
- - haproxy_frontend_http_responses_total
weight: 1
- queries:
+ metrics:
- id: response_metrics_ha_proxy_http_error_rate
query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m]))'
label: HTTP Errors
unit: "%"
- group: Response metrics (AWS ELB)
priority: 10
- metrics:
+ panels:
- title: "Throughput"
+ type: "area-chart"
y_label: "Requests / Sec"
- required_metrics:
- - aws_elb_request_count_sum
weight: 1
- queries:
+ metrics:
- id: response_metrics_aws_elb_throughput_requests
query_range: 'sum(aws_elb_request_count_sum{%{environment_filter}}) / 60'
label: Total
unit: req / sec
- title: "Latency"
+ type: "area-chart"
y_label: "Latency (ms)"
- required_metrics:
- - aws_elb_latency_average
weight: 1
- queries:
+ metrics:
- id: response_metrics_aws_elb_latency_average
query_range: 'avg(aws_elb_latency_average{%{environment_filter}}) * 1000'
label: Average
unit: ms
- title: "HTTP Error Rate"
+ type: "area-chart"
y_label: "Error Rate (%)"
- required_metrics:
- - aws_elb_request_count_sum
- - aws_elb_httpcode_backend_5_xx_sum
weight: 1
- queries:
+ metrics:
- id: response_metrics_aws_elb_http_error_rate
query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{%{environment_filter}}) / sum(aws_elb_request_count_sum{%{environment_filter}})'
label: HTTP Errors
unit: "%"
- group: Response metrics (NGINX)
priority: 10
- metrics:
+ panels:
- title: "Throughput"
+ type: "area-chart"
y_label: "Requests / Sec"
- required_metrics:
- - nginx_server_requests
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_throughput_status_code
query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)'
unit: req / sec
label: Status Code
- series:
- - label: status_code
- when:
- - value: 2xx
- color: green
- - value: 4xx
- color: orange
- - value: 5xx
- color: red
- title: "Latency"
+ type: "area-chart"
y_label: "Latency (ms)"
- required_metrics:
- - nginx_server_requestMsec
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_latency
query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})'
label: Upstream
unit: ms
- title: "HTTP Error Rate"
+ type: "area-chart"
y_label: "HTTP 500 Errors / Sec"
- required_metrics:
- - nginx_server_requests
weight: 1
- queries:
+ metrics:
- id: response_metrics_nginx_http_error_rate
query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
unit: "errors / sec"
- group: System metrics (Kubernetes)
priority: 5
- metrics:
+ panels:
- title: "Memory Usage (Total)"
+ type: "area-chart"
y_label: "Total Memory Used"
- required_metrics:
- - container_memory_usage_bytes
weight: 4
- queries:
+ 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
- title: "Core Usage (Total)"
+ type: "area-chart"
y_label: "Total Cores"
- required_metrics:
- - container_cpu_usage_seconds_total
weight: 3
- queries:
+ 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"
- title: "Memory Usage (Pod average)"
+ type: "area-chart"
y_label: "Memory Used per Pod"
- required_metrics:
- - container_memory_usage_bytes
weight: 2
- queries:
+ 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
- title: "Canary: Memory Usage (Pod Average)"
+ type: "area-chart"
y_label: "Memory Used per Pod"
- required_metrics:
- - container_memory_usage_bytes
weight: 2
- queries:
+ metrics:
- id: system_metrics_kubernetes_container_memory_average_canary
query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Pod average
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
+ type: "area-chart"
y_label: "Cores per Pod"
- required_metrics:
- - container_cpu_usage_seconds_total
weight: 1
- queries:
+ metrics:
- id: system_metrics_kubernetes_container_core_usage
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
+ type: "area-chart"
y_label: "Cores per Pod"
- required_metrics:
- - container_cpu_usage_seconds_total
weight: 1
- queries:
+ metrics:
- id: system_metrics_kubernetes_container_core_usage_canary
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Pod average
unit: "cores"
track: canary
- title: "Knative function invocations"
+ type: "area-chart"
y_label: "Invocations"
- required_metrics:
- - istio_revision_request_count
weight: 1
- queries:
+ metrics:
- id: system_metrics_knative_function_invocation_count
- query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))'
+ query_range: 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])/3))'
label: invocations / minute
unit: requests
diff --git a/config/routes.rb b/config/routes.rb
index bbf00208545..f5957f43655 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,7 @@ Rails.application.routes.draw do
scope :applications do
post '/:application', to: 'clusters/applications#create', as: :install_applications
patch '/:application', to: 'clusters/applications#update', as: :update_applications
+ delete '/:application', to: 'clusters/applications#destroy', as: :uninstall_applications
end
get :cluster_status, format: :json
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index a01003b6039..90d7f4a04d4 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -132,5 +132,7 @@ namespace :admin do
end
end
+ concerns :clusterable
+
root to: 'dashboard#index'
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 93d168fc595..93d746f3282 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -201,6 +201,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :failures
get :status
end
+
+ member do
+ resources :stages, only: [], param: :name do
+ post :play_manual
+ end
+ end
end
resources :pipeline_schedules, except: [:show] do
@@ -218,6 +224,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :terminal
get :metrics
get :additional_metrics
+ get :metrics_dashboard
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
get '/prometheus/api/v1/*proxy_path', to: 'environments/prometheus_api#proxy'
@@ -360,7 +367,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :toggle_subscription
post :mark_as_spam
post :move
- get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :realtime_changes
diff --git a/danger/ce_ee_vue_templates/Dangerfile b/danger/ce_ee_vue_templates/Dangerfile
new file mode 100644
index 00000000000..f7715eb2a89
--- /dev/null
+++ b/danger/ce_ee_vue_templates/Dangerfile
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+require 'cgi'
+
+def get_vue_files_with_ce_and_ee_versions(files)
+ files.select do |file|
+ if file.end_with?('.vue')
+ counterpart_path = if file.start_with?('ee/')
+ file.delete_prefix('ee/')
+ else
+ "ee/#{file}"
+ end
+
+ escaped_path = CGI.escape(counterpart_path)
+ api_endpoint = "https://gitlab.com/api/v4/projects/gitlab-org%2Fgitlab-ee/repository/files/#{escaped_path}?ref=master"
+ response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty
+ response.code != 404
+ else
+ false
+ end
+ end
+end
+
+vue_candidates = get_vue_files_with_ce_and_ee_versions(helper.all_changed_files)
+
+return if vue_candidates.empty?
+
+message 'This merge request includes changes to Vue files that have both CE and EE versions.'
+
+markdown(<<~MARKDOWN)
+ ## Vue `<template>` in CE and EE
+
+ Some Vue files in CE have a counterpart in EE.
+ (For example, `path/to/file.vue` and `ee/path/to/file.vue`.)
+
+ When run in the context of CE, the `<template>` of the CE Vue file is used.
+ When run in the context of EE, the `<template>` of the EE Vue file is used.
+
+ It's easy to accidentally make a change to a CE `<template>` that _should_
+ appear in both CE and EE without making the change in both places.
+ When this happens, the change only takes effect in CE.
+
+ The following Vue files were changed as part of this merge request that
+ include both a CE and EE version of the file:
+
+ * #{vue_candidates.map { |path| "`#{path}`" }.join("\n* ")}
+
+ If you made a change to the `<template>` of any of these Vue files that
+ should be visible in both CE and EE, please ensure you have made your
+ change to both versions of the file.
+
+ ### A better alternative
+
+ An even _better_ alternative is to refactor this component to only use
+ a single template for both CE and EE. More info on this approach here:
+ https://docs.gitlab.com/ee/development/ee_features.html#template-tag
+MARKDOWN
diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile
index 9be1ce2ff86..048c539bcf9 100644
--- a/danger/commit_messages/Dangerfile
+++ b/danger/commit_messages/Dangerfile
@@ -21,7 +21,7 @@ class EmojiChecker
# alone is not enough, as we'd match `:foo:bar:baz`. Instead, we use this
# regex to save us from having to check for all possible emoji names when we
# know one definitely is not included.
- LIKELY_EMOJI = /:[\+a-z0-9_\-]+:/
+ LIKELY_EMOJI = /:[\+a-z0-9_\-]+:/.freeze
def initialize
names = JSON.parse(File.read(DIGESTS)).keys +
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index e6820f49ee2..62e5526c02b 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -31,26 +31,54 @@ Please consider creating a merge request to
for them.
MARKDOWN
-def spin(team, project, category, branch_name)
+def spin_for_category(team, project, category, branch_name)
rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16))
reviewers = team.select { |member| member.reviewer?(project, category) }
traintainers = team.select { |member| member.traintainer?(project, category) }
maintainers = team.select { |member| member.maintainer?(project, category) }
- # TODO: filter out people who are currently not in the office
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/57652
- #
# TODO: take CODEOWNERS into account?
# https://gitlab.com/gitlab-org/gitlab-ce/issues/57653
# Make traintainers have triple the chance to be picked as a reviewer
- reviewer = (reviewers + traintainers + traintainers).sample(random: rng)
- maintainer = maintainers.sample(random: rng)
+ reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng)
+ maintainer = spin_for_person(maintainers, random: rng)
"| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |"
end
+# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
+# selection will change on next spin
+def spin_for_person(people, random:)
+ person = nil
+ people = people.dup
+
+ people.size.times do
+ person = people.sample(random: random)
+
+ break person unless out_of_office?(person)
+
+ people -= [person]
+ end
+
+ person
+end
+
+def out_of_office?(person)
+ username = CGI.escape(person.username)
+ api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status"
+ response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty
+
+ if response.code == 200
+ response["message"]&.match(/OOO/i)
+ else
+ false # this is no worse than not checking for OOO
+ end
+rescue
+ false
+end
+
def build_list(items)
list = items.map { |filename| "* `#{filename}`" }.join("\n")
@@ -63,7 +91,7 @@ end
changes = helper.changes_by_category
-# Ignore any files that are known but uncategoried. Prompt for any unknown files
+# Ignore any files that are known but uncategorized. Prompt for any unknown files
changes.delete(:none)
categories = changes.keys - [:unknown]
@@ -92,7 +120,7 @@ if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_l
project = helper.project_name
unknown = changes.fetch(:unknown, [])
- rows = categories.map { |category| spin(team, project, category, canonical_branch_name) }
+ rows = categories.map { |category| spin_for_category(team, project, category, canonical_branch_name) }
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 926401d8b9e..582a5203d1d 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -1,23 +1,5 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- Project.all.each do |project|
- 10.times do
- label_ids = project.labels.pluck(:id).sample(3)
- label_ids += project.group.labels.sample(3) if project.group
-
- issue_params = {
- title: FFaker::Lorem.sentence(6),
- description: FFaker::Lorem.sentence,
- state: ['opened', 'closed'].sample,
- milestone: project.milestones.sample,
- assignees: [project.team.users.sample],
- created_at: rand(12).months.ago,
- label_ids: label_ids
- }
-
- Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
- print '.'
- end
- end
+ Rake::Task["gitlab:seed:issues"].invoke
end
diff --git a/db/importers/common_metrics_importer.rb b/db/importers/common_metrics_importer.rb
index deadd653ae9..195bde8f34a 100644
--- a/db/importers/common_metrics_importer.rb
+++ b/db/importers/common_metrics_importer.rb
@@ -53,7 +53,7 @@ module Importers
private
def process_content(&blk)
- content.map do |group|
+ content['panel_groups'].map do |group|
process_group(group, &blk)
end
end
@@ -63,28 +63,28 @@ module Importers
group: find_group_title_key(group['group'])
}
- group['metrics'].map do |metric|
- process_metric(metric, attributes, &blk)
+ group['panels'].map do |panel|
+ process_panel(panel, attributes, &blk)
end
end
- def process_metric(metric, attributes, &blk)
+ def process_panel(panel, attributes, &blk)
attributes = attributes.merge(
- title: metric['title'],
- y_label: metric['y_label'])
+ title: panel['title'],
+ y_label: panel['y_label'])
- metric['queries'].map do |query|
- process_metric_query(query, attributes, &blk)
+ panel['metrics'].map do |metric_details|
+ process_metric_details(metric_details, attributes, &blk)
end
end
- def process_metric_query(query, attributes, &blk)
+ def process_metric_details(metric_details, attributes, &blk)
attributes = attributes.merge(
- legend: query['label'],
- query: query['query_range'],
- unit: query['unit'])
+ legend: metric_details['label'],
+ query: metric_details['query_range'],
+ unit: metric_details['unit'])
- yield(query['id'], attributes)
+ yield(metric_details['id'], attributes)
end
def find_or_build_metric!(id)
diff --git a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
index 148b46f8830..1f9ae3f0080 100644
--- a/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
+++ b/db/migrate/20140414131055_change_state_to_allow_empty_merge_request_diffs.rb
@@ -1,4 +1,3 @@
-# rubocop:disable all
class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration[4.2]
def up
change_column :merge_request_diffs, :state, :string, null: true,
diff --git a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
index 23a775d6282..9005b42b41f 100644
--- a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
+++ b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
@@ -9,11 +9,11 @@ class AddUniqueIndexToSubscriptions < ActiveRecord::Migration[4.2]
def up
add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id, :project_id], { unique: true, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' }
- remove_index :subscriptions, name: 'subscriptions_user_id_and_ref_fields' if index_name_exists?(:subscriptions, 'subscriptions_user_id_and_ref_fields', false)
+ remove_index :subscriptions, name: 'subscriptions_user_id_and_ref_fields' if index_name_exists?(:subscriptions, 'subscriptions_user_id_and_ref_fields')
end
def down
add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], { unique: true, name: 'subscriptions_user_id_and_ref_fields' }
- remove_index :subscriptions, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' if index_name_exists?(:subscriptions, 'index_subscriptions_on_subscribable_and_user_id_and_project_id', false)
+ remove_index :subscriptions, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' if index_name_exists?(:subscriptions, 'index_subscriptions_on_subscribable_and_user_id_and_project_id')
end
end
diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
index cfa63b65df4..65b2c6a57be 100644
--- a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
+++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
@@ -46,13 +46,6 @@ class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration[4.2]
bulk_insert_members(rows)
- # This method relies on the parent to determine the proper path.
- # Because we reset "parent_id" this method will not return the right path
- # when moving namespaces.
- full_path_was = namespace.send(:full_path_was)
-
- namespace.define_singleton_method(:full_path_was) { full_path_was }
-
namespace.update!(parent_id: nil, path: new_path_for(namespace))
end
end
diff --git a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
new file mode 100644
index 00000000000..e9cf2af84a5
--- /dev/null
+++ b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
@@ -0,0 +1,15 @@
+# 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 AddLetsEncryptNotificationEmailToApplicationSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :lets_encrypt_notification_email, :string
+ end
+end
diff --git a/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb b/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb
new file mode 100644
index 00000000000..e74a9535ddf
--- /dev/null
+++ b/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddAutoSslEnabledToPagesDomain < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :pages_domains, :auto_ssl_enabled, :boolean, default: false
+ end
+
+ def down
+ remove_column :pages_domains, :auto_ssl_enabled
+ end
+end
diff --git a/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
new file mode 100644
index 00000000000..16de63f207f
--- /dev/null
+++ b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_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 AddLetsEncryptTermsOfServiceAcceptedToApplicationSettings < ActiveRecord::Migration[5.0]
+ 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(:application_settings, :lets_encrypt_terms_of_service_accepted, :boolean, default: false)
+ end
+
+ def down
+ remove_column :application_settings, :lets_encrypt_terms_of_service_accepted
+ end
+end
diff --git a/db/migrate/20190408163745_prometheus_knative05_fix.rb b/db/migrate/20190408163745_prometheus_knative05_fix.rb
new file mode 100644
index 00000000000..c11f6f0e29b
--- /dev/null
+++ b/db/migrate/20190408163745_prometheus_knative05_fix.rb
@@ -0,0 +1,20 @@
+# 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 PrometheusKnative05Fix < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ require Rails.root.join('db/importers/common_metrics_importer.rb')
+
+ DOWNTIME = false
+
+ def up
+ Importers::CommonMetricsImporter.new.execute
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb b/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb
new file mode 100644
index 00000000000..433f510299a
--- /dev/null
+++ b/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddVariableTypeToCiVariables < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ ENV_VAR_VARIABLE_TYPE = 1
+
+ def up
+ add_column_with_default(:ci_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ end
+
+ def down
+ remove_column(:ci_variables, :variable_type)
+ end
+end
diff --git a/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb b/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb
new file mode 100644
index 00000000000..99625981563
--- /dev/null
+++ b/db/migrate/20190415095825_add_packages_size_to_project_statistics.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddPackagesSizeToProjectStatistics < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :project_statistics, :packages_size, :bigint
+ end
+end
diff --git a/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
new file mode 100644
index 00000000000..eb0e7c41f98
--- /dev/null
+++ b/db/migrate/20190416185130_add_merge_train_enabled_to_ci_cd_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddMergeTrainEnabledToCiCdSettings < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :project_ci_cd_settings, :merge_trains_enabled
+ end
+end
diff --git a/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb b/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb
new file mode 100644
index 00000000000..dce73caeb5e
--- /dev/null
+++ b/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddVariableTypeToCiGroupVariables < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ ENV_VAR_VARIABLE_TYPE = 1
+
+ def up
+ add_column_with_default(:ci_group_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ end
+
+ def down
+ remove_column(:ci_group_variables, :variable_type)
+ end
+end
diff --git a/db/migrate/20190416213615_add_variable_type_to_ci_pipeline_variables.rb b/db/migrate/20190416213615_add_variable_type_to_ci_pipeline_variables.rb
new file mode 100644
index 00000000000..1010d9bd29e
--- /dev/null
+++ b/db/migrate/20190416213615_add_variable_type_to_ci_pipeline_variables.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddVariableTypeToCiPipelineVariables < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ ENV_VAR_VARIABLE_TYPE = 1
+
+ def up
+ add_column_with_default(:ci_pipeline_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ end
+
+ def down
+ remove_column(:ci_pipeline_variables, :variable_type)
+ end
+end
diff --git a/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb b/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb
new file mode 100644
index 00000000000..3079b2afd9c
--- /dev/null
+++ b/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddVariableTypeToCiPipelineScheduleVariables < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ ENV_VAR_VARIABLE_TYPE = 1
+
+ def up
+ add_column_with_default(:ci_pipeline_schedule_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ end
+
+ def down
+ remove_column(:ci_pipeline_schedule_variables, :variable_type)
+ end
+end
diff --git a/db/migrate/20190418182545_create_merge_request_trains_table.rb b/db/migrate/20190418182545_create_merge_request_trains_table.rb
new file mode 100644
index 00000000000..ac927c9c6b9
--- /dev/null
+++ b/db/migrate/20190418182545_create_merge_request_trains_table.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class CreateMergeRequestTrainsTable < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :merge_trains, id: :bigserial do |t|
+ t.references :merge_request, foreign_key: { on_delete: :cascade }, type: :integer, index: false, null: false
+ t.references :user, foreign_key: { on_delete: :cascade }, type: :integer, null: false
+ t.references :pipeline, foreign_key: { to_table: :ci_pipelines, on_delete: :nullify }, type: :integer
+ t.timestamps_with_timezone null: false
+
+ t.index [:merge_request_id], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20190422082247_create_project_metrics_settings.rb b/db/migrate/20190422082247_create_project_metrics_settings.rb
new file mode 100644
index 00000000000..3e21dd0a934
--- /dev/null
+++ b/db/migrate/20190422082247_create_project_metrics_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateProjectMetricsSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_metrics_settings, id: :int, primary_key: :project_id, default: nil do |t|
+ t.string :external_dashboard_url, null: false
+ t.foreign_key :projects, column: :project_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20190426180107_add_deployment_events_to_services.rb b/db/migrate/20190426180107_add_deployment_events_to_services.rb
new file mode 100644
index 00000000000..1fb137fb5f9
--- /dev/null
+++ b/db/migrate/20190426180107_add_deployment_events_to_services.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddDeploymentEventsToServices < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :deployment_events, :boolean, default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :deployment_events)
+ end
+end
diff --git a/db/migrate/20190506135337_add_temporary_indexes_to_state_id.rb b/db/migrate/20190506135337_add_temporary_indexes_to_state_id.rb
new file mode 100644
index 00000000000..8e9838e1afb
--- /dev/null
+++ b/db/migrate/20190506135337_add_temporary_indexes_to_state_id.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# This migration adds temporary indexes to state_id column of issues
+# and merge_requests tables. It will be used only to peform the scheduling
+# for populating state_id in a post migrate and will be removed after it.
+# Check: ScheduleSyncIssuablesStateIdWhereNil.
+
+class AddTemporaryIndexesToStateId < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ %w(issues merge_requests).each do |table|
+ add_concurrent_index(
+ table,
+ 'id',
+ name: index_name_for(table),
+ where: "state_id IS NULL"
+ )
+ end
+ end
+
+ def down
+ remove_concurrent_index_by_name(:issues, index_name_for("issues"))
+ remove_concurrent_index_by_name(:merge_requests, index_name_for("merge_requests"))
+ end
+
+ def index_name_for(table)
+ "idx_on_#{table}_where_state_id_is_null"
+ end
+end
diff --git a/db/post_migrate/20190424134256_drop_projects_ci_id.rb b/db/post_migrate/20190424134256_drop_projects_ci_id.rb
new file mode 100644
index 00000000000..79fa9704f1f
--- /dev/null
+++ b/db/post_migrate/20190424134256_drop_projects_ci_id.rb
@@ -0,0 +1,28 @@
+# 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 DropProjectsCiId < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if index_exists?(:projects, :ci_id)
+ remove_concurrent_index :projects, :ci_id
+ end
+
+ if column_exists?(:projects, :ci_id)
+ remove_column :projects, :ci_id
+ end
+ end
+
+ def down
+ add_column :projects, :ci_id, :integer
+ add_concurrent_index :projects, :ci_id
+ end
+end
diff --git a/db/post_migrate/20190506135400_schedule_sync_issuables_state_id_where_nil.rb b/db/post_migrate/20190506135400_schedule_sync_issuables_state_id_where_nil.rb
new file mode 100644
index 00000000000..4c31b5968ff
--- /dev/null
+++ b/db/post_migrate/20190506135400_schedule_sync_issuables_state_id_where_nil.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+class ScheduleSyncIssuablesStateIdWhereNil < ActiveRecord::Migration[5.1]
+ # Issues and MergeRequests imported by GitHub are being created with
+ # state_id = null, this fixes them.
+ #
+ # Part of a bigger plan: https://gitlab.com/gitlab-org/gitlab-ce/issues/51789
+
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # 2019-05-02 gitlab.com issuable numbers
+ # issues with state_id nil: ~40000
+ # merge requests with state_id nil: ~200000
+ #
+ # Using 5000 as batch size and 120 seconds interval will create:
+ # ~8 jobs for issues - taking ~16 minutes
+ # ~40 jobs for merge requests - taking ~1.34 hours
+ #
+ BATCH_SIZE = 5000
+ DELAY_INTERVAL = 120.seconds.to_i
+ ISSUES_MIGRATION = 'SyncIssuesStateId'.freeze
+ MERGE_REQUESTS_MIGRATION = 'SyncMergeRequestsStateId'.freeze
+
+ disable_ddl_transaction!
+
+ class Issue < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'issues'
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'merge_requests'
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ Issue.where(state_id: nil),
+ ISSUES_MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE
+ )
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ MergeRequest.where(state_id: nil),
+ MERGE_REQUESTS_MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE
+ )
+
+ # Remove temporary indexes added on "AddTemporaryIndexesToStateId"
+ remove_concurrent_index_by_name(:issues, "idx_on_issues_where_state_id_is_null")
+ remove_concurrent_index_by_name(:merge_requests, "idx_on_merge_requests_where_state_id_is_null")
+ end
+
+ def down
+ # No op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d1b3672725d..deaf406fe3d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,13 +10,13 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190326164045) do
+ActiveRecord::Schema.define(version: 20190506135400) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
- create_table "abuse_reports", force: :cascade do |t|
+ create_table "abuse_reports", id: :serial, force: :cascade do |t|
t.integer "reporter_id"
t.integer "user_id"
t.text "message"
@@ -26,7 +26,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.integer "cached_markdown_version"
end
- create_table "appearances", force: :cascade do |t|
+ create_table "appearances", id: :serial, force: :cascade do |t|
t.string "title", null: false
t.text "description", null: false
t.string "header_logo"
@@ -47,13 +47,13 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.boolean "email_header_and_footer_enabled", default: false, null: false
end
- create_table "application_setting_terms", force: :cascade do |t|
+ create_table "application_setting_terms", id: :serial, force: :cascade do |t|
t.integer "cached_markdown_version"
t.text "terms", null: false
t.text "terms_html"
end
- create_table "application_settings", force: :cascade do |t|
+ create_table "application_settings", id: :serial, force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
t.boolean "gravatar_enabled"
@@ -187,10 +187,12 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
+ t.string "lets_encrypt_notification_email"
+ t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
- create_table "audit_events", force: :cascade do |t|
+ create_table "audit_events", id: :serial, force: :cascade do |t|
t.integer "author_id", null: false
t.string "type", null: false
t.integer "entity_id", null: false
@@ -201,7 +203,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
end
- create_table "award_emoji", force: :cascade do |t|
+ create_table "award_emoji", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "user_id"
t.integer "awardable_id"
@@ -212,7 +214,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
end
- create_table "badges", force: :cascade do |t|
+ create_table "badges", id: :serial, force: :cascade do |t|
t.string "link_url", null: false
t.string "image_url", null: false
t.integer "project_id"
@@ -224,7 +226,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_badges_on_project_id", using: :btree
end
- create_table "board_group_recent_visits", id: :bigserial, force: :cascade do |t|
+ create_table "board_group_recent_visits", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
@@ -236,7 +238,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_board_group_recent_visits_on_user_id", using: :btree
end
- create_table "board_project_recent_visits", id: :bigserial, force: :cascade do |t|
+ create_table "board_project_recent_visits", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
@@ -248,7 +250,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_board_project_recent_visits_on_user_id", using: :btree
end
- create_table "boards", force: :cascade do |t|
+ create_table "boards", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -257,7 +259,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_boards_on_project_id", using: :btree
end
- create_table "broadcast_messages", force: :cascade do |t|
+ create_table "broadcast_messages", id: :serial, force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at", null: false
t.datetime "ends_at", null: false
@@ -270,7 +272,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id", using: :btree
end
- create_table "chat_names", force: :cascade do |t|
+ create_table "chat_names", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "service_id", null: false
t.string "team_id", null: false
@@ -284,7 +286,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
end
- create_table "chat_teams", force: :cascade do |t|
+ create_table "chat_teams", id: :serial, force: :cascade do |t|
t.integer "namespace_id", null: false
t.string "team_id"
t.string "name"
@@ -293,7 +295,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
end
- create_table "ci_build_trace_chunks", id: :bigserial, force: :cascade do |t|
+ create_table "ci_build_trace_chunks", force: :cascade do |t|
t.integer "build_id", null: false
t.integer "chunk_index", null: false
t.integer "data_store", null: false
@@ -301,13 +303,13 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["build_id", "chunk_index"], name: "index_ci_build_trace_chunks_on_build_id_and_chunk_index", unique: true, using: :btree
end
- create_table "ci_build_trace_section_names", force: :cascade do |t|
+ create_table "ci_build_trace_section_names", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.index ["project_id", "name"], name: "index_ci_build_trace_section_names_on_project_id_and_name", unique: true, using: :btree
end
- create_table "ci_build_trace_sections", force: :cascade do |t|
+ create_table "ci_build_trace_sections", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "date_start", null: false
t.datetime_with_timezone "date_end", null: false
@@ -320,7 +322,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["section_name_id"], name: "index_ci_build_trace_sections_on_section_name_id", using: :btree
end
- create_table "ci_builds", force: :cascade do |t|
+ create_table "ci_builds", id: :serial, force: :cascade do |t|
t.string "status"
t.datetime "finished_at"
t.text "trace"
@@ -387,7 +389,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
end
- create_table "ci_builds_metadata", force: :cascade do |t|
+ create_table "ci_builds_metadata", id: :serial, force: :cascade do |t|
t.integer "build_id", null: false
t.integer "project_id", null: false
t.integer "timeout"
@@ -398,7 +400,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
end
- create_table "ci_builds_runner_session", id: :bigserial, force: :cascade do |t|
+ create_table "ci_builds_runner_session", force: :cascade do |t|
t.integer "build_id", null: false
t.string "url", null: false
t.string "certificate"
@@ -406,7 +408,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["build_id"], name: "index_ci_builds_runner_session_on_build_id", unique: true, using: :btree
end
- create_table "ci_group_variables", force: :cascade do |t|
+ create_table "ci_group_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false
t.text "value"
t.text "encrypted_value"
@@ -417,10 +419,11 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.boolean "masked", default: false, null: false
+ t.integer "variable_type", limit: 2, default: 1, null: false
t.index ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
end
- create_table "ci_job_artifacts", force: :cascade do |t|
+ create_table "ci_job_artifacts", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "job_id", null: false
t.integer "file_type", null: false
@@ -439,7 +442,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
end
- create_table "ci_pipeline_chat_data", id: :bigserial, force: :cascade do |t|
+ create_table "ci_pipeline_chat_data", force: :cascade do |t|
t.integer "pipeline_id", null: false
t.integer "chat_name_id", null: false
t.text "response_url", null: false
@@ -456,10 +459,11 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.integer "pipeline_schedule_id", null: false
t.datetime_with_timezone "created_at"
t.datetime_with_timezone "updated_at"
+ t.integer "variable_type", limit: 2, default: 1, null: false
t.index ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
end
- create_table "ci_pipeline_schedules", force: :cascade do |t|
+ create_table "ci_pipeline_schedules", id: :serial, force: :cascade do |t|
t.string "description"
t.string "ref"
t.string "cron"
@@ -475,17 +479,18 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_ci_pipeline_schedules_on_project_id", using: :btree
end
- create_table "ci_pipeline_variables", force: :cascade do |t|
+ create_table "ci_pipeline_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "pipeline_id", null: false
+ t.integer "variable_type", limit: 2, default: 1, null: false
t.index ["pipeline_id", "key"], name: "index_ci_pipeline_variables_on_pipeline_id_and_key", unique: true, using: :btree
end
- create_table "ci_pipelines", force: :cascade do |t|
+ create_table "ci_pipelines", id: :serial, force: :cascade do |t|
t.string "ref"
t.string "sha"
t.string "before_sha"
@@ -525,14 +530,14 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
end
- create_table "ci_runner_namespaces", force: :cascade do |t|
+ create_table "ci_runner_namespaces", id: :serial, force: :cascade do |t|
t.integer "runner_id"
t.integer "namespace_id"
t.index ["namespace_id"], name: "index_ci_runner_namespaces_on_namespace_id", using: :btree
t.index ["runner_id", "namespace_id"], name: "index_ci_runner_namespaces_on_runner_id_and_namespace_id", unique: true, using: :btree
end
- create_table "ci_runner_projects", force: :cascade do |t|
+ create_table "ci_runner_projects", id: :serial, force: :cascade do |t|
t.integer "runner_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
@@ -541,7 +546,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
end
- create_table "ci_runners", force: :cascade do |t|
+ create_table "ci_runners", id: :serial, force: :cascade do |t|
t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
@@ -569,7 +574,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["token_encrypted"], name: "index_ci_runners_on_token_encrypted", using: :btree
end
- create_table "ci_stages", force: :cascade do |t|
+ create_table "ci_stages", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.integer "pipeline_id"
t.datetime "created_at"
@@ -584,7 +589,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_ci_stages_on_project_id", using: :btree
end
- create_table "ci_trigger_requests", force: :cascade do |t|
+ create_table "ci_trigger_requests", id: :serial, force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
t.datetime "created_at"
@@ -594,7 +599,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["trigger_id"], name: "index_ci_trigger_requests_on_trigger_id", using: :btree
end
- create_table "ci_triggers", force: :cascade do |t|
+ create_table "ci_triggers", id: :serial, force: :cascade do |t|
t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
@@ -606,7 +611,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
end
- create_table "ci_variables", force: :cascade do |t|
+ create_table "ci_variables", id: :serial, force: :cascade do |t|
t.string "key", null: false
t.text "value"
t.text "encrypted_value"
@@ -616,17 +621,18 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.boolean "protected", default: false, null: false
t.string "environment_scope", default: "*", null: false
t.boolean "masked", default: false, null: false
+ t.integer "variable_type", limit: 2, default: 1, null: false
t.index ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree
end
- create_table "cluster_groups", force: :cascade do |t|
+ create_table "cluster_groups", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "group_id", null: false
t.index ["cluster_id", "group_id"], name: "index_cluster_groups_on_cluster_id_and_group_id", unique: true, using: :btree
t.index ["group_id"], name: "index_cluster_groups_on_group_id", using: :btree
end
- create_table "cluster_platforms_kubernetes", force: :cascade do |t|
+ create_table "cluster_platforms_kubernetes", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -642,7 +648,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree
end
- create_table "cluster_projects", force: :cascade do |t|
+ create_table "cluster_projects", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -651,7 +657,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_cluster_projects_on_project_id", using: :btree
end
- create_table "cluster_providers_gcp", force: :cascade do |t|
+ create_table "cluster_providers_gcp", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status"
t.integer "num_nodes", null: false
@@ -669,7 +675,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_cluster_providers_gcp_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters", force: :cascade do |t|
+ create_table "clusters", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "provider_type"
t.integer "platform_type"
@@ -685,7 +691,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_clusters_on_user_id", using: :btree
end
- create_table "clusters_applications_cert_managers", force: :cascade do |t|
+ create_table "clusters_applications_cert_managers", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
t.string "version", null: false
@@ -696,7 +702,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_clusters_applications_cert_managers_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters_applications_helm", force: :cascade do |t|
+ create_table "clusters_applications_helm", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -709,7 +715,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_clusters_applications_helm_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters_applications_ingress", force: :cascade do |t|
+ create_table "clusters_applications_ingress", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -723,7 +729,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters_applications_jupyter", force: :cascade do |t|
+ create_table "clusters_applications_jupyter", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "oauth_application_id"
t.integer "status", null: false
@@ -736,7 +742,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["oauth_application_id"], name: "index_clusters_applications_jupyter_on_oauth_application_id", using: :btree
end
- create_table "clusters_applications_knative", force: :cascade do |t|
+ create_table "clusters_applications_knative", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -749,7 +755,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_clusters_applications_knative_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters_applications_prometheus", force: :cascade do |t|
+ create_table "clusters_applications_prometheus", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
t.string "version", null: false
@@ -759,7 +765,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["cluster_id"], name: "index_clusters_applications_prometheus_on_cluster_id", unique: true, using: :btree
end
- create_table "clusters_applications_runners", force: :cascade do |t|
+ create_table "clusters_applications_runners", id: :serial, force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "runner_id"
t.integer "status", null: false
@@ -772,7 +778,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree
end
- create_table "clusters_kubernetes_namespaces", id: :bigserial, force: :cascade do |t|
+ create_table "clusters_kubernetes_namespaces", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "project_id"
t.integer "cluster_project_id"
@@ -788,7 +794,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_clusters_kubernetes_namespaces_on_project_id", using: :btree
end
- create_table "container_repositories", force: :cascade do |t|
+ create_table "container_repositories", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
@@ -797,7 +803,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_container_repositories_on_project_id", using: :btree
end
- create_table "conversational_development_index_metrics", force: :cascade do |t|
+ create_table "conversational_development_index_metrics", id: :serial, force: :cascade do |t|
t.float "leader_issues", null: false
t.float "instance_issues", null: false
t.float "leader_notes", null: false
@@ -832,7 +838,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.float "percentage_service_desk_issues", default: 0.0, null: false
end
- create_table "deploy_keys_projects", force: :cascade do |t|
+ create_table "deploy_keys_projects", id: :serial, force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
@@ -841,7 +847,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
end
- create_table "deploy_tokens", force: :cascade do |t|
+ create_table "deploy_tokens", id: :serial, force: :cascade do |t|
t.boolean "revoked", default: false
t.boolean "read_repository", default: false, null: false
t.boolean "read_registry", default: false, null: false
@@ -853,7 +859,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["token"], name: "index_deploy_tokens_on_token", unique: true, using: :btree
end
- create_table "deployments", force: :cascade do |t|
+ create_table "deployments", id: :serial, force: :cascade do |t|
t.integer "iid", null: false
t.integer "project_id", null: false
t.integer "environment_id", null: false
@@ -879,7 +885,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree
end
- create_table "emails", force: :cascade do |t|
+ create_table "emails", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
t.datetime "created_at"
@@ -892,7 +898,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_emails_on_user_id", using: :btree
end
- create_table "environments", force: :cascade do |t|
+ create_table "environments", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
@@ -905,7 +911,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
end
- create_table "events", force: :cascade do |t|
+ create_table "events", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.integer "author_id", null: false
t.integer "target_id"
@@ -920,7 +926,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
end
- create_table "feature_gates", force: :cascade do |t|
+ create_table "feature_gates", id: :serial, force: :cascade do |t|
t.string "feature_key", null: false
t.string "key", null: false
t.string "value"
@@ -929,14 +935,14 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["feature_key", "key", "value"], name: "index_feature_gates_on_feature_key_and_key_and_value", unique: true, using: :btree
end
- create_table "features", force: :cascade do |t|
+ create_table "features", id: :serial, force: :cascade do |t|
t.string "key", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["key"], name: "index_features_on_key", unique: true, using: :btree
end
- create_table "fork_network_members", force: :cascade do |t|
+ create_table "fork_network_members", id: :serial, force: :cascade do |t|
t.integer "fork_network_id", null: false
t.integer "project_id", null: false
t.integer "forked_from_project_id"
@@ -945,13 +951,13 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree
end
- create_table "fork_networks", force: :cascade do |t|
+ create_table "fork_networks", id: :serial, force: :cascade do |t|
t.integer "root_project_id"
t.string "deleted_root_project_name"
t.index ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree
end
- create_table "forked_project_links", force: :cascade do |t|
+ create_table "forked_project_links", id: :serial, force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
t.datetime "created_at"
@@ -959,7 +965,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
end
- create_table "gpg_key_subkeys", force: :cascade do |t|
+ create_table "gpg_key_subkeys", id: :serial, force: :cascade do |t|
t.integer "gpg_key_id", null: false
t.binary "keyid"
t.binary "fingerprint"
@@ -968,7 +974,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
end
- create_table "gpg_keys", force: :cascade do |t|
+ create_table "gpg_keys", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
@@ -980,7 +986,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
end
- create_table "gpg_signatures", force: :cascade do |t|
+ create_table "gpg_signatures", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
@@ -998,7 +1004,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
end
- create_table "group_custom_attributes", force: :cascade do |t|
+ create_table "group_custom_attributes", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "group_id", null: false
@@ -1008,7 +1014,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["key", "value"], name: "index_group_custom_attributes_on_key_and_value", using: :btree
end
- create_table "identities", force: :cascade do |t|
+ create_table "identities", id: :serial, force: :cascade do |t|
t.string "extern_uid"
t.string "provider"
t.integer "user_id"
@@ -1017,7 +1023,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_identities_on_user_id", using: :btree
end
- create_table "import_export_uploads", force: :cascade do |t|
+ create_table "import_export_uploads", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.text "import_file"
@@ -1026,7 +1032,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
end
- create_table "internal_ids", id: :bigserial, force: :cascade do |t|
+ create_table "internal_ids", force: :cascade do |t|
t.integer "project_id"
t.integer "usage", null: false
t.integer "last_value", null: false
@@ -1044,7 +1050,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_issue_assignees_on_user_id", using: :btree
end
- create_table "issue_metrics", force: :cascade do |t|
+ create_table "issue_metrics", id: :serial, force: :cascade do |t|
t.integer "issue_id", null: false
t.datetime "first_mentioned_in_commit_at"
t.datetime "first_associated_with_milestone_at"
@@ -1054,7 +1060,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["issue_id"], name: "index_issue_metrics", using: :btree
end
- create_table "issues", force: :cascade do |t|
+ create_table "issues", id: :serial, force: :cascade do |t|
t.string "title"
t.integer "author_id"
t.integer "project_id"
@@ -1097,7 +1103,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["updated_by_id"], name: "index_issues_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
end
- create_table "keys", force: :cascade do |t|
+ create_table "keys", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
@@ -1111,7 +1117,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_keys_on_user_id", using: :btree
end
- create_table "label_links", force: :cascade do |t|
+ create_table "label_links", id: :serial, force: :cascade do |t|
t.integer "label_id"
t.integer "target_id"
t.string "target_type"
@@ -1121,7 +1127,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
end
- create_table "label_priorities", force: :cascade do |t|
+ create_table "label_priorities", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "label_id", null: false
t.integer "priority", null: false
@@ -1132,7 +1138,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "label_id"], name: "index_label_priorities_on_project_id_and_label_id", unique: true, using: :btree
end
- create_table "labels", force: :cascade do |t|
+ create_table "labels", id: :serial, force: :cascade do |t|
t.string "title"
t.string "color"
t.integer "project_id"
@@ -1151,7 +1157,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
end
- create_table "lfs_file_locks", force: :cascade do |t|
+ create_table "lfs_file_locks", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
@@ -1160,7 +1166,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_lfs_file_locks_on_user_id", using: :btree
end
- create_table "lfs_objects", force: :cascade do |t|
+ create_table "lfs_objects", id: :serial, force: :cascade do |t|
t.string "oid", null: false
t.bigint "size", null: false
t.datetime "created_at"
@@ -1171,7 +1177,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
end
- create_table "lfs_objects_projects", force: :cascade do |t|
+ create_table "lfs_objects_projects", id: :serial, force: :cascade do |t|
t.integer "lfs_object_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
@@ -1179,7 +1185,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
end
- create_table "lists", force: :cascade do |t|
+ create_table "lists", id: :serial, force: :cascade do |t|
t.integer "board_id", null: false
t.integer "label_id"
t.integer "list_type", default: 1, null: false
@@ -1191,7 +1197,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["list_type"], name: "index_lists_on_list_type", using: :btree
end
- create_table "members", force: :cascade do |t|
+ create_table "members", id: :serial, force: :cascade do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -1254,7 +1260,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["merge_request_diff_id", "relative_order"], name: "index_merge_request_diff_files_on_mr_diff_id_and_order", unique: true, using: :btree
end
- create_table "merge_request_diffs", force: :cascade do |t|
+ create_table "merge_request_diffs", id: :serial, force: :cascade do |t|
t.string "state"
t.integer "merge_request_id", null: false
t.datetime "created_at"
@@ -1271,7 +1277,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["merge_request_id", "id"], name: "index_merge_request_diffs_on_merge_request_id_and_id_partial", where: "((NOT stored_externally) OR (stored_externally IS NULL))", using: :btree
end
- create_table "merge_request_metrics", force: :cascade do |t|
+ create_table "merge_request_metrics", id: :serial, force: :cascade do |t|
t.integer "merge_request_id", null: false
t.datetime "latest_build_started_at"
t.datetime "latest_build_finished_at"
@@ -1292,7 +1298,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
end
- create_table "merge_requests", force: :cascade do |t|
+ create_table "merge_requests", id: :serial, force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
t.integer "source_project_id"
@@ -1350,7 +1356,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["updated_by_id"], name: "index_merge_requests_on_updated_by_id", where: "(updated_by_id IS NOT NULL)", using: :btree
end
- create_table "merge_requests_closing_issues", force: :cascade do |t|
+ create_table "merge_requests_closing_issues", id: :serial, force: :cascade do |t|
t.integer "merge_request_id", null: false
t.integer "issue_id", null: false
t.datetime "created_at", null: false
@@ -1359,7 +1365,18 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
end
- create_table "milestones", force: :cascade do |t|
+ create_table "merge_trains", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.integer "user_id", null: false
+ t.integer "pipeline_id"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.index ["merge_request_id"], name: "index_merge_trains_on_merge_request_id", unique: true, using: :btree
+ t.index ["pipeline_id"], name: "index_merge_trains_on_pipeline_id", using: :btree
+ t.index ["user_id"], name: "index_merge_trains_on_user_id", using: :btree
+ end
+
+ create_table "milestones", id: :serial, force: :cascade do |t|
t.string "title", null: false
t.integer "project_id"
t.text "description"
@@ -1381,7 +1398,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
end
- create_table "namespaces", force: :cascade do |t|
+ create_table "namespaces", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "path", null: false
t.integer "owner_id"
@@ -1416,7 +1433,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["type"], name: "index_namespaces_on_type", using: :btree
end
- create_table "note_diff_files", force: :cascade do |t|
+ create_table "note_diff_files", id: :serial, force: :cascade do |t|
t.integer "diff_note_id", null: false
t.text "diff", null: false
t.boolean "new_file", null: false
@@ -1429,7 +1446,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
end
- create_table "notes", force: :cascade do |t|
+ create_table "notes", id: :serial, force: :cascade do |t|
t.text "note"
t.string "noteable_type"
t.integer "author_id"
@@ -1464,7 +1481,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
end
- create_table "notification_settings", force: :cascade do |t|
+ create_table "notification_settings", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "source_id"
t.string "source_type"
@@ -1490,7 +1507,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_notification_settings_on_user_id", using: :btree
end
- create_table "oauth_access_grants", force: :cascade do |t|
+ create_table "oauth_access_grants", id: :serial, force: :cascade do |t|
t.integer "resource_owner_id", null: false
t.integer "application_id", null: false
t.string "token", null: false
@@ -1502,7 +1519,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
end
- create_table "oauth_access_tokens", force: :cascade do |t|
+ create_table "oauth_access_tokens", id: :serial, force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
t.string "token", null: false
@@ -1516,7 +1533,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
end
- create_table "oauth_applications", force: :cascade do |t|
+ create_table "oauth_applications", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "uid", null: false
t.string "secret", null: false
@@ -1531,13 +1548,13 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
end
- create_table "oauth_openid_requests", force: :cascade do |t|
+ create_table "oauth_openid_requests", id: :serial, force: :cascade do |t|
t.integer "access_grant_id", null: false
t.string "nonce", null: false
t.index ["access_grant_id"], name: "index_oauth_openid_requests_on_access_grant_id", using: :btree
end
- create_table "pages_domains", force: :cascade do |t|
+ create_table "pages_domains", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.text "certificate"
t.text "encrypted_key"
@@ -1548,6 +1565,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "verification_code", null: false
t.datetime_with_timezone "enabled_until"
t.datetime_with_timezone "remove_at"
+ t.boolean "auto_ssl_enabled", default: false, null: false
t.index ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
t.index ["project_id", "enabled_until"], name: "index_pages_domains_on_project_id_and_enabled_until", using: :btree
t.index ["project_id"], name: "index_pages_domains_on_project_id", using: :btree
@@ -1556,7 +1574,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["verified_at"], name: "index_pages_domains_on_verified_at", using: :btree
end
- create_table "personal_access_tokens", force: :cascade do |t|
+ create_table "personal_access_tokens", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.string "name", null: false
t.boolean "revoked", default: false
@@ -1570,7 +1588,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
end
- create_table "pool_repositories", id: :bigserial, force: :cascade do |t|
+ create_table "pool_repositories", force: :cascade do |t|
t.integer "shard_id", null: false
t.string "disk_path"
t.string "state"
@@ -1580,7 +1598,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["source_project_id"], name: "index_pool_repositories_on_source_project_id", unique: true, using: :btree
end
- create_table "programming_languages", force: :cascade do |t|
+ create_table "programming_languages", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.string "color", null: false
t.datetime_with_timezone "created_at", null: false
@@ -1595,7 +1613,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
end
- create_table "project_auto_devops", force: :cascade do |t|
+ create_table "project_auto_devops", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -1605,14 +1623,15 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
end
- create_table "project_ci_cd_settings", force: :cascade do |t|
+ create_table "project_ci_cd_settings", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.boolean "group_runners_enabled", default: true, null: false
t.boolean "merge_pipelines_enabled"
+ t.boolean "merge_trains_enabled", default: false, null: false
t.index ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
end
- create_table "project_custom_attributes", force: :cascade do |t|
+ create_table "project_custom_attributes", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "project_id", null: false
@@ -1622,14 +1641,14 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "key"], name: "index_project_custom_attributes_on_project_id_and_key", unique: true, using: :btree
end
- create_table "project_daily_statistics", id: :bigserial, force: :cascade do |t|
+ create_table "project_daily_statistics", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "fetch_count", null: false
t.date "date"
t.index ["project_id", "date"], name: "index_project_daily_statistics_on_project_id_and_date", unique: true, order: { date: :desc }, using: :btree
end
- create_table "project_deploy_tokens", force: :cascade do |t|
+ create_table "project_deploy_tokens", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "deploy_token_id", null: false
t.datetime_with_timezone "created_at", null: false
@@ -1637,7 +1656,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree
end
- create_table "project_error_tracking_settings", primary_key: "project_id", id: :integer, force: :cascade do |t|
+ create_table "project_error_tracking_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
t.boolean "enabled", default: false, null: false
t.string "api_url"
t.string "encrypted_token"
@@ -1646,7 +1665,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "organization_name"
end
- create_table "project_features", force: :cascade do |t|
+ create_table "project_features", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "merge_requests_access_level"
t.integer "issues_access_level"
@@ -1660,7 +1679,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_project_features_on_project_id", unique: true, using: :btree
end
- create_table "project_group_links", force: :cascade do |t|
+ create_table "project_group_links", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
t.datetime "created_at"
@@ -1671,7 +1690,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_project_group_links_on_project_id", using: :btree
end
- create_table "project_import_data", force: :cascade do |t|
+ create_table "project_import_data", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.text "data"
t.text "encrypted_credentials"
@@ -1680,7 +1699,11 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
end
- create_table "project_mirror_data", force: :cascade do |t|
+ create_table "project_metrics_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
+ t.string "external_dashboard_url", null: false
+ end
+
+ create_table "project_mirror_data", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "status"
t.string "jid"
@@ -1690,7 +1713,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["status"], name: "index_project_mirror_data_on_status", using: :btree
end
- create_table "project_repositories", id: :bigserial, force: :cascade do |t|
+ create_table "project_repositories", force: :cascade do |t|
t.integer "shard_id", null: false
t.string "disk_path", null: false
t.integer "project_id", null: false
@@ -1699,7 +1722,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["shard_id"], name: "index_project_repositories_on_shard_id", using: :btree
end
- create_table "project_statistics", force: :cascade do |t|
+ create_table "project_statistics", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
t.bigint "commit_count", default: 0, null: false
@@ -1707,11 +1730,12 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.bigint "repository_size", default: 0, null: false
t.bigint "lfs_objects_size", default: 0, null: false
t.bigint "build_artifacts_size", default: 0, null: false
+ t.bigint "packages_size"
t.index ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
t.index ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
end
- create_table "projects", force: :cascade do |t|
+ create_table "projects", id: :serial, force: :cascade do |t|
t.string "name"
t.string "path"
t.text "description"
@@ -1729,7 +1753,6 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "import_type"
t.string "import_source"
t.text "import_error"
- t.integer "ci_id"
t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
@@ -1768,7 +1791,6 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.string "bfg_object_map"
t.boolean "detected_repository_languages"
t.string "external_authorization_classification_label"
- t.index ["ci_id"], name: "index_projects_on_ci_id", using: :btree
t.index ["created_at"], name: "index_projects_on_created_at", using: :btree
t.index ["creator_id"], name: "index_projects_on_creator_id", using: :btree
t.index ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -1791,7 +1813,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
end
- create_table "prometheus_metrics", force: :cascade do |t|
+ create_table "prometheus_metrics", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.string "title", null: false
t.string "query", null: false
@@ -1809,7 +1831,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_prometheus_metrics_on_project_id", using: :btree
end
- create_table "protected_branch_merge_access_levels", force: :cascade do |t|
+ create_table "protected_branch_merge_access_levels", id: :serial, force: :cascade do |t|
t.integer "protected_branch_id", null: false
t.integer "access_level", default: 40, null: false
t.datetime "created_at", null: false
@@ -1817,7 +1839,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
end
- create_table "protected_branch_push_access_levels", force: :cascade do |t|
+ create_table "protected_branch_push_access_levels", id: :serial, force: :cascade do |t|
t.integer "protected_branch_id", null: false
t.integer "access_level", default: 40, null: false
t.datetime "created_at", null: false
@@ -1825,7 +1847,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
end
- create_table "protected_branches", force: :cascade do |t|
+ create_table "protected_branches", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
@@ -1833,7 +1855,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
end
- create_table "protected_tag_create_access_levels", force: :cascade do |t|
+ create_table "protected_tag_create_access_levels", id: :serial, force: :cascade do |t|
t.integer "protected_tag_id", null: false
t.integer "access_level", default: 40
t.integer "user_id"
@@ -1845,7 +1867,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree
end
- create_table "protected_tags", force: :cascade do |t|
+ create_table "protected_tags", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
@@ -1866,7 +1888,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true, using: :btree
end
- create_table "redirect_routes", force: :cascade do |t|
+ create_table "redirect_routes", id: :serial, force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
t.string "path", null: false
@@ -1876,7 +1898,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
end
- create_table "release_links", id: :bigserial, force: :cascade do |t|
+ create_table "release_links", force: :cascade do |t|
t.integer "release_id", null: false
t.string "url", null: false
t.string "name", null: false
@@ -1886,7 +1908,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["release_id", "url"], name: "index_release_links_on_release_id_and_url", unique: true, using: :btree
end
- create_table "releases", force: :cascade do |t|
+ create_table "releases", id: :serial, force: :cascade do |t|
t.string "tag"
t.text "description"
t.integer "project_id"
@@ -1902,7 +1924,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id"], name: "index_releases_on_project_id", using: :btree
end
- create_table "remote_mirrors", force: :cascade do |t|
+ create_table "remote_mirrors", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.string "url"
t.boolean "enabled", default: false
@@ -1930,7 +1952,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["project_id", "programming_language_id"], name: "index_repository_languages_on_project_and_languages_id", unique: true, using: :btree
end
- create_table "resource_label_events", id: :bigserial, force: :cascade do |t|
+ create_table "resource_label_events", force: :cascade do |t|
t.integer "action", null: false
t.integer "issue_id"
t.integer "merge_request_id"
@@ -1946,7 +1968,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_resource_label_events_on_user_id", using: :btree
end
- create_table "routes", force: :cascade do |t|
+ create_table "routes", id: :serial, force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
t.string "path", null: false
@@ -1958,7 +1980,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
end
- create_table "sent_notifications", force: :cascade do |t|
+ create_table "sent_notifications", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
t.string "noteable_type"
@@ -1972,7 +1994,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
end
- create_table "services", force: :cascade do |t|
+ create_table "services", id: :serial, force: :cascade do |t|
t.string "type"
t.string "title"
t.integer "project_id"
@@ -1994,17 +2016,18 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events", default: true
+ t.boolean "deployment_events", default: false, null: false
t.index ["project_id"], name: "index_services_on_project_id", using: :btree
t.index ["template"], name: "index_services_on_template", using: :btree
t.index ["type"], name: "index_services_on_type", using: :btree
end
- create_table "shards", force: :cascade do |t|
+ create_table "shards", id: :serial, force: :cascade do |t|
t.string "name", null: false
t.index ["name"], name: "index_shards_on_name", unique: true, using: :btree
end
- create_table "snippets", force: :cascade do |t|
+ create_table "snippets", id: :serial, force: :cascade do |t|
t.string "title"
t.text "content"
t.integer "author_id", null: false
@@ -2027,7 +2050,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
end
- create_table "spam_logs", force: :cascade do |t|
+ create_table "spam_logs", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.string "source_ip"
t.string "user_agent"
@@ -2041,7 +2064,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.boolean "recaptcha_verified", default: false, null: false
end
- create_table "subscriptions", force: :cascade do |t|
+ create_table "subscriptions", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
t.string "subscribable_type"
@@ -2053,7 +2076,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["subscribable_id", "subscribable_type", "user_id", "project_id"], name: "index_subscriptions_on_subscribable_and_user_id_and_project_id", unique: true, using: :btree
end
- create_table "suggestions", id: :bigserial, force: :cascade do |t|
+ create_table "suggestions", force: :cascade do |t|
t.integer "note_id", null: false
t.integer "relative_order", limit: 2, null: false
t.boolean "applied", default: false, null: false
@@ -2066,7 +2089,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["note_id", "relative_order"], name: "index_suggestions_on_note_id_and_relative_order", unique: true, using: :btree
end
- create_table "system_note_metadata", force: :cascade do |t|
+ create_table "system_note_metadata", id: :serial, force: :cascade do |t|
t.integer "note_id", null: false
t.integer "commit_count"
t.string "action"
@@ -2075,7 +2098,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["note_id"], name: "index_system_note_metadata_on_note_id", unique: true, using: :btree
end
- create_table "taggings", force: :cascade do |t|
+ create_table "taggings", id: :serial, force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
@@ -2089,14 +2112,14 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["taggable_id", "taggable_type"], name: "index_taggings_on_taggable_id_and_taggable_type", using: :btree
end
- create_table "tags", force: :cascade do |t|
+ create_table "tags", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "taggings_count", default: 0
t.index ["name"], name: "index_tags_on_name", unique: true, using: :btree
t.index ["name"], name: "index_tags_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
end
- create_table "term_agreements", force: :cascade do |t|
+ create_table "term_agreements", id: :serial, force: :cascade do |t|
t.integer "term_id", null: false
t.integer "user_id", null: false
t.boolean "accepted", default: false, null: false
@@ -2107,7 +2130,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_term_agreements_on_user_id", using: :btree
end
- create_table "timelogs", force: :cascade do |t|
+ create_table "timelogs", id: :serial, force: :cascade do |t|
t.integer "time_spent", null: false
t.integer "user_id"
t.datetime "created_at", null: false
@@ -2120,7 +2143,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_timelogs_on_user_id", using: :btree
end
- create_table "todos", force: :cascade do |t|
+ create_table "todos", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id"
t.integer "target_id"
@@ -2144,12 +2167,12 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_todos_on_user_id", using: :btree
end
- create_table "trending_projects", force: :cascade do |t|
+ create_table "trending_projects", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.index ["project_id"], name: "index_trending_projects_on_project_id", unique: true, using: :btree
end
- create_table "u2f_registrations", force: :cascade do |t|
+ create_table "u2f_registrations", id: :serial, force: :cascade do |t|
t.text "certificate"
t.string "key_handle"
t.string "public_key"
@@ -2162,7 +2185,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
end
- create_table "uploads", force: :cascade do |t|
+ create_table "uploads", id: :serial, force: :cascade do |t|
t.bigint "size", null: false
t.string "path", limit: 511, null: false
t.string "checksum", limit: 64
@@ -2179,7 +2202,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["uploader", "path"], name: "index_uploads_on_uploader_and_path", using: :btree
end
- create_table "user_agent_details", force: :cascade do |t|
+ create_table "user_agent_details", id: :serial, force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
t.integer "subject_id", null: false
@@ -2190,14 +2213,14 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
end
- create_table "user_callouts", force: :cascade do |t|
+ create_table "user_callouts", id: :serial, force: :cascade do |t|
t.integer "feature_name", null: false
t.integer "user_id", null: false
t.index ["user_id", "feature_name"], name: "index_user_callouts_on_user_id_and_feature_name", unique: true, using: :btree
t.index ["user_id"], name: "index_user_callouts_on_user_id", using: :btree
end
- create_table "user_custom_attributes", force: :cascade do |t|
+ create_table "user_custom_attributes", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.integer "user_id", null: false
@@ -2214,7 +2237,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
end
- create_table "user_preferences", force: :cascade do |t|
+ create_table "user_preferences", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_notes_filter", limit: 2, default: 0, null: false
t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
@@ -2226,7 +2249,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
- create_table "user_statuses", primary_key: "user_id", force: :cascade do |t|
+ create_table "user_statuses", primary_key: "user_id", id: :serial, force: :cascade do |t|
t.integer "cached_markdown_version"
t.string "emoji", default: "speech_balloon", null: false
t.string "message", limit: 100
@@ -2234,7 +2257,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_user_statuses_on_user_id", using: :btree
end
- create_table "user_synced_attributes_metadata", force: :cascade do |t|
+ create_table "user_synced_attributes_metadata", id: :serial, force: :cascade do |t|
t.boolean "name_synced", default: false
t.boolean "email_synced", default: false
t.boolean "location_synced", default: false
@@ -2243,7 +2266,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id"], name: "index_user_synced_attributes_metadata_on_user_id", unique: true, using: :btree
end
- create_table "users", force: :cascade do |t|
+ create_table "users", id: :serial, force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
@@ -2329,7 +2352,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"}
end
- create_table "users_star_projects", force: :cascade do |t|
+ create_table "users_star_projects", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
@@ -2338,7 +2361,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
end
- create_table "web_hook_logs", force: :cascade do |t|
+ create_table "web_hook_logs", id: :serial, force: :cascade do |t|
t.integer "web_hook_id", null: false
t.string "trigger"
t.string "url"
@@ -2355,7 +2378,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
t.index ["web_hook_id"], name: "index_web_hook_logs_on_web_hook_id", using: :btree
end
- create_table "web_hooks", force: :cascade do |t|
+ create_table "web_hooks", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
@@ -2506,6 +2529,9 @@ ActiveRecord::Schema.define(version: 20190326164045) do
add_foreign_key "merge_requests", "users", column: "updated_by_id", name: "fk_641731faff", on_delete: :nullify
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_trains", "ci_pipelines", column: "pipeline_id", on_delete: :nullify
+ add_foreign_key "merge_trains", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_trains", "users", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
add_foreign_key "note_diff_files", "notes", column: "diff_note_id", on_delete: :cascade
@@ -2528,6 +2554,7 @@ ActiveRecord::Schema.define(version: 20190326164045) do
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
+ add_foreign_key "project_metrics_settings", "projects", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
add_foreign_key "project_repositories", "projects", on_delete: :cascade
add_foreign_key "project_repositories", "shards", on_delete: :restrict
diff --git a/doc/README.md b/doc/README.md
index 14a1eeffda0..dd4909ce303 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -218,12 +218,12 @@ scales to run your tests faster.
The following documentation relates to the DevOps **Verify** stage:
-| Verify Topics | Description |
-|:---------------------------------------------------|:-----------------------------------------------------------------------------|
-| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
-| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
-| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
-| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
+| Verify Topics | Description |
+|:---------------------------------------------------------|:-----------------------------------------------------------------------------|
+| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
+| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
+| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
+| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -288,7 +288,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [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. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
-| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
+| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
@@ -418,7 +418,7 @@ We have the following documentation to rapidly uplift your GitLab knowledge:
| Topic | Description |
|:-----------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------|
-| [GitLab Basics](gitlab-basics/README.md) | Start working on the command line and with GitLab. |
+| [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. |
| [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. |
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
new file mode 100644
index 00000000000..d7a2e13b53e
--- /dev/null
+++ b/doc/administration/audit_events.md
@@ -0,0 +1,116 @@
+---
+last_updated: 2019-02-04
+---
+
+# Audit Events **[STARTER]**
+
+GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan][ee].
+
+GitLab system administrators can also take advantage of the logs located on the
+filesystem, see [the logs system documentation](logs.md) for more details.
+
+## Overview
+
+**Audit Events** is a tool for GitLab owners and administrators to be
+able to track important events such as who performed certain actions and the
+time they happened. These actions could be, for example, change a user
+permission level, who added a new user, or who removed a user.
+
+## Use-cases
+
+- Check who was the person who changed the permission level of a particular
+ user for a project in GitLab.
+- Use it to track which users have access to a certain group of projects
+ in GitLab, and who gave them that permission level.
+
+## List of events
+
+There are two kinds of events logged:
+
+- Events scoped to the group or project, used by group / project managers
+ to look up who made what change.
+- Instance events scoped to the whole GitLab instance, used by your Compliance team to
+ perform formal audits.
+
+### Group events **[STARTER]**
+
+NOTE: **Note:**
+You need Owner [permissions] to view the group Audit Events page.
+
+To view a group's audit events, navigate to **Group > Settings > Audit Events**.
+From there, you can see the following actions:
+
+- Group name/path changed
+- Group repository size limit changed
+- Group created/deleted
+- Group changed visibility
+- User was added to group and with which [permissions]
+- Permissions changes of a user assigned to a group
+- Removed user from group
+- Project added to group and with which visibility level
+- Project removed from group
+- [Project shared with group](../user/project/members/share_project_with_groups.md)
+ and with which [permissions]
+- Removal of a previously shared group with a project
+- LFS enabled/disabled
+- Shared runners minutes limit changed
+- Membership lock enabled/disabled
+- Request access enabled/disabled
+- 2FA enforcement/grace period changed
+- Roles allowed to create project changed
+
+### Project events **[STARTER]**
+
+NOTE: **Note:**
+You need Maintainer [permissions] or higher to view the project Audit Events page.
+
+To view a project's audit events, navigate to **Project > Settings > Audit Events**.
+From there, you can see the following actions:
+
+- Added/removed deploy keys
+- Project created/deleted/renamed/moved(transferred)/changed path
+- Project changed visibility level
+- User was added to project and with which [permissions]
+- Permission changes of a user assigned to a project
+- User was removed from project
+
+### Instance events **[PREMIUM ONLY]**
+
+> [Introduced][ee-2336] in [GitLab Premium][ee] 9.3.
+
+Server-wide audit logging introduces the ability to observe user actions across
+the entire instance of your GitLab server, making it easy to understand who
+changed what and when for audit purposes.
+
+To view the server-wide admin log, visit **Admin Area > Monitoring > Audit Log**.
+
+In addition to the group and project events, the following user actions are also
+recorded:
+
+- Failed Logins
+- Sign-in events and the authentication type (standard, LDAP, OmniAuth, etc.)
+- Added SSH key
+- Added/removed email
+- Changed password
+- Ask for password reset
+- Grant OAuth access
+
+It is possible to filter particular actions by choosing an audit data type from
+the filter drop-down. You can further filter by specific group, project or user
+(for authentication events).
+
+![audit log](audit_log.png)
+
+### Missing events
+
+Some events are not being tracked in Audit Events. Please see the following
+epics for more detail on which events are not being tracked and our progress
+on adding these events into GitLab:
+
+- [Project settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/474)
+- [Group settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/475)
+- [Instance-level settings and activity](https://gitlab.com/groups/gitlab-org/-/epics/476)
+
+[ee-2336]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2336
+[ee]: https://about.gitlab.com/pricing/
+[permissions]: ../user/permissions.md
diff --git a/doc/administration/audit_log.png b/doc/administration/audit_log.png
new file mode 100644
index 00000000000..d4f4c2abf38
--- /dev/null
+++ b/doc/administration/audit_log.png
Binary files differ
diff --git a/doc/administration/auditor_access_form.png b/doc/administration/auditor_access_form.png
new file mode 100644
index 00000000000..c179a7d3b0a
--- /dev/null
+++ b/doc/administration/auditor_access_form.png
Binary files differ
diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md
new file mode 100644
index 00000000000..ef8c8197d6d
--- /dev/null
+++ b/doc/administration/auditor_users.md
@@ -0,0 +1,88 @@
+# Auditor users **[PREMIUM ONLY]**
+
+>[Introduced][ee-998] in [GitLab Premium][eep] 8.17.
+
+Auditor users are given read-only access to all projects, groups, and other
+resources on the GitLab instance.
+
+## Overview
+
+Auditor users can have full access to their own resources (projects, groups,
+snippets, etc.), and read-only access to **all** other resources, except the
+Admin area. To put another way, they are just regular users (who can be added
+to projects, create personal snippets, create milestones on their groups, etc.)
+who also happen to have read-only access to all projects on the system that
+they haven't been explicitly [given access][permissions] to.
+
+The Auditor role is _not_ a read-only version of the Admin role. Auditor users
+will not be able to access the project/group settings pages, or the Admin Area.
+
+To sum up, assuming you have logged-in as an Auditor user:
+
+- For a project the Auditor is not member of, the Auditor should have
+ read-only access. If the project is public or internal, they would have the
+ same access as the users that are not members of that project/group.
+- For a project the Auditor owns, the Auditor should have full access to
+ everything.
+- For a project the Auditor has been added to as a member, the Auditor should
+ have the same access as the [permissions] they were given to. For example, if
+ they were added as a Developer, they could then push commits or comment on
+ issues.
+- The Auditor cannot view the Admin area, or perform any admin actions.
+
+For more information about what an Auditor can or can't do, see the
+[Permissions and restrictions of an Auditor user](#permissions-and-restrictions-of-an-auditor-user)
+section.
+
+## Use cases
+
+1. Your compliance department wants to run tests against the entire GitLab base
+ to ensure users are complying with password, credit card, and other sensitive
+ data policies. With Auditor users, this can be achieved very easily without
+ resulting to tactics like giving a user admin rights or having to use the API
+ to add them to all projects.
+1. If particular users need visibility or access to most of all projects in
+ your GitLab instance, instead of manually adding the user to all projects,
+ you can simply create an Auditor user and share the credentials with those
+ that you want to grant access to.
+
+## Adding an Auditor user
+
+1. Create a new user or edit an existing one by navigating to
+ **Admin Area > Users**. You will find the option of the access level under
+ the 'Access' section.
+
+ ![Admin Area Form](auditor_access_form.png)
+
+1. Click **Save changes** or **Create user** for the changes to take effect.
+
+To revoke the Auditor permissions from a user, simply make them a Regular user
+following the same steps as above.
+
+## Permissions and restrictions of an Auditor user
+
+An Auditor user should be able to access all projects and groups of a GitLab
+instance, with the following permissions/restrictions:
+
+- Has read-only access to the API
+- Can access projects that are:
+ - Private
+ - Public
+ - Internal
+- Can read all files in a repository
+- Can read issues / MRs
+- Can read project snippets
+- Cannot be Admin and Auditor at the same time
+- Cannot access the Admin area
+- In a group / project they're not a member of:
+ - Cannot access project settings
+ - Cannot access group settings
+ - Cannot commit to repository
+ - Cannot create / comment on issues / MRs
+ - Cannot create/modify files from the Web UI
+ - Cannot merge a merge request
+ - Cannot create project snippets
+
+[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998
+[eep]: https://about.gitlab.com/pricing/
+[permissions]: ../user/permissions.md
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 54be7b616cc..e215a0df6ec 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -9,9 +9,11 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
+ - [LDAP for GitLab EE](ldap-ee.md): LDAP additions to GitLab Enterprise Editions **[STARTER ONLY]**
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd, Azure, Authentiq ID, and JWT
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication
+- [Smartcard](smartcard.md) Smartcard authentication **[PREMIUM ONLY]**
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
index 15276d364a0..1f67e8f5744 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -14,7 +14,7 @@ Managing a large number of users in GitLab can become a burden for system admini
In this guide we will focus on configuring GitLab with Active Directory. [Active Directory](https://en.wikipedia.org/wiki/Active_Directory) is a popular LDAP compatible directory service provided by Microsoft, included in all modern Windows Server operating systems.
-GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html#group-sync) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
+GitLab has supported LDAP integration since [version 2.2](https://about.gitlab.com/2012/02/22/gitlab-version-2-2/). With GitLab LDAP [group syncing](../how_to_configure_ldap_gitlab_ee/index.html#group-sync) being added to GitLab Enterprise Edition in [version 6.0](https://about.gitlab.com/2013/08/20/gitlab-6-dot-0-released/). LDAP integration has become one of the most popular features in GitLab.
## Getting started
@@ -111,7 +111,7 @@ The initial configuration of LDAP in GitLab requires changes to the `gitlab.rb`
The two Active Directory specific values are `active_directory: true` and `uid: 'sAMAccountName'`. `sAMAccountName` is an attribute returned by Active Directory used for GitLab usernames. See the example output from `ldapsearch` for a full list of attributes a "person" object (user) has in **AD** - [`ldapsearch` example](#using-ldapsearch-unix)
-> Both group_base and admin_group configuration options are only available in GitLab Enterprise Edition. See [GitLab EE - LDAP Features](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html#gitlab-enterprise-edition---ldap-features)
+> Both group_base and admin_group configuration options are only available in GitLab Enterprise Edition. See [GitLab EE - LDAP Features](../how_to_configure_ldap_gitlab_ee/index.html#gitlab-enterprise-edition---ldap-features) **[STARTER ONLY]**
### Example `gitlab.rb` LDAP
@@ -145,7 +145,7 @@ Securing LDAP (enabling LDAPS) on Windows Server 2012 involves installing a vali
> By default a LDAP service listens for connections on TCP and UDP port 389. LDAPS (LDAP over SSL) listens on port 636
-### Testing you AD server
+### Testing your AD server
#### Using **AdFind** (Windows)
@@ -267,4 +267,4 @@ have extended functionalities with LDAP, such as:
- Updating user permissions
- Multiple LDAP servers
-Read through the article on [LDAP for GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/) for an overview.
+Read through the article on [LDAP for GitLab EE](../how_to_configure_ldap_gitlab_ee/index.md) **[STARTER ONLY]** for an overview.
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/admin_group.png b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/admin_group.png
new file mode 100644
index 00000000000..9896379d669
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/admin_group.png
Binary files differ
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_link_final.png b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_link_final.png
new file mode 100644
index 00000000000..21fb5a7d0ce
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_link_final.png
Binary files differ
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif
new file mode 100644
index 00000000000..d35cf55804f
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/group_linking.gif
Binary files differ
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif
new file mode 100644
index 00000000000..29b28df1cbd
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/manual_permissions.gif
Binary files differ
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/multi_login.gif b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/multi_login.gif
new file mode 100644
index 00000000000..d317add9837
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/img/multi_login.gif
Binary files differ
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
new file mode 100644
index 00000000000..4d82a7370bb
--- /dev/null
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
@@ -0,0 +1,119 @@
+---
+author: Chris Wilson
+author_gitlab: MrChrisW
+level: intermediary
+article_type: admin guide
+date: 2017-05-03
+---
+
+# How to configure LDAP with GitLab EE **[STARTER ONLY]**
+
+## Introduction
+
+The present article follows [How to Configure LDAP with GitLab CE](../how_to_configure_ldap_gitlab_ce/index.md). Make sure to read through it before moving forward.
+
+## GitLab Enterprise Edition - LDAP features
+
+[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has a number of advantages when it comes to integrating with Active Directory (LDAP):
+
+- [Administrator Sync](../ldap-ee.md#administrator-sync): As an extension of group sync, you can automatically manage your global GitLab administrators. Specify a group CN for `admin_group` and all members of the LDAP group will be given administrator privileges.
+- [Group Sync](#group-sync): This allows GitLab group membership to be automatically updated based on LDAP group members.
+- [Multiple LDAP servers](#multiple-ldap-servers): The ability to configure multiple LDAP servers. This is useful if an organization has different LDAP servers within departments. This is not designed for failover. We're working on [supporting LDAP failover](https://gitlab.com/gitlab-org/gitlab-ee/issues/139) in GitLab.
+
+- Daily user synchronization: Once a day, GitLab will run a synchronization to check and update GitLab users against LDAP. This process updates all user details automatically.
+
+On the following section, you'll find a description for each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
+
+![GitLab OU Structure](img/admin_group.png)
+
+All members of the group `Global Admins` will be given **administrator** access to GitLab, allowing them to view the `/admin` dashboard.
+
+### Group Sync
+
+Group syncing allows AD (LDAP) groups to be mapped to GitLab groups. This provides more control over per-group user management. To configure group syncing edit the `group_base` **DN** (`'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org'`). This **OU** contains all groups that will be associated with [GitLab groups](../../../user/group/index.md).
+
+#### Creating group links - example
+
+As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, it is important that users of this group are given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
+
+**UK Developers** of our "UKGov" group are given **"developer"** permissions.
+
+_The developer permission allows the development staff to effectively manage all project code, issues, and merge requests._
+
+**UK Support** staff of our "UKGov" group are given **"reporter"** permissions.
+
+_The reporter permission allows support staff to manage issues, labels, and review project code._
+
+**US People Ops** of our "UKGov" group are given **"guest"** permissions.
+
+![Creating group links](img/group_linking.gif)
+
+> Guest permissions allows people ops staff to review and lodge new issues while allowing no read or write access to project code or [confidential issues](../../../user/project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues) created by other users.
+
+See the [permission list](../../../user/permissions.md) for complementary info.
+
+#### Group permissions - example
+
+Considering the previous example, our staff will have
+access to our GitLab instance with the following structure:
+
+![GitLab OU Structure](img/group_link_final.png)
+
+Using this permission structure in our example allows only UK staff access to sensitive information stored in the projects code, while still allowing other teams to work effectively. As all permissions are controlled via AD groups new users can be quickly added to existing groups. New group members will then automatically inherit the required permissions.
+
+> [More information](../ldap-ee.md#group-sync) on group syncing.
+
+### Updating user permissions - new feature
+
+Since GitLab [v8.15](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/822) LDAP user permissions can now be manually overridden by an admin user. To override a user's permissions visit the groups **Members** page and select **Edit permissions**.
+
+![Setting manual permissions](img/manual_permissions.gif)
+
+### Multiple LDAP servers
+
+GitLab EE can support multiple LDAP servers. Simply configure another server in the `gitlab.rb` file within the `ldap_servers` block. In the example below we configure a new secondary server with the label **GitLab Secondary AD**. This is shown on the GitLab login screen. Large enterprises often utilize multiple LDAP servers for segregating organizational departments.
+
+![Multiple LDAP Servers Login](img/multi_login.gif)
+
+Considering the example illustrated on the image above,
+our `gitlab.rb` configuration would look like:
+
+```ruby
+gitlab_rails['ldap_enabled'] = true
+gitlab_rails['ldap_servers'] = {
+'main' => {
+ 'label' => 'GitLab AD',
+ 'host' => 'ad.example.org',
+ 'port' => 636,
+ 'uid' => 'sAMAccountName',
+ 'method' => 'ssl',
+ 'bind_dn' => 'CN=GitLabSRV,CN=Users,DC=GitLab,DC=org',
+ 'password' => 'Password1',
+ 'active_directory' => true,
+ 'base' => 'OU=GitLab INT,DC=GitLab,DC=org',
+ 'group_base' => 'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org',
+ 'admin_group' => 'Global Admins'
+ },
+
+'secondary' => {
+ 'label' => 'GitLab Secondary AD',
+ 'host' => 'ad-secondary.example.net',
+ 'port' => 636,
+ 'uid' => 'sAMAccountName',
+ 'method' => 'ssl',
+ 'bind_dn' => 'CN=GitLabSRV,CN=Users,DC=GitLab,DC=com',
+ 'password' => 'Password1',
+ 'active_directory' => true,
+ 'base' => 'OU=GitLab Secondary,DC=GitLab,DC=com',
+ 'group_base' => 'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=com',
+ 'admin_group' => 'Global Admins'
+ }
+}
+```
+
+## Conclusion
+
+Integration of GitLab with Active Directory (LDAP) reduces the complexity of user management.
+It has the advantage of improving user permission controls, whilst easing the deployment of GitLab into an existing [IT environment](https://www.techopedia.com/definition/29199/it-infrastructure). GitLab EE offers advanced group management and multiple LDAP servers.
+
+With the assistance of the [GitLab Support](https://about.gitlab.com/support) team, setting up GitLab with an existing AD/LDAP solution will be a smooth and painless process.
diff --git a/doc/administration/auth/ldap-ee.md b/doc/administration/auth/ldap-ee.md
new file mode 100644
index 00000000000..30095d35705
--- /dev/null
+++ b/doc/administration/auth/ldap-ee.md
@@ -0,0 +1,557 @@
+# LDAP Additions in GitLab EE **[STARTER ONLY]**
+
+This is a continuation of the main [LDAP documentation](ldap.md), detailing LDAP
+features specific to GitLab Enterprise Edition Starter, Premium and Ultimate.
+
+## Overview
+
+[LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
+stands for **Lightweight Directory Access Protocol**, which
+is a standard application protocol for
+accessing and maintaining distributed directory information services
+over an Internet Protocol (IP) network.
+
+GitLab integrates with LDAP to support **user authentication**. This integration
+works with most LDAP-compliant directory servers, including Microsoft Active
+Directory, Apple Open Directory, Open LDAP, and 389 Server.
+**GitLab Enterprise Edition** includes enhanced integration, including group
+membership syncing.
+
+## Use cases
+
+- User sync: Once a day, GitLab will update users against LDAP
+- Group sync: Once an hour, GitLab will update group membership
+ based on LDAP group members
+
+## Multiple LDAP servers
+
+With GitLab Enterprise Edition Starter, you can configure multiple LDAP servers
+that your GitLab instance will connect to.
+
+To add another LDAP server, you can start by duplicating the settings under
+[the main configuration](ldap.md#configuration) and edit them to match the
+additional LDAP server.
+
+Be sure to choose a different provider ID made of letters a-z and numbers 0-9.
+This ID will be stored in the database so that GitLab can remember which LDAP
+server a user belongs to.
+
+## User sync
+
+Once per day, GitLab will run a worker to check and update GitLab
+users against LDAP.
+
+The process will execute the following access checks:
+
+1. Ensure the user is still present in LDAP
+1. If the LDAP server is Active Directory, ensure the user is active (not
+ blocked/disabled state). This will only be checked if
+ `active_directory: true` is set in the LDAP configuration [^1]
+
+The user will be set to `ldap_blocked` state in GitLab if the above conditions
+fail. This means the user will not be able to login or push/pull code.
+
+The process will also update the following user information:
+
+1. Email address
+1. If `sync_ssh_keys` is set, SSH public keys
+1. If Kerberos is enabled, Kerberos identity
+
+NOTE: **Note:**
+The LDAP sync process updates existing users while new users will
+be created on first sign in.
+
+## Group Sync
+
+If your LDAP supports the `memberof` property, when the user signs in for the
+first time GitLab will trigger a sync for groups the user should be a member of.
+That way they don't need to wait for the hourly sync to be granted
+access to their groups and projects.
+
+In GitLab Premium, we can also add a GitLab group to sync with one or multiple LDAP groups or we can
+also add a filter. The filter must comply with the syntax defined in [RFC 2254](https://tools.ietf.org/search/rfc2254).
+
+A group sync process will run every hour on the hour, and `group_base` must be set
+in LDAP configuration for LDAP synchronizations based on group CN to work. This allows
+GitLab group membership to be automatically updated based on LDAP group members.
+
+The `group_base` configuration should be a base LDAP 'container', such as an
+'organization' or 'organizational unit', that contains LDAP groups that should
+be available to GitLab. For example, `group_base` could be
+`ou=groups,dc=example,dc=com`. In the config file it will look like the
+following.
+
+**Omnibus configuration**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+ main:
+ ## snip...
+ ##
+ ## Base where we can search for groups
+ ##
+ ## Ex. ou=groups,dc=gitlab,dc=example
+ ##
+ ##
+ group_base: ou=groups,dc=example,dc=com
+ EOS
+ ```
+
+1. [Reconfigure GitLab][reconfigure] for the changes to take effect.
+
+**Source configuration**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ group_base: ou=groups,dc=example,dc=com
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+---
+
+To take advantage of group sync, group owners or maintainers will need to create an
+LDAP group link in their group **Settings > LDAP Groups** page. Multiple LDAP
+groups and/or filters can be linked with a single GitLab group. When the link is
+created, an access level/role is specified (Guest, Reporter, Developer, Maintainer,
+or Owner).
+
+## Administrator sync
+
+As an extension of group sync, you can automatically manage your global GitLab
+administrators. Specify a group CN for `admin_group` and all members of the
+LDAP group will be given administrator privileges. The configuration will look
+like the following.
+
+NOTE: **Note:**
+Administrators will not be synced unless `group_base` is also
+specified alongside `admin_group`. Also, only specify the CN of the admin
+group, as opposed to the full DN.
+
+**Omnibus configuration**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+ main:
+ ## snip...
+ ##
+ ## Base where we can search for groups
+ ##
+ ## Ex. ou=groups,dc=gitlab,dc=example
+ ##
+ ##
+ group_base: ou=groups,dc=example,dc=com
+
+ ##
+ ## The CN of a group containing GitLab administrators
+ ##
+ ## Ex. administrators
+ ##
+ ## Note: Not `cn=administrators` or the full DN
+ ##
+ ##
+ admin_group: my_admin_group
+
+ EOS
+ ```
+
+1. [Reconfigure GitLab][reconfigure] for the changes to take effect.
+
+**Source configuration**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ group_base: ou=groups,dc=example,dc=com
+ admin_group: my_admin_group
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Adjusting LDAP user sync schedule
+
+> Introduced in GitLab Enterprise Edition Starter.
+
+NOTE: **Note:**
+These are cron formatted values. You can use a crontab generator to create
+these values, for example http://www.crontabgenerator.com/.
+
+By default, GitLab will run a worker once per day at 01:30 a.m. server time to
+check and update GitLab users against LDAP.
+
+You can manually configure LDAP user sync times by setting the
+following configuration values. The example below shows how to set LDAP user
+sync to run once every 12 hours at the top of the hour.
+
+**Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_sync_worker_cron'] = "0 */12 * * *"
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+**Source installations**
+
+1. Edit `config/gitlab.yaml`:
+
+ ```yaml
+ cron_jobs:
+ ldap_sync_worker_cron:
+ "0 */12 * * *"
+ ```
+
+1. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
+
+## Adjusting LDAP group sync schedule
+
+NOTE: **Note:**
+These are cron formatted values. You can use a crontab generator to create
+these values, for example http://www.crontabgenerator.com/.
+
+By default, GitLab will run a group sync process every hour, on the hour.
+
+CAUTION: **Important:**
+It's recommended that you do not run too short intervals as this
+could lead to multiple syncs running concurrently. This is primarily a concern
+for installations with a large number of LDAP users. Please review the
+[LDAP group sync benchmark metrics](#benchmarks) to see how
+your installation compares before proceeding.
+
+You can manually configure LDAP group sync times by setting the
+following configuration values. The example below shows how to set group
+sync to run once every 2 hours at the top of the hour.
+
+**Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_group_sync_worker_cron'] = "0 */2 * * * *"
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+**Source installations**
+
+1. Edit `config/gitlab.yaml`:
+
+ ```yaml
+ cron_jobs:
+ ldap_group_sync_worker_cron:
+ "*/30 * * * *"
+ ```
+
+1. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
+
+## External groups
+
+> Introduced in GitLab Enterprise Edition Starter 8.9.
+
+Using the `external_groups` setting will allow you to mark all users belonging
+to these groups as [external users](../../user/permissions.md#external-users-permissions).
+Group membership is checked periodically through the `LdapGroupSync` background
+task.
+
+**Omnibus configuration**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+ main:
+ ## snip...
+ ##
+ ## An array of CNs of groups containing users that should be considered external
+ ##
+ ## Ex. ['interns', 'contractors']
+ ##
+ ## Note: Not `cn=interns` or the full DN
+ ##
+ external_groups: ['interns', 'contractors']
+ EOS
+ ```
+
+1. [Reconfigure GitLab][reconfigure] for the changes to take effect.
+
+**Source configuration**
+
+1. Edit `config/gitlab.yaml`:
+
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ external_groups: ['interns', 'contractors']
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Group sync technical details
+
+There is a lot going on with group sync 'under the hood'. This section
+outlines what LDAP queries are executed and what behavior you can expect
+from group sync.
+
+Group member access will be downgraded from a higher level if their LDAP group
+membership changes. For example, if a user has 'Owner' rights in a group and the
+next group sync reveals they should only have 'Developer' privileges, their
+access will be adjusted accordingly. The only exception is if the user is the
+*last* owner in a group. Groups need at least one owner to fulfill
+administrative duties.
+
+### Supported LDAP group types/attributes
+
+GitLab supports LDAP groups that use member attributes `member`, `submember`,
+`uniquemember`, `memberof` and `memberuid`. This means group sync supports, at
+least, LDAP groups with object class `groupOfNames`, `posixGroup`, and
+`groupOfUniqueName`. Other object classes should work fine as long as members
+are defined as one of the mentioned attributes. This also means GitLab supports
+Microsoft Active Directory, Apple Open Directory, Open LDAP, and 389 Server.
+Other LDAP servers should work, too.
+
+Active Directory also supports nested groups. Group sync will recursively
+resolve membership if `active_directory: true` is set in the configuration file.
+
+> **Note:** Nested group membership will only be resolved if the nested group
+ also falls within the configured `group_base`. For example, if GitLab sees a
+ nested group with DN `cn=nested_group,ou=special_groups,dc=example,dc=com` but
+ the configured `group_base` is `ou=groups,dc=example,dc=com`, `cn=nested_group`
+ will be ignored.
+
+### Queries
+
+- Each LDAP group is queried a maximum of one time with base `group_base` and
+ filter `(cn=<cn_from_group_link>)`.
+- If the LDAP group has the `memberuid` attribute, GitLab will execute another
+ LDAP query per member to obtain each user's full DN. These queries are
+ executed with base `base`, scope 'base object', and a filter depending on
+ whether `user_filter` is set. Filter may be `(uid=<uid_from_group>)` or a
+ joining of `user_filter`.
+
+### Benchmarks
+
+Group sync was written to be as performant as possible. Data is cached, database
+queries are optimized, and LDAP queries are minimized. The last benchmark run
+revealed the following metrics:
+
+For 20,000 LDAP users, 11,000 LDAP groups and 1,000 GitLab groups with 10
+LDAP group links each:
+
+- Initial sync (no existing members assigned in GitLab) took 1.8 hours
+- Subsequent syncs (checking membership, no writes) took 15 minutes
+
+These metrics are meant to provide a baseline and performance may vary based on
+any number of factors. This was a pretty extreme benchmark and most instances will
+not have near this many users or groups. Disk speed, database performance,
+network and LDAP server response time will affect these metrics.
+
+## Troubleshooting
+
+### Referral error
+
+If you see `LDAP search error: Referral` in the logs, or when troubleshooting
+LDAP Group Sync, this error may indicate a configuration problem. The LDAP
+configuration `/etc/gitlab/gitlab.rb` (Omnibus) or `config/gitlab.yml` (source)
+is in YAML format and is sensitive to indentation. Check that `group_base` and
+`admin_group` configuration keys are indented 2 spaces past the server
+identifier. The default identifier is `main` and an example snippet looks like
+the following:
+
+```yaml
+main: # 'main' is the GitLab 'provider ID' of this LDAP server
+ label: 'LDAP'
+ host: 'ldap.example.com'
+ ...
+ group_base: 'cn=my_group,ou=groups,dc=example,dc=com'
+ admin_group: 'my_admin_group'
+```
+
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
+
+[^1]: In Active Directory, a user is marked as disabled/blocked if the user
+ account control attribute (`userAccountControl:1.2.840.113556.1.4.803`)
+ has bit 2 set. See https://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
+ for more information.
+
+### User DN has changed
+
+When an LDAP user is created in GitLab, their LDAP DN is stored for later reference.
+
+If GitLab cannot find a user by their DN, it will attempt to fallback
+to finding the user by their email. If the lookup is successful, GitLab will
+update the stored DN to the new value.
+
+### User is not being added to a group
+
+Sometimes you may think a particular user should be added to a GitLab group via
+LDAP group sync, but for some reason it's not happening. There are several
+things to check to debug the situation.
+
+- Ensure LDAP configuration has a `group_base` specified. This configuration is
+ required for group sync to work properly.
+- Ensure the correct LDAP group link is added to the GitLab group. Check group
+ links by visiting the GitLab group, then **Settings dropdown -> LDAP groups**.
+- Check that the user has an LDAP identity
+ 1. Sign in to GitLab as an administrator user.
+ 1. Navigate to **Admin area -> Users**.
+ 1. Search for the user
+ 1. Open the user, by clicking on their name. Do not click 'Edit'.
+ 1. Navigate to the **Identities** tab. There should be an LDAP identity with
+ an LDAP DN as the 'Identifier'.
+
+If all of the above looks good, jump in to a little more advanced debugging.
+Often, the best way to learn more about why group sync is behaving a certain
+way is to enable debug logging. There is verbose output that details every
+step of the sync.
+
+1. Start a Rails console
+
+ ```bash
+ # For Omnibus installations
+ sudo gitlab-rails console
+
+ # For installations from source
+ sudo -u git -H bundle exec rails console production
+ ```
+1. Set the log level to debug (only for this session):
+
+ ```ruby
+ Rails.logger.level = Logger::DEBUG
+ ```
+1. Choose a GitLab group to test with. This group should have an LDAP group link
+ already configured. If the output is `nil`, the group could not be found.
+ If a bunch of group attributes are output, your group was found successfully.
+
+ ```ruby
+ group = Group.find_by(name: 'my_group')
+
+ # Output
+ => #<Group:0x007fe825196558 id: 1234, name: "my_group"...>
+ ```
+1. Run a group sync for this particular group.
+
+ ```ruby
+ EE::Gitlab::Auth::LDAP::Sync::Group.execute_all_providers(group)
+ ```
+1. Look through the output of the sync. See [example log output](#example-log-output)
+ below for more information about the output.
+1. If you still aren't able to see why the user isn't being added, query the
+ LDAP group directly to see what members are listed. Still in the Rails console,
+ run the following query:
+
+ ```ruby
+ adapter = Gitlab::Auth::LDAP::Adapter.new('ldapmain') # If `main` is the LDAP provider
+ ldap_group = EE::Gitlab::Auth::LDAP::Group.find_by_cn('group_cn_here', adapter)
+
+ # Output
+ => #<EE::Gitlab::Auth::LDAP::Group:0x007fcbdd0bb6d8
+ ```
+1. Query the LDAP group's member DNs and see if the user's DN is in the list.
+ One of the DNs here should match the 'Identifier' from the LDAP identity
+ checked earlier. If it doesn't, the user does not appear to be in the LDAP
+ group.
+
+ ```ruby
+ ldap_group.member_dns
+
+ # Output
+ => ["uid=john,ou=people,dc=example,dc=com", "uid=mary,ou=people,dc=example,dc=com"]
+ ```
+1. Some LDAP servers don't store members by DN. Rather, they use UIDs instead.
+ If you didn't see results from the last query, try querying by UIDs instead.
+
+ ```ruby
+ ldap_group.member_uids
+
+ # Output
+ => ['john','mary']
+ ```
+
+#### Example log output
+
+The output of the last command will be very verbose, but contains lots of
+helpful information. For the most part you can ignore log entries that are SQL
+statements.
+
+Indicates the point where syncing actually begins:
+
+```bash
+Started syncing all providers for 'my_group' group
+```
+
+The follow entry shows an array of all user DNs GitLab sees in the LDAP server.
+Note that these are the users for a single LDAP group, not a GitLab group. If
+you have multiple LDAP groups linked to this GitLab group, you will see multiple
+log entries like this - one for each LDAP group. If you don't see an LDAP user
+DN in this log entry, LDAP is not returning the user when we do the lookup.
+Verify the user is actually in the LDAP group.
+
+```bash
+Members in 'ldap_group_1' LDAP group: ["uid=john0,ou=people,dc=example,dc=com",
+"uid=mary0,ou=people,dc=example,dc=com", "uid=john1,ou=people,dc=example,dc=com",
+"uid=mary1,ou=people,dc=example,dc=com", "uid=john2,ou=people,dc=example,dc=com",
+"uid=mary2,ou=people,dc=example,dc=com", "uid=john3,ou=people,dc=example,dc=com",
+"uid=mary3,ou=people,dc=example,dc=com", "uid=john4,ou=people,dc=example,dc=com",
+"uid=mary4,ou=people,dc=example,dc=com"]
+```
+
+Shortly after each of the above entries, you will see a hash of resolved member
+access levels. This hash represents all user DNs GitLab thinks should have
+access to this group, and at which access level (role). This hash is additive,
+and more DNs may be added, or existing entries modified, based on additional
+LDAP group lookups. The very last occurrence of this entry should indicate
+exactly which users GitLab believes should be added to the group.
+
+> **Note:** 10 is 'Guest', 20 is 'Reporter', 30 is 'Developer', 40 is 'Maintainer'
+ and 50 is 'Owner'
+
+```bash
+Resolved 'my_group' group member access: {"uid=john0,ou=people,dc=example,dc=com"=>30,
+"uid=mary0,ou=people,dc=example,dc=com"=>30, "uid=john1,ou=people,dc=example,dc=com"=>30,
+"uid=mary1,ou=people,dc=example,dc=com"=>30, "uid=john2,ou=people,dc=example,dc=com"=>30,
+"uid=mary2,ou=people,dc=example,dc=com"=>30, "uid=john3,ou=people,dc=example,dc=com"=>30,
+"uid=mary3,ou=people,dc=example,dc=com"=>30, "uid=john4,ou=people,dc=example,dc=com"=>30,
+"uid=mary4,ou=people,dc=example,dc=com"=>30}
+```
+
+It's not uncommon to see warnings like the following. These indicate that GitLab
+would have added the user to a group, but the user could not be found in GitLab.
+Usually this is not a cause for concern.
+
+If you think a particular user should already exist in GitLab, but you're seeing
+this entry, it could be due to a mismatched DN stored in GitLab. See
+[User DN has changed](#User-DN-has-changed) to update the user's LDAP identity.
+
+```bash
+User with DN `uid=john0,ou=people,dc=example,dc=com` should have access
+to 'my_group' group but there is no user in GitLab with that
+identity. Membership will be updated once the user signs in for
+the first time.
+```
+
+Finally, the following entry says syncing has finished for this group:
+
+```bash
+Finished syncing all providers for 'my_group' group
+```
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 2d057dc7509..423a79c56d8 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -14,7 +14,7 @@ including group membership syncing as well as multiple LDAP servers support.
The information on this page is relevant for both GitLab CE and EE. For more
details about EE-specific LDAP features, see the
-[LDAP Enterprise Edition documentation](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html).
+[LDAP Enterprise Edition documentation](ldap-ee.md). **[STARTER ONLY]**
## Security
@@ -39,7 +39,7 @@ immediately block all access.
NOTE: **Note**:
GitLab Enterprise Edition Starter supports a
-[configurable sync time](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#adjusting-ldap-user-and-group-sync-schedules),
+[configurable sync time](ldap-ee.md#adjusting-ldap-user-sync-schedule),
with a default of one hour.
## Git password authentication
@@ -64,6 +64,7 @@ to connect to one GitLab server.
For a complete guide on configuring LDAP with GitLab Community Edition, please check
the admin guide [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md).
+For GitLab Enterprise Editions, see also [How to configure LDAP with GitLab EE](how_to_configure_ldap_gitlab_ee/index.md). **[STARTER ONLY]**
To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml` for Omnibus
@@ -388,7 +389,7 @@ group, you can use the following syntax:
Find more information about this "LDAP_MATCHING_RULE_IN_CHAIN" filter at
<https://docs.microsoft.com/en-us/windows/desktop/ADSI/search-filter-syntax>. Support for
nested members in the user filter should not be confused with
-[group sync nested groups support (EE only)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#supported-ldap-group-types-attributes).
+[group sync nested groups support](ldap-ee.md#supported-ldap-group-typesattributes). **[STARTER ONLY]**
Please note that GitLab does not support the custom filter syntax used by
omniauth-ldap.
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
new file mode 100644
index 00000000000..e55f7dbb4df
--- /dev/null
+++ b/doc/administration/auth/oidc.md
@@ -0,0 +1,105 @@
+# OpenID Connect OmniAuth provider
+
+GitLab can use [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) as an OmniAuth provider.
+
+To enable the OpenID Connect OmniAuth provider, you must register your application with an OpenID Connect provider.
+The OpenID Connect will provide you with a client details and secret for you to use.
+
+1. On your GitLab server, open the configuration file.
+
+ For Omnibus GitLab:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+ See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration.
+
+ For Omnibus GitLab:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ { 'name' => 'openid_connect',
+ 'label' => '<your_oidc_label>',
+ 'args' => {
+ 'scope' => ['openid','profile'],
+ 'response_type' => 'code',
+ 'issuer' => '<your_oidc_url>',
+ 'discovery' => true,
+ 'client_auth_method' => 'query',
+ 'uid_field' => '<uid_field>',
+ 'client_options' => {
+ 'identifier' => '<your_oidc_client_id>',
+ 'secret' => '<your_oidc_client_secret>',
+ 'redirect_uri' => '<your_gitlab_url>/users/auth/openid_connect/callback'
+ }
+ }
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```yaml
+ - { name: 'openid_connect',
+ label: '<your_oidc_label>',
+ args: {
+ scope: ['openid','profile'],
+ response_type: 'code',
+ issuer: '<your_oidc_url>',
+ discovery: true,
+ client_auth_method: 'query',
+ uid_field: '<uid_field>',
+ client_options: {
+ identifier: '<your_oidc_client_id>',
+ secret: '<your_oidc_client_secret>',
+ redirect_uri: '<your_gitlab_url>/users/auth/openid_connect/callback'
+ }
+ }
+ }
+ ```
+
+ > **Note:**
+ >
+ > - For more information on each configuration option refer to
+ the [OmniAuth OpenID Connect usage documentation](https://github.com/m0n9oose/omniauth_openid_connect#usage) and
+ the [OpenID Connect Core 1.0 specification](https://openid.net/specs/openid-connect-core-1_0.html).
+
+1. For the configuration above, change the values for the provider to match your OpenID Connect client setup. Use the following as a guide:
+ - `<your_oidc_label>` is the label that will be displayed on the login page.
+ - `<your_oidc_url>` (optional) is the URL that points to the OpenID Connect provider. For example, `https://example.com/auth/realms/your-realm`.
+ If this value is not provided, the URL is constructed from the `client_options` in the following format: `<client_options.scheme>://<client_options.host>:<client_options.port>`.
+ - If `discovery` is set to `true`, the OpenID Connect provider will try to auto discover the client options using `<your_oidc_url>/.well-known/openid-configuration`. Defaults to `false`.
+ - `<uid_field>` (optional) is the field name from the `user_info` details that will be used as `uid` value. For example, `preferred_username`.
+ If this value is not provided or the field with the configured value is missing from the `user_info` details, the `uid` will use the `sub` field.
+ - `client_options` are the OpenID Connect client-specific options. Specifically:
+
+ - `identifier` is the client identifier as configured in the OpenID Connect service provider.
+ - `secret` is the client secret as configured in the OpenID Connect service provider.
+ - `redirect_uri` is the GitLab URL to redirect the user to after successful login. For example, `http://example.com/users/auth/openid_connect/callback`.
+ - `end_session_endpoint` (optional) is the URL to the endpoint that end the session (logout). Can be provided if auto-discovery disabled or unsuccessful.
+
+ The following `client_options` are optional unless auto-discovery is disabled or unsuccessful:
+
+ - `authorization_endpoint` is the URL to the endpoint that authorizes the end user.
+ - `token_endpoint` is the URL to the endpoint that provides Access Token.
+ - `userinfo_endpoint` is the URL to the endpoint that provides the user information.
+ - `jwks_uri` is the URL to the endpoint where the Token signer publishes its keys.
+
+1. Save the configuration file.
+1. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source)
+ for the changes to take effect if you installed GitLab via Omnibus or from source respectively.
+
+On the sign in page, there should now be an OpenID Connect icon below the regular sign in form.
+Click the icon to begin the authentication process. The OpenID Connect provider will ask the user to
+sign in and authorize the GitLab application (if confirmation required by the client). If everything goes well, the user
+will be redirected to GitLab and will be signed in.
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
new file mode 100644
index 00000000000..b33c5359b44
--- /dev/null
+++ b/doc/administration/auth/smartcard.md
@@ -0,0 +1,186 @@
+# Smartcard authentication **[PREMIUM ONLY]**
+
+GitLab supports authentication using smartcards.
+
+## Authentication methods
+
+GitLab supports two authentication methods:
+
+- X.509 certificates with local databases.
+- LDAP servers.
+
+### Authentication against a local database with X.509 certificates
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/726) in
+[GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental
+feature. Smartcard authentication against local databases may change or be
+removed completely in future releases.
+
+Smartcards with X.509 certificates can be used to authenticate with GitLab.
+
+To use a smartcard with an X.509 certificate to authenticate against a local
+database with GitLab, `CN` and `emailAddress` must be defined in the
+certificate. For example:
+
+```
+Certificate:
+ Data:
+ Version: 1 (0x0)
+ Serial Number: 12856475246677808609 (0xb26b601ecdd555e1)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: O=Random Corp Ltd, CN=Random Corp
+ Validity
+ Not Before: Oct 30 12:00:00 2018 GMT
+ Not After : Oct 30 12:00:00 2019 GMT
+ Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com
+```
+
+### Authentication against an LDAP server
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7693) in
+[GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental
+feature. Smartcard authentication against an LDAP server may change or be
+removed completely in future releases.
+
+GitLab implements a standard way of certificate matching following
+[RFC4523](https://tools.ietf.org/html/rfc4523). It uses the
+`certificateExactMatch` certificate matching rule against the `userCertificate`
+attribute. As a prerequisite, you must use an LDAP server that:
+
+- Supports the `certificateExactMatch` matching rule.
+- Has the certificate stored in the `userCertificate` attribute.
+
+## Configure GitLab for smartcard authentication
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['smartcard_enabled'] = true
+ gitlab_rails['smartcard_ca_file'] = "/etc/ssl/certs/CA.pem"
+ gitlab_rails['smartcard_client_certificate_required_port'] = 3444
+ ```
+
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Configure NGINX to request a client side certificate
+
+ In NGINX configuration, an **additional** server context must be defined with
+ the same configuration except:
+
+ - The additional NGINX server context must be configured to run on a different
+ port:
+
+ ```
+ listen *:3444 ssl;
+ ```
+
+ - The additional NGINX server context must be configured to require the client
+ side certificate:
+
+ ```
+ ssl_verify_depth 2;
+ ssl_client_certificate /etc/ssl/certs/CA.pem;
+ ssl_verify_client on;
+ ```
+
+ - The additional NGINX server context must be configured to forward the client
+ side certificate:
+
+ ```
+ proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
+ ```
+
+ For example, the following is an example server context in an NGINX
+ configuration file (eg. in `/etc/nginx/sites-available/gitlab-ssl`):
+
+ ```
+ server {
+ listen *:3444 ssl;
+
+ # certificate for configuring SSL
+ ssl_certificate /path/to/example.com.crt;
+ ssl_certificate_key /path/to/example.com.key;
+
+ ssl_verify_depth 2;
+ # CA certificate for client side certificate verification
+ ssl_client_certificate /etc/ssl/certs/CA.pem;
+ ssl_verify_client on;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+
+ proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
+
+ proxy_read_timeout 300;
+
+ proxy_pass http://gitlab-workhorse;
+ }
+ }
+ ```
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ ## Smartcard authentication settings
+ smartcard:
+ # Allow smartcard authentication
+ enabled: true
+
+ # Path to a file containing a CA certificate
+ ca_file: '/etc/ssl/certs/CA.pem'
+
+ # Port where the client side certificate is requested by NGINX
+ client_certificate_required_port: 3444
+ ```
+
+1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
+ GitLab for the changes to take effect.
+
+### Additional steps when authenticating against an LDAP server
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = YAML.load <<-EOS
+ main:
+ # snip...
+ # Enable smartcard authentication against the LDAP server. Valid values
+ # are "false", "optional", and "required".
+ smartcard_auth: optional
+ EOS
+ ```
+
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ # Enable smartcard authentication against the LDAP server. Valid values
+ # are "false", "optional", and "required".
+ smartcard_auth: optional
+ ```
+
+1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
+ GitLab for the changes to take effect.
diff --git a/doc/administration/compliance.md b/doc/administration/compliance.md
index 72cb57fb36c..9c13ff772b3 100644
--- a/doc/administration/compliance.md
+++ b/doc/administration/compliance.md
@@ -11,8 +11,8 @@ GitLab’s [security features](../security/README.md) may also help you meet rel
|**[Enforce TOS acceptance](../user/admin_area/settings/terms.md)**<br>Enforce your users accepting new terms of service by blocking GitLab traffic.|Core+||
|**[Email all users of a project, group, or entire server](../user/admin_area/settings/terms.md)**<br>An admin can email groups of users based on project or group membership, or email everyone using the GitLab instance. This is great for scheduled maintenance or upgrades.|Starter+||
|**[Omnibus package supports log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-forwarding)**<br>Forward your logs to a central system.|Starter+||
-|**[Lock project membership to group](https://docs.gitlab.com/ee/user/group/index.html#member-lock-starter)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|
+|**[Lock project membership to group](../user/group/index.md#member-lock-starter)**<br>Group owners can prevent new members from being added to projects within a group.|Starter+|✓|
|**[LDAP group sync](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition gives admins the ability to automatically sync groups and manage SSH keys, permissions, and authentication, so you can focus on building your product, not configuring your tools.|Starter+||
|**[LDAP group sync filters](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#group-sync)**<br>GitLab Enterprise Edition Premium gives more flexibility to synchronize with LDAP based on filters, meaning you can leverage LDAP attributes to map GitLab permissions.|Premium+||
-|**[Audit logs](https://docs.gitlab.com/ee/administration/audit_events.html)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
-|**[Auditor users](https://docs.gitlab.com/ee/administration/auditor_users.html)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+|| \ No newline at end of file
+|**[Audit logs](audit_events.md)**<br>To maintain the integrity of your code, GitLab Enterprise Edition Premium gives admins the ability to view any modifications made within the GitLab server in an advanced audit log system, so you can control, analyze and track every change.|Premium+||
+|**[Auditor users](auditor_users.md)**<br>Auditor users are users who are given read-only access to all projects, groups, and other resources on the GitLab instance.|Premium+||
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 05beb724d4d..4d55f2357c1 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -384,11 +384,12 @@ CAUTION: **Warning:** GitLab will not backup Docker images that are not stored o
filesystem. Remember to enable backups with your object storage provider if
desired.
----
+NOTE: **Note:**
+`regionendpoint` is only required when configuring an S3 compatible service such as Minio. It takes a URL such as `http://127.0.0.1:9000`.
**Omnibus GitLab installations**
-To configure the storage driver in Omnibus:
+To configure the `s3` storage driver in Omnibus:
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -398,29 +399,29 @@ To configure the storage driver in Omnibus:
'accesskey' => 's3-access-key',
'secretkey' => 's3-secret-key-for-access-key',
'bucket' => 'your-s3-bucket',
- 'region' => 'your-s3-region'
+ 'region' => 'your-s3-region',
+ 'regionendpoint' => 'your-s3-regionendpoint'
}
}
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
----
-
**Installations from source**
Configuring the storage driver is done in your registry config YML file created
when you [deployed your docker registry][registry-deploy].
-Example:
+`s3` storage driver example:
-```
+```yml
storage:
s3:
accesskey: 'AKIAKIAKI'
secretkey: 'secret123'
bucket: 'gitlab-registry-bucket-AKIAKIAKI'
region: 'your-s3-region'
+ regionendpoint: 'your-s3-regionendpoint'
cache:
blobdescriptor: inmemory
delete:
@@ -658,6 +659,37 @@ Start with a value between `25000000` (25MB) and `50000000` (50MB).
1. Save the file and [restart GitLab][] for the changes to take effect.
+### Supporting older Docker clients
+
+As of GitLab 11.9, we began shipping version 2.7.1 of the Docker container registry, which disables the schema1 manifest by default. If you are still using older Docker clients (1.9 or older), you may experience an error pushing images. See [omnibus-4145](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4145) for more details.
+
+You can add a configuration option for backwards compatibility.
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['compatibility_schema1_enabled'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit the YML configuration file you created when you [deployed the registry][registry-deploy]. Add the following snippet:
+
+ ```yaml
+ compatibility:
+ schema1:
+ enabled: true
+ ```
+
+1. Restart the registry for the changes to take affect.
+
+
[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
[docker-insecure-self-signed]: https://docs.docker.com/registry/insecure/#use-self-signed-certificates
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 28afaf84f5a..288cb1bf0bb 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -1,12 +1,16 @@
-# Custom Git Hooks
+# Custom server-side Git hooks
-> **Note:** Custom Git hooks must be configured on the filesystem of the GitLab
+NOTE: **Note:**
+Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
Please explore [webhooks] and [CI] as an option if you do not
have filesystem access. For a user configurable Git hook interface, see
[Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html),
available in GitLab Enterprise Edition.
+NOTE: **Note:**
+Custom Git hooks won't be replicated to secondary nodes if you use [GitLab Geo][gitlab-geo]
+
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
See [Git SCM Server-Side Hooks][hooks] for more information about each hook type.
@@ -14,15 +18,14 @@ See [Git SCM Server-Side Hooks][hooks] for more information about each hook type
As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab
administrators can add custom git hooks to any GitLab project.
-## Setup
-
-Normally, Git hooks are placed in the repository or project's `hooks` directory.
-GitLab creates a symlink from each project's `hooks` directory to the
-gitlab-shell `hooks` directory for ease of maintenance between gitlab-shell
-upgrades. As such, custom hooks are implemented a little differently. Behavior
-is exactly the same once the hook is created, though.
+## Create a custom Git hook for a repository
-Follow the steps below to set up a custom hook:
+Server-side Git hooks are typically placed in the repository's `hooks`
+subdirectory. In GitLab, hook directories are are symlinked to the gitlab-shell
+`hooks` directory for ease of maintenance between gitlab-shell upgrades.
+Custom hooks are implemented differently, but the behavior is exactly the same
+once the hook is created. Follow the steps below to set up a custom hook for a
+repository:
1. Pick a project that needs a custom Git hook.
1. On the GitLab server, navigate to the project's repository directory.
@@ -42,33 +45,56 @@ Follow the steps below to set up a custom hook:
That's it! Assuming the hook code is properly implemented the hook will fire
as appropriate.
+## Set a global Git hook for all repositories
+
+To create a Git hook that applies to all of your repositories in
+your instance, set a global Git hook. Since all the repositories' `hooks`
+directories are symlinked to gitlab-shell's `hooks` directory, adding any hook
+to the gitlab-shell `hooks` directory will also apply it to all repositories. Follow
+the steps below to properly set up a custom hook for all repositories:
+
+1. On the GitLab server, navigate to the configured custom hook directory. The
+ default is in the gitlab-shell directory. The gitlab-shell `hook` directory
+ for an installation from source the path is usually
+ `/home/git/gitlab-shell/hooks`. For Omnibus installs the path is usually
+ `/opt/gitlab/embedded/service/gitlab-shell/hooks`.
+ To look in a different directory for the global custom hooks,
+ set `custom_hooks_dir` in the gitlab-shell config. For
+ Omnibus installations, this can be set in `gitlab.rb`; and in source
+ installations, this can be set in `gitlab-shell/config.yml`.
+1. Create a new directory in this location. Depending on your hook, it will be
+ either a `pre-receive.d`, `post-receive.d`, or `update.d` directory.
+1. Inside this new directory, add your hook. Hooks can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+1. Make the hook file executable and make sure it's owned by Git.
+
+Now test the hook to see that it's functioning properly.
+
## Chained hooks support
> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
-Hooks can be also placed in `hooks/<hook_name>.d` (global) or
-`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+Hooks can be also global or be set per project directories and support a chained
execution of the hooks.
-NOTE: **Note:** `<hook_name>.d` would need to be either `pre-receive.d`,
+NOTE: **Note:**
+`<hook_name>.d` would need to be either `pre-receive.d`,
`post-receive.d`, or `update.d` to work properly. Any other names will be ignored.
-To look in a different directory for the global custom hooks (those in
-`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
-Omnibus installations, this can be set in `gitlab.rb`; and in source
-installations, this can be set in `gitlab-shell/config.yml`.
+NOTE: **Note:**
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
The hooks are searched and executed in this order:
1. `gitlab-shell/hooks` directory as known to Gitaly
-1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
+1. `<project>.git/hooks/<hook_name>` - executed by `git` itself, this is symlinked to `gitlab-shell/hooks/<hook_name>`
1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
-Files in `.d` directories need to be executable and not match the backup file
-pattern (`*~`).
-
The hooks of the same type are executed in order and execution stops on the
first script exiting with a non-zero value.
@@ -97,5 +123,6 @@ exit 1
[CI]: ../ci/README.md
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
[webhooks]: ../user/project/integrations/webhooks.md
+[gitlab-geo]: https://docs.gitlab.com/ee/administration/geo/replication/index.html
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md
new file mode 100644
index 00000000000..7f3be402b84
--- /dev/null
+++ b/doc/administration/database_load_balancing.md
@@ -0,0 +1,277 @@
+# Database Load Balancing **[PREMIUM ONLY]**
+
+> [Introduced][ee-1283] in [GitLab Premium][eep] 9.0.
+
+Distribute read-only queries among multiple database servers.
+
+## Overview
+
+Database load balancing improves the distribution of database workloads across
+multiple computing resources. Load balancing aims to optimize resource use,
+maximize throughput, minimize response time, and avoid overload of any single
+resource. Using multiple components with load balancing instead of a single
+component may increase reliability and availability through redundancy.
+[_Wikipedia article_][wikipedia]
+
+When database load balancing is enabled in GitLab, the load is balanced using
+a simple round-robin algorithm, without any external dependencies such as Redis.
+Load balancing is not enabled for Sidekiq as this would lead to consistency
+problems, and Sidekiq mostly performs writes anyway.
+
+In the following image, you can see the load is balanced rather evenly among
+all the secondaries (`db4`, `db5`, `db6`). Because `SELECT` queries are not
+sent to the primary (unless necessary), the primary (`db3`) hardly has any load.
+
+![DB load balancing graph](img/db_load_balancing_postgres_stats.png)
+
+## Requirements
+
+For load balancing to work you will need at least PostgreSQL 9.2 or newer,
+[**MySQL is not supported**][db-req]. You also need to make sure that you have
+at least 1 secondary in [hot standby][hot-standby] mode.
+
+Load balancing also requires that the configured hosts **always** point to the
+primary, even after a database failover. Furthermore, the additional hosts to
+balance load among must **always** point to secondary databases. This means that
+you should put a load balance in front of every database, and have GitLab connect
+to those load balancers.
+
+For example, say you have a primary (`db1.gitlab.com`) and two secondaries,
+`db2.gitlab.com` and `db3.gitlab.com`. For this setup you will need to have 3
+load balancers, one for every host. For example:
+
+* `primary.gitlab.com` forwards to `db1.gitlab.com`
+* `secondary1.gitlab.com` forwards to `db2.gitlab.com`
+* `secondary2.gitlab.com` forwards to `db3.gitlab.com`
+
+Now let's say that a failover happens and db2 becomes the new primary. This
+means forwarding should now happen as follows:
+
+* `primary.gitlab.com` forwards to `db2.gitlab.com`
+* `secondary1.gitlab.com` forwards to `db1.gitlab.com`
+* `secondary2.gitlab.com` forwards to `db3.gitlab.com`
+
+GitLab does not take care of this for you, so you will need to do so yourself.
+
+Finally, load balancing requires that GitLab can connect to all hosts using the
+same credentials and port as configured in the
+[Enabling load balancing](#enabling-load-balancing) section. Using
+different ports or credentials for different hosts is not supported.
+
+## Use cases
+
+- For GitLab instances with thousands of users and high traffic, you can use
+ database load balancing to reduce the load on the primary database and
+ increase responsiveness, thus resulting in faster page load inside GitLab.
+
+## Enabling load balancing
+
+For the environment in which you want to use load balancing, you'll need to add
+the following. This will balance the load between `host1.example.com` and
+`host2.example.com`.
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['db_load_balancing'] = { 'hosts' => ['host1.example.com', 'host2.example.com'] }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/database.yml` and add or amend the following lines:
+
+ ```yaml
+ production:
+ username: gitlab
+ database: gitlab
+ encoding: unicode
+ load_balancing:
+ hosts:
+ - host1.example.com
+ - host2.example.com
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Service Discovery
+
+> [Introduced][ee-5883] in [GitLab Premium][eep] 11.0.
+
+Service discovery allows GitLab to automatically retrieve a list of secondary
+databases to use, instead of having to manually specify these in the
+`database.yml` configuration file. Service discovery works by periodically
+checking a DNS A record, using the IPs returned by this record as the addresses
+for the secondaries. For service discovery to work, all you need is a DNS server
+and an A record containing the IP addresses of your secondaries.
+
+To use service discovery you need to change your `database.yml` configuration
+file so it looks like the following:
+
+```yaml
+production:
+ username: gitlab
+ database: gitlab
+ encoding: unicode
+ load_balancing:
+ discover:
+ nameserver: localhost
+ record: secondary.postgresql.service.consul
+ port: 8600
+ interval: 60
+ disconnect_timeout: 120
+```
+
+Here the `discover:` section specifies the configuration details to use for
+service discovery.
+
+### Configuration
+
+The following options can be set:
+
+| Option | Description | Default |
+|----------------------|---------------------------------------------------------------------------------------------------|-----------|
+| `nameserver` | The nameserver to use for looking up the DNS record. | localhost |
+| `record` | The A record to look up. This option is required for service discovery to work. | |
+| `port` | The port of the nameserver. | 8600 |
+| `interval` | The minimum time in seconds between checking the DNS record. | 60 |
+| `disconnect_timeout` | The time in seconds after which an old connection is closed, after the list of hosts was updated. | 120 |
+| `use_tcp` | Lookup DNS resources using TCP instead of UDP | false |
+
+The `interval` value specifies the _minimum_ time between checks. If the A
+record has a TTL greater than this value, then service discovery will honor said
+TTL. For example, if the TTL of the A record is 90 seconds, then service
+discovery will wait at least 90 seconds before checking the A record again.
+
+When the list of hosts is updated, it might take a while for the old connections
+to be terminated. The `disconnect_timeout` setting can be used to enforce an
+upper limit on the time it will take to terminate all old database connections.
+
+Some nameservers (like [Consul][consul-udp]) can return a truncated list of hosts when
+queried over UDP. To overcome this issue, you can use TCP for querying by setting
+`use_tcp` to `true`.
+
+### Forking
+
+If you use an application server that forks, such as Unicorn, you _have to_
+update your Unicorn configuration to start service discovery _after_ a fork.
+Failure to do so will lead to service discovery only running in the parent
+process. If you are using Unicorn, then you can add the following to your
+Unicorn configuration file:
+
+```ruby
+after_fork do |server, worker|
+ defined?(Gitlab::Database::LoadBalancing) &&
+ Gitlab::Database::LoadBalancing.start_service_discovery
+end
+```
+
+This will ensure that service discovery is started in both the parent and all
+child processes.
+
+## Balancing queries
+
+Read-only `SELECT` queries will be balanced among all the secondary hosts.
+Everything else (including transactions) will be executed on the primary.
+Queries such as `SELECT ... FOR UPDATE` are also executed on the primary.
+
+## Prepared statements
+
+Prepared statements don't work well with load balancing and are disabled
+automatically when load balancing is enabled. This should have no impact on
+response timings.
+
+## Primary sticking
+
+After a write has been performed, GitLab will stick to using the primary for a
+certain period of time, scoped to the user that performed the write. GitLab will
+revert back to using secondaries when they have either caught up, or after 30
+seconds.
+
+## Failover handling
+
+In the event of a failover or an unresponsive database, the load balancer will
+try to use the next available host. If no secondaries are available the
+operation is performed on the primary instead.
+
+In the event of a connection error being produced when writing data, the
+operation will be retried up to 3 times using an exponential back-off.
+
+When using load balancing, you should be able to safely restart a database server
+without it immediately leading to errors being presented to the users.
+
+## Logging
+
+The load balancer logs various messages, such as:
+
+* When a host is marked as offline
+* When a host comes back online
+* When all secondaries are offline
+
+Each log message contains the tag `[DB-LB]` to make searching/filtering of such
+log entries easier. For example:
+
+```
+[DB-LB] Host 10.123.2.5 came back online
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Host 10.123.2.6 came back online
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Marking host 10.123.2.7 as offline
+[DB-LB] Host 10.123.2.7 came back online
+[DB-LB] Host 10.123.2.7 came back online
+```
+
+## Handling Stale Reads
+
+> [Introduced][ee-3526] in [GitLab Premium][eep] 10.3.
+
+To prevent reading from an outdated secondary the load balancer will check if it
+is in sync with the primary. If the data is determined to be recent enough the
+secondary can be used, otherwise it will be ignored. To reduce the overhead of
+these checks we only perform these checks at certain intervals.
+
+There are three configuration options that influence this behaviour:
+
+| Option | Description | Default |
+|------------------------------|----------------------------------------------------------------------------------------------------------------|------------|
+| `max_replication_difference` | The amount of data (in bytes) a secondary is allowed to lag behind when it hasn't replicated data for a while. | 8 MB |
+| `max_replication_lag_time` | The maximum number of seconds a secondary is allowed to lag behind before we stop using it. | 60 seconds |
+| `replica_check_interval` | The minimum number of seconds we have to wait before checking the status of a secondary. | 60 seconds |
+
+The defaults should be sufficient for most users. Should you want to change them
+you can specify them in `config/database.yml` like so:
+
+```yaml
+production:
+ username: gitlab
+ database: gitlab
+ encoding: unicode
+ load_balancing:
+ hosts:
+ - host1.example.com
+ - host2.example.com
+ max_replication_difference: 16777216 # 16 MB
+ max_replication_lag_time: 30
+ replica_check_interval: 30
+```
+
+[hot-standby]: https://www.postgresql.org/docs/9.6/static/hot-standby.html
+[ee-1283]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1283
+[eep]: https://about.gitlab.com/pricing/
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+[wikipedia]: https://en.wikipedia.org/wiki/Load_balancing_(computing)
+[db-req]: ../install/requirements.md#database
+[ee-3526]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3526
+[ee-5883]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5883
+[consul-udp]: https://www.consul.io/docs/agent/dns.html#udp-based-dns-queries
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
new file mode 100644
index 00000000000..7d2fd51f834
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -0,0 +1,172 @@
+# Automatic background verification **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+Automatic background verification of repositories and wikis was added in
+GitLab EE 10.6 but is enabled by default only on GitLab EE 11.1. You can
+disable or enable this feature manually by following
+[these instructions](#disabling-or-enabling-the-automatic-background-verification).
+
+Automatic background verification ensures that the transferred data matches a
+calculated checksum. If the checksum of the data on the **primary** node matches checksum of the
+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 succeeds on the **primary** node but fails on the **secondary** node,
+this indicates that the object was corrupted during the replication process.
+Geo actively try to correct verification failures marking the repository to
+be resynced with a backoff period. If you want to reset the verification for
+these failures, so you should follow [these instructions][reset-verification].
+
+If verification is lagging significantly behind replication, consider giving
+the node more time before scheduling a planned failover.
+
+## Disabling or enabling the automatic background verification
+
+Run the following commands in a Rails console on the **primary** node:
+
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+To check if automatic background verification is enabled:
+
+```ruby
+Gitlab::Geo.repository_verification_enabled?
+```
+
+To disable automatic background verification:
+
+```ruby
+Feature.disable('geo_repository_verification')
+```
+
+To enable automatic background verification:
+
+```ruby
+Feature.enable('geo_repository_verification')
+```
+
+## Repository verification
+
+Navigate to the **Admin Area > Geo** dashboard on the **primary** node and expand
+the **Verification information** tab for that node to view automatic checksumming
+status for repositories and wikis. Successes are shown in green, pending work
+in grey, and failures in red.
+
+![Verification status](img/verification-status-primary.png)
+
+Navigate to the **Admin Area > Geo** dashboard on the **secondary** node and expand
+the **Verification information** tab for that node to view automatic verification
+status for repositories and wikis. As with checksumming, successes are shown in
+green, pending work in grey, and failures in red.
+
+![Verification status](img/verification-status-secondary.png)
+
+## Using checksums to compare Geo nodes
+
+To check the health of Geo **secondary** nodes, we use a checksum over the list of
+Git references and their values. The checksum includes `HEAD`, `heads`, `tags`,
+`notes`, and GitLab-specific references to ensure true consistency. If two nodes
+have the same checksum, then they definitely hold the same references. We compute
+the checksum for every node after every update to make sure that they are all
+in sync.
+
+## Repository re-verification
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8550) in GitLab Enterprise Edition 11.6. Available in [GitLab Premium](https://about.gitlab.com/pricing/).
+
+Due to bugs or transient infrastructure failures, it is possible for Git
+repositories to change unexpectedly without being marked for verification.
+Geo constantly reverifies the repositories to ensure the integrity of the
+data. The default and recommended re-verification interval is 7 days, though
+an interval as short as 1 day can be set. Shorter intervals reduce risk but
+increase load and vice versa.
+
+Navigate to the **Admin Area > Geo** dashboard on the **primary** node, and
+click the **Edit** button for the **primary** node to customize the minimum
+re-verification interval:
+
+![Re-verification interval](img/reverification-interval.png)
+
+The automatic background re-verification is enabled by default, but you can
+disable if you need. Run the following commands in a Rails console on the
+**primary** node:
+
+```sh
+# Omnibus GitLab
+gitlab-rails console
+
+# Installation from source
+cd /home/git/gitlab
+sudo -u git -H bin/rails console RAILS_ENV=production
+```
+
+To disable automatic background re-verification:
+
+```ruby
+Feature.disable('geo_repository_reverification')
+```
+
+To enable automatic background re-verification:
+
+```ruby
+Feature.enable('geo_repository_reverification')
+```
+
+## Reset verification for projects where verification has failed
+
+Geo actively try to correct verification failures marking the repository to
+be resynced with a backoff period. If you want to reset them manually, this
+rake task marks projects where verification has failed or the checksum mismatch
+to be resynced without the backoff period:
+
+For repositories:
+
+- Omnibus Installation
+
+ ```sh
+ sudo gitlab-rake geo:verification:repository:reset
+ ```
+
+- Source Installation
+
+ ```sh
+ sudo -u git -H bundle exec rake geo:verification:repository:reset RAILS_ENV=production
+ ```
+
+For wikis:
+
+- Omnibus Installation
+
+ ```sh
+ sudo gitlab-rake geo:verification:wiki:reset
+ ```
+
+- Source Installation
+
+ ```sh
+ sudo -u git -H bundle exec rake geo:verification:wiki:reset RAILS_ENV=production
+ ```
+
+## Current limitations
+
+Until [issue #5064][ee-5064] is completed, background verification doesn't cover
+CI job artifacts and traces, LFS objects, or user uploads in file storage.
+Verify their integrity manually by following [these instructions][foreground-verification]
+on both nodes, and comparing the output between them.
+
+Data in object storage is **not verified**, as the object store is responsible
+for ensuring the integrity of the data.
+
+[reset-verification]: background_verification.md#reset-verification-for-projects-where-verification-has-failed
+[foreground-verification]: ../../raketasks/check.md
+[ee-5064]: https://gitlab.com/gitlab-org/gitlab-ee/issues/5064
diff --git a/doc/administration/geo/disaster_recovery/bring_primary_back.md b/doc/administration/geo/disaster_recovery/bring_primary_back.md
new file mode 100644
index 00000000000..f4d31a98080
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/bring_primary_back.md
@@ -0,0 +1,61 @@
+# Bring a demoted primary node back online **[PREMIUM ONLY]**
+
+After a failover, it is possible to fail back to the demoted **primary** node to
+restore your original configuration. This process consists of two steps:
+
+1. Making the old **primary** node a **secondary** node.
+1. Promoting a **secondary** node to a **primary** node.
+
+CAUTION: **Caution:**
+If you have any doubts about the consistency of the data on this node, we recommend setting it up from scratch.
+
+## Configure the former **primary** node to be a **secondary** node
+
+Since the former **primary** node will be out of sync with the current **primary** node, the first step is to bring the former **primary** node up to date. Note, deletion of data stored on disk like
+repositories and uploads will not be replayed when bringing the former **primary** node back
+into sync, which may result in increased disk usage.
+Alternatively, you can [set up a new **secondary** GitLab instance][setup-geo] to avoid this.
+
+To bring the former **primary** node up to date:
+
+1. SSH into the former **primary** node that has fallen behind.
+1. Make sure all the services are up:
+
+ ```sh
+ sudo gitlab-ctl start
+ ```
+
+ > **Note 1:** If you [disabled the **primary** node permanently][disaster-recovery-disable-primary],
+ > you need to undo those steps now. For Debian/Ubuntu you just need to run
+ > `sudo systemctl enable gitlab-runsvdir`. For CentOS 6, you need to install
+ > the GitLab instance from scratch and set it up as a **secondary** node by
+ > following [Setup instructions][setup-geo]. In this case, you don't need to follow the next step.
+ >
+ > **Note 2:** If you [changed the DNS records](index.md#step-4-optional-updating-the-primary-domain-dns-record)
+ > for this node during disaster recovery procedure you may need to [block
+ > all the writes to this node](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/gitlab-geo/planned-failover.md#block-primary-traffic)
+ > during this procedure.
+
+1. [Setup database replication][database-replication]. Note that in this
+ case, **primary** node refers to the current **primary** node, and **secondary** node refers to the
+ former **primary** node.
+
+If you have lost your original **primary** node, follow the
+[setup instructions][setup-geo] to set up a new **secondary** node.
+
+## Promote the **secondary** node to **primary** node
+
+When the initial replication is complete and the **primary** node and **secondary** node are
+closely in sync, you can do a [planned failover].
+
+## Restore the **secondary** node
+
+If your objective is to have two nodes again, you need to bring your **secondary**
+node back online as well by repeating the first step
+([configure the former **primary** node to be a **secondary** node](#configure-the-former-primary-node-to-be-a-secondary-node))
+for the **secondary** node.
+
+[setup-geo]: ../replication/index.md#setup-instructions
+[database-replication]: ../replication/database.md
+[disaster-recovery-disable-primary]: index.md#step-2-permanently-disable-the-primary-node
+[planned failover]: planned_failover.md
diff --git a/doc/administration/geo/disaster_recovery/img/replication-status.png b/doc/administration/geo/disaster_recovery/img/replication-status.png
new file mode 100644
index 00000000000..d7085927c75
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/replication-status.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/img/reverification-interval.png b/doc/administration/geo/disaster_recovery/img/reverification-interval.png
new file mode 100644
index 00000000000..ad4597a4f49
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/reverification-interval.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/img/verification-status-primary.png b/doc/administration/geo/disaster_recovery/img/verification-status-primary.png
new file mode 100644
index 00000000000..2503408ec5d
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/verification-status-primary.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/img/verification-status-secondary.png b/doc/administration/geo/disaster_recovery/img/verification-status-secondary.png
new file mode 100644
index 00000000000..462274d8b14
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/verification-status-secondary.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
new file mode 100644
index 00000000000..71dc797f281
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -0,0 +1,322 @@
+# Disaster Recovery **[PREMIUM ONLY]**
+
+Geo replicates your database, your Git repositories, and few other assets.
+We will support and replicate more data in the future, that will enable you to
+failover with minimal effort, in a disaster situation.
+
+See [Geo current limitations][geo-limitations] for more information.
+
+CAUTION: **Warning:**
+Disaster recovery for multi-secondary configurations is in **Alpha**.
+For the latest updates, check the multi-secondary [Disaster Recovery epic][gitlab-org&65].
+
+## Promoting a **secondary** Geo node in single-secondary configurations
+
+We don't currently provide an automated way to promote a Geo replica and do a
+failover, but you can do it manually if you have `root` access to the machine.
+
+This process promotes a **secondary** Geo node to a **primary** node. To regain
+geographic redundancy as quickly as possible, you should add a new **secondary** node
+immediately after following these instructions.
+
+### Step 1. Allow replication to finish if possible
+
+If the **secondary** node is still replicating data from the **primary** node, follow
+[the planned failover docs][planned-failover] as closely as possible in
+order to avoid unnecessary data loss.
+
+### Step 2. Permanently disable the **primary** node
+
+CAUTION: **Warning:**
+If the **primary** node goes offline, there may be data saved on the **primary** node
+that has not been replicated to the **secondary** node. This data should be treated
+as lost if you proceed.
+
+If an outage on the **primary** node happens, you should do everything possible to
+avoid a split-brain situation where writes can occur in two different GitLab
+instances, complicating recovery efforts. So to prepare for the failover, we
+must disable the **primary** node.
+
+1. SSH into the **primary** node to stop and disable GitLab, if possible:
+
+ ```sh
+ sudo gitlab-ctl stop
+ ```
+
+ Prevent GitLab from starting up again if the server unexpectedly reboots:
+
+ ```sh
+ sudo systemctl disable gitlab-runsvdir
+ ```
+
+ > **CentOS only**: In CentOS 6 or older, there is no easy way to prevent GitLab from being
+ > started if the machine reboots isn't available (see [gitlab-org/omnibus-gitlab#3058]).
+ > It may be safest to uninstall the GitLab package completely:
+
+ ```sh
+ yum remove gitlab-ee
+ ```
+
+ > **Ubuntu 14.04 LTS**: If you are using an older version of Ubuntu
+ > or any other distro based on the Upstart init system, you can prevent GitLab
+ > from starting if the machine reboots by doing the following:
+
+ ```sh
+ initctl stop gitlab-runsvvdir
+ echo 'manual' > /etc/init/gitlab-runsvdir.override
+ initctl reload-configuration
+ ```
+
+1. If you do not have SSH access to the **primary** node, take the machine offline and
+ prevent it from rebooting by any means at your disposal.
+ Since there are many ways you may prefer to accomplish this, we will avoid a
+ single recommendation. You may need to:
+ - Reconfigure the load balancers.
+ - Change DNS records (e.g., point the primary DNS record to the **secondary**
+ node in order to stop usage of the **primary** node).
+ - Stop the virtual servers.
+ - Block traffic through a firewall.
+ - Revoke object storage permissions from the **primary** node.
+ - Physically disconnect a machine.
+
+1. If you plan to
+ [update the primary domain DNS record](#step-4-optional-updating-the-primary-domain-dns-record),
+ you may wish to lower the TTL now to speed up propagation.
+
+### Step 3. Promoting a **secondary** node
+
+NOTE: **Note:**
+A new **secondary** should not be added at this time. If you want to add a new
+**secondary**, do this after you have completed the entire process of promoting
+the **secondary** to the **primary**.
+
+#### Promoting a **secondary** node running on a single machine
+
+1. SSH in to your **secondary** node and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` to reflect its new status as **primary** by
+ removing any lines that enabled the `geo_secondary_role`:
+
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
+
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
+
+1. Promote the **secondary** node to the **primary** node. Execute:
+
+ ```sh
+ gitlab-ctl promote-to-primary-node
+ ```
+
+1. Verify you can connect to the newly promoted **primary** node using the URL used
+ previously for the **secondary** node.
+1. If successful, the **secondary** node has now been promoted to the **primary** node.
+
+#### Promoting a **secondary** node with HA
+
+The `gitlab-ctl promote-to-primary-node` command cannot be used yet in
+conjunction with High Availability or with multiple machines, as it can only
+perform changes on a **secondary** with only a single machine. Instead, you must
+do this manually.
+
+1. SSH in to the database node in the **secondary** and trigger PostgreSQL to
+ promote to read-write:
+
+ ```bash
+ sudo gitlab-pg-ctl promote
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` on every machine in the **secondary** to
+ reflect its new status as **primary** by removing any lines that enabled the
+ `geo_secondary_role`:
+
+ ```ruby
+ ## In pre-11.5 documentation, the role was enabled as follows. Remove this line.
+ geo_secondary_role['enable'] = true
+
+ ## In 11.5+ documentation, the role was enabled as follows. Remove this line.
+ roles ['geo_secondary_role']
+ ```
+
+ After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) each
+ machine so the changes take effect.
+
+1. Promote the **secondary** to **primary**. SSH into a single application
+ server and execute:
+
+ ```bash
+ sudo gitlab-rake geo:set_secondary_as_primary
+ ```
+
+1. Verify you can connect to the newly promoted **primary** using the URL used
+ previously for the **secondary**.
+1. Success! The **secondary** has now been promoted to **primary**.
+
+### Step 4. (Optional) Updating the primary domain DNS record
+
+Updating the DNS records for the primary domain to point to the **secondary** node
+will prevent the need to update all references to the primary domain to the
+secondary domain, like changing Git remotes and API URLs.
+
+1. SSH into the **secondary** node and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Update the primary domain's DNS record. After updating the primary domain's
+ DNS records to point to the **secondary** node, edit `/etc/gitlab/gitlab.rb` on the
+ **secondary** node to reflect the new URL:
+
+ ```ruby
+ # Change the existing external_url configuration
+ external_url 'https://<new_external_url>'
+ ```
+
+ NOTE: **Note**
+ Changing `external_url` won't prevent access via the old secondary URL, as
+ long as the secondary DNS records are still intact.
+
+1. Reconfigure the **secondary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+1. Execute the command below to update the newly promoted **primary** node URL:
+
+ ```sh
+ gitlab-rake geo:update_primary_node_url
+ ```
+
+ This command will use the changed `external_url` configuration defined
+ in `/etc/gitlab/gitlab.rb`.
+
+1. Verify you can connect to the newly promoted **primary** using its URL.
+ If you updated the DNS records for the primary domain, these changes may
+ not have yet propagated depending on the previous DNS records TTL.
+
+### Step 5. (Optional) Add **secondary** Geo node to a promoted **primary** node
+
+Promoting a **secondary** node to **primary** node using the process above does not enable
+Geo on the new **primary** node.
+
+To bring a new **secondary** node online, follow the [Geo setup instructions][setup-geo].
+
+### Step 6. (Optional) Removing the secondary's tracking database
+
+Every **secondary** has a special tracking database that is used to save the status of the synchronization of all the items from the **primary**.
+Because the **secondary** is already promoted, that data in the tracking database is no longer required.
+
+The data can be removed with the following command:
+
+```sh
+sudo rm -rf /var/opt/gitlab/geo-postgresql
+```
+
+## Promoting secondary Geo replica in multi-secondary configurations
+
+If you have more than one **secondary** node and you need to promote one of them, we suggest you follow
+[Promoting a **secondary** Geo node in single-secondary configurations](#promoting-a-secondary-geo-node-in-single-secondary-configurations)
+and after that you also need two extra steps.
+
+### Step 1. Prepare the new **primary** node to serve one or more **secondary** nodes
+
+1. SSH into the new **primary** node and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ ## Enable a Geo Primary role (if you haven't yet)
+ roles ['geo_primary_role']
+
+ ##
+ # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
+ # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
+ ##
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
+
+ # Every secondary server needs to have its own slot so specify the number of secondary nodes you're going to have
+ postgresql['max_replication_slots'] = 1
+
+ ##
+ ## Disable automatic database migrations temporarily
+ ## (until PostgreSQL is restarted and listening on the private address).
+ ##
+ gitlab_rails['auto_migrate'] = false
+
+ ```
+
+ (For more details about these settings you can read [Configure the primary server][configure-the-primary-server])
+
+1. Save the file and reconfigure GitLab for the database listen changes and
+ the replication slot changes to be applied.
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+ Restart PostgreSQL for its changes to take effect:
+
+ ```sh
+ gitlab-ctl restart postgresql
+ ```
+
+1. Re-enable migrations now that PostgreSQL is restarted and listening on the
+ private address.
+
+ Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
+
+ ```ruby
+ gitlab_rails['auto_migrate'] = true
+ ```
+
+ Save the file and reconfigure GitLab:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+### Step 2. Initiate the replication process
+
+Now we need to make each **secondary** node listen to changes on the new **primary** node. To do that you need
+to [initiate the replication process][initiate-the-replication-process] again but this time
+for another **primary** node. All the old replication settings will be overwritten.
+
+## Troubleshooting
+
+### I followed the disaster recovery instructions and now two-factor auth is broken!
+
+The setup instructions for Geo prior to 10.5 failed to replicate the
+`otp_key_base` secret, which is used to encrypt the two-factor authentication
+secrets stored in the database. If it differs between **primary** and **secondary**
+nodes, users with two-factor authentication enabled won't be able to log in
+after a failover.
+
+If you still have access to the old **primary** node, you can follow the
+instructions in the
+[Upgrading to GitLab 10.5][updating-geo]
+section to resolve the error. Otherwise, the secret is lost and you'll need to
+[reset two-factor authentication for all users][sec-tfa].
+
+[gitlab-org&65]: https://gitlab.com/groups/gitlab-org/-/epics/65
+[geo-limitations]: ../replication/index.md#current-limitations
+[planned-failover]: planned_failover.md
+[setup-geo]: ../replication/index.md#setup-instructions
+[updating-geo]: ../replication/updating_the_geo_nodes.md#upgrading-to-gitlab-105
+[sec-tfa]: ../../../security/two_factor_authentication.md#disabling-2fa-for-everyone
+[gitlab-org/omnibus-gitlab#3058]: https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3058
+[gitlab-org/gitlab-ee#4284]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4284
+[initiate-the-replication-process]: ../replication/database.html#step-3-initiate-the-replication-process
+[configure-the-primary-server]: ../replication/database.html#step-1-configure-the-primary-server
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
new file mode 100644
index 00000000000..88ab12d910a
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -0,0 +1,227 @@
+# Disaster recovery for planned failover **[PREMIUM ONLY]**
+
+The primary use-case of Disaster Recovery is to ensure business continuity in
+the event of unplanned outage, but it can be used in conjunction with a planned
+failover to migrate your GitLab instance between regions without extended
+downtime.
+
+As replication between Geo nodes is asynchronous, a planned failover requires
+a maintenance window in which updates to the **primary** node are blocked. The
+length of this window is determined by your replication capacity - once the
+**secondary** node is completely synchronized with the **primary** node, the failover can occur without
+data loss.
+
+This document assumes you already have a fully configured, working Geo setup.
+Please read it and the [Disaster Recovery][disaster-recovery] failover
+documentation in full before proceeding. Planned failover is a major operation,
+and if performed incorrectly, there is a high risk of data loss. Consider
+rehearsing the procedure until you are comfortable with the necessary steps and
+have a high degree of confidence in being able to perform them accurately.
+
+## Not all data is automatically replicated
+
+If you are using any GitLab features that Geo [doesn't support][limitations],
+you must make separate provisions to ensure that the **secondary** node has an
+up-to-date copy of any data associated with that feature. This may extend the
+required scheduled maintenance period significantly.
+
+A common strategy for keeping this period as short as possible for data stored
+in files is to use `rsync` to transfer the data. An initial `rsync` can be
+performed ahead of the maintenance window; subsequent `rsync`s (including a
+final transfer inside the maintenance window) will then transfer only the
+*changes* between the **primary** node and the **secondary** nodes.
+
+Repository-centric strategies for using `rsync` effectively can be found in the
+[moving repositories][moving-repositories] documentation; these strategies can
+be adapted for use with any other file-based data, such as GitLab Pages (to
+be found in `/var/opt/gitlab/gitlab-rails/shared/pages` if using Omnibus).
+
+## Pre-flight checks
+
+Follow these steps before scheduling a planned failover to ensure the process
+will go smoothly.
+
+### Object storage
+
+Some classes of non-repository data can use object storage in preference to
+file storage. Geo [does not replicate data in object storage](../replication/object_storage.md),
+leaving that task up to the object store itself. For a planned failover, this
+means you can decouple the replication of this data from the failover of the
+GitLab service.
+
+If you're already using object storage, simply verify that your **secondary**
+node has access to the same data as the **primary** node - they must either they share the
+same object storage configuration, or the **secondary** node should be configured to
+access a [geographically-replicated][os-repl] copy provided by the object store
+itself.
+
+If you have a large GitLab installation or cannot tolerate downtime, consider
+[migrating to Object Storage][os-conf] **before** scheduling a planned failover.
+Doing so reduces both the length of the maintenance window, and the risk of data
+loss as a result of a poorly executed planned failover.
+
+### Review the configuration of each **secondary** node
+
+Database settings are automatically replicated to the **secondary** node, but the
+`/etc/gitlab/gitlab.rb` file must be set up manually, and differs between
+nodes. If features such as Mattermost, OAuth or LDAP integration are enabled
+on the **primary** node but not the **secondary** node, they will be lost during failover.
+
+Review the `/etc/gitlab/gitlab.rb` file for both nodes and ensure the **secondary** node
+supports everything the **primary** node does **before** scheduling a planned failover.
+
+### Run system checks
+
+Run the following on both **primary** and **secondary** nodes:
+
+```sh
+gitlab-rake gitlab:check
+gitlab-rake gitlab:geo:check
+```
+
+If any failures are reported on either node, they should be resolved **before**
+scheduling a planned failover.
+
+### Check that secrets match between nodes
+
+The SSH host keys and `/etc/gitlab/gitlab-secrets.json` files should be
+identical on all nodes. Check this by running the following on all nodes and
+comparing the output:
+
+```sh
+sudo sha256sum /etc/ssh/ssh_host* /etc/gitlab/gitlab-secrets.json
+```
+
+If any files differ, replace the content on the **secondary** node with the
+content from the **primary** node.
+
+### Ensure Geo replication is up-to-date
+
+The maintenance window won't end until Geo replication and verification is
+completely finished. To keep the window as short as possible, you should
+ensure these processes are close to 100% as possible during active use.
+
+Navigate to the **Admin Area > Geo** dashboard on the **secondary** node to
+review status. Replicated objects (shown in green) should be close to 100%,
+and there should be no failures (shown in red). If a large proportion of
+objects aren't yet replicated (shown in grey), consider giving the node more
+time to complete
+
+![Replication status](img/replication-status.png)
+
+If any objects are failing to replicate, this should be investigated before
+scheduling the maintenance window. Following a planned failover, anything that
+failed to replicate will be **lost**.
+
+You can use the [Geo status API](https://docs.gitlab.com/ee/api/geo_nodes.html#retrieve-project-sync-or-verification-failures-that-occurred-on-the-current-node) to review failed objects and
+the reasons for failure.
+
+A common cause of replication failures is the data being missing on the
+**primary** node - you can resolve these failures by restoring the data from backup,
+or removing references to the missing data.
+
+### Verify the integrity of replicated data
+
+This [content was moved to another location][background-verification].
+
+### Notify users of scheduled maintenance
+
+On the **primary** node, navigate to **Admin Area > Messages**, add a broadcast
+message. You can check under **Admin Area > Geo** to estimate how long it
+will take to finish syncing. An example message would be:
+
+> A scheduled maintenance will take place at XX:XX UTC. We expect it to take
+> less than 1 hour.
+
+## Prevent updates to the **primary** node
+
+Until a [read-only mode][ce-19739] is implemented, updates must be prevented
+from happening manually. Note that your **secondary** node still needs read-only
+access to the **primary** node during the maintenance window.
+
+1. At the scheduled time, using your cloud provider or your node's firewall, block
+ all HTTP, HTTPS and SSH traffic to/from the **primary** node, **except** for your IP and
+ the **secondary** node's IP.
+
+ For instance, you might run the following commands on the server(s) making up your **primary** node:
+
+ ```sh
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 22 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 22 -j ACCEPT
+ sudo iptables -A INPUT --destination-port 22 -j REJECT
+
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 80 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 80 -j ACCEPT
+ sudo iptables -A INPUT --tcp-dport 80 -j REJECT
+
+ sudo iptables -A INPUT -p tcp -s <secondary_node_ip> --destination-port 443 -j ACCEPT
+ sudo iptables -A INPUT -p tcp -s <your_ip> --destination-port 443 -j ACCEPT
+ sudo iptables -A INPUT --tcp-dport 443 -j REJECT
+ ```
+
+ From this point, users will be unable to view their data or make changes on the
+ **primary** node. They will also be unable to log in to the **secondary** node.
+ However, existing sessions will work for the remainder of the maintenance period, and
+ public data will be accessible throughout.
+
+1. Verify the **primary** node is blocked to HTTP traffic by visiting it in browser via
+ another IP. The server should refuse connection.
+
+1. Verify the **primary** node is blocked to Git over SSH traffic by attempting to pull an
+ existing Git repository with an SSH remote URL. The server should refuse
+ connection.
+
+1. Disable non-Geo periodic background jobs on the primary node by navigating
+ to **Admin Area > Monitoring > Background Jobs > Cron** , pressing `Disable All`,
+ and then pressing `Enable` for the `geo_sidekiq_cron_config_worker` cron job.
+ This job will re-enable several other cron jobs that are essential for planned
+ failover to complete successfully.
+
+## Finish replicating and verifying all data
+
+1. If you are manually replicating any data not managed by Geo, trigger the
+ final replication process now.
+1. On the **primary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
+ and wait for all queues except those with `geo` in the name to drop to 0.
+ These queues contain work that has been submitted by your users; failing over
+ before it is completed will cause the work to be lost.
+1. On the **primary** node, navigate to **Admin Area > Geo** and wait for the
+ following conditions to be true of the **secondary** node you are failing over to:
+ - All replication meters to each 100% replicated, 0% failures.
+ - All verification meters reach 100% verified, 0% failures.
+ - Database replication lag is 0ms.
+ - The Geo log cursor is up to date (0 events behind).
+
+1. On the **secondary** node, navigate to **Admin Area > Monitoring > Background Jobs > Queues**
+ and wait for all the `geo` queues to drop to 0 queued and 0 running jobs.
+1. On the **secondary** node, use [these instructions][foreground-verification]
+ to verify the integrity of CI artifacts, LFS objects and uploads in file
+ storage.
+
+At this point, your **secondary** node will contain an up-to-date copy of everything the
+**primary** node has, meaning nothing will be lost when you fail over.
+
+## Promote the **secondary** node
+
+Finally, follow the [Disaster Recovery docs][disaster-recovery] to promote the
+**secondary** node to a **primary** node. This process will cause a brief outage on the **secondary** node, and users may need to log in again.
+
+Once it is completed, the maintenance window is over! Your new **primary** node will now
+begin to diverge from the old one. If problems do arise at this point, failing
+back to the old **primary** node [is possible][bring-primary-back], but likely to result
+in the loss of any data uploaded to the new primary in the meantime.
+
+Don't forget to remove the broadcast message after failover is complete.
+
+[bring-primary-back]: bring_primary_back.md
+[ce-19739]: https://gitlab.com/gitlab-org/gitlab-ce/issues/19739
+[container-registry]: ../replication/container_registry.md
+[disaster-recovery]: index.md
+[ee-4930]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4930
+[ee-5064]: https://gitlab.com/gitlab-org/gitlab-ee/issues/5064
+[foreground-verification]: ../../raketasks/check.md
+[background-verification]: background_verification.md
+[limitations]: ../replication/index.md#current-limitations
+[moving-repositories]: ../../operations/moving_repositories.md
+[os-conf]: ../replication/object_storage.md#configuration
+[os-repl]: ../replication/object_storage.md#replication
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
new file mode 100644
index 00000000000..57735b21cda
--- /dev/null
+++ b/doc/administration/geo/replication/configuration.md
@@ -0,0 +1,314 @@
+# Geo configuration (GitLab Omnibus) **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+This is the documentation for the Omnibus GitLab packages. For installations
+from source, follow the [**Geo nodes configuration for installations
+from source**][configuration-source] guide.
+
+## Configuring a new **secondary** node
+
+NOTE: **Note:**
+This is the final step in setting up a **secondary** Geo node. Stages of the
+setup process must be completed in the documented order.
+Before attempting the steps in this stage, [complete all prior stages][setup-geo-omnibus].
+
+The basic steps of configuring a **secondary** node are to:
+
+- Replicate required configurations between the **primary** node and the **secondary** nodes.
+- Configure a tracking database on each **secondary** node.
+- Start GitLab on each **secondary** node.
+
+You are encouraged to first read through all the steps before executing them
+in your testing/production environment.
+
+> **Notes:**
+> - **Do not** setup any custom authentication for the **secondary** nodes. This will be
+ handled by the **primary** node.
+> - Any change that requires access to the **Admin Area** needs to be done in the
+ **primary** node because the **secondary** node is a read-only replica.
+
+### Step 1. Manually replicate secret GitLab values
+
+GitLab stores a number of secret values in the `/etc/gitlab/gitlab-secrets.json`
+file which *must* be the same on all nodes. Until there is
+a means of automatically replicating these between nodes (see issue [gitlab-org/gitlab-ee#3789]),
+they must be manually replicated to the **secondary** node.
+
+1. SSH into the **primary** node, and execute the command below:
+
+ ```sh
+ sudo cat /etc/gitlab/gitlab-secrets.json
+ ```
+
+ This will display the secrets that need to be replicated, in JSON format.
+
+1. SSH into the **secondary** node and login as the `root` user:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Make a backup of any existing secrets:
+
+ ```sh
+ mv /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.`date +%F`
+ ```
+
+1. Copy `/etc/gitlab/gitlab-secrets.json` from the **primary** node to the **secondary** node, or
+ copy-and-paste the file contents between nodes:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab-secrets.json
+
+ # paste the output of the `cat` command you ran on the primary
+ # save and exit
+ ```
+
+1. Ensure the file permissions are correct:
+
+ ```sh
+ chown root:root /etc/gitlab/gitlab-secrets.json
+ chmod 0600 /etc/gitlab/gitlab-secrets.json
+ ```
+
+1. Reconfigure the **secondary** node for the change to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ gitlab-ctl restart
+ ```
+
+### Step 2. Manually replicate the **primary** node's SSH host keys
+
+GitLab integrates with the system-installed SSH daemon, designating a user
+(typically named git) through which all access requests are handled.
+
+In a [Disaster Recovery] situation, GitLab system
+administrators will promote a **secondary** node to the **primary** node. DNS records for the
+**primary** domain should also be updated to point to the new **primary** node
+(previously a **secondary** node). Doing so will avoid the need to update Git remotes and API URLs.
+
+This will cause all SSH requests to the newly promoted **primary** node to
+fail due to SSH host key mismatch. To prevent this, the primary SSH host
+keys must be manually replicated to the **secondary** node.
+
+1. SSH into the **secondary** node and login as the `root` user:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Make a backup of any existing SSH host keys:
+
+ ```sh
+ find /etc/ssh -iname ssh_host_* -exec cp {} {}.backup.`date +%F` \;
+ ```
+
+1. Copy OpenSSH host keys from the **primary** node:
+
+ If you can access your **primary** node using the **root** user:
+
+ ```sh
+ # Run this from the secondary node, change `<primary_node_fqdn>` for the IP or FQDN of the server
+ scp root@<primary_node_fqdn>:/etc/ssh/ssh_host_*_key* /etc/ssh
+ ```
+
+ If you only have access through a user with **sudo** privileges:
+
+ ```sh
+ # Run this from your primary node:
+ sudo tar --transform 's/.*\///g' -zcvf ~/geo-host-key.tar.gz /etc/ssh/ssh_host_*_key*
+
+ # Run this from your secondary node:
+ scp <user_with_sudo>@<primary_node_fqdn>:geo-host-key.tar.gz .
+ tar zxvf ~/geo-host-key.tar.gz -C /etc/ssh
+ ```
+
+1. On your **secondary** node, ensure the file permissions are correct:
+
+ ```sh
+ chown root:root /etc/ssh/ssh_host_*_key*
+ chmod 0600 /etc/ssh/ssh_host_*_key*
+ ```
+
+1. To verify key fingerprint matches, execute the following command on both nodes:
+
+ ```sh
+ for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
+ ```
+
+ You should get an output similar to this one and they should be identical on both nodes:
+
+ ```sh
+ 1024 SHA256:FEZX2jQa2bcsd/fn/uxBzxhKdx4Imc4raXrHwsbtP0M root@serverhostname (DSA)
+ 256 SHA256:uw98R35Uf+fYEQ/UnJD9Br4NXUFPv7JAUln5uHlgSeY root@serverhostname (ECDSA)
+ 256 SHA256:sqOUWcraZQKd89y/QQv/iynPTOGQxcOTIXU/LsoPmnM root@serverhostname (ED25519)
+ 2048 SHA256:qwa+rgir2Oy86QI+PZi/QVR+MSmrdrpsuH7YyKknC+s root@serverhostname (RSA)
+ ```
+
+1. Verify that you have the correct public keys for the existing private keys:
+
+ ```sh
+ # This will print the fingerprint for private keys:
+ for file in /etc/ssh/ssh_host_*_key; do ssh-keygen -lf $file; done
+
+ # This will print the fingerprint for public keys:
+ for file in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf $file; done
+ ```
+
+ NOTE: **Note**:
+ The output for private keys and public keys command should generate the same fingerprint.
+
+1. Restart sshd on your **secondary** node:
+
+ ```sh
+ # Debian or Ubuntu installations
+ sudo service ssh reload
+
+ # CentOS installations
+ sudo service sshd reload
+ ```
+
+### Step 3. Add the **secondary** node
+
+1. Visit the **primary** node's **Admin Area > Geo**
+ (`/admin/geo/nodes`) in your browser.
+1. Add the **secondary** node by providing its full URL. **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. SSH into your GitLab **secondary** server and restart the services:
+
+ ```sh
+ gitlab-ctl restart
+ ```
+
+ Check if there are any common issue with your Geo setup by running:
+
+ ```sh
+ gitlab-rake gitlab:geo:check
+ ```
+
+1. SSH into your **primary** server and login as root to verify the
+ **secondary** node is reachable or there are any common issue with your Geo setup:
+
+ ```sh
+ gitlab-rake gitlab:geo:check
+ ```
+
+Once added to the admin panel and restarted, the **secondary** node will automatically start
+replicating missing data from the **primary** node in a process known as **backfill**.
+Meanwhile, the **primary** node will start to notify each **secondary** node of any changes, so
+that the **secondary** node can act on those notifications immediately.
+
+Make sure the **secondary** node is running and accessible.
+You can login to the **secondary** node with the same credentials as used for the **primary** node.
+
+### Step 4. Enabling Hashed Storage
+
+Using Hashed Storage significantly improves Geo replication. Project and group
+renames no longer require synchronization between nodes.
+
+1. Visit the **primary** node's **Admin Area > Settings > Repository**
+ (`/admin/application_settings/repository`) in your browser.
+1. In the **Repository storage** section, check **Use hashed storage paths for newly created and renamed projects**.
+
+### Step 5. (Optional) Configuring the **secondary** node to trust the **primary** node
+
+You can safely skip this step if your **primary** node uses a CA-issued HTTPS certificate.
+
+If your **primary** node is using a self-signed certificate for *HTTPS* support, you will
+need to add that certificate to the **secondary** node's trust store. Retrieve the
+certificate from the **primary** node and follow
+[these instructions][omnibus-ssl]
+on the **secondary** node.
+
+### Step 6. Enable Git access over HTTP/HTTPS
+
+Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
+method to be enabled. Navigate to **Admin Area > Settings**
+(`/admin/application_settings`) on the **primary** node, and set
+`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
+
+### Step 7. Verify proper functioning of the **secondary** node
+
+Your **secondary** node is now configured!
+
+You can login to the **secondary** node with the same credentials you used for the
+**primary** node. Visit the **secondary** node's **Admin Area > Geo**
+(`/admin/geo/nodes`) in your browser to check if it's correctly identified as a
+**secondary** Geo node and if Geo is enabled.
+
+The initial replication, or 'backfill', will probably still be in progress. You
+can monitor the synchronization process on each geo node from the **primary**
+node's Geo Nodes dashboard in your browser.
+
+![Geo dashboard](img/geo_node_dashboard.png)
+
+If your installation isn't working properly, check the
+[troubleshooting document].
+
+The two most obvious issues that can become apparent in the dashboard are:
+
+1. Database replication not working well.
+1. Instance to instance notification not working. In that case, it can be
+ something of the following:
+ - You are using a custom certificate or custom CA (see the
+ [troubleshooting document]).
+ - The instance is firewalled (check your firewall rules).
+
+Please note that disabling a **secondary** node will stop the synchronization process.
+
+Please note that if `git_data_dirs` is customized on the **primary** node for multiple
+repository shards you must duplicate the same configuration on each **secondary** node.
+
+Point your users to the ["Using a Geo Server" guide][using-geo].
+
+Currently, this is what is synced:
+
+- Git repositories.
+- Wikis.
+- LFS objects.
+- Issues, merge requests, snippets, and comment attachments.
+- Users, groups, and project avatars.
+
+## Selective synchronization
+
+Geo supports selective synchronization, which allows admins to choose
+which projects should be synchronized by **secondary** nodes.
+A subset of projects can be chosen, either by group or by storage shard. The
+former is ideal for replicating data belonging to a subset of users, while the
+latter is more suited to progressively rolling out Geo to a large GitLab
+instance.
+
+It is important to note that selective synchronization:
+
+1. Does not restrict permissions from **secondary** nodes.
+1. Does not hide project metadata from **secondary** nodes.
+ - Since Geo currently relies on PostgreSQL replication, all project metadata
+ gets replicated to **secondary** nodes, but repositories that have not been
+ selected will be empty.
+1. Does not reduce the number of events generated for the Geo event log.
+ - The **primary** node generates events as long as any **secondary** nodes are present.
+ Selective synchronization restrictions are implemented on the **secondary** nodes,
+ not the **primary** node.
+
+## Upgrading Geo
+
+See the [updating the Geo nodes document](updating_the_geo_nodes.md).
+
+## Troubleshooting
+
+See the [troubleshooting document](troubleshooting.md).
+
+[configuration-source]: configuration_source.md
+[setup-geo-omnibus]: index.md#using-omnibus-gitlab
+[Hashed Storage]: ../../repository_storage_types.md
+[Disaster Recovery]: ../disaster_recovery/index.md
+[gitlab-org/gitlab-ee#3789]: https://gitlab.com/gitlab-org/gitlab-ee/issues/3789
+[gitlab-com/infrastructure#2821]: https://gitlab.com/gitlab-com/infrastructure/issues/2821
+[omnibus-ssl]: https://docs.gitlab.com/omnibus/settings/ssl.html
+[troubleshooting document]: troubleshooting.md
+[using-geo]: using_a_geo_server.md
diff --git a/doc/administration/geo/replication/configuration_source.md b/doc/administration/geo/replication/configuration_source.md
new file mode 100644
index 00000000000..10dd9405b4b
--- /dev/null
+++ b/doc/administration/geo/replication/configuration_source.md
@@ -0,0 +1,172 @@
+# Geo configuration (source) **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+This documentation applies to GitLab source installations. In GitLab 11.5, this documentation was deprecated and will be removed in a future release.
+Please consider [migrating to GitLab Omnibus install](https://docs.gitlab.com/omnibus/update/convert_to_omnibus.html). For installations
+using the Omnibus GitLab packages, follow the
+[**Omnibus Geo nodes configuration**][configuration] guide.
+
+## Configuring a new **secondary** node
+
+NOTE: **Note:**
+This is the final step in setting up a **secondary** node. Stages of the setup
+process must be completed in the documented order. Before attempting the steps
+in this stage, [complete all prior stages](index.md#using-gitlab-installed-from-source-deprecated).
+
+The basic steps of configuring a **secondary** node are to:
+
+- Replicate required configurations between the **primary** and **secondary** nodes.
+- Configure a tracking database on each **secondary** node.
+- Start GitLab on the **secondary** node.
+
+You are encouraged to first read through all the steps before executing them
+in your testing/production environment.
+
+NOTE: **Note:**
+**Do not** set up any custom authentication on **secondary** nodes, this will be handled by the **primary** node.
+
+NOTE: **Note:**
+**Do not** add anything in the **secondary** node's admin area (**Admin Area > Geo**). This is handled solely by the **primary** node.
+
+### Step 1. Manually replicate secret GitLab values
+
+GitLab stores a number of secret values in the `/home/git/gitlab/config/secrets.yml`
+file which *must* match between the **primary** and **secondary** nodes. Until there is
+a means of automatically replicating these between nodes (see [gitlab-org/gitlab-ee#3789]), they must
+be manually replicated to **secondary** nodes.
+
+1. SSH into the **primary** node, and execute the command below:
+
+ ```sh
+ sudo cat /home/git/gitlab/config/secrets.yml
+ ```
+
+ This will display the secrets that need to be replicated, in YAML format.
+
+1. SSH into the **secondary** node and login as the `git` user:
+
+ ```sh
+ sudo -i -u git
+ ```
+
+1. Make a backup of any existing secrets:
+
+ ```sh
+ mv /home/git/gitlab/config/secrets.yml /home/git/gitlab/config/secrets.yml.`date +%F`
+ ```
+
+1. Copy `/home/git/gitlab/config/secrets.yml` from the **primary** node to the **secondary** node, or
+ copy-and-paste the file contents between nodes:
+
+ ```sh
+ sudo editor /home/git/gitlab/config/secrets.yml
+
+ # paste the output of the `cat` command you ran on the primary
+ # save and exit
+ ```
+
+1. Ensure the file permissions are correct:
+
+ ```sh
+ chown git:git /home/git/gitlab/config/secrets.yml
+ chmod 0600 /home/git/gitlab/config/secrets.yml
+ ```
+
+1. Restart GitLab
+
+ ```sh
+ service gitlab restart
+ ```
+
+Once restarted, the **secondary** node will automatically start replicating missing data
+from the **primary** node in a process known as backfill. Meanwhile, the **primary** node
+will start to notify the **secondary** node of any changes, so that the **secondary** node can
+act on those notifications immediately.
+
+Make sure the **secondary** node is running and accessible. You can login to
+the **secondary** node with the same credentials as used for the **primary** node.
+
+### Step 2. Manually replicate the **primary** node's SSH host keys
+
+Read [Manually replicate the **primary** node's SSH host keys](configuration.md#step-2-manually-replicate-the-primary-nodes-ssh-host-keys)
+
+### Step 3. Add the **secondary** GitLab node
+
+1. Navigate to the **primary** node's **Admin Area > Geo**
+ (`/admin/geo/nodes`) in your browser.
+1. Add the **secondary** node by providing its full URL. **Do NOT** check the
+ **This is a primary node** checkbox.
+1. Optionally, choose which namespaces 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. SSH into your GitLab **secondary** server and restart the services:
+
+ ```sh
+ service gitlab restart
+ ```
+
+ Check if there are any common issue with your Geo setup by running:
+
+ ```sh
+ bundle exec rake gitlab:geo:check
+ ```
+
+1. SSH into your GitLab **primary** server and login as root to verify the
+ **secondary** node is reachable or there are any common issue with your Geo setup:
+
+ ```sh
+ bundle exec rake gitlab:geo:check
+ ```
+
+Once reconfigured, the **secondary** node will automatically start
+replicating missing data from the **primary** node in a process known as backfill.
+Meanwhile, the **primary** node will start to notify the **secondary** node of any changes, so
+that the **secondary** node can act on those notifications immediately.
+
+Make sure the **secondary** node is running and accessible.
+You can log in to the **secondary** node with the same credentials as used for the **primary** node.
+
+### Step 4. Enabling Hashed Storage
+
+Read [Enabling Hashed Storage](configuration.md#step-4-enabling-hashed-storage).
+
+### Step 5. (Optional) Configuring the secondary to trust the primary
+
+You can safely skip this step if your **primary** node uses a CA-issued HTTPS certificate.
+
+If your **primary** node is using a self-signed certificate for *HTTPS* support, you will
+need to add that certificate to the **secondary** node's trust store. Retrieve the
+certificate from the **primary** node and follow your distribution's instructions for
+adding it to the **secondary** node's trust store. In Debian/Ubuntu, you would follow these steps:
+
+```sh
+sudo -i
+cp <primary_node_certification_file> /usr/local/share/ca-certificates
+update-ca-certificates
+```
+
+### Step 6. Enable Git access over HTTP/HTTPS
+
+Geo synchronizes repositories over HTTP/HTTPS, and therefore requires this clone
+method to be enabled. Navigate to **Admin Area > Settings**
+(`/admin/application_settings`) on the **primary** node, and set
+`Enabled Git access protocols` to `Both SSH and HTTP(S)` or `Only HTTP(S)`.
+
+### Step 7. Verify proper functioning of the secondary node
+
+Read [Verify proper functioning of the secondary node][configuration-verify-node].
+
+## Selective synchronization
+
+Read [Selective synchronization][configuration-selective-replication].
+
+## Troubleshooting
+
+Read the [troubleshooting document][troubleshooting].
+
+[gitlab-org/gitlab-ee#3789]: https://gitlab.com/gitlab-org/gitlab-ee/issues/3789
+[configuration]: configuration.md
+[configuration-selective-replication]: configuration.md#selective-synchronization
+[configuration-verify-node]: configuration.md#step-7-verify-proper-functioning-of-the-secondary-node
+[troubleshooting]: troubleshooting.md
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
new file mode 100644
index 00000000000..e57583a3bf9
--- /dev/null
+++ b/doc/administration/geo/replication/database.md
@@ -0,0 +1,507 @@
+# Geo database replication (GitLab Omnibus) **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+This is the documentation for the Omnibus GitLab packages. For installations
+from source, follow the
+[Geo database replication (source)](database_source.md) guide.
+
+NOTE: **Note:**
+If your GitLab installation uses external (not managed by Omnibus) PostgreSQL
+instances, the Omnibus roles will not be able to perform all necessary
+configuration steps. In this case,
+[follow the Geo with external PostgreSQL instances document instead](external_database.md).
+
+NOTE: **Note:**
+The stages of the setup process must be completed in the documented order.
+Before attempting the steps in this stage, [complete all prior stages][toc].
+
+This document describes the minimal steps you have to take in order to
+replicate your **primary** GitLab database to a **secondary** node's database. You may
+have to change some values according to your database setup, how big it is, etc.
+
+You are encouraged to first read through all the steps before executing them
+in your testing/production environment.
+
+## PostgreSQL replication
+
+The GitLab **primary** node where the write operations happen will connect to
+the **primary** database server, and **secondary** nodes will
+connect to their own database servers (which are also read-only).
+
+NOTE: **Note:**
+In database documentation, you may see "**primary**" being referenced as "master"
+and "**secondary**" as either "slave" or "standby" server (read-only).
+
+We recommend using [PostgreSQL replication slots][replication-slots-article]
+to ensure that the **primary** node retains all the data necessary for the **secondary** nodes to
+recover. See below for more details.
+
+The following guide assumes that:
+
+- You are using Omnibus and therefore you are using PostgreSQL 9.6 or later
+ which includes the [`pg_basebackup` tool][pgback] and improved
+ [Foreign Data Wrapper][FDW] support.
+- You have a **primary** node already set up (the GitLab server you are
+ replicating from), running Omnibus' PostgreSQL (or equivalent version), and
+ you have a new **secondary** server set up with the same versions of the OS,
+ PostgreSQL, and GitLab on all nodes.
+
+CAUTION: **Warning:**
+Geo works with streaming replication. Logical replication is not supported at this time.
+There is an [issue where support is being discussed](https://gitlab.com/gitlab-org/gitlab-ee/issues/7420).
+
+### Step 1. Configure the **primary** server
+
+1. SSH into your GitLab **primary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Execute the command below to define the node as **primary** node:
+
+ ```sh
+ gitlab-ctl set-geo-primary-node
+ ```
+
+ This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
+
+1. GitLab 10.4 and up only: Do the following to make sure the `gitlab` database user has a password defined:
+
+ Generate a MD5 hash of the desired password:
+
+ ```sh
+ gitlab-ctl pg-password-md5 gitlab
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
+
+ Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
+ ```
+
+1. Omnibus GitLab already has a [replication user]
+ called `gitlab_replicator`. You must set the password for this user manually.
+ You will be prompted to enter a password:
+
+ ```sh
+ gitlab-ctl set-replication-password
+ ```
+
+ This command will also read the `postgresql['sql_replication_user']` Omnibus
+ setting in case you have changed `gitlab_replicator` username to something
+ else.
+
+ If you are using an external database not managed by Omnibus GitLab, you need
+ to create the replicator user and define a password to it manually.
+ For information on how to create a replication user, refer to the
+ [appropriate step](database_source.md#step-1-configure-the-primary-server)
+ in [Geo database replication (source)](database_source.md).
+
+1. Configure PostgreSQL to listen on network interfaces:
+
+ For security reasons, PostgreSQL does not listen on any network interfaces
+ by default. However, Geo requires the **secondary** node to be able to
+ connect to the **primary** node's database. For this reason, we need the address of
+ each node. Note: For external PostgreSQL instances, see [additional instructions](external_database.md).
+
+ If you are using a cloud provider, you can lookup the addresses for each
+ Geo node through your cloud provider's management console.
+
+ To lookup the address of a Geo node, SSH in to the Geo node and execute:
+
+ ```sh
+ ##
+ ## Private address
+ ##
+ ip route get 255.255.255.255 | awk '{print "Private address:", $NF; exit}'
+
+ ##
+ ## Public address
+ ##
+ echo "External address: $(curl --silent ipinfo.io/ip)"
+ ```
+
+ In most cases, the following addresses will be used to configure GitLab
+ Geo:
+
+ | Configuration | Address |
+ |:----------------------------------------|:------------------------------------------------------|
+ | `postgresql['listen_address']` | **Primary** node's public or VPC private address. |
+ | `postgresql['md5_auth_cidr_addresses']` | **Secondary** node's public or VPC private addresses. |
+
+ If you are using Google Cloud Platform, SoftLayer, or any other vendor that
+ provides a virtual private cloud (VPC) you can use the **secondary** node's private
+ address (corresponds to "internal address" for Google Cloud Platform) for
+ `postgresql['md5_auth_cidr_addresses']` and `postgresql['listen_address']`.
+
+ The `listen_address` option opens PostgreSQL up to network connections
+ with the interface corresponding to the given address. See [the PostgreSQL
+ documentation][pg-docs-runtime-conn] for more details.
+
+ Depending on your network configuration, the suggested addresses may not
+ be correct. If your **primary** node and **secondary** nodes connect over a local
+ area network, or a virtual network connecting availability zones like
+ [Amazon's VPC](https://aws.amazon.com/vpc/) or [Google's VPC](https://cloud.google.com/vpc/)
+ you should use the **secondary** node's private address for `postgresql['md5_auth_cidr_addresses']`.
+
+ Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
+ addresses with addresses appropriate to your network configuration:
+
+ ```ruby
+ ##
+ ## Geo Primary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_primary_role']
+
+ ##
+ ## Primary address
+ ## - replace '<primary_node_ip>' with the public or VPC address of your Geo primary node
+ ##
+ postgresql['listen_address'] = '<primary_node_ip>'
+
+ ##
+ # Allow PostgreSQL client authentication from the primary and secondary IPs. These IPs may be
+ # public or VPC addresses in CIDR format, for example ['198.51.100.1/32', '198.51.100.2/32']
+ ##
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32']
+
+ ##
+ ## Replication settings
+ ## - set this to be the number of Geo secondary nodes you have
+ ##
+ postgresql['max_replication_slots'] = 1
+ # postgresql['max_wal_senders'] = 10
+ # postgresql['wal_keep_segments'] = 10
+
+ ##
+ ## Disable automatic database migrations temporarily
+ ## (until PostgreSQL is restarted and listening on the private address).
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+1. Optional: If you want to add another **secondary** node, the relevant setting would look like:
+
+ ```ruby
+ postgresql['md5_auth_cidr_addresses'] = ['<primary_node_ip>/32', '<secondary_node_ip>/32', '<another_secondary_node_ip>/32']
+ ```
+
+ You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
+ match your database replication requirements. Consult the [PostgreSQL -
+ Replication documentation][pg-docs-runtime-replication]
+ for more information.
+
+1. Save the file and reconfigure GitLab for the database listen changes and
+ the replication slot changes to be applied:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+ Restart PostgreSQL for its changes to take effect:
+
+ ```sh
+ gitlab-ctl restart postgresql
+ ```
+
+1. Re-enable migrations now that PostgreSQL is restarted and listening on the
+ private address.
+
+ Edit `/etc/gitlab/gitlab.rb` and **change** the configuration to `true`:
+
+ ```ruby
+ gitlab_rails['auto_migrate'] = true
+ ```
+
+ Save the file and reconfigure GitLab:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+1. Now that the PostgreSQL server is set up to accept remote connections, run
+ `netstat -plnt | grep 5432` to make sure that PostgreSQL is listening on port
+ `5432` to the **primary** server's private address.
+
+1. A certificate was automatically generated when GitLab was reconfigured. This
+ will be used automatically to protect your PostgreSQL traffic from
+ eavesdroppers, but to protect against active ("man-in-the-middle") attackers,
+ the **secondary** node needs a copy of the certificate. Make a copy of the PostgreSQL
+ `server.crt` file on the **primary** node by running this command:
+
+ ```sh
+ cat ~gitlab-psql/data/server.crt
+ ```
+
+ Copy the output into a clipboard or into a local file. You
+ will need it when setting up the **secondary** node! The certificate is not sensitive
+ data.
+
+### Step 2. Configure the **secondary** server
+
+1. SSH into your GitLab **secondary** server and login as root:
+
+ ```
+ sudo -i
+ ```
+
+1. Stop application server and Sidekiq
+
+ ```
+ gitlab-ctl stop unicorn
+ gitlab-ctl stop sidekiq
+ ```
+
+ NOTE: **Note**:
+ This step is important so we don't try to execute anything before the node is fully configured.
+
+1. [Check TCP connectivity][rake-maintenance] to the **primary** node's PostgreSQL server:
+
+ ```sh
+ gitlab-rake gitlab:tcp_check[<primary_node_ip>,5432]
+ ```
+
+ NOTE: **Note**:
+ If this step fails, you may be using the wrong IP address, or a firewall may
+ be preventing access to the server. Check the IP address, paying close
+ attention to the difference between public and private addresses and ensure
+ that, if a firewall is present, the **secondary** node is permitted to connect to the
+ **primary** node on port 5432.
+
+1. Create a file `server.crt` in the **secondary** server, with the content you got on the last step of the **primary** node's setup:
+
+ ```
+ editor server.crt
+ ```
+
+1. Set up PostgreSQL TLS verification on the **secondary** node:
+
+ Install the `server.crt` file:
+
+ ```sh
+ install \
+ -D \
+ -o gitlab-psql \
+ -g gitlab-psql \
+ -m 0400 \
+ -T server.crt ~gitlab-psql/.postgresql/root.crt
+ ```
+
+ PostgreSQL will now only recognize that exact certificate when verifying TLS
+ connections. The certificate can only be replicated by someone with access
+ to the private key, which is **only** present on the **primary** node.
+
+1. Test that the `gitlab-psql` user can connect to the **primary** node's database
+ (the default Omnibus database name is gitlabhq_production):
+
+ ```sh
+ sudo \
+ -u gitlab-psql /opt/gitlab/embedded/bin/psql \
+ --list \
+ -U gitlab_replicator \
+ -d "dbname=gitlabhq_production sslmode=verify-ca" \
+ -W \
+ -h <primary_node_ip>
+ ```
+
+ When prompted enter the password you set in the first step for the
+ `gitlab_replicator` user. If all worked correctly, you should see
+ the list of **primary** node's databases.
+
+ A failure to connect here indicates that the TLS configuration is incorrect.
+ Ensure that the contents of `~gitlab-psql/data/server.crt` on the **primary** node
+ match the contents of `~gitlab-psql/.postgresql/root.crt` on the **secondary** node.
+
+1. Configure PostgreSQL to enable FDW support:
+
+ This step is similar to how we configured the **primary** instance.
+ We need to enable this, to enable FDW support, even if using a single node.
+
+ Edit `/etc/gitlab/gitlab.rb` and add the following, replacing the IP
+ addresses with addresses appropriate to your network configuration:
+
+ ```ruby
+ ##
+ ## Geo Secondary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_secondary_role']
+
+ ##
+ ## Secondary address
+ ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
+ ##
+ postgresql['listen_address'] = '<secondary_node_ip>'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
+
+ ##
+ ## Database credentials password (defined previously in primary node)
+ ## - replicate same values here as defined in primary node
+ ##
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ ##
+ ## Enable FDW support for the Geo Tracking Database (improves performance)
+ ##
+ geo_secondary['db_fdw'] = true
+ ```
+
+ For external PostgreSQL instances, see [additional instructions](external_database.md).
+ If you bring a former **primary** node back online to serve as a **secondary** node, then you also need to remove `roles ['geo_primary_role']` or `geo_primary_role['enable'] = true`.
+
+1. Reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+1. Restart PostgreSQL for the IP change to take effect and reconfigure again:
+
+ ```sh
+ gitlab-ctl restart postgresql
+ gitlab-ctl reconfigure
+ ```
+
+ This last reconfigure will provision the FDW configuration and enable it.
+
+### Step 3. Initiate the replication process
+
+Below we provide a script that connects the database on the **secondary** node to
+the database on the **primary** node, replicates the database, and creates the
+needed files for streaming replication.
+
+The directories used are the defaults that are set up in Omnibus. If you have
+changed any defaults or are using a source installation, configure it as you
+see fit replacing the directories and paths.
+
+CAUTION: **Warning:**
+Make sure to run this on the **secondary** server as it removes all PostgreSQL's
+data before running `pg_basebackup`.
+
+1. SSH into your GitLab **secondary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Choose a database-friendly name to use for your **secondary** node to
+ use as the replication slot name. For example, if your domain is
+ `secondary.geo.example.com`, you may use `secondary_example` as the slot
+ name as shown in the commands below.
+
+1. Execute the command below to start a backup/restore and begin the replication
+ CAUTION: **Warning:** Each Geo **secondary** node must have its own unique replication slot name.
+ Using the same slot name between two secondaries will break PostgreSQL replication.
+
+ ```sh
+ gitlab-ctl replicate-geo-database \
+ --slot-name=<secondary_node_name> \
+ --host=<primary_node_ip>
+ ```
+
+ When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
+ user in the first step.
+
+ This command also takes a number of additional options. You can use `--help`
+ to list them all, but here are a couple of tips:
+ - If PostgreSQL is listening on a non-standard port, add `--port=` as well.
+ - If your database is too large to be transferred in 30 minutes, you will need
+ to increase the timeout, e.g., `--backup-timeout=3600` if you expect the
+ initial replication to take under an hour.
+ - Pass `--sslmode=disable` to skip PostgreSQL TLS authentication altogether
+ (e.g., you know the network path is secure, or you are using a site-to-site
+ VPN). This is **not** safe over the public Internet!
+ - You can read more details about each `sslmode` in the
+ [PostgreSQL documentation][pg-docs-ssl];
+ the instructions above are carefully written to ensure protection against
+ both passive eavesdroppers and active "man-in-the-middle" attackers.
+ - Change the `--slot-name` to the name of the replication slot
+ to be used on the **primary** database. The script will attempt to create the
+ replication slot automatically if it does not exist.
+ - If you're repurposing an old server into a Geo **secondary** node, you'll need to
+ add `--force` to the command line.
+ - When not in a production machine you can disable backup step if you
+ really sure this is what you want by adding `--skip-backup`
+
+The replication process is now complete.
+
+## PGBouncer support (optional)
+
+[PGBouncer](http://pgbouncer.github.io/) 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
+information, see the [Omnibus HA](https://docs.gitlab.com/ee/administration/high_availability/database.html#configure-using-omnibus-for-high-availability)
+documentation.
+
+For a Geo **secondary** node to work properly with PGBouncer in front of the database,
+it will need a separate read-only user to make [PostgreSQL FDW queries][FDW]
+work:
+
+1. On the **primary** Geo database, enter the PostgreSQL on the console as an
+ admin user. If you are using an Omnibus-managed database, log onto the **primary**
+ node that is running the PostgreSQL database (the default Omnibus database name is gitlabhq_production):
+
+ ```sh
+ sudo \
+ -u gitlab-psql /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/postgresql gitlabhq_production
+ ```
+
+1. Then create the read-only user:
+
+ ```sql
+ -- NOTE: Use the password defined earlier
+ CREATE USER gitlab_geo_fdw WITH password 'mypassword';
+ GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
+ GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
+
+ -- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
+ -- automatically.
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
+ ```
+
+1. On the **secondary** nodes, change `/etc/gitlab/gitlab.rb`:
+
+ ```
+ geo_postgresql['fdw_external_user'] = 'gitlab_geo_fdw'
+ ```
+
+1. Save the file and reconfigure GitLab for the changes to be applied:
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+## MySQL replication
+
+MySQL replication is not supported for Geo.
+
+## Troubleshooting
+
+Read the [troubleshooting document](troubleshooting.md).
+
+[replication-slots-article]: https://medium.com/@tk512/replication-slots-in-postgresql-b4b03d277c75
+[pgback]: http://www.postgresql.org/docs/9.2/static/app-pgbasebackup.html
+[replication user]:https://wiki.postgresql.org/wiki/Streaming_Replication
+[FDW]: https://www.postgresql.org/docs/9.6/static/postgres-fdw.html
+[toc]: index.md#using-omnibus-gitlab
+[rake-maintenance]: ../../raketasks/maintenance.md
+[pg-docs-ssl]: https://www.postgresql.org/docs/9.6/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION
+[pg-docs-runtime-conn]: https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
+[pg-docs-runtime-replication]: https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html
diff --git a/doc/administration/geo/replication/database_source.md b/doc/administration/geo/replication/database_source.md
new file mode 100644
index 00000000000..67cf8b6535f
--- /dev/null
+++ b/doc/administration/geo/replication/database_source.md
@@ -0,0 +1,439 @@
+# Geo database replication (source) **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+This documentation applies to GitLab source installations. In GitLab 11.5, this documentation was deprecated and will be removed in a future release.
+Please consider [migrating to GitLab Omnibus install](https://docs.gitlab.com/omnibus/update/convert_to_omnibus.html). For installations
+using the Omnibus GitLab packages, follow the
+[**database replication for Omnibus GitLab**][database] guide.
+
+NOTE: **Note:**
+The stages of the setup process must be completed in the documented order.
+Before attempting the steps in this stage, [complete all prior stages](index.md#using-gitlab-installed-from-source-deprecated).
+
+This document describes the minimal steps you have to take in order to
+replicate your **primary** GitLab database to a **secondary** node's database. You may
+have to change some values according to your database setup, how big it is, etc.
+
+You are encouraged to first read through all the steps before executing them
+in your testing/production environment.
+
+## PostgreSQL replication
+
+The GitLab **primary** node where the write operations happen will connect to
+**primary** database server, and the **secondary** ones which are read-only will
+connect to **secondary** database servers (which are read-only too).
+
+NOTE: **Note:**
+In many databases' documentation, you will see "**primary**" being referenced as "master"
+and "**secondary**" as either "slave" or "standby" server (read-only).
+
+We recommend using [PostgreSQL replication slots][replication-slots-article]
+to ensure the **primary** node retains all the data necessary for the secondaries to
+recover. See below for more details.
+
+The following guide assumes that:
+
+- You are using PostgreSQL 9.6 or later which includes the
+ [`pg_basebackup` tool][pgback] and improved [Foreign Data Wrapper][FDW] support.
+- You have a **primary** node already set up (the GitLab server you are
+ replicating from), running PostgreSQL 9.6 or later, and
+ you have a new **secondary** server set up with the same versions of the OS,
+ PostgreSQL, and GitLab on all nodes.
+- The IP of the **primary** server for our examples is `198.51.100.1`, whereas the
+ **secondary** node's IP is `198.51.100.2`. Note that the **primary** and **secondary** servers
+ **must** be able to communicate over these addresses. These IP addresses can either
+ be public or private.
+
+CAUTION: **Warning:**
+Geo works with streaming replication. Logical replication is not supported at this time.
+There is an [issue where support is being discussed](https://gitlab.com/gitlab-org/gitlab-ee/issues/7420).
+
+### Step 1. Configure the **primary** server
+
+1. SSH into your GitLab **primary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Add this node as the Geo **primary** by running:
+
+ ```sh
+ bundle exec rake geo:set_primary_node
+ ```
+
+1. Create a [replication user] named `gitlab_replicator`:
+
+ ```sql
+ --- Create a new user 'replicator'
+ CREATE USER gitlab_replicator;
+
+ --- Set/change a password and grants replication privilege
+ ALTER USER gitlab_replicator WITH REPLICATION ENCRYPTED PASSWORD '<replication_password>';
+ ```
+
+1. Make sure your the `gitlab` database user has a password defined:
+
+ ```sh
+ sudo \
+ -u postgres psql \
+ -d template1 \
+ -c "ALTER USER gitlab WITH ENCRYPTED PASSWORD '<database_password>';"
+ ```
+
+1. Edit the content of `database.yml` in `production:` and add the password like the example below:
+
+ ```yaml
+ #
+ # PRODUCTION
+ #
+ production:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_production
+ pool: 10
+ username: gitlab
+ password: <database_password>
+ host: /var/opt/gitlab/geo-postgresql
+ ```
+
+1. Set up TLS support for the PostgreSQL **primary** server:
+
+ CAUTION: **Warning**:
+ Only skip this step if you **know** that PostgreSQL traffic
+ between the **primary** and **secondary** nodes will be secured through some other
+ means, e.g., a known-safe physical network path or a site-to-site VPN that
+ you have configured.
+
+ If you are replicating your database across the open Internet, it is
+ **essential** that the connection is TLS-secured. Correctly configured, this
+ provides protection against both passive eavesdroppers and active
+ "man-in-the-middle" attackers.
+
+ To generate a self-signed certificate and key, run this command:
+
+ ```sh
+ openssl req \
+ -nodes \
+ -batch \
+ -x509 \
+ -newkey rsa:4096 \
+ -keyout server.key \
+ -out server.crt \
+ -days 3650
+ ```
+
+ This will create two files - `server.key` and `server.crt` - that you can
+ use for authentication.
+
+ Copy them to the correct location for your PostgreSQL installation:
+
+ ```sh
+ # Copying a self-signed certificate and key
+ install -o postgres -g postgres -m 0400 -T server.crt ~postgres/9.x/main/data/server.crt
+ install -o postgres -g postgres -m 0400 -T server.key ~postgres/9.x/main/data/server.key
+ ```
+
+ Add this configuration to `postgresql.conf`, removing any existing
+ configuration for `ssl_cert_file` or `ssl_key_file`:
+
+ ```
+ ssl = on
+ ssl_cert_file='server.crt'
+ ssl_key_file='server.key'
+ ```
+
+1. Edit `postgresql.conf` to configure the **primary** server for streaming replication
+ (for Debian/Ubuntu that would be `/etc/postgresql/9.x/main/postgresql.conf`):
+
+ ```
+ listen_address = '<primary_node_ip>'
+ wal_level = hot_standby
+ max_wal_senders = 5
+ min_wal_size = 80MB
+ max_wal_size = 1GB
+ max_replicaton_slots = 1 # Number of Geo secondary nodes
+ wal_keep_segments = 10
+ hot_standby = on
+ ```
+
+ NOTE: **Note**:
+ Be sure to set `max_replication_slots` to the number of Geo **secondary**
+ nodes that you may potentially have (at least 1).
+
+ For security reasons, PostgreSQL by default only listens on the local
+ interface (e.g. 127.0.0.1). However, Geo needs to communicate
+ between the **primary** and **secondary** nodes over a common network, such as a
+ corporate LAN or the public Internet. For this reason, we need to
+ configure PostgreSQL to listen on more interfaces.
+
+ The `listen_address` option opens PostgreSQL up to external connections
+ with the interface corresponding to the given IP. See [the PostgreSQL
+ documentation][pg-docs-runtime-conn] for more details.
+
+ You may also want to edit the `wal_keep_segments` and `max_wal_senders` to
+ match your database replication requirements. Consult the
+ [PostgreSQL - Replication documentation][pg-docs-runtime-replication] for more information.
+
+1. Set the access control on the **primary** node to allow TCP connections using the
+ server's public IP and set the connection from the **secondary** node to require a
+ password. Edit `pg_hba.conf` (for Debian/Ubuntu that would be
+ `/etc/postgresql/9.x/main/pg_hba.conf`):
+
+ ```sh
+ host all all <primary_node_ip>/32 md5
+ host replication gitlab_replicator <secondary_node_ip>/32 md5
+ ```
+
+ If you want to add another secondary, add one more row like the replication
+ one and change the IP address:
+
+ ```sh
+ host all all <primary_node_ip>/32 md5
+ host replication gitlab_replicator <secondary_node_ip>/32 md5
+ host replication gitlab_replicator <another_secondary_node_ip>/32 md5
+ ```
+
+1. Restart PostgreSQL for the changes to take effect.
+
+1. Choose a database-friendly name to use for your secondary to use as the
+ replication slot name. For example, if your domain is
+ `secondary.geo.example.com`, you may use `secondary_example` as the slot
+ name.
+
+1. Create the replication slot on the **primary** node:
+
+ ```sh
+ $ sudo -u postgres psql -c "SELECT * FROM pg_create_physical_replication_slot('secondary_example');"
+ slot_name | xlog_position
+ ------------------+---------------
+ secondary_example |
+ (1 row)
+ ```
+
+1. Now that the PostgreSQL server is set up to accept remote connections, run
+ `netstat -plnt` to make sure that PostgreSQL is listening to the server's
+ public IP.
+
+### Step 2. Configure the secondary server
+
+Follow the first steps in ["configure the secondary server"][database-replication] and note that since you are installing from source, the username and
+group listed as `gitlab-psql` in those steps should be replaced by `postgres`
+instead. After completing the "Test that the `gitlab-psql` user can connect to
+the **primary** node's database" step, continue here:
+
+1. Edit `postgresql.conf` to configure the secondary for streaming replication
+ (for Debian/Ubuntu that would be `/etc/postgresql/9.*/main/postgresql.conf`):
+
+ ```sh
+ wal_level = hot_standby
+ max_wal_senders = 5
+ checkpoint_segments = 10
+ wal_keep_segments = 10
+ hot_standby = on
+ ```
+
+1. Restart PostgreSQL for the changes to take effect.
+
+#### Enable tracking database on the secondary server
+
+Geo secondary nodes use a tracking database to keep track of replication status
+and recover automatically from some replication issues. Follow the steps below to create
+the tracking database.
+
+1. On the secondary node, run the following command to create `database_geo.yml` with the
+ information of your secondary PostgreSQL instance:
+
+ ```sh
+ sudo cp /home/git/gitlab/config/database_geo.yml.postgresql /home/git/gitlab/config/database_geo.yml
+ ```
+
+1. Edit the content of `database_geo.yml` in `production:` as in the example below:
+
+ ```yaml
+ #
+ # PRODUCTION
+ #
+ production:
+ adapter: postgresql
+ encoding: unicode
+ database: gitlabhq_geo_production
+ pool: 10
+ username: gitlab_geo
+ # password:
+ host: /var/opt/gitlab/geo-postgresql
+ ```
+
+1. Create the database `gitlabhq_geo_production` on the PostgreSQL instance of the **secondary** node.
+
+1. Set up the Geo tracking database:
+
+ ```sh
+ bundle exec rake geo:db:migrate
+ ```
+
+1. Configure the [PostgreSQL FDW][FDW] connection and credentials:
+
+ Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
+ params to match your environment. Execute it to set up the FDW connection.
+
+ ```sh
+ #!/bin/bash
+
+ # Secondary Database connection params:
+ DB_HOST="/var/opt/gitlab/postgresql" # change to the public IP or VPC private IP if its an external server
+ DB_NAME="gitlabhq_production"
+ DB_USER="gitlab"
+ DB_PORT="5432"
+
+ # Tracking Database connection params:
+ GEO_DB_HOST="/var/opt/gitlab/geo-postgresql" # change to the public IP or VPC private IP if its an external server
+ GEO_DB_NAME="gitlabhq_geo_production"
+ GEO_DB_USER="gitlab_geo"
+ GEO_DB_PORT="5432"
+
+ query_exec () {
+ gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
+ }
+
+ query_exec "CREATE EXTENSION postgres_fdw;"
+ query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
+ query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}');"
+ query_exec "CREATE SCHEMA gitlab_secondary;"
+ query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
+ ```
+
+ And edit the content of `database_geo.yml` and to add `fdw: true` to
+ the `production:` block.
+
+### Step 3. Initiate the replication process
+
+Below we provide a script that connects the database on the **secondary** node to
+the database on the **primary** node, replicates the database, and creates the
+needed files for streaming replication.
+
+The directories used are the defaults for Debian/Ubuntu. If you have changed
+any defaults, configure it as you see fit replacing the directories and paths.
+
+CAUTION: **Warning:**
+Make sure to run this on the **secondary** server as it removes all PostgreSQL's
+data before running `pg_basebackup`.
+
+1. SSH into your GitLab **secondary** server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Save the snippet below in a file, let's say `/tmp/replica.sh`. Modify the
+ embedded paths if necessary:
+
+ ```
+ #!/bin/bash
+
+ PORT="5432"
+ USER="gitlab_replicator"
+ echo ---------------------------------------------------------------
+ echo WARNING: Make sure this script is run from the secondary server
+ echo ---------------------------------------------------------------
+ echo
+ echo Enter the IP or FQDN of the primary PostgreSQL server
+ read HOST
+ echo Enter the password for $USER@$HOST
+ read -s PASSWORD
+ echo Enter the required sslmode
+ read SSLMODE
+
+ echo Stopping PostgreSQL and all GitLab services
+ sudo service gitlab stop
+ sudo service postgresql stop
+
+ echo Backing up postgresql.conf
+ sudo -u postgres mv /var/opt/gitlab/postgresql/data/postgresql.conf /var/opt/gitlab/postgresql/
+
+ echo Cleaning up old cluster directory
+ sudo -u postgres rm -rf /var/opt/gitlab/postgresql/data
+
+ echo Starting base backup as the replicator user
+ echo Enter the password for $USER@$HOST
+ sudo -u postgres /opt/gitlab/embedded/bin/pg_basebackup -h $HOST -D /var/opt/gitlab/postgresql/data -U gitlab_replicator -v -x -P
+
+ echo Writing recovery.conf file
+ sudo -u postgres bash -c "cat > /var/opt/gitlab/postgresql/data/recovery.conf <<- _EOF1_
+ standby_mode = 'on'
+ primary_conninfo = 'host=$HOST port=$PORT user=$USER password=$PASSWORD sslmode=$SSLMODE'
+ _EOF1_
+ "
+
+ echo Restoring postgresql.conf
+ sudo -u postgres mv /var/opt/gitlab/postgresql/postgresql.conf /var/opt/gitlab/postgresql/data/
+
+ echo Starting PostgreSQL
+ sudo service postgresql start
+ ```
+
+1. Run it with:
+
+ ```sh
+ bash /tmp/replica.sh
+ ```
+
+ When prompted, enter the IP/FQDN of the **primary** node, and the password you set up
+ for the `gitlab_replicator` user in the first step.
+
+ You should use `verify-ca` for the `sslmode`. You can use `disable` if you
+ are happy to skip PostgreSQL TLS authentication altogether (e.g., you know
+ the network path is secure, or you are using a site-to-site VPN). This is
+ **not** safe over the public Internet!
+
+ You can read more details about each `sslmode` in the
+ [PostgreSQL documentation][pg-docs-ssl];
+ the instructions above are carefully written to ensure protection against
+ both passive eavesdroppers and active "man-in-the-middle" attackers.
+
+The replication process is now over.
+
+## PGBouncer support (optional)
+
+1. First, enter the PostgreSQL console as an admin user.
+
+1. Then create the read-only user:
+
+ ```sql
+ -- NOTE: Use the password defined earlier
+ CREATE USER gitlab_geo_fdw WITH password '<your_password_here>';
+ GRANT CONNECT ON DATABASE gitlabhq_production to gitlab_geo_fdw;
+ GRANT USAGE ON SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO gitlab_geo_fdw;
+ GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO gitlab_geo_fdw;
+
+ -- Tables created by "gitlab" should be made read-only for "gitlab_geo_fdw"
+ -- automatically.
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON TABLES TO gitlab_geo_fdw;
+ ALTER DEFAULT PRIVILEGES FOR USER gitlab IN SCHEMA public GRANT SELECT ON SEQUENCES TO gitlab_geo_fdw;
+ ```
+
+1. Enter the PostgreSQL console on the **secondary** tracking database and change the user mapping to this new user:
+
+ ```
+ ALTER USER MAPPING FOR gitlab_geo SERVER gitlab_secondary OPTIONS (SET user 'gitlab_geo_fdw')
+ ```
+
+## MySQL replication
+
+MySQL replication is not supported for Geo.
+
+## Troubleshooting
+
+Read the [troubleshooting document](troubleshooting.md).
+
+[replication-slots-article]: https://medium.com/@tk512/replication-slots-in-postgresql-b4b03d277c75
+[pgback]: http://www.postgresql.org/docs/9.6/static/app-pgbasebackup.html
+[replication user]:https://wiki.postgresql.org/wiki/Streaming_Replication
+[FDW]: https://www.postgresql.org/docs/9.6/static/postgres-fdw.html
+[database]: database.md
+[add-geo-node]: configuration.md#step-3-add-the-secondary-gitlab-node
+[database-replication]: database.md#step-2-configure-the-secondary-server
+[pg-docs-ssl]: https://www.postgresql.org/docs/9.6/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION
+[pg-docs-runtime-conn]: https://www.postgresql.org/docs/9.6/static/runtime-config-connection.html
+[pg-docs-runtime-replication]: https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html
diff --git a/doc/administration/geo/replication/docker_registry.md b/doc/administration/geo/replication/docker_registry.md
new file mode 100644
index 00000000000..5b02b861c61
--- /dev/null
+++ b/doc/administration/geo/replication/docker_registry.md
@@ -0,0 +1,23 @@
+# Docker Registry for a secondary node **[PREMIUM ONLY]**
+
+You can set up a [Docker Registry] on your
+**secondary** Geo node that mirrors the one on the **primary** Geo node.
+
+## Storage support
+
+CAUTION: **Warning:**
+If you use [local storage][registry-storage]
+for the Container Registry you **cannot** replicate it to a **secondary** node.
+
+Docker Registry currently supports a few types of storages. If you choose a
+distributed storage (`azure`, `gcs`, `s3`, `swift`, or `oss`) for your Docker
+Registry on the **primary** node, you can use the same storage for a **secondary**
+Docker Registry as well. For more information, read the
+[Load balancing considerations][registry-load-balancing]
+when deploying the Registry, and how to set up the storage driver for GitLab's
+integrated [Container Registry][registry-storage].
+
+[ee]: https://about.gitlab.com/pricing/
+[Docker Registry]: https://docs.docker.com/registry/
+[registry-storage]: ../../container_registry.md#container-registry-storage-driver
+[registry-load-balancing]: https://docs.docker.com/registry/deploying/#load-balancing-considerations
diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md
new file mode 100644
index 00000000000..dae5ed911b0
--- /dev/null
+++ b/doc/administration/geo/replication/external_database.md
@@ -0,0 +1,219 @@
+# Geo with external PostgreSQL instances **[PREMIUM ONLY]**
+
+This document is relevant if you are using a PostgreSQL instance that is *not
+managed by Omnibus*. This includes cloud-managed instances like AWS RDS, or
+manually installed and configured PostgreSQL instances.
+
+NOTE: **Note**:
+We strongly recommend running Omnibus-managed instances as they are actively
+developed and tested. We aim to be compatible with most external
+(not managed by Omnibus) databases but we do not guarantee compatibility.
+
+## **Primary** node
+
+1. SSH into a GitLab **primary** application server and login as root:
+
+ ```sh
+ sudo -i
+ ```
+
+1. Execute the command below to define the node as **primary** node:
+
+ ```sh
+ gitlab-ctl set-geo-primary-node
+ ```
+
+ This command will use your defined `external_url` in `/etc/gitlab/gitlab.rb`.
+
+
+### Configure the external database to be replicated
+
+To set up an external database, you can either:
+
+- Set up streaming replication yourself (for example, in AWS RDS).
+- Perform the Omnibus configuration manually as follows.
+
+#### Leverage your cloud provider's tools to replicate the primary database
+
+Given you have a primary node set up on AWS EC2 that uses RDS.
+You can now just create a read-only replica in a different region and the
+replication process will be managed by AWS. Make sure you've set Network ACL, Subnet, and
+Security Group according to your needs, so the secondary application node can access the database.
+Skip to the [Configure secondary application node](#configure-secondary-application-nodes-to-use-the-external-read-replica) section below.
+
+#### Manually configure the primary database for replication
+
+The [geo_primary_role](https://docs.gitlab.com/omnibus/roles/#gitlab-geo-roles)
+configures the **primary** node's database to be replicated by making changes to
+`pg_hba.conf` and `postgresql.conf`. Make the following configuration changes
+manually to your external database configuration:
+
+```
+##
+## Geo Primary Role
+## - pg_hba.conf
+##
+host replication gitlab_replicator <trusted secondary IP>/32 md5
+```
+
+```
+##
+## Geo Primary Role
+## - postgresql.conf
+##
+sql_replication_user = gitlab_replicator
+wal_level = hot_standby
+max_wal_senders = 10
+wal_keep_segments = 50
+max_replication_slots = 1 # number of secondary instances
+hot_standby = on
+```
+
+## **Secondary** nodes
+
+### Manually configure the replica database
+
+Make the following configuration changes manually to your `postgresql.conf`
+of external replica database:
+
+```
+##
+## Geo Secondary Role
+## - postgresql.conf
+##
+wal_level = hot_standby
+max_wal_senders = 10
+wal_keep_segments = 10
+hot_standby = on
+```
+
+### Configure **secondary** application nodes to use the external read-replica
+
+With Omnibus, the
+[geo_secondary_role](https://docs.gitlab.com/omnibus/roles/#gitlab-geo-roles)
+has three main functions:
+
+1. Configure the replica database.
+1. Configure the tracking database.
+1. Enable the [Geo Log Cursor](index.md#geo-log-cursor) (not covered in this section).
+
+To configure the connection to the external read-replica database and enable Log Cursor:
+
+1. SSH into a GitLab **secondary** application server and login as root:
+
+ ```bash
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following
+
+ ```ruby
+ ##
+ ## Geo Secondary role
+ ## - configure dependent flags automatically to enable Geo
+ ##
+ roles ['geo_secondary_role']
+
+ # note this is shared between both databases,
+ # make sure you define the same password in both
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ gitlab_rails['db_username'] = 'gitlab'
+ gitlab_rails['db_host'] = '<database_read_replica_host>'
+ ```
+1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
+
+### Configure the tracking database
+
+**Secondary** nodes use a separate PostgreSQL installation as a tracking
+database to keep track of replication status and automatically recover from
+potential replication issues. Omnibus automatically configures a tracking database
+when `roles ['geo_secondary_role']` is set. For high availability,
+refer to [Geo High Availability](https://docs.gitlab.com/ee/administration/high_availability).
+If you want to run this database external to Omnibus, please follow the instructions below.
+
+The tracking database requires an [FDW](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
+connection with the **secondary** replica database for improved performance.
+
+If you have an external database ready to be used as the tracking database,
+follow the instructions below to use it:
+
+NOTE: **Note:**
+If you want to use AWS RDS as a tracking database, make sure it has access to
+the secondary database. Unfortunately, just assigning the same security group is not enough as
+outbound rules do not apply to RDS PostgreSQL databases. Therefore, you need to explicitly add an inbound
+rule to the read-replica's security group allowing any TCP traffic from
+the tracking database on port 5432.
+
+1. SSH into a GitLab **secondary** server and login as root:
+
+ ```bash
+ sudo -i
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` with the connection params and credentials for
+ the machine with the PostgreSQL instance:
+
+ ```ruby
+ geo_secondary['db_username'] = 'gitlab_geo'
+ geo_secondary['db_password'] = '<your_password_here>'
+
+ geo_secondary['db_host'] = '<tracking_database_host>'
+ geo_secondary['db_port'] = <tracking_database_port> # change to the correct port
+ geo_secondary['db_fdw'] = true # enable FDW
+ geo_postgresql['enable'] = false # don't use internal managed instance
+ ```
+
+1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
+
+1. Run the tracking database migrations:
+
+ ```bash
+ gitlab-rake geo:db:create
+ gitlab-rake geo:db:migrate
+ ```
+
+1. Configure the
+ [PostgreSQL FDW](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
+ connection and credentials:
+
+ Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
+ params to match your environment. Execute it to set up the FDW connection.
+
+ ```bash
+ #!/bin/bash
+
+ # Secondary Database connection params:
+ DB_HOST="<public_ip_or_vpc_private_ip>"
+ DB_NAME="gitlabhq_production"
+ DB_USER="gitlab"
+ DB_PASS="<your_password_here>"
+ DB_PORT="5432"
+
+ # Tracking Database connection params:
+ GEO_DB_HOST="<public_ip_or_vpc_private_ip>"
+ GEO_DB_NAME="gitlabhq_geo_production"
+ GEO_DB_USER="gitlab_geo"
+ GEO_DB_PORT="5432"
+
+ query_exec () {
+ gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
+ }
+
+ query_exec "CREATE EXTENSION postgres_fdw;"
+ query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
+ query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}', password '${DB_PASS}');"
+ query_exec "CREATE SCHEMA gitlab_secondary;"
+ query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
+ ```
+
+ NOTE: **Note:** The script template above uses `gitlab-psql` as it's intended to be executed from the Geo machine,
+ but you can change it to `psql` and run it from any machine that has access to the database. We also recommend using
+ `psql` for AWS RDS.
+
+1. Save the file and [restart GitLab](../../restart_gitlab.md#omnibus-gitlab-restart)
+1. Populate the FDW tables:
+
+ ```bash
+ gitlab-rake geo:db:refresh_foreign_tables
+ ```
diff --git a/doc/administration/geo/replication/faq.md b/doc/administration/geo/replication/faq.md
new file mode 100644
index 00000000000..dd1af0dbf9c
--- /dev/null
+++ b/doc/administration/geo/replication/faq.md
@@ -0,0 +1,62 @@
+# Geo Frequently Asked Questions **[PREMIUM ONLY]**
+
+## What are the minimum requirements to run Geo?
+
+The requirements are listed [on the index page](index.md#requirements-for-running-geo)
+
+## How does Geo know which projects to sync?
+
+On each **secondary** node, there is a read-only replicated copy of the GitLab database.
+A **secondary** node also has a tracking database where it stores which projects have been synced.
+Geo compares the two databases to find projects that are not yet tracked.
+
+At the start, this tracking database is empty, so Geo will start trying to update from every project that it can see in the GitLab database.
+
+For each project to sync:
+
+1. Geo will issue a `git fetch geo --mirror` to get the latest information from the **primary** node.
+If there are no changes, the sync will be fast and end quickly. Otherwise, it will pull the latest commits.
+1. The **secondary** node will update the tracking database to store the fact that it has synced projects A, B, C, etc.
+1. Repeat until all projects are synced.
+
+When someone pushes a commit to the **primary** node, it generates an event in the GitLab database that the repository has changed.
+The **secondary** node sees this event, marks the project in question as dirty, and schedules the project to be resynced.
+
+To ensure that problems with pipelines (for example, syncs failing too many times or jobs being lost) don't permanently stop projects syncing, Geo also periodically checks the tracking database for projects that are marked as dirty. This check happens when
+the number of concurrent syncs falls below `repos_max_capacity` and there are no new projects waiting to be synced.
+
+Geo also has a checksum feature which runs a SHA256 sum across all the Git references to the SHA values.
+If the refs don't match between the **primary** node and the **secondary** node, then the **secondary** node will mark that project as dirty and try to resync it.
+So even if we have an outdated tracking database, the validation should activate and find discrepancies in the repository state and resync.
+
+## Can I use Geo in a disaster recovery situation?
+
+Yes, but there are limitations to what we replicate (see
+[What data is replicated to a **secondary** node?](#what-data-is-replicated-to-a-secondary-node)).
+
+Read the documentation for [Disaster Recovery](../disaster_recovery/index.md).
+
+## What data is replicated to a **secondary** node?
+
+We currently replicate project repositories, LFS objects, generated
+attachments / avatars and the whole database. This means user accounts,
+issues, merge requests, groups, project data, etc., will be available for
+query.
+
+## Can I git push to a **secondary** node?
+
+Yes! Pushing directly to a **secondary** node (for both HTTP and SSH, including git-lfs) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+
+## How long does it take to have a commit replicated to a **secondary** node?
+
+All replication operations are asynchronous and are queued to be dispatched. Therefore, it depends on a lot of
+factors including the amount of traffic, how big your commit is, the
+connectivity between your nodes, your hardware, etc.
+
+## What if the SSH server runs at a different port?
+
+That's totally fine. We use HTTP(s) to fetch repository changes from the **primary** node to all **secondary** nodes.
+
+## Is this possible to set up a Docker Registry for a **secondary** node that mirrors the one on the **primary** node?
+
+Yes. See [Docker Registry for a **secondary** node](docker_registry.md).
diff --git a/doc/administration/geo/replication/high_availability.md b/doc/administration/geo/replication/high_availability.md
new file mode 100644
index 00000000000..715a83a9ff3
--- /dev/null
+++ b/doc/administration/geo/replication/high_availability.md
@@ -0,0 +1,295 @@
+# Geo High Availability **[PREMIUM ONLY]**
+
+This document describes a minimal reference architecture for running Geo
+in a high availability configuration. If your HA setup differs from the one
+described, it is possible to adapt these instructions to your needs.
+
+## Architecture overview
+
+![Geo HA Diagram](https://docs.gitlab.com/ee/administration/img/high_availability/geo-ha-diagram.png)
+
+_[diagram source - gitlab employees only][diagram-source]_
+
+The topology above assumes that the **primary** and **secondary** Geo clusters
+are located in two separate locations, on their own virtual network
+with private IP addresses. The network is configured such that all machines within
+one geographic location can communicate with each other using their private IP addresses.
+The IP addresses given are examples and may be different depending on the
+network topology of your deployment.
+
+The only external way to access the two Geo deployments is by HTTPS at
+`gitlab.us.example.com` and `gitlab.eu.example.com` in the example above.
+
+NOTE: **Note:**
+The **primary** and **secondary** Geo deployments must be able to communicate to each other over HTTPS.
+
+## Redis and PostgreSQL High Availability
+
+The **primary** and **secondary** Redis and PostgreSQL should be configured
+for high availability. Because of the additional complexity involved
+in setting up this configuration for PostgreSQL and Redis,
+it is not covered by this Geo HA documentation.
+
+For more information about setting up a highly available PostgreSQL cluster and Redis cluster using the omnibus package see the high availability documentation for
+[PostgreSQL](../../high_availability/database.md) and
+[Redis](../../high_availability/redis.md), respectively.
+
+NOTE: **Note:**
+It is possible to use cloud hosted services for PostgreSQL and Redis, but this is beyond the scope of this document.
+
+## Prerequisites: A working GitLab HA cluster
+
+This cluster will serve as the **primary** node. Use the
+[GitLab HA documentation](../../high_availability/README.md) to set this up.
+
+## Configure the GitLab cluster to be the **primary** node
+
+The following steps enable a GitLab cluster to serve as the **primary** node.
+
+### Step 1: Configure the **primary** frontend servers
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following:
+
+ ```ruby
+ ##
+ ## Enable the Geo primary role
+ ##
+ roles ['geo_primary_role']
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
+
+NOTE: **Note:** PostgreSQL and Redis should have already been disabled on the
+application servers, and connections from the application servers to those
+services on the backend servers configured, during normal GitLab HA set up. See
+high availability configuration documentation for
+[PostgreSQL](https://docs.gitlab.com/ee/administration/high_availability/database.html#configuring-the-application-nodes)
+and [Redis](../../high_availability/redis.md#example-configuration-for-the-gitlab-application).
+
+The **primary** database will require modification later, as part of
+[step 2](#step-2-configure-the-main-read-only-replica-postgresql-database-on-the-secondary-node).
+
+## Configure a **secondary** node
+
+A **secondary** cluster is similar to any other GitLab HA cluster, with two
+major differences:
+
+* The main PostgreSQL database is a read-only replica of the **primary** node's
+ PostgreSQL database.
+* There is also a single PostgreSQL database for the **secondary** cluster,
+ called the "tracking database", which tracks the synchronization state of
+ various resources.
+
+Therefore, we will set up the HA components one-by-one, and include deviations
+from the normal HA setup.
+
+### Step 1: Configure the Redis and NFS services on the **secondary** node
+
+Configure the following services, again using the non-Geo high availability
+documentation:
+
+* [Configuring Redis for GitLab HA](../../high_availability/redis.md) for high
+ availability.
+* [NFS](../../high_availability/nfs.md) which will store data that is
+ synchronized from the **primary** node.
+
+### Step 2: Configure the main read-only replica PostgreSQL database on the **secondary** node
+
+NOTE: **Note:** The following documentation assumes the database will be run on
+a single node only, rather than as a PostgreSQL cluster.
+
+Configure the [**secondary** database](database.md) as a read-only replica of
+the **primary** database. Use the following as a guide.
+
+1. Edit `/etc/gitlab/gitlab.rb` in the replica database machine, and add the
+ following:
+
+ ```ruby
+ ##
+ ## Configure the PostgreSQL role
+ ##
+ roles ['postgres_role']
+
+ ##
+ ## Secondary address
+ ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
+ ## - replace '<tracking_database_ip>' with the public or VPC address of your Geo tracking database node
+ ##
+ postgresql['listen_address'] = '<secondary_node_ip>'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32', '<tracking_database_ip>/32']
+
+ ##
+ ## Database credentials password (defined previously in primary node)
+ ## - replicate same values here as defined in primary node
+ ##
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ ##
+ ## When running the Geo tracking database on a separate machine, disable it
+ ## here and allow connections from the tracking database host. And ensure
+ ## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
+ ##
+ geo_postgresql['enable'] = false
+ ```
+
+After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
+
+If using an external PostgreSQL instance, refer also to
+[Geo with external PostgreSQL instances](external_database.md).
+
+### Step 3: Configure the tracking database on the **secondary** node
+
+NOTE: **Note:** This documentation assumes the tracking database will be run on
+only a single machine, rather than as a PostgreSQL cluster.
+
+Configure the tracking database.
+
+1. Edit `/etc/gitlab/gitlab.rb` in the tracking database machine, and add the
+ following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary tracking database
+ ##
+ geo_postgresql['enable'] = true
+ geo_postgresql['listen_address'] = '<ip_address_of_this_host>'
+ geo_postgresql['sql_user_password'] = '<tracking_database_password_md5_hash>'
+
+ ##
+ ## Configure FDW connection to the replica database
+ ##
+ geo_secondary['db_fdw'] = true
+ geo_postgresql['fdw_external_password'] = '<replica_database_password_plaintext>'
+ geo_postgresql['md5_auth_cidr_addresses'] = ['<replica_database_ip>/32']
+ gitlab_rails['db_host'] = '<replica_database_ip>'
+
+ # Prevent reconfigure from attempting to run migrations on the replica DB
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Disable all other services that aren't needed, since we don't have a role
+ ## that does this.
+ ##
+ alertmanager['enable'] = false
+ consul['enable'] = false
+ gitaly['enable'] = false
+ gitlab_monitor['enable'] = false
+ gitlab_workhorse['enable'] = false
+ nginx['enable'] = false
+ node_exporter['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ postgresql['enable'] = false
+ prometheus['enable'] = false
+ redis['enable'] = false
+ redis_exporter['enable'] = false
+ repmgr['enable'] = false
+ sidekiq['enable'] = false
+ unicorn['enable'] = false
+ ```
+
+After making these changes, [reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
+
+If using an external PostgreSQL instance, refer also to
+[Geo with external PostgreSQL instances](external_database.md).
+
+### Step 4: Configure the frontend application servers on the **secondary** node
+
+In the architecture overview, there are two machines running the GitLab
+application services. These services are enabled selectively in the
+configuration.
+
+Configure the application servers following
+[Configuring GitLab for HA](../../high_availability/gitlab.md), then make the
+following modifications:
+
+1. Edit `/etc/gitlab/gitlab.rb` on each application server in the **secondary**
+ cluster, and add the following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary role
+ ##
+ roles ['geo_secondary_role', 'application_role']
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Configure the connection to the tracking DB. And disable application
+ ## servers from running tracking databases.
+ ##
+ geo_secondary['db_host'] = '<geo_tracking_db_host>'
+ geo_secondary['db_password'] = '<geo_tracking_db_password>'
+ geo_postgresql['enable'] = false
+
+ ##
+ ## Configure connection to the streaming replica database, if you haven't
+ ## already
+ ##
+ gitlab_rails['db_host'] = '<replica_database_host>'
+ gitlab_rails['db_password'] = '<replica_database_password>'
+
+ ##
+ ## Configure connection to Redis, if you haven't already
+ ##
+ gitlab_rails['redis_host'] = '<redis_host>'
+ gitlab_rails['redis_password'] = '<redis_password>'
+
+ ##
+ ## If you are using custom users not managed by Omnibus, you need to specify
+ ## UIDs and GIDs like below, and ensure they match between servers in a
+ ## cluster to avoid permissions issues
+ ##
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
+ ```
+
+NOTE: **Note:**
+If you had set up PostgreSQL cluster using the omnibus package and you had set
+up `postgresql['sql_user_password'] = 'md5 digest of secret'` setting, keep in
+mind that `gitlab_rails['db_password']` and `geo_secondary['db_password']`
+mentioned above contains the plaintext passwords. This is used to let the Rails
+servers connect to the databases.
+
+NOTE: **Note:**
+Make sure that current node IP is listed in `postgresql['md5_auth_cidr_addresses']` setting of your remote database.
+
+After making these changes [Reconfigure GitLab][gitlab-reconfigure] so the changes take effect.
+
+On the secondary the following GitLab frontend services will be enabled:
+
+* geo-logcursor
+* gitlab-pages
+* gitlab-workhorse
+* logrotate
+* nginx
+* registry
+* remote-syslog
+* sidekiq
+* unicorn
+
+Verify these services by running `sudo gitlab-ctl status` on the frontend
+application servers.
+
+### Step 5: Set up the LoadBalancer for the **secondary** node
+
+In this topology, a load balancer is required at each geographic location to
+route traffic to the application servers.
+
+See [Load Balancer for GitLab HA](../../high_availability/load_balancer.md) for
+more information.
+
+[diagram-source]: https://docs.google.com/drawings/d/1z0VlizKiLNXVVVaERFwgsIOuEgjcUqDTWPdQYsE7Z4c/edit
+[gitlab-reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/geo/replication/img/geo_architecture.png b/doc/administration/geo/replication/img/geo_architecture.png
new file mode 100644
index 00000000000..d318cd5d0f4
--- /dev/null
+++ b/doc/administration/geo/replication/img/geo_architecture.png
Binary files differ
diff --git a/doc/administration/geo/replication/img/geo_node_dashboard.png b/doc/administration/geo/replication/img/geo_node_dashboard.png
new file mode 100644
index 00000000000..99792d0770d
--- /dev/null
+++ b/doc/administration/geo/replication/img/geo_node_dashboard.png
Binary files differ
diff --git a/doc/administration/geo/replication/img/geo_node_healthcheck.png b/doc/administration/geo/replication/img/geo_node_healthcheck.png
new file mode 100644
index 00000000000..33a31f7ab49
--- /dev/null
+++ b/doc/administration/geo/replication/img/geo_node_healthcheck.png
Binary files differ
diff --git a/doc/administration/geo/replication/img/geo_overview.png b/doc/administration/geo/replication/img/geo_overview.png
new file mode 100644
index 00000000000..01c1615212c
--- /dev/null
+++ b/doc/administration/geo/replication/img/geo_overview.png
Binary files differ
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
new file mode 100644
index 00000000000..6abea2cf271
--- /dev/null
+++ b/doc/administration/geo/replication/index.md
@@ -0,0 +1,309 @@
+# Geo Replication **[PREMIUM ONLY]**
+
+Geo is the solution for widely distributed development teams.
+
+## Overview
+
+Fetching large repositories can take a long time for teams located far from a single GitLab instance.
+
+Geo provides local, read-only instances of your GitLab instances, reducing the time it takes to clone and fetch large repositories and speeding up development.
+
+> - Geo is part of [GitLab Premium](https://about.gitlab.com/pricing/#self-managed).
+> - Introduced in GitLab Enterprise Edition 8.9.
+> - We recommend you use:
+> - At least GitLab Enterprise Edition 10.0 for basic Geo features.
+> - The latest version for a better experience.
+> - Make sure that all nodes run the same GitLab version.
+> - Geo requires PostgreSQL 9.6 and Git 2.9, in addition to GitLab's usual [minimum requirements](../../../install/requirements.md).
+> - Using Geo in combination with [High Availability](../../high_availability/README.md) is considered **Generally Available** (GA) in GitLab [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
+
+For a video introduction to Geo, see [Introduction to GitLab Geo - GitLab Features](https://www.youtube.com/watch?v=-HDLxSjEh6w).
+
+CAUTION: **Caution:**
+Geo undergoes significant changes from release to release. Upgrades **are** supported and [documented](#updating-geo), but you should ensure that you're using the right version of the documentation for your installation.
+
+To make sure you're using the right version of the documentation, navigate to [the source version of this page on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/administration/geo/replication/index.md) and choose the appropriate release from the **Switch branch/tag** dropdown. For example, [`v11.2.3-ee`](https://gitlab.com/gitlab-org/gitlab-ee/blob/v11.2.3-ee/doc/administration/geo/replication/index.md).
+
+## Use cases
+
+Implementing Geo provides the following benefits:
+
+- Reduce from minutes to seconds the time taken for your distributed developers to clone and fetch large repositories and projects.
+- Enable all of your developers to contribute ideas and work in parallel, no matter where they are.
+- Balance the load between your **primary** and **secondary** nodes, or offload your automated tests to a **secondary** node.
+
+In addition, it:
+
+- Can be used for cloning and fetching projects, in addition to reading any data available in the GitLab web interface (see [current limitations](#current-limitations)).
+- Overcomes slow connections between distant offices, saving time by improving speed for distributed teams.
+- Helps reducing the loading time for automated tasks, custom integrations, and internal workflows.
+- Can quickly fail over to a **secondary** node in a [disaster recovery](../disaster_recovery/index.md) scenario.
+- Allows [planned failover](../disaster_recovery/planned_failover.md) to a **secondary** node.
+
+Geo provides:
+
+- Read-only **secondary** nodes: Maintain one **primary** GitLab node while still enabling read-only **secondary** nodes for each of your distributed teams.
+- Authentication system hooks: **Secondary** nodes receives all authentication data (like user accounts and logins) from the **primary** instance.
+- An intuitive UI: **Secondary** nodes utilize the same web interface your team has grown accustomed to. In addition, there are visual notifications that block write operations and make it clear that a user is on a **secondary** node.
+
+## How it works
+
+Your Geo instance can be used for cloning and fetching projects, in addition to reading any data. This will make working with large repositories over large distances much faster.
+
+![Geo overview](img/geo_overview.png)
+
+When Geo is enabled, the:
+
+- Original instance is known as the **primary** node.
+- Replicated read-only nodes are known as **secondary** nodes.
+
+Keep in mind that:
+
+- **Secondary** nodes talk to the **primary** node to:
+ - Get user data for logins (API).
+ - Replicate repositories, LFS Objects, and Attachments (HTTPS + JWT).
+- Since GitLab Premium 10.0, the **primary** node no longer talks to **secondary** nodes to notify for changes (API).
+- Pushing directly to a **secondary** node (for both HTTP and SSH, including git-lfs) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+- There are [limitations](#current-limitations) in the current implementation.
+
+### Architecture
+
+The following diagram illustrates the underlying architecture of Geo.
+
+![Geo architecture](img/geo_architecture.png)
+
+In this diagram:
+
+- There is the **primary** node and the details of one **secondary** node.
+- Writes to the database can only be performed on the **primary** node. A **secondary** node receives database
+ updates via PostgreSQL streaming replication.
+- If present, the [LDAP server](#ldap) should be configured to replicate for [Disaster Recovery](../disaster_recovery/index.md) scenarios.
+- A **secondary** node performs different type of synchronizations against the **primary** node, using a special
+ authorization protected by JWT:
+ - Repositories are cloned/updated via Git over HTTPS.
+ - Attachments, LFS objects, and other files are downloaded via HTTPS using a private API endpoint.
+
+From the perspective of a user performing Git operations:
+
+- The **primary** node behaves as a full read-write GitLab instance.
+- **Secondary** nodes are read-only but proxy Git push operations to the **primary** node. This makes **secondary** nodes appear to support push operations themselves.
+
+To simplify the diagram, some necessary components are omitted. Note that:
+
+- Git over SSH requires [`gitlab-shell`](https://gitlab.com/gitlab-org/gitlab-shell) and OpenSSH.
+- Git over HTTPS required [`gitlab-workhorse`](https://gitlab.com/gitlab-org/gitlab-workhorse).
+
+Note that a **secondary** node needs two different PostgreSQL databases:
+
+- A read-only database instance that streams data from the main GitLab database.
+- [Another database instance](#geo-tracking-database) used internally by the **secondary** node to record what data has been replicated.
+
+In **secondary** nodes, there is an additional daemon: [Geo Log Cursor](#geo-log-cursor).
+
+## Requirements for running Geo
+
+The following are required to run Geo:
+
+- An operating system that supports OpenSSH 6.9+ (needed for
+ [fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md))
+ The following operating systems are known to ship with a current version of OpenSSH:
+ - [CentOS](https://www.centos.org) 7.4+
+ - [Ubuntu](https://www.ubuntu.com) 16.04+
+- PostgreSQL 9.6+ with [FDW](https://www.postgresql.org/docs/9.6/postgres-fdw.html) support and [Streaming Replication](https://wiki.postgresql.org/wiki/Streaming_Replication)
+- Git 2.9+
+
+### Firewall rules
+
+The following table lists basic ports that must be open between the **primary** and **secondary** nodes for Geo.
+
+| **Primary** node | **Secondary** node | Protocol |
+|:-----------------|:-------------------|:-------------|
+| 80 | 80 | HTTP |
+| 443 | 443 | TCP or HTTPS |
+| 22 | 22 | TCP |
+| 5432 | | PostgreSQL |
+
+See the full list of ports used by GitLab in [Package defaults](https://docs.gitlab.com/omnibus/package-information/defaults.html)
+
+NOTE: **Note:**
+[Web terminal](../../../ci/environments.md#web-terminals) support requires your load balancer to correctly handle WebSocket connections.
+When using HTTP or HTTPS proxying, your load balancer must be configured to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the [web terminal](../../integration/terminal.md) integration guide for more details.
+
+NOTE: **Note:**
+When using HTTPS protocol for port 443, you will need to add an SSL certificate to the load balancers.
+If you wish to terminate SSL at the GitLab application server instead, use TCP protocol.
+
+### LDAP
+
+We recommend that if you use LDAP on your **primary** node, you also set up secondary LDAP servers on each **secondary** node. Otherwise, users will not be able to perform Git operations over HTTP(s) on the **secondary** node using HTTP Basic Authentication. However, Git via SSH and personal access tokens will still work.
+
+NOTE: **Note:**
+It is possible for all **secondary** nodes to share an LDAP server, but additional latency can be an issue. Also, consider what LDAP server will be available in a [disaster recovery](../disaster_recovery/index.md) scenario if a **secondary** node is promoted to be a **primary** node.
+
+Check for instructions on how to set up replication in your LDAP service. Instructions will be different depending on the software or service used. For example, OpenLDAP provides [these instructions](https://www.openldap.org/doc/admin24/replication.html).
+
+### Geo Tracking Database
+
+The tracking database instance is used as metadata to control what needs to be updated on the disk of the local instance. For example:
+
+- Download new assets.
+- Fetch new LFS Objects.
+- Fetch changes from a repository that has recently been updated.
+
+Because the replicated database instance is read-only, we need this additional database instance for each **secondary** node.
+The tracking database requires the `postgres_fdw` extension.
+
+### Geo Log Cursor
+
+This daemon:
+
+- Reads a log of events replicated by the **primary** node to the **secondary** database instance.
+- Updates the Geo Tracking Database instance with changes that need to be executed.
+
+When something is marked to be updated in the tracking database instance, asynchronous jobs running on the **secondary** node will execute the required operations and update the state.
+
+This new architecture allows GitLab to be resilient to connectivity issues between the nodes. It doesn't matter how long the **secondary** node is disconnected from the **primary** node as it will be able to replay all the events in the correct order and become synchronized with the **primary** node again.
+
+## Setup instructions
+
+These instructions assume you have a working instance of GitLab. They guide you through:
+
+1. Making your existing instance the **primary** node.
+1. Adding **secondary** nodes.
+
+CAUTION: **Caution:**
+The steps below should be followed in the order they appear. **Make sure the GitLab version is the same on all nodes.**
+
+### Using Omnibus GitLab
+
+If you installed GitLab using the Omnibus packages (highly recommended):
+
+1. [Install GitLab Enterprise Edition](https://about.gitlab.com/installation/) on the server that will serve as the **secondary** node. Do not create an account or log in to the new **secondary** node.
+1. [Upload the GitLab License](https://docs.gitlab.com/ee/user/admin_area/license.html) on the **primary** node to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
+1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology).
+1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). This step is required and needs to be done on **both** the **primary** and **secondary** nodes.
+1. [Configure GitLab](configuration.md) to set the **primary** and **secondary** nodes.
+1. Optional: [Configure a secondary LDAP server](../../auth/ldap.md) for the **secondary** node. See [notes on LDAP](#ldap).
+1. [Follow the "Using a Geo Server" guide](using_a_geo_server.md).
+
+### Using GitLab installed from source (Deprecated)
+
+NOTE: **Note:**
+In GitLab 11.5, support for using Geo in GitLab source installations was deprecated and will be removed in a future release. Please consider [migrating to GitLab Omnibus install](https://docs.gitlab.com/omnibus/update/convert_to_omnibus.html).
+
+If you installed GitLab from source:
+
+1. [Install GitLab Enterprise Edition](../../../install/installation.md) on the server that will serve as the **secondary** node. Do not create an account or log in to the new **secondary** node.
+1. [Upload the GitLab License](https://docs.gitlab.com/ee/user/admin_area/license.html) on the **primary** node to unlock Geo. The license must be for [GitLab Premium](https://about.gitlab.com/pricing/) or higher.
+1. [Set up the database replication](database_source.md) (`primary (read-write) <-> secondary (read-only)` topology).
+1. [Configure fast lookup of authorized SSH keys in the database](../../operations/fast_ssh_key_lookup.md). Do this step for **both** **primary** and **secondary** nodes.
+1. [Configure GitLab](configuration_source.md) to set the **primary** and **secondary** nodes.
+1. [Follow the "Using a Geo Server" guide](using_a_geo_server.md).
+
+## Post-installation documentation
+
+After installing GitLab on the **secondary** nodes and performing the initial configuration, see the following documentation for post-installation information.
+
+### Configuring Geo
+
+For information on configuring Geo, see:
+
+- [Geo configuration (GitLab Omnibus)](configuration.md).
+- [Geo configuration (source)](configuration_source.md). Configuring Geo in GitLab source installations was **deprecated** in GitLab 11.5.
+
+### Updating Geo
+
+For information on how to update your Geo nodes to the latest GitLab version, see [Updating the Geo nodes](updating_the_geo_nodes.md).
+
+### Configuring Geo high availability
+
+For information on configuring Geo for high availability, see [Geo High Availability](high_availability.md).
+
+### Configuring Geo with Object Storage
+
+For information on configuring Geo with object storage, see [Geo with Object storage](object_storage.md).
+
+### Disaster Recovery
+
+For information on using Geo in disaster recovery situations to mitigate data-loss and restore services, see [Disaster Recovery](../disaster_recovery/index.md).
+
+### Replicating the Container Registry
+
+For more information on how to replicate the Container Registry, see [Docker Registry for a **secondary** node](docker_registry.md).
+
+### Security Review
+
+For more information on Geo security, see [Geo security review](security_review.md).
+
+### Tuning Geo
+
+For more information on tuning Geo, see [Tuning Geo](tuning.md).
+
+## Remove Geo node
+
+For more information on removing a Geo node, see [Removing **secondary** Geo nodes](remove_geo_node.md).
+
+## Current limitations
+
+CAUTION: **Caution:**
+This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
+
+- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab-ee/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
+- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected.
+- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [gitlab-org/omnibus-gitlab#2978](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2978) for details.
+- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
+- [Selective synchronization](configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
+- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
+- [External merge request diffs](../../merge_request_diffs.md) will not be replicated if they are on-disk, and viewing merge requests will fail. However, external MR diffs in object storage **are** supported. The default configuration (in-database) does work.
+
+### Limitations on replication
+
+Only the following items are replicated to the **secondary** node:
+
+- All database content. For example, snippets, epics, issues, merge requests, groups, and project metadata.
+- Project repositories.
+- Project wiki repositories.
+- User uploads. For example, attachments to issues, merge requests, epics, and avatars.
+- CI job artifacts and traces.
+
+DANGER: **DANGER**
+Data not on this list is unavailable on the **secondary** node. Failing over without manually replicating data not on this list will cause the data to be **lost**.
+
+### Examples of data not replicated
+
+Take special note that these examples of GitLab features are both:
+
+- Commonly used.
+- **Not** replicated by Geo at present.
+
+Examples include:
+
+- [Elasticsearch integration](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+- [Container Registry](../../container_registry.md). [Object Storage](object_storage.md) can mitigate this.
+- [GitLab Pages](../../pages/index.md).
+- [Mattermost integration](https://docs.gitlab.com/omnibus/gitlab-mattermost/).
+
+CAUTION: **Caution:**
+If you wish to use them on a **secondary** node, or to execute a failover successfully, you will need to replicate their data using some other means.
+
+## Frequently Asked Questions
+
+For answers to common questions, see the [Geo FAQ](faq.md).
+
+## Log files
+
+Since GitLab 9.5, Geo stores structured log messages in a `geo.log` file. For Omnibus installations, this file is at `/var/log/gitlab/gitlab-rails/geo.log`.
+
+This file contains information about when Geo attempts to sync repositories and files. Each line in the file contains a separate JSON entry that can be ingested into Elasticsearch, Splunk, etc.
+
+For example:
+
+```json
+{"severity":"INFO","time":"2017-08-06T05:40:16.104Z","message":"Repository update","project_id":1,"source":"repository","resync_repository":true,"resync_wiki":true,"class":"Gitlab::Geo::LogCursor::Daemon","cursor_delay_s":0.038}
+```
+
+This message shows that Geo detected that a repository update was needed for project `1`.
+
+## Troubleshooting
+
+For troubleshooting steps, see [Geo Troubleshooting](troubleshooting.md).
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
new file mode 100644
index 00000000000..c3c11dbaf1e
--- /dev/null
+++ b/doc/administration/geo/replication/object_storage.md
@@ -0,0 +1,43 @@
+# Geo with Object storage **[PREMIUM ONLY]**
+
+Geo can be used in combination with Object Storage (AWS S3, or
+other compatible object storage).
+
+## Configuration
+
+At this time it is required that if object storage is enabled on the
+**primary** node, it must also be enabled on each **secondary** node.
+
+**Secondary** nodes can use the same storage bucket as the **primary** node, or
+they can use a replicated storage bucket. At this time GitLab does not
+take care of content replication in object storage.
+
+For LFS, follow the documentation to
+[set up LFS object storage](../../../workflow/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)
+
+For user uploads, there is similar documentation to configure [upload object storage](../../uploads.md#using-object-storage-core-only)
+
+You should enable and configure object storage on both **primary** and **secondary**
+nodes. Migrating existing data to object storage should be performed on the
+**primary** node only. **Secondary** nodes will automatically notice that the migrated
+files are now in object storage.
+
+## Replication
+
+When using Amazon S3, you can use
+[CRR](https://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) to
+have automatic replication between the bucket used by the **primary** node and
+the bucket used by **secondary** nodes.
+
+If you are using Google Cloud Storage, consider using
+[Multi-Regional Storage](https://cloud.google.com/storage/docs/storage-classes#multi-regional).
+Or you can use the [Storage Transfer Service](https://cloud.google.com/storage/transfer/),
+although this only supports daily synchronization.
+
+For manual synchronization, or scheduled by `cron`, please have a look at:
+
+- [`s3cmd sync`](http://s3tools.org/s3cmd-sync)
+- [`gsutil rsync`](https://cloud.google.com/storage/docs/gsutil/commands/rsync)
diff --git a/doc/administration/geo/replication/remove_geo_node.md b/doc/administration/geo/replication/remove_geo_node.md
new file mode 100644
index 00000000000..b190fe7d42d
--- /dev/null
+++ b/doc/administration/geo/replication/remove_geo_node.md
@@ -0,0 +1,50 @@
+# Removing secondary Geo nodes **[PREMIUM ONLY]**
+
+**Secondary** nodes can be removed from the Geo cluster using the Geo admin page of the **primary** node. To remove a **secondary** node:
+
+1. Navigate to **Admin Area > Geo** (`/admin/geo/nodes`).
+1. Click the **Remove** button for the **secondary** node you want to remove.
+1. Confirm by clicking **Remove** when the prompt appears.
+
+Once removed from the Geo admin page, you must stop and uninstall the **secondary** node:
+
+1. On the **secondary** node, stop GitLab:
+
+ ```bash
+ sudo gitlab-ctl stop
+ ```
+1. On the **secondary** node, uninstall GitLab:
+
+ ```bash
+ # Stop gitlab and remove its supervision process
+ sudo gitlab-ctl uninstall
+
+ # Debian/Ubuntu
+ sudo dpkg --remove gitlab-ee
+
+ # Redhat/Centos
+ sudo rpm --erase gitlab-ee
+ ```
+
+Once GitLab has been uninstalled from the **secondary** node, the replication slot must be dropped from the **primary** node's database as follows:
+
+1. On the **primary** node, start a PostgreSQL console session:
+
+ ```bash
+ sudo gitlab-psql
+ ```
+
+ NOTE: **Note:**
+ Using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
+
+1. Find the name of the relevant replication slot. This is the slot that is specified with `--slot-name` when running the replicate command: `gitlab-ctl replicate-geo-database`.
+
+ ```sql
+ SELECT * FROM pg_replication_slots;
+ ```
+
+1. Remove the replication slot for the **secondary** node:
+
+ ```sql
+ SELECT pg_drop_replication_slot('<name_of_slot>');
+ ```
diff --git a/doc/administration/geo/replication/security_review.md b/doc/administration/geo/replication/security_review.md
new file mode 100644
index 00000000000..46d3e68ab63
--- /dev/null
+++ b/doc/administration/geo/replication/security_review.md
@@ -0,0 +1,286 @@
+# Geo security review (Q&A) **[PREMIUM ONLY]**
+
+The following security review of the Geo feature set focuses on security
+aspects of the feature as they apply to customers running their own GitLab
+instances. The review questions are based in part on the [application security architecture](https://www.owasp.org/index.php/Application_Security_Architecture_Cheat_Sheet)
+questions from [owasp.org](https://www.owasp.org).
+
+## Business Model
+
+### What geographic areas does the application service?
+
+- This varies by customer. Geo allows customers to deploy to multiple areas,
+ and they get to choose where they are.
+- Region and node selection is entirely manual.
+
+## Data Essentials
+
+### What data does the application receive, produce, and process?
+
+- Geo streams almost all data held by a GitLab instance between sites. This
+ includes full database replication, most files (user-uploaded attachments,
+ etc) and repository + wiki data. In a typical configuration, this will
+ happen across the public Internet, and be TLS-encrypted.
+- PostgreSQL replication is TLS-encrypted.
+- See also: [only TLSv1.2 should be supported](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2948)
+
+### How can the data be classified into categories according to its sensitivity?
+
+- GitLab’s model of sensitivity is centered around public vs. internal vs.
+ private projects. Geo replicates them all indiscriminately. “Selective sync”
+ exists for files and repositories (but not database content), which would permit
+ only less-sensitive projects to be replicated to a **secondary** node if desired.
+- See also: [developing a data classification policy](https://gitlab.com/gitlab-com/security/issues/4).
+
+### What data backup and retention requirements have been defined for the application?
+
+- Geo is designed to provide replication of a certain subset of the application
+ data. It is part of the solution, rather than part of the problem.
+
+## End-Users
+
+### Who are the application's end‐users?
+
+- **Secondary** nodes are created in regions that are distant (in terms of
+ Internet latency) from the main GitLab installation (the **primary** node). They are
+ intended to be used by anyone who would ordinarily use the **primary** node, who finds
+ that the **secondary** node is closer to them (in terms of Internet latency).
+
+### How do the end‐users interact with the application?
+
+- **Secondary** nodes provide all the interfaces a **primary** node does
+ (notably a HTTP/HTTPS web application, and HTTP/HTTPS or SSH git repository
+ access), but is constrained to read-only activities. The principal use case is
+ envisioned to be cloning git repositories from the **secondary** node in favor of the
+ **primary** node, but end-users may use the GitLab web interface to view projects,
+ issues, merge requests, snippets, etc.
+
+### What security expectations do the end‐users have?
+
+- The replication process must be secure. It would typically be unacceptable to
+ transmit the entire database contents or all files and repositories across the
+ public Internet in plaintext, for instance.
+- **Secondary** nodes must have the same access controls over its content as the
+ **primary** node - unauthenticated users must not be able to gain access to privileged
+ information on the **primary** node by querying the **secondary** node.
+- Attackers must not be able to impersonate the **secondary** node to the **primary** node, and
+ thus gain access to privileged information.
+
+## Administrators
+
+### Who has administrative capabilities in the application?
+
+- Nothing Geo-specific. Any user where `admin: true` is set in the database is
+ considered an admin with super-user privileges.
+- See also: [more granular access control](https://gitlab.com/gitlab-org/gitlab-ce/issues/32730)
+ (not geo-specific)
+- Much of Geo’s integration (database replication, for instance) must be
+ configured with the application, typically by system administrators.
+
+### What administrative capabilities does the application offer?
+
+- **Secondary** nodes may be added, modified, or removed by users with
+ administrative access.
+- The replication process may be controlled (start/stop) via the Sidekiq
+ administrative controls.
+
+## Network
+
+### What details regarding routing, switching, firewalling, and load‐balancing have been defined?
+
+- Geo requires the **primary** node and **secondary** node to be able to communicate with each
+ other across a TCP/IP network. In particular, the **secondary** nodes must be able to
+ access HTTP/HTTPS and PostgreSQL services on the **primary** node.
+
+### What core network devices support the application?
+
+- Varies from customer to customer.
+
+### What network performance requirements exist?
+
+- Maximum replication speeds between **primary** node and **secondary** node is limited by the
+ available bandwidth between sites. No hard requirements exist - time to complete
+ replication (and ability to keep up with changes on the **primary** node) is a function
+ of the size of the data set, tolerance for latency, and available network
+ capacity.
+
+### What private and public network links support the application?
+
+- Customers choose their own networks. As sites are intended to be
+ geographically separated, it is envisioned that replication traffic will pass
+ over the public Internet in a typical deployment, but this is not a requirement.
+
+## Systems
+
+### What operating systems support the application?
+
+- Geo imposes no additional restrictions on operating system (see the
+ [GitLab installation](https://about.gitlab.com/installation/) page for more
+ details), however we recommend using the operating systems listed in the [Geo documentation](index.md#requirements-for-running-geo).
+
+### What details regarding required OS components and lock‐down needs have been defined?
+
+- The recommended installation method (Omnibus) packages most components itself.
+ A from-source installation method exists. Both are documented at
+ <https://docs.gitlab.com/ee/administration/geo/replication/index.html>
+- There are significant dependencies on the system-installed OpenSSH daemon (Geo
+ requires users to set up custom authentication methods) and the omnibus or
+ system-provided PostgreSQL daemon (it must be configured to listen on TCP,
+ additional users and replication slots must be added, etc).
+- The process for dealing with security updates (for example, if there is a
+ significant vulnerability in OpenSSH or other services, and the customer
+ wants to patch those services on the OS) is identical to the non-Geo
+ situation: security updates to OpenSSH would be provided to the user via the
+ usual distribution channels. Geo introduces no delay there.
+
+## Infrastructure Monitoring
+
+### What network and system performance monitoring requirements have been defined?
+
+- None specific to Geo.
+
+### What mechanisms exist to detect malicious code or compromised application components?
+
+- None specific to Geo.
+
+### What network and system security monitoring requirements have been defined?
+
+- None specific to Geo.
+
+## Virtualization and Externalization
+
+### What aspects of the application lend themselves to virtualization?
+
+- All.
+
+## What virtualization requirements have been defined for the application?
+
+- Nothing Geo-specific, but everything in GitLab needs to have full
+ functionality in such an environment.
+
+### What aspects of the product may or may not be hosted via the cloud computing model?
+
+- GitLab is “cloud native” and this applies to Geo as much as to the rest of the
+ product. Deployment in clouds is a common and supported scenario.
+
+## If applicable, what approach(es) to cloud computing will be taken (Managed Hosting versus "Pure" Cloud, a "full machine" approach such as AWS-EC2 versus a "hosted database" approach such as AWS-RDS and Azure, etc)?
+
+- To be decided by our customers, according to their operational needs.
+
+## Environment
+
+### What frameworks and programming languages have been used to create the application?
+
+- Ruby on Rails, Ruby.
+
+### What process, code, or infrastructure dependencies have been defined for the application?
+
+- Nothing specific to Geo.
+
+### What databases and application servers support the application?
+
+- PostgreSQL >= 9.6, Redis, Sidekiq, Unicorn.
+
+### How will database connection strings, encryption keys, and other sensitive components be stored, accessed, and protected from unauthorized detection?
+
+- There are some Geo-specific values. Some are shared secrets which must be
+ securely transmitted from the **primary** node to the **secondary** node at setup time. Our
+ documentation recommends transmitting them from the **primary** node to the system
+ administrator via SSH, and then back out to the **secondary** node in the same manner.
+ In particular, this includes the PostgreSQL replication credentials and a secret
+ key (`db_key_base`) which is used to decrypt certain columns in the database.
+ The `db_key_base` secret is stored unencrypted on the filesystem, in
+ `/etc/gitlab/gitlab-secrets.json`, along with a number of other secrets. There is
+ no at-rest protection for them.
+
+## Data Processing
+
+### What data entry paths does the application support?
+
+- Data is entered via the web application exposed by GitLab itself. Some data is
+ also entered using system administration commands on the GitLab servers (e.g.,
+ `gitlab-ctl set-primary-node`).
+- **Secondary** nodes also receive inputs via PostgreSQL streaming replication from the **primary** node.
+
+### What data output paths does the application support?
+
+- **Primary** nodes output via PostgreSQL streaming replication to the **secondary** node.
+ Otherwise, principally via the web application exposed by GitLab itself, and via
+ SSH `git clone` operations initiated by the end-user.
+
+### How does data flow across the application's internal components?
+
+- **Secondary** nodes and **primary** nodes interact via HTTP/HTTPS (secured with JSON web
+ tokens) and via PostgreSQL streaming replication.
+- Within a **primary** node or **secondary** node, the SSOT is the filesystem and the database
+ (including Geo tracking database on **secondary** node). The various internal components
+ are orchestrated to make alterations to these stores.
+
+### What data input validation requirements have been defined?
+
+- **Secondary** nodes must have a faithful replication of the **primary** node’s data.
+
+### What data does the application store and how?
+
+- Git repositories and files, tracking information related to the them, and the GitLab database contents.
+
+### What data is or may need to be encrypted and what key management requirements have been defined?
+
+- Neither **primary** nodes or **secondary** nodes encrypt Git repository or filesystem data at
+ rest. A subset of database columns are encrypted at rest using the `db_otp_key`.
+- A static secret shared across all hosts in a GitLab deployment.
+- In transit, data should be encrypted, although the application does permit
+ communication to proceed unencrypted. The two main transits are the **secondary** node’s
+ replication process for PostgreSQL, and for git repositories/files. Both should
+ be protected using TLS, with the keys for that managed via Omnibus per existing
+ configuration for end-user access to GitLab.
+
+### What capabilities exist to detect the leakage of sensitive data?
+
+- Comprehensive system logs exist, tracking every connection to GitLab and PostgreSQL.
+
+### What encryption requirements have been defined for data in transit - including transmission over WAN, LAN, SecureFTP, or publicly accessible protocols such as http: and https:?
+
+- Data must have the option to be encrypted in transit, and be secure against
+ both passive and active attack (e.g., MITM attacks should not be possible).
+
+## Access
+
+### What user privilege levels does the application support?
+
+- Geo adds one type of privilege: **secondary** nodes can access a special Geo API to
+ download files over HTTP/HTTPS, and to clone repositories using HTTP/HTTPS.
+
+### What user identification and authentication requirements have been defined?
+
+- **Secondary** nodes identify to Geo **primary** nodes via OAuth or JWT authentication
+ based on the shared database (HTTP access) or a PostgreSQL replication user (for
+ database replication). The database replication also requires IP-based access
+ controls to be defined.
+
+### What user authorization requirements have been defined?
+
+- **Secondary** nodes must only be able to *read* data. They are not currently able to mutate data on the **primary** node.
+
+### What session management requirements have been defined?
+
+- Geo JWTs are defined to last for only two minutes before needing to be regenerated.
+- Geo JWTs are generated for one of the following specific scopes:
+ - Geo API access.
+ - Git access.
+ - LFS and File ID.
+ - Upload and File ID.
+ - Job Artifact and File ID.
+
+### What access requirements have been defined for URI and Service calls?
+
+- **Secondary** nodes make many calls to the **primary** node's API. This is how file
+ replication proceeds, for instance. This endpoint is only accessible with a JWT token.
+- The **primary** node also makes calls to the **secondary** node to get status information.
+
+## Application Monitoring
+
+### What application auditing requirements have been defined? How are audit and debug logs accessed, stored, and secured?
+
+- Structured JSON log is written to the filesystem, and can also be ingested
+ into a Kibana installation for further analysis.
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
new file mode 100644
index 00000000000..9c95720487d
--- /dev/null
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -0,0 +1,457 @@
+# Geo Troubleshooting **[PREMIUM ONLY]**
+
+NOTE: **Note:**
+This list is an attempt to document all the moving parts that can go wrong.
+We are working into getting all this steps verified automatically in a
+rake task in the future.
+
+Setting up Geo requires careful attention to details and sometimes it's easy to
+miss a step. Here is a list of questions you should ask to try to detect
+what you need to fix (all commands and path locations are for Omnibus installs):
+
+## First check the health of the **secondary** node
+
+Visit the **primary** node's **Admin Area > Geo** (`/admin/geo/nodes`) in
+your browser. We perform the following health checks on each **secondary** node
+to help identify if something is wrong:
+
+- Is the node running?
+- Is the node's secondary database configured for streaming replication?
+- Is the node's secondary tracking database configured?
+- Is the node's secondary tracking database connected?
+- Is the node's secondary tracking database up-to-date?
+
+![Geo health check](img/geo_node_healthcheck.png)
+
+If the UI is not working, or you are unable to log in, you can run the Geo
+health check manually to get this information as well as a few more details.
+This rake task can be run on an app node in the **primary** or **secondary**
+Geo nodes:
+
+```sh
+sudo gitlab-rake gitlab:geo:check
+```
+
+Example output:
+
+```
+Checking Geo ...
+
+GitLab Geo is available ... yes
+GitLab Geo is enabled ... yes
+GitLab Geo secondary database is correctly configured ... yes
+Using database streaming replication? ... yes
+GitLab Geo tracking database is configured to use Foreign Data Wrapper? ... yes
+GitLab Geo tracking database Foreign Data Wrapper schema is up-to-date? ... yes
+GitLab Geo HTTP(S) connectivity ...
+* Can connect to the primary node ... yes
+HTTP/HTTPS repository cloning is enabled ... yes
+Machine clock is synchronized ... yes
+Git user has default SSH configuration? ... yes
+OpenSSH configured to use AuthorizedKeysCommand ... yes
+GitLab configured to disable writing to authorized_keys file ... yes
+GitLab configured to store new projects in hashed storage? ... yes
+All projects are in hashed storage? ... yes
+
+Checking Geo ... Finished
+```
+
+Current sync information can be found manually by running this rake task on any
+**secondary** app node:
+
+```sh
+sudo gitlab-rake geo:status
+```
+
+Example output:
+
+```
+http://secondary.example.com/
+-----------------------------------------------------
+ GitLab Version: 11.8.1-ee
+ Geo Role: Secondary
+ Health Status: Healthy
+ Repositories: 190/190 (100%)
+ Verified Repositories: 190/190 (100%)
+ Wikis: 190/190 (100%)
+ Verified Wikis: 190/190 (100%)
+ LFS Objects: 35/35 (100%)
+ Attachments: 528/528 (100%)
+ CI job artifacts: 477/477 (100%)
+ Repositories Checked: 0/190 (0%)
+ Sync Settings: Full
+ Database replication lag: 0 seconds
+ Last event ID seen from primary: 2158 (about 2 minute ago)
+ Last event ID processed by cursor: 2158 (about 2 minute ago)
+ Last status report was: 4 minutes ago
+```
+
+## Is Postgres replication working?
+
+### Are my nodes pointing to the correct database instance?
+
+You should make sure your **primary** Geo node points to the instance with
+writing permissions.
+
+Any **secondary** nodes should point only to read-only instances.
+
+### Can Geo detect my 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"`.
+
+To check if the node on the current machine is correctly detected type:
+
+```sh
+sudo gitlab-rails runner "puts Gitlab::Geo.current_node.inspect"
+```
+
+and expect something like:
+
+```
+#<GeoNode id: 2, schema: "https", host: "gitlab.example.com", port: 443, relative_url_root: "", primary: false, ...>
+```
+
+By running the command above, `primary` should be `true` when executed in
+the **primary** node, and `false` on any **secondary** node.
+
+## How do I fix the message, "ERROR: replication slots can only be used if max_replication_slots > 0"?
+
+This means that the `max_replication_slots` PostgreSQL variable needs to
+be set on the **primary** database. In GitLab 9.4, we have made this setting
+default to 1. You may need to increase this value if you have more
+**secondary** nodes. Be sure to restart PostgreSQL for this to take
+effect. See the [PostgreSQL replication
+setup][database-pg-replication] guide for more details.
+
+## How do I fix the message, "FATAL: could not start WAL streaming: ERROR: replication slot "geo_secondary_my_domain_com" does not exist"?
+
+This occurs when PostgreSQL does not have a replication slot for the
+**secondary** node by that name. You may want to rerun the [replication
+process](database.md) on the **secondary** node .
+
+## How do I fix the message, "Command exceeded allowed execution time" when setting up replication?
+
+This may happen while [initiating the replication process][database-start-replication] on the **secondary** node,
+and indicates that your initial dataset is too large to be replicated in the default timeout (30 minutes).
+
+Re-run `gitlab-ctl replicate-geo-database`, but include a larger value for
+`--backup-timeout`:
+
+```sh
+sudo gitlab-ctl \
+ replicate-geo-database \
+ --host=<primary_node_hostname> \
+ --slot-name=<secondary_slot_name> \
+ --backup-timeout=21600
+```
+
+This will give the initial replication up to six hours to complete, rather than
+the default thirty minutes. Adjust as required for your installation.
+
+## How do I fix the message, "PANIC: could not write to file 'pg_xlog/xlogtemp.123': No space left on device"
+
+Determine if you have any unused replication slots in the **primary** database. This can cause large amounts of
+log data to build up in `pg_xlog`. Removing the unused slots can reduce the amount of space used in the `pg_xlog`.
+
+1. Start a PostgreSQL console session:
+
+ ```sh
+ sudo gitlab-psql gitlabhq_production
+ ```
+
+ > Note that using `gitlab-rails dbconsole` will not work, because managing replication slots requires superuser permissions.
+
+1. View your replication slots with:
+
+ ```sql
+ SELECT * FROM pg_replication_slots;
+ ```
+
+Slots where `active` is `f` are not active.
+
+- When this slot should be active, because you have a **secondary** node configured using that slot,
+ log in to that **secondary** node and check the PostgreSQL logs why the replication is not running.
+
+- If you are no longer using the slot (e.g. you no longer have Geo enabled), you can remove it with in the
+ PostgreSQL console session:
+
+ ```sql
+ SELECT pg_drop_replication_slot('<name_of_extra_slot>');
+ ```
+
+## Very large repositories never successfully synchronize on the **secondary** node
+
+GitLab places a timeout on all repository clones, including project imports
+and Geo synchronization operations. If a fresh `git clone` of a repository
+on the primary takes more than a few minutes, you may be affected by this.
+To increase the timeout, add the following line to `/etc/gitlab/gitlab.rb`
+on the **secondary** node:
+
+```ruby
+gitlab_rails['gitlab_shell_git_timeout'] = 10800
+```
+
+Then reconfigure GitLab:
+
+```sh
+sudo gitlab-ctl reconfigure
+```
+
+This will increase the timeout to three hours (10800 seconds). Choose a time
+long enough to accommodate a full clone of your largest repositories.
+
+## How to reset Geo **secondary** node replication
+
+If you get a **secondary** node in a broken state and want to reset the replication state,
+to start again from scratch, there are a few steps that can help you:
+
+1. Stop Sidekiq and the Geo LogCursor
+
+ It's possible to make Sidekiq stop gracefully, but making it stop getting new jobs and
+ wait until the current jobs to finish processing.
+
+ You need to send a **SIGTSTP** kill signal for the first phase and them a **SIGTERM**
+ when all jobs have finished. Otherwise just use the `gitlab-ctl stop` commands.
+
+ ```sh
+ gitlab-ctl status sidekiq
+ # run: sidekiq: (pid 10180) <- this is the PID you will use
+ kill -TSTP 10180 # change to the correct PID
+
+ gitlab-ctl stop sidekiq
+ gitlab-ctl stop geo-logcursor
+ ```
+
+ You can watch sidekiq logs to know when sidekiq jobs processing have finished:
+
+ ```sh
+ gitlab-ctl tail sidekiq
+ ```
+
+1. Rename repository storage folders and create new ones
+
+ ```sh
+ mv /var/opt/gitlab/git-data/repositories /var/opt/gitlab/git-data/repositories.old
+ mkdir -p /var/opt/gitlab/git-data/repositories
+ chown git:git /var/opt/gitlab/git-data/repositories
+ ```
+
+ TIP: **Tip**
+ You may want to remove the `/var/opt/gitlab/git-data/repositories.old` in the future
+ as soon as you confirmed that you don't need it anymore, to save disk space.
+
+1. _(Optional)_ Rename other data folders and create new ones
+
+ CAUTION: **Caution**:
+ You may still have files on the **secondary** node that have been removed from **primary** node but
+ removal have not been reflected. If you skip this step, they will never be removed
+ from this Geo node.
+
+ Any uploaded content like file attachments, avatars or LFS objects are stored in a
+ subfolder in one of the two paths below:
+
+ 1. /var/opt/gitlab/gitlab-rails/shared
+ 1. /var/opt/gitlab/gitlab-rails/uploads
+
+ To rename all of them:
+
+ ```sh
+ gitlab-ctl stop
+
+ mv /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared.old
+ mkdir -p /var/opt/gitlab/gitlab-rails/shared
+
+ mv /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads.old
+ mkdir -p /var/opt/gitlab/gitlab-rails/uploads
+ ```
+
+ Reconfigure in order to recreate the folders and make sure permissions and ownership
+ are correctly
+
+ ```sh
+ gitlab-ctl reconfigure
+ ```
+
+1. Reset the Tracking Database
+
+ ```sh
+ gitlab-rake geo:db:reset
+ ```
+
+1. Restart previously stopped services
+
+ ```sh
+ gitlab-ctl start
+ ```
+
+## How do I fix a "Foreign Data Wrapper (FDW) is not configured" error?
+
+When setting up Geo, you might see this warning in the `gitlab-rake
+gitlab:geo:check` output:
+
+```
+GitLab Geo tracking database Foreign Data Wrapper schema is up-to-date? ... foreign data wrapper is not configured
+```
+
+There are a few key points to remember:
+
+1. The FDW settings are configured on the Geo **tracking** database.
+1. The configured foreign server enables a login to the Geo
+**secondary**, read-only database.
+
+By default, the Geo secondary and tracking database are running on the
+same host on different ports. That is, 5432 and 5431 respectively.
+
+### Checking configuration
+
+NOTE: **Note:**
+The following steps are for Omnibus installs only. Using Geo with source-based installs [is deprecated](index.md#using-gitlab-installed-from-source-deprecated).
+
+To check the configuration:
+
+1. Enter the database console:
+
+ ```sh
+ gitlab-geo-psql
+ ```
+
+1. Check whether any tables are present. If everything is working, you
+should see something like this:
+
+ ```sql
+ gitlabhq_geo_production=# SELECT * from information_schema.foreign_tables;
+ foreign_table_catalog | foreign_table_schema | foreign_table_name | foreign_server_catalog | foreign_server_n
+ ame
+ -------------------------+----------------------+-------------------------------------------------+-------------------------+-----------------
+ ----
+ gitlabhq_geo_production | gitlab_secondary | abuse_reports | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | appearances | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | application_setting_terms | gitlabhq_geo_production | gitlab_secondary
+ gitlabhq_geo_production | gitlab_secondary | application_settings | gitlabhq_geo_production | gitlab_secondary
+ <snip>
+ ```
+
+ However, if the query returns with `0 rows`, then continue onto the next steps.
+
+1. Check that the foreign server mapping is correct via `\des+`. The
+ results should look something like this:
+
+ ```sql
+ gitlabhq_geo_production=# \des+
+ List of foreign servers
+ -[ RECORD 1 ]--------+------------------------------------------------------------
+ Name | gitlab_secondary
+ Owner | gitlab-psql
+ Foreign-data wrapper | postgres_fdw
+ Access privileges | "gitlab-psql"=U/"gitlab-psql" +
+ | gitlab_geo=U/"gitlab-psql"
+ Type |
+ Version |
+ FDW Options | (host '0.0.0.0', port '5432', dbname 'gitlabhq_production')
+ Description |
+ ```
+
+ NOTE: **Note:** Pay particular attention to the host and port under
+ FDW options. That configuration should point to the Geo secondary
+ database.
+
+ If you need to experiment with changing the host or password, the
+ following queries demonstrate how:
+
+ ```sql
+ ALTER SERVER gitlab_secondary OPTIONS (SET host '<my_new_host>');
+ ALTER SERVER gitlab_secondary OPTIONS (SET port 5432);
+ ```
+
+ If you change the host and/or port, you will also have to adjust the
+ following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
+ reconfigure`:
+
+ - `gitlab_rails['db_host']`
+ - `gitlab_rails['db_port']`
+
+1. Check that the user mapping is configured properly via `\deu+`:
+
+ ```sql
+ gitlabhq_geo_production=# \deu+
+ List of user mappings
+ Server | User name | FDW Options
+ ------------------+------------+--------------------------------------------------------------------------------
+ gitlab_secondary | gitlab_geo | ("user" 'gitlab', password 'YOUR-PASSWORD-HERE')
+ (1 row)
+ ```
+
+ Make sure the password is correct. You can test that logins work by running `psql`:
+
+ ```sh
+ # Connect to the tracking database as the `gitlab_geo` user
+ sudo \
+ -u git /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/geo-postgresql \
+ -p 5431 \
+ -U gitlab_geo \
+ -W \
+ -d gitlabhq_geo_production
+ ```
+
+ If you need to correct the password, the following query shows how:
+
+ ```sql
+ ALTER USER MAPPING FOR gitlab_geo SERVER gitlab_secondary OPTIONS (SET password '<my_new_password>');
+ ```
+
+ If you change the user or password, you will also have to adjust the
+ following settings in `/etc/gitlab/gitlab.rb` and run `gitlab-ctl
+ reconfigure`:
+
+ - `gitlab_rails['db_username']`
+ - `gitlab_rails['db_password']`
+
+ If you are using [PgBouncer in front of the secondary
+ database](database.md#pgbouncer-support-optional), be sure to update
+ the following settings:
+
+ - `geo_postgresql['fdw_external_user']`
+ - `geo_postgresql['fdw_external_password']`
+
+### Manual reload of FDW schema
+
+If you're still unable to get FDW working, you may want to try a manual
+reload of the FDW schema. To manually reload the FDW schema:
+
+1. On the node running the Geo tracking database, enter the PostgreSQL console via
+ the `gitlab_geo` user:
+
+ ```sh
+ sudo \
+ -u git /opt/gitlab/embedded/bin/psql \
+ -h /var/opt/gitlab/geo-postgresql \
+ -p 5431 \
+ -U gitlab_geo \
+ -W \
+ -d gitlabhq_geo_production
+ ```
+
+ Be sure to adjust the port and hostname for your configuration. You
+ may be asked to enter a password.
+
+1. Reload the schema via:
+
+ ```sql
+ DROP SCHEMA IF EXISTS gitlab_secondary CASCADE;
+ CREATE SCHEMA gitlab_secondary;
+ GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO gitlab_geo;
+ IMPORT FOREIGN SCHEMA public FROM SERVER gitlab_secondary INTO gitlab_secondary;
+ ```
+
+1. Test that queries work:
+
+ ```sql
+ SELECT * from information_schema.foreign_tables;
+ SELECT * FROM gitlab_secondary.projects limit 1;
+ ```
+
+[database-start-replication]: database.md#step-3-initiate-the-replication-process
+[database-pg-replication]: database.md#postgresql-replication
diff --git a/doc/administration/geo/replication/tuning.md b/doc/administration/geo/replication/tuning.md
new file mode 100644
index 00000000000..b9921b2e69f
--- /dev/null
+++ b/doc/administration/geo/replication/tuning.md
@@ -0,0 +1,17 @@
+# Tuning Geo **[PREMIUM ONLY]**
+
+## Changing the sync capacity values
+
+In the Geo admin page (`/admin/geo/nodes`), there are several variables that
+can be tuned to improve performance of Geo:
+
+- Repository sync capacity.
+- File sync capacity.
+
+Increasing these values will increase the number of jobs that are scheduled.
+However, this may not lead to more downloads in parallel unless the number of
+available Sidekiq threads is also increased. For example, if repository sync
+capacity is increased from 25 to 50, you may also want to increase the number
+of Sidekiq threads from 25 to 50. See the
+[Sidekiq concurrency documentation](https://docs.gitlab.com/ee/administration/operations/extra_sidekiq_processes.html#number-of-threads)
+for more details.
diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md
new file mode 100644
index 00000000000..66728366e24
--- /dev/null
+++ b/doc/administration/geo/replication/updating_the_geo_nodes.md
@@ -0,0 +1,403 @@
+# Updating the Geo nodes **[PREMIUM ONLY]**
+
+Depending on which version of Geo you are updating to/from, there may be
+different steps.
+
+## General update steps
+
+In order to update the Geo nodes when a new GitLab version is released,
+all you need to do is update GitLab itself:
+
+1. Log into each node (**primary** and **secondary** nodes).
+1. [Update GitLab][update].
+1. [Update tracking database on **secondary** node](#update-tracking-database-on-secondary-node) when
+ the tracking database is enabled.
+1. [Test](#check-status-after-updating) **primary** and **secondary** nodes, and check version in each.
+
+## Upgrading to GitLab 10.8
+
+Before 10.8, broadcast messages would not propagate without flushing the cache on the **secondary** nodes. This has been fixed in 10.8, but requires one last cache flush on each **secondary** node:
+
+```sh
+sudo gitlab-rake cache:clear
+```
+
+## Upgrading to GitLab 10.6
+
+In 10.4, we started to recommend that you define a password for database user (`gitlab`).
+
+We now require this change as we use this password to enable the Foreign Data Wrapper, as a way to optimize
+the Geo Tracking Database. We are also improving security by disabling the use of **trust**
+authentication method.
+
+1. **[primary]** Login to your **primary** node and run:
+
+ ```sh
+ gitlab-ctl pg-password-md5 gitlab
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
+
+ Copy the generated hash and edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
+ ```
+
+ Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
+
+ ```ruby
+ postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','1.2.3.4/32'] # <- Remove this
+ ```
+
+1. **[primary]** Reconfigure and restart:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart
+ ```
+
+1. **[secondary]** Login to all **secondary** nodes and edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ # Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+
+ # Every node that runs Unicorn or Sidekiq needs to have the database
+ # password specified as below. If you have a high-availability setup, this
+ # must be present in all application nodes.
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ # Enable Foreign Data Wrapper
+ geo_secondary['db_fdw'] = true
+
+ # Secondary address in CIDR format, for example '5.6.7.8/32'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32']
+ ```
+
+ Still in the configuration file, locate and remove the `trust_auth_cidr_address`:
+
+ ```ruby
+ postgresql['trust_auth_cidr_addresses'] = ['127.0.0.1/32','5.6.7.8/32'] # <- Remove this
+ ```
+
+1. **[secondary]** Reconfigure and restart:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart
+ ```
+
+## Upgrading to GitLab 10.5
+
+For Geo Disaster Recovery to work with minimum downtime, your **secondary** node
+should use the same set of secrets as the **primary** node. However, setup instructions
+prior to the 10.5 release only synchronized the `db_key_base` secret.
+
+To rectify this error on existing installations, you should **overwrite** the
+contents of `/etc/gitlab/gitlab-secrets.json` on each **secondary** node with the
+contents of `/etc/gitlab/gitlab-secrets.json` on the **primary** node, then run the
+following command on each **secondary** node:
+
+```sh
+sudo gitlab-ctl reconfigure
+```
+
+If you do not perform this step, you may find that two-factor authentication
+[is broken following DR](../disaster_recovery/index.html#i-followed-the-disaster-recovery-instructions-and-now-two-factor-auth-is-broken).
+
+To prevent SSH requests to the newly promoted **primary** node from failing
+due to SSH host key mismatch when updating the **primary** node domain's DNS record
+you should perform the step to [Manually replicate **primary** SSH host keys](configuration.md#step-2-manually-replicate-the-primary-nodes-ssh-host-keys) in each
+**secondary** node.
+
+## Upgrading to GitLab 10.4
+
+There are no Geo-specific steps to take!
+
+## Upgrading to GitLab 10.3
+
+### Support for SSH repository synchronization removed
+
+In GitLab 10.2, synchronizing secondaries over SSH was deprecated. In 10.3,
+support is removed entirely. All installations will switch to the HTTP/HTTPS
+cloning method instead. Before upgrading, ensure that all your Geo nodes are
+configured to use this method and that it works for your installation. In
+particular, ensure that [Git access over HTTP/HTTPS is enabled](configuration.md#step-6-enable-git-access-over-httphttps).
+
+Synchronizing repositories over the public Internet using HTTP is insecure, so
+you should ensure that you have HTTPS configured before upgrading. Note that
+file synchronization is **also** insecure in these cases!
+
+## Upgrading to GitLab 10.2
+
+### Secure PostgreSQL replication
+
+Support for TLS-secured PostgreSQL replication has been added. If you are
+currently using PostgreSQL replication across the open internet without an
+external means of securing the connection (e.g., a site-to-site VPN), then you
+should immediately reconfigure your **primary** and **secondary** PostgreSQL instances
+according to the [updated instructions][database].
+
+If you *are* securing the connections externally and wish to continue doing so,
+ensure you include the new option `--sslmode=prefer` in future invocations of
+`gitlab-ctl replicate-geo-database`.
+
+### HTTPS repository sync
+
+Support for replicating repositories and wikis over HTTP/HTTPS has been added.
+Replicating over SSH has been deprecated, and support for this option will be
+removed in a future release.
+
+To switch to HTTP/HTTPS replication, log into the **primary** node as an admin and visit
+**Admin Area > Geo** (`/admin/geo/nodes`). For each **secondary** node listed,
+press the "Edit" button, change the "Repository cloning" setting from
+"SSH (deprecated)" to "HTTP/HTTPS", and press "Save changes". This should take
+effect immediately.
+
+Any new secondaries should be created using HTTP/HTTPS replication - this is the
+default setting.
+
+After you've verified that HTTP/HTTPS replication is working, you should remove
+the now-unused SSH keys from your secondaries, as they may cause problems if the
+**secondary** node if ever promoted to a **primary** node:
+
+1. **[secondary]** Login to **all** your **secondary** nodes and run:
+
+ ```ruby
+ sudo -u git -H rm ~git/.ssh/id_rsa ~git/.ssh/id_rsa.pub
+ ```
+
+### Hashed Storage
+
+CAUTION: **Warning:**
+Hashed storage is in **Alpha**. It is considered experimental and not
+production-ready. See [Hashed Storage] for more detail.
+
+If you previously enabled Hashed Storage and migrated all your existing
+projects to Hashed Storage, disabling hashed storage will not migrate projects
+to their previous project based storage path. As such, once enabled and
+migrated we recommend leaving Hashed Storage enabled.
+
+## Upgrading to GitLab 10.1
+
+CAUTION: **Warning:**
+Hashed storage is in **Alpha**. It is considered experimental and not
+production-ready. See [Hashed Storage] for more detail.
+
+[Hashed storage] was introduced in GitLab 10.0, and a [migration path][hashed-migration]
+for existing repositories was added in GitLab 10.1.
+
+## Upgrading to GitLab 10.0
+
+Since GitLab 10.0, we require all **Geo** systems to [use SSH key lookups via
+the database][ssh-fast-lookup] to avoid having to maintain consistency of the
+`authorized_keys` file for SSH access. Failing to do this will prevent users
+from being able to clone via SSH.
+
+Note that in older versions of Geo, attachments downloaded on the **secondary**
+nodes would be saved to the wrong directory. We recommend that you do the
+following to clean this up.
+
+On the **secondary** Geo nodes, run as root:
+
+```sh
+mv /var/opt/gitlab/gitlab-rails/working /var/opt/gitlab/gitlab-rails/working.old
+mkdir /var/opt/gitlab/gitlab-rails/working
+chmod 700 /var/opt/gitlab/gitlab-rails/working
+chown git:git /var/opt/gitlab/gitlab-rails/working
+```
+
+You may delete `/var/opt/gitlab/gitlab-rails/working.old` any time.
+
+Once this is done, we advise restarting GitLab on the **secondary** nodes for the
+new working directory to be used:
+
+```sh
+sudo gitlab-ctl restart
+```
+
+## Upgrading from GitLab 9.3 or older
+
+If you started running Geo on GitLab 9.3 or older, we recommend that you
+resync your **secondary** PostgreSQL databases to use replication slots. If you
+started using Geo with GitLab 9.4 or 10.x, no further action should be
+required because replication slots are used by default. However, if you
+started with GitLab 9.3 and upgraded later, you should still follow the
+instructions below.
+
+When in doubt, it does not hurt to do a resync. The easiest way to do this in
+Omnibus is the following:
+
+ 1. Make sure you have Omnibus GitLab on the **primary** server.
+ 1. Run `gitlab-ctl reconfigure` and `gitlab-ctl restart postgresql`. This will enable replication slots on the **primary** database.
+ 1. Check the steps about defining `postgresql['sql_user_password']`, `gitlab_rails['db_password']`.
+ 1. Make sure `postgresql['max_replication_slots']` matches the number of **secondary** Geo nodes locations.
+ 1. Install GitLab on the **secondary** server.
+ 1. Re-run the [database replication process][database-replication].
+
+## Special update notes for 9.0.x
+
+> **IMPORTANT**:
+With GitLab 9.0, the PostgreSQL version is upgraded to 9.6 and manual steps are
+required in order to update the **secondary** nodes and keep the Streaming
+Replication working. Downtime is required, so plan ahead.
+
+The following steps apply only if you upgrade from a 8.17 GitLab version to
+9.0+. For previous versions, update to GitLab 8.17 first before attempting to
+upgrade to 9.0+.
+
+---
+
+Make sure to follow the steps in the exact order as they appear below and pay
+extra attention in what node (either **primary** or **secondary**) you execute them! Each step
+is prepended with the relevant node for better clarity:
+
+1. **[secondary]** Login to **all** your **secondary** nodes and stop all services:
+
+ ```ruby
+ sudo gitlab-ctl stop
+ ```
+
+1. **[secondary]** Make a backup of the `recovery.conf` file on **all**
+ **secondary** nodes to preserve PostgreSQL's credentials:
+
+ ```sh
+ sudo cp /var/opt/gitlab/postgresql/data/recovery.conf /var/opt/gitlab/
+ ```
+
+1. **[primary]** Update the **primary** node to GitLab 9.0 following the
+ [regular update docs][update]. At the end of the update, the **primary** node
+ will be running with PostgreSQL 9.6.
+
+1. **[primary]** To prevent a de-synchronization of the repository replication,
+ stop all services except `postgresql` as we will use it to re-initialize the
+ **secondary** node's database:
+
+ ```sh
+ sudo gitlab-ctl stop
+ sudo gitlab-ctl start postgresql
+ ```
+
+1. **[secondary]** Run the following steps on each of the **secondary** nodes:
+
+ 1. **[secondary]** Stop all services:
+
+ ```sh
+ sudo gitlab-ctl stop
+ ```
+
+ 1. **[secondary]** Prevent running database migrations:
+
+ ```sh
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
+
+ 1. **[secondary]** Move the old database to another directory:
+
+ ```sh
+ sudo mv /var/opt/gitlab/postgresql{,.bak}
+ ```
+
+ 1. **[secondary]** Update to GitLab 9.0 following the [regular update docs][update].
+ At the end of the update, the node will be running with PostgreSQL 9.6.
+
+ 1. **[secondary]** Make sure all services are up:
+
+ ```sh
+ sudo gitlab-ctl start
+ ```
+
+ 1. **[secondary]** Reconfigure GitLab:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+ 1. **[secondary]** Run the PostgreSQL upgrade command:
+
+ ```sh
+ sudo gitlab-ctl pg-upgrade
+ ```
+
+ 1. **[secondary]** See the stored credentials for the database that you will
+ need to re-initialize the replication:
+
+ ```sh
+ sudo grep -s primary_conninfo /var/opt/gitlab/recovery.conf
+ ```
+
+ 1. **[secondary]** Create the `replica.sh` script as described in the
+ [database configuration document][database-source-replication].
+
+ 1. **[secondary]** Run the recovery script using the credentials from the
+ previous step:
+
+ ```sh
+ sudo bash /tmp/replica.sh
+ ```
+
+ 1. **[secondary]** Reconfigure GitLab:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+ 1. **[secondary]** Start all services:
+
+ ```sh
+ sudo gitlab-ctl start
+ ```
+
+ 1. **[secondary]** Repeat the steps for the remaining **secondary** nodes.
+
+1. **[primary]** After all **secondary** nodes are updated, start all services in
+ **primary** node:
+
+ ```sh
+ sudo gitlab-ctl start
+ ```
+
+## Check status after updating
+
+Now that the update process is complete, you may want to check whether
+everything is working correctly:
+
+1. Run the Geo raketask on all nodes, everything should be green:
+
+ ```sh
+ sudo gitlab-rake gitlab:geo:check
+ ```
+
+1. Check the **primary** node's Geo dashboard for any errors.
+1. Test the data replication by pushing code to the **primary** node and see if it
+ is received by **secondary** nodes.
+
+## Update tracking database on **secondary** node
+
+After updating a **secondary** node, you might need to run migrations on
+the tracking database. The tracking database was added in GitLab 9.1,
+and it is required since 10.0.
+
+1. Run database migrations on tracking database:
+
+ ```sh
+ sudo gitlab-rake geo:db:migrate
+ ```
+
+1. Repeat this step for each **secondary** node.
+
+[update]: ../../../update/README.md
+[database]: database.md
+[database-replication]: database.md#step-3-initiate-the-replication-process
+[database-source-replication]: database_source.md#step-3-initiate-the-replication-process
+[Hashed Storage]: ../../repository_storage_types.md
+[hashed-migration]: ../../raketasks/storage.md
+[ssh-fast-lookup]: ../../operations/fast_ssh_key_lookup.md
diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md
new file mode 100644
index 00000000000..f1f1fe48748
--- /dev/null
+++ b/doc/administration/geo/replication/using_a_geo_server.md
@@ -0,0 +1,18 @@
+[//]: # (Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved)
+
+# Using a Geo Server **[PREMIUM ONLY]**
+
+After you set up the [database replication and configure the Geo nodes][req], use your closest GitLab node as you would a normal standalone GitLab instance.
+
+Pushing directly to a **secondary** node (for both HTTP, SSH including git-lfs) was [introduced](https://about.gitlab.com/2018/09/22/gitlab-11-3-released/) in [GitLab Premium](https://about.gitlab.com/pricing/#self-managed) 11.3.
+
+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
+Everything up-to-date
+```
+
+[req]: index.md#setup-instructions
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 49fe80fb2a6..de61c01991b 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -1,4 +1,4 @@
-# High Availability
+# Scaling and High Availability
GitLab supports several different types of clustering and high-availability.
The solution you choose will be based on the level of scalability and
@@ -13,51 +13,173 @@ of Git, developers can still commit code locally even when GitLab is not
available. However, some GitLab features such as the issue tracker and
Continuous Integration are not available when GitLab is down.
-**Keep in mind that all Highly Available solutions come with a trade-off between
+**Keep in mind that all highly-available solutions come with a trade-off between
cost/complexity and uptime**. The more uptime you want, the more complex the
solution. And the more complex the solution, the more work is involved in
setting up and maintaining it. High availability is not free and every HA
solution should balance the costs against the benefits.
-## Architecture
-
-There are two kinds of setups:
-
-- active/active
-- active/passive
-
-### Active/Active
-
-This architecture scales easily because all application servers handle
-user requests simultaneously. The database, Redis, and GitLab application are
-all deployed on separate servers. The configuration is **only** highly-available
-if the database, Redis and storage are also configured as such.
-
-Follow the steps below to configure an active/active setup:
+There are many options when choosing a highly-available GitLab architecture. We
+recommend engaging with GitLab Support to choose the best architecture for your
+use-case. This page contains some various options and guidelines based on
+experience with GitLab.com and Enterprise Edition on-premises customers.
+
+For a detailed insight into how GitLab scales and configures GitLab.com, you can
+watch [this 1 hour Q&A](https://www.youtube.com/watch?v=uCU8jdYzpac)
+with [John Northrup](https://gitlab.com/northrup), one of our infrastructure
+engineers, and live questions coming in from some of our customers.
+
+## GitLab Components
+
+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)
+- 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
+
+## Scalable Architecture Examples
+
+When an organization reaches a certain threshold it will be necessary to scale
+the GitLab instance. Still, true high availability may not be necessary. There
+are options for scaling GitLab instances relatively easily without incurring the
+infrastructure and maintenance costs of full high availability.
+
+### Basic Scaling
+
+This is the simplest form of scaling and will work for the majority of
+cases. Backend components such as PostgreSQL, Redis and storage are offloaded
+to their own nodes while the remaining GitLab components all run on 2 or more
+application nodes.
+
+This form of scaling also works well in a cloud environment when it is more
+cost-effective to deploy several small nodes rather than a single
+larger one.
+
+- 1 PostgreSQL node
+- 1 Redis node
+- 2 or more GitLab application nodes (Unicorn, Workhorse, Sidekiq)
+- 1 NFS/Gitaly storage server
+
+#### Installation Instructions
+
+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. [Redis](./redis.md#redis-in-a-scaled-environment)
+1. [Gitaly](./gitaly.md) (recommended) or [NFS](./nfs.md)
+1. [GitLab application nodes](./gitlab.md)
+
+### Full Scaling
+
+For very large installations it may be necessary to further split components
+for maximum scalability. In a fully-scaled architecture the application node
+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 not enough resources.
+
+- 1 PostgreSQL node
+- 1 Redis node
+- 2 or more GitLab application nodes (Unicorn, Workhorse)
+- 2 or more Sidekiq nodes
+- 2 or more NFS/Gitaly storage servers
+
+## High Availability Architecture Examples
+
+When organizations require scaling *and* high availability the following
+architectures can be utilized. As the introduction section at the top of this
+page mentions, there is a tradeoff between cost/complexity and uptime. Be sure
+this complexity is absolutely required before taking the step into full
+high availability.
+
+For all examples below, we recommend running Consul and Redis Sentinel on
+dedicated nodes. If Consul is running on PostgreSQL nodes or Sentinel on
+Redis nodes there is a potential that high resource usage by PostgreSQL or
+Redis could prevent communication between the other Consul and Sentinel nodes.
+This may lead to the other nodes believing a failure has occurred and automated
+failover is necessary. Isolating them from the services they monitor reduces
+the chances of split-brain.
+
+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).
+
+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.
+
+### Horizontal
+
+This is the simplest form of high availability and scaling. It requires the
+fewest number of individual servers (virtual or physical) but does have some
+trade-offs and limits.
+
+This architecture will work well for many GitLab customers. Larger customers
+may begin to notice certain events cause contention/high load - for example,
+cloning many large repositories with binary files, high API usage, a large
+number of enqueued Sidekiq jobs, etc. If this happens you should consider
+moving to a hybrid or fully distributed architecture depending on what is causing
+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
+
+![Horizontal architecture diagram](https://docs.gitlab.com/ee/administration/img/high_availability/horizontal.png)
+
+### Hybrid
+
+In this architecture, certain components are split on dedicated nodes so high
+resource usage of one component does not interfere with others. In larger
+environments this is a good architecture to consider if you foresee or do have
+contention due to certain workloads.
+
+- 3 PostgreSQL nodes
+- 2 Redis nodes
+- 3 Consul/Sentinel nodes
+- 2 or more Sidekiq nodes
+- 2 or more Web nodes (Unicorn, Workhorse, PGBouncer)
+- 1 or more NFS/Gitaly servers
+
+![Hybrid architecture diagram](https://docs.gitlab.com/ee/administration/img/high_availability/hybrid.png)
+
+### Fully Distributed
+
+This architecture scales to hundreds of thousands of users and projects and is
+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
+- 4 or more Redis nodes (2 separate clusters for persistent and cache data)
+- 3 Consul nodes
+- 3 Sentinel nodes
+- Multiple dedicated Sidekiq nodes (Split into real-time, best effort, ASAP,
+ CI Pipeline and Pull Mirror sets)
+- 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
+
+![Fully Distributed architecture diagram](https://docs.gitlab.com/ee/administration/img/high_availability/fully-distributed.png)
+
+The following pages outline the steps necessary to configure each component
+separately:
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)
-![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
-
-### Active/Passive
-
-For pure high-availability/failover with no scaling you can use an
-active/passive configuration. This utilizes DRBD (Distributed Replicated
-Block Device) to keep all data in sync. DRBD requires a low latency link to
-remain in sync. It is not advisable to attempt to run DRBD between data centers
-or in different cloud availability zones.
-
-> **Note:** GitLab recommends against choosing this HA method because of the
- complexity of managing DRBD and crafting automatic failover. This is
- *compatible* with GitLab, but not officially *supported*. If you are
- an EE customer, support will help you with GitLab related problems, but if the
- root cause is identified as DRBD, we will not troubleshoot further.
-
-Components/Servers Required: 2 servers/virtual machines (one active/one passive)
-
-![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
diff --git a/doc/administration/high_availability/alpha_database.md b/doc/administration/high_availability/alpha_database.md
new file mode 100644
index 00000000000..7bf20be60e6
--- /dev/null
+++ b/doc/administration/high_availability/alpha_database.md
@@ -0,0 +1,6 @@
+---
+redirect_to: 'database.md'
+---
+
+This documentation has been moved to the main
+[database documentation](database.md#configure_using_omnibus_for_high_availability).
diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md
new file mode 100644
index 00000000000..056b7fc15d9
--- /dev/null
+++ b/doc/administration/high_availability/consul.md
@@ -0,0 +1,105 @@
+# Working with the bundled Consul service **[PREMIUM ONLY]**
+
+## Overview
+
+As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](http://consul.io) that can be managed through `/etc/gitlab/gitlab.rb`.
+
+A Consul cluster consists of multiple server agents, as well as client agents that run on other nodes which need to talk to the consul cluster.
+
+## Operations
+
+### Checking cluster membership
+
+To see which nodes are part of the cluster, run the following on any member in the cluster
+```
+# /opt/gitlab/embedded/bin/consul members
+Node Address Status Type Build Protocol DC
+consul-b XX.XX.X.Y:8301 alive server 0.9.0 2 gitlab_consul
+consul-c XX.XX.X.Y:8301 alive server 0.9.0 2 gitlab_consul
+consul-c XX.XX.X.Y:8301 alive server 0.9.0 2 gitlab_consul
+db-a XX.XX.X.Y:8301 alive client 0.9.0 2 gitlab_consul
+db-b XX.XX.X.Y:8301 alive client 0.9.0 2 gitlab_consul
+```
+
+Ideally all nodes will have a `Status` of `alive`.
+
+### Restarting the server cluster
+
+**Note**: This section only applies to server agents. It is safe to restart client agents whenever needed.
+
+If it is necessary to restart the server cluster, it is important to do this in a controlled fashion in order to maintain quorum. If quorum is lost, you will need to follow the consul [outage recovery](#outage-recovery) process to recover the cluster.
+
+To be safe, we recommend you only restart one server agent at a time to ensure the cluster remains intact.
+
+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.
+
+## Troubleshooting
+
+### Consul server agents unable to communicate
+
+By default, the server agents will attempt to [bind](https://www.consul.io/docs/agent/options.html#_bind) to '0.0.0.0', but they will advertise the first private IP address on the node for other agents to communicate with them. If the other nodes cannot communicate with a node on this address, then the cluster will have a failed status.
+
+You will see messages like the following in `gitlab-ctl tail consul` output if you are running into this issue:
+
+```
+2017-09-25_19:53:39.90821 2017/09/25 19:53:39 [WARN] raft: no known peers, aborting election
+2017-09-25_19:53:41.74356 2017/09/25 19:53:41 [ERR] agent: failed to sync remote state: No cluster leader
+```
+
+
+To fix this:
+
+1. Pick an address on each node that all of the other nodes can reach this node through.
+1. Update your `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ consul['configuration'] = {
+ ...
+ bind_addr: 'IP ADDRESS'
+ }
+ ```
+1. Run `gitlab-ctl reconfigure`
+
+If you still see the errors, you may have to [erase the consul database and reinitialize](#recreate-from-scratch) on the affected node.
+
+### Consul agents do not start - Multiple private IPs
+
+In the case that a node has multiple private IPs the agent be confused as to which of the private addresses to advertise, and then immediately exit on start.
+
+You will see messages like the following in `gitlab-ctl tail consul` output if you are running into this issue:
+
+```
+2017-11-09_17:41:45.52876 ==> Starting Consul agent...
+2017-11-09_17:41:45.53057 ==> Error creating agent: Failed to get advertise address: Multiple private IPs found. Please configure one.
+```
+
+To fix this:
+
+1. Pick an address on the node that all of the other nodes can reach this node through.
+1. Update your `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ consul['configuration'] = {
+ ...
+ bind_addr: 'IP ADDRESS'
+ }
+ ```
+1. Run `gitlab-ctl reconfigure`
+
+### Outage recovery
+
+If you lost enough server agents in the cluster to break quorum, then the cluster is considered failed, and it will not function without manual intervenetion.
+
+#### Recreate from scratch
+By default, GitLab does not store anything in the consul cluster that cannot be recreated. To erase the consul database and reinitialize
+
+```
+# gitlab-ctl stop consul
+# rm -rf /var/opt/gitlab/consul/data
+# gitlab-ctl start consul
+```
+
+After this, the cluster should start back up, and the server agents rejoin. Shortly after that, the client agents should rejoin as well.
+
+#### Recover a failed cluster
+If you have taken advantage of consul to store other data, and want to restore the failed cluster, please follow the [Consul guide](https://www.consul.io/docs/guides/outage.html) to recover a failed cluster.
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index c1eeb40b98f..1648b6b848a 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -1,11 +1,6 @@
-# Configuring a Database for GitLab HA
+# Configuring PostgreSQL for Scaling and High Availability
-You can choose to install and manage a database server (PostgreSQL/MySQL)
-yourself, or you can use GitLab Omnibus packages to help. GitLab recommends
-PostgreSQL. This is the database that will be installed if you use the
-Omnibus package to manage your database.
-
-## Configure your own database server
+## Provide your own PostgreSQL instance **[CORE ONLY]**
If you're hosting GitLab on a cloud provider, you can optionally use a
managed service for PostgreSQL. For example, AWS offers a managed Relational
@@ -20,91 +15,1147 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Configure the GitLab application servers with the appropriate details.
This step is covered in [Configuring GitLab for HA](gitlab.md).
-## Configure using Omnibus
+## PostgreSQL in a Scaled Environment
-1. Download/install GitLab Omnibus using **steps 1 and 2** from
- [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other
- steps on the download page.
-1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
- Be sure to change the `external_url` to match your eventual GitLab front-end
- URL. If there is a directive listed below that you do not see in the configuration, be sure to add it.
+This section is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
+environments including [Basic Scaling](./README.md#basic-scaling) and
+[Full Scaling](./README.md#full-scaling).
- ```ruby
- external_url 'https://gitlab.example.com'
+### Provide your own PostgreSQL instance **[CORE ONLY]**
+
+If you want to use your own deployed PostgreSQL instance(s),
+see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
+for more details. However, you can use the GitLab Omnibus package to easily
+deploy the bundled PostgreSQL.
+
+### Standalone PostgreSQL using GitLab Omnibus **[CORE ONLY]**
+
+1. SSH into the PostgreSQL server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Do not complete any other steps on the download page.
+1. Generate a password hash for PostgreSQL. This assumes you will use the default
+ username of `gitlab` (recommended). The command will request a password
+ and confirmation. Use the value that is output by this command in the next
+ step as the value of `POSTGRESQL_PASSWORD_HASH`.
+ ```sh
+ sudo gitlab-ctl pg-password-md5 gitlab
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents below, updating placeholder
+ values appropriately.
+
+ - `POSTGRESQL_PASSWORD_HASH` - The value output from the previous step
+ - `APPLICATION_SERVER_IP_BLOCKS` - A space delimited list of IP subnets or IP
+ addresses of the GitLab application servers that will connect to the
+ database. Example: `%w(123.123.123.123/32 123.123.123.234/32)`
+
+ ```ruby
# Disable all components except PostgreSQL
roles ['postgres_role']
+ repmgr['enable'] = false
+ consul['enable'] = false
+ prometheus['enable'] = false
+ alertmanager['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ redis_exporter['enable'] = false
+ gitlab_monitor['enable'] = false
+
+ postgresql['listen_address'] = '0.0.0.0'
+ postgresql['port'] = 5432
+
+ # Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
+ postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
+
+ # Replace XXX.XXX.XXX.XXX/YY with Network Address
+ # ????
+ postgresql['trust_auth_cidr_addresses'] = %w(APPLICATION_SERVER_IP_BLOCKS)
+
+ # Disable automatic database migrations
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+ NOTE: **Note:** The role `postgres_role` was introduced with GitLab 10.3
+
+1. [Reconfigure GitLab] for the changes to take effect.
+1. Note the PostgreSQL node's IP address or hostname, port, and
+ plain text password. These will be necessary when configuring the GitLab
+ application servers later.
+
+Advanced configuration options are supported and can be added if
+needed.
+
+Continue configuration of other components by going
+[back to Scaled Architectures](./README.md#scalable-architecture-examples)
+
+## PostgreSQL with High Availability
+
+This section is relevant for [High Availability Architecture](./README.md#high-availability-architecture-examples)
+environments including [Horizontal](./README.md#horizontal),
+[Hybrid](./README.md#hybrid), and
+[Fully Distributed](./README.md#fully-distributed).
+
+### Provide your own PostgreSQL instance **[CORE ONLY]**
+
+If you want to use your own deployed PostgreSQL instance(s),
+see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
+for more details. However, you can use the GitLab Omnibus package to easily
+deploy the bundled PostgreSQL.
+
+### High Availability with GitLab Omnibus **[PREMIUM ONLY]**
+
+> Important notes:
+> - This document will focus only on configuration supported with [GitLab Premium](https://about.gitlab.com/pricing/), using the Omnibus GitLab package.
+> - If you are a Community Edition or Starter user, consider using a cloud hosted solution.
+> - This document will not cover installations from source.
+>
+> - If HA setup is not what you were looking for, see the [database configuration document](http://docs.gitlab.com/omnibus/settings/database.html)
+> for the Omnibus GitLab packages.
+
+> Please read this document fully before attempting to configure PostgreSQL HA
+> for GitLab.
+>
+> This configuration is GA in EE 10.2.
+
+The recommended configuration for a PostgreSQL HA requires:
+
+- A minimum of three database nodes
+ - Each node will run the following services:
+ - `PostgreSQL` - The database itself
+ - `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
+
+You also need to take into consideration the underlying network topology,
+making sure you have redundant connectivity between all Database and GitLab instances,
+otherwise the networks will become a single point of failure.
+
+#### Architecture
+
+![PG HA Architecture](pg_ha_architecture.png)
+
+Database nodes run two services with PostgreSQL:
+
+- Repmgrd. Monitors the cluster and handles failover when issues with the master occur. The failover consists of:
+ - Selecting a new master for the cluster.
+ - Promoting the new node to master.
+ - Instructing remaining servers to follow the new master node.
+
+ 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
+
+##### 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)
+- 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)
+- Consul servers and agents connect to each others [Consul default ports](https://docs.gitlab.com/omnibus/package-information/defaults.html#consul)
+
+#### Required information
+
+Before proceeding with configuration, you will need to collect all the necessary
+information.
+
+##### Network information
+
+PostgreSQL does not listen on any network interface by default. It needs to know
+which IP address to listen on in order to be accessible to other services.
+Similarly, PostgreSQL access is controlled based on the network source.
+
+This is why you will need:
+
+> IP address of each nodes network interface
+> - This can be set to `0.0.0.0` to listen on all interfaces. It cannot
+> be set to the loopack address `127.0.0.1`
+>
+> Network Address
+> - This can be in subnet (i.e. `192.168.0.0/255.255.255.0`) or CIDR (i.e.
+> `192.168.0.0/24`) form.
+
+##### User information
+
+Various services require different configuration to secure
+the communication as well as information required for running the service.
+Bellow you will find details on each service and the minimum required
+information you need to provide.
+
+##### Consul information
+
+When using default setup, minimum configuration requires:
+
+- `CONSUL_USERNAME`. Defaults to `gitlab-consul`
+- `CONSUL_DATABASE_PASSWORD`. Password for the database user.
+- `CONSUL_PASSWORD_HASH`. This is a hash generated out of consul username/password pair.
+ Can be generated with:
+
+ ```sh
+ sudo gitlab-ctl pg-password-md5 CONSUL_USERNAME
+ ```
+
+- `CONSUL_SERVER_NODES`. The IP addresses or DNS records of the Consul server nodes.
+
+Few notes on the service itself:
+
+- The service runs under a system account, by default `gitlab-consul`.
+ - If you are using a different username, you will have to specify it. We
+will refer to it with `CONSUL_USERNAME`,
+- There will be a database user created with read only access to the repmgr
+database
+- Passwords will be stored in the following locations:
+ - `/etc/gitlab/gitlab.rb`: hashed
+ - `/var/opt/gitlab/pgbouncer/pg_auth`: hashed
+ - `/var/opt/gitlab/gitlab-consul/.pgpass`: plaintext
+
+##### PostgreSQL information
+
+When configuring PostgreSQL, we will set `max_wal_senders` to one more than
+the number of database nodes in the cluster.
+This is used to prevent replication from using up all of the
+available database connections.
+
+> Note:
+> - In this document we are assuming 3 database nodes, which makes this configuration:
+
+```
+postgresql['max_wal_senders'] = 4
+```
+
+As previously mentioned, you'll have to prepare the network subnets that will
+be allowed to authenticate with the database.
+You'll also need to supply the IP addresses or DNS records of Consul
+server nodes.
+
+We will need the following password information for the application's database user:
+
+- `POSTGRESQL_USERNAME`. Defaults to `gitlab`
+- `POSTGRESQL_USER_PASSWORD`. The password for the database user
+- `POSTGRESQL_PASSWORD_HASH`. This is a hash generated out of the username/password pair.
+ Can be generated with:
+
+ ```sh
+ sudo gitlab-ctl pg-password-md5 POSTGRESQL_USERNAME
+ ```
+
+##### Pgbouncer information
+
+When using default setup, minimum configuration requires:
+
+- `PGBOUNCER_USERNAME`. Defaults to `pgbouncer`
+- `PGBOUNCER_PASSWORD`. This is a password for pgbouncer service.
+- `PGBOUNCER_PASSWORD_HASH`. This is a hash generated out of pgbouncer username/password pair.
+ Can be generated with:
+
+ ```sh
+ sudo gitlab-ctl pg-password-md5 PGBOUNCER_USERNAME
+ ```
+
+- `PGBOUNCER_NODE`, is the IP address or a FQDN of the node running Pgbouncer.
+
+Few notes on the service itself:
+
+- The service runs as the same system account as the database
+ - In the package, this is by default `gitlab-psql`
+- If you use a non-default user account for Pgbouncer service (by default `pgbouncer`), you will have to specify this username. We will refer to this requirement with `PGBOUNCER_USERNAME`.
+- The service will have a regular database user account generated for it
+ - This defaults to `repmgr`
+- Passwords will be stored in the following locations:
+ - `/etc/gitlab/gitlab.rb`: hashed, and in plain text
+ - `/var/opt/gitlab/pgbouncer/pg_auth`: hashed
+
+##### Repmgr information
+
+When using default setup, you will only have to prepare the network subnets that will
+be allowed to authenticate with the service.
+
+Few notes on the service itself:
+
+- The service runs under the same system account as the database
+ - In the package, this is by default `gitlab-psql`
+- The service will have a superuser database user account generated for it
+ - This defaults to `gitlab_repmgr`
+
+#### Installing Omnibus GitLab
+
+First, make sure to [download/install](https://about.gitlab.com/installation)
+GitLab Omnibus **on each node**.
+
+Make sure you install the necessary dependencies from step 1,
+add GitLab package repository from step 2.
+When installing the GitLab package, do not supply `EXTERNAL_URL` value.
+
+#### Configuring the Consul nodes
+
+On each Consul node perform the following:
+
+1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul-information) before executing the next step.
+
+1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+
+ ```ruby
+ # Disable all components except Consul
+ roles ['consul_role']
+
+ # START user configuration
+ # Replace placeholders:
+ #
+ # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
+ # with the addresses gathered for CONSUL_SERVER_NODES
+ consul['configuration'] = {
+ server: true,
+ retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
+ }
+
+ # Disable auto migrations
+ gitlab_rails['auto_migrate'] = false
+ #
+ # END user configuration
+ ```
+
+ > `consul_role` was introduced with GitLab 10.3
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+##### Consul Checkpoint
+
+Before moving on, make sure Consul is configured correctly. Run the following
+command to verify all server nodes are communicating:
+
+```
+/opt/gitlab/embedded/bin/consul members
+```
+
+The output should be similar to:
+
+```
+Node Address Status Type Build Protocol DC
+CONSUL_NODE_ONE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
+CONSUL_NODE_TWO XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
+CONSUL_NODE_THREE XXX.XXX.XXX.YYY:8301 alive server 0.9.2 2 gitlab_consul
+```
+
+If any of the nodes isn't `alive` or if any of the three nodes are missing,
+check the [Troubleshooting section](#troubleshooting) before proceeding.
+
+#### Configuring the Database nodes
+
+1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul-information), [`PGBOUNCER_PASSWORD_HASH`](#pgbouncer-information), [`POSTGRESQL_PASSWORD_HASH`](#postgresql-information), the [number of db nodes](#postgresql-information), and the [network address](#network-information) before executing the next step.
+
+1. On the master database node, edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+
+ ```ruby
+ # Disable all components except PostgreSQL and Repmgr and Consul
+ roles ['postgres_role']
# PostgreSQL configuration
- gitlab_rails['db_password'] = 'DB password'
- postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
postgresql['listen_address'] = '0.0.0.0'
+ postgresql['hot_standby'] = 'on'
+ postgresql['wal_level'] = 'replica'
+ postgresql['shared_preload_libraries'] = 'repmgr_funcs'
# Disable automatic database migrations
gitlab_rails['auto_migrate'] = false
+
+ # Configure the consul agent
+ consul['services'] = %w(postgresql)
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ #
+ # Replace PGBOUNCER_PASSWORD_HASH with a generated md5 value
+ postgresql['pgbouncer_user_password'] = 'PGBOUNCER_PASSWORD_HASH'
+ # Replace POSTGRESQL_PASSWORD_HASH with a generated md5 value
+ postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
+ # Replace X with value of number of db nodes + 1
+ postgresql['max_wal_senders'] = X
+
+ # Replace XXX.XXX.XXX.XXX/YY with Network Address
+ postgresql['trust_auth_cidr_addresses'] = %w(XXX.XXX.XXX.XXX/YY)
+ repmgr['trust_auth_cidr_addresses'] = %w(127.0.0.1/32 XXX.XXX.XXX.XXX/YY)
+
+ # Replace placeholders:
+ #
+ # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
+ # with the addresses gathered for CONSUL_SERVER_NODES
+ consul['configuration'] = {
+ retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
+ }
+ #
+ # END user configuration
```
-1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
+ > `postgres_role` was introduced with GitLab 10.3
+
+1. On secondary nodes, add all the configuration specified above for primary node
+ to `/etc/gitlab/gitlab.rb`. In addition, append the following configuration
+ to inform gitlab-ctl that they are standby nodes initially and it need not
+ attempt to register them as primary node
+ ```
+ # HA setting to specify if a node should attempt to be master on initialization
+ repmgr['master_on_initialization'] = false
+ ```
- > **Note**: This `reconfigure` step will result in some errors.
- That's OK - don't be alarmed.
+1. [Reconfigure GitLab] for te changes to take effect.
+
+> Please note:
+> - If you want your database to listen on a specific interface, change the config:
+> `postgresql['listen_address'] = '0.0.0.0'`
+> - If your Pgbouncer service runs under a different user account,
+> you also need to specify: `postgresql['pgbouncer_user'] = PGBOUNCER_USERNAME` in
+> your configuration
+
+##### Database nodes post-configuration
+
+###### Primary node
+
+Select one node as a primary node.
1. Open a database prompt:
+ ```sh
+ gitlab-psql -d gitlabhq_production
```
- su - gitlab-psql
- /bin/bash
- psql -h /var/opt/gitlab/postgresql -d template1
- # Output:
+1. Enable the `pg_trgm` extension:
- psql (9.2.15)
- Type "help" for help.
+ ```sh
+ CREATE EXTENSION pg_trgm;
+ ```
+
+1. Exit the database prompt by typing `\q` and Enter.
+
+1. Verify the cluster is initialized with one node:
- template1=#
+ ```sh
+ gitlab-ctl repmgr cluster show
```
-1. Run the following command at the database prompt and you will be asked to
- enter the new password for the PostgreSQL superuser.
+ The output should be similar to the following:
```
- \password
+ Role | Name | Upstream | Connection String
+ ----------+----------|----------|----------------------------------------
+ * master | HOSTNAME | | host=HOSTNAME user=gitlab_repmgr dbname=gitlab_repmgr
+ ```
- # Output:
+1. Note down the hostname/ip in the connection string: `host=HOSTNAME`. We will
+ refer to the hostname in the next section as `MASTER_NODE_NAME`. If the value
+ is not an IP address, it will need to be a resolvable name (via DNS or
+ `/etc/hosts`)
- Enter new password:
- Enter it again:
+
+###### Secondary nodes
+
+1. Set up the repmgr standby:
+
+ ```sh
+ gitlab-ctl repmgr standby setup MASTER_NODE_NAME
```
-1. Similarly, set the password for the `gitlab` database user. Use the same
- password that you specified in the `/etc/gitlab/gitlab.rb` file for
- `gitlab_rails['db_password']`.
+ Do note that this will remove the existing data on the node. The command
+ has a wait time.
+ The output should be similar to the following:
+
+ ```console
+ # gitlab-ctl repmgr standby setup MASTER_NODE_NAME
+ Doing this will delete the entire contents of /var/opt/gitlab/postgresql/data
+ If this is not what you want, hit Ctrl-C now to exit
+ To skip waiting, rerun with the -w option
+ Sleeping for 30 seconds
+ Stopping the database
+ Removing the data
+ Cloning the data
+ Starting the database
+ Registering the node with the cluster
+ ok: run: repmgrd: (pid 19068) 0s
```
- \password gitlab
- # Output:
+1. Verify the node now appears in the cluster:
- Enter new password:
- Enter it again:
+ ```sh
+ gitlab-ctl repmgr cluster show
```
-1. Exit from editing `template1` prompt by typing `\q` and Enter.
-1. Enable the `pg_trgm` extension within the `gitlabhq_production` database:
-
+
+ The output should be similar to the following:
+
+ ```
+ Role | Name | Upstream | Connection String
+ ----------+---------|-----------|------------------------------------------------
+ * master | MASTER | | host=MASTER_NODE_NAME user=gitlab_repmgr dbname=gitlab_repmgr
+ standby | STANDBY | MASTER | host=STANDBY_HOSTNAME user=gitlab_repmgr dbname=gitlab_repmgr
+ ```
+
+Repeat the above steps on all secondary nodes.
+
+##### Database checkpoint
+
+Before moving on, make sure the databases are configured correctly. Run the
+following command on the **primary** node to verify that replication is working
+properly:
+
+```
+gitlab-ctl repmgr cluster show
+```
+
+The output should be similar to:
+
+```
+Role | Name | Upstream | Connection String
+----------+--------------|--------------|--------------------------------------------------------------------
+* master | MASTER | | host=MASTER port=5432 user=gitlab_repmgr dbname=gitlab_repmgr
+ standby | STANDBY | MASTER | host=STANDBY port=5432 user=gitlab_repmgr dbname=gitlab_repmgr
+```
+
+If the 'Role' column for any node says "FAILED", check the
+[Troubleshooting section](#troubleshooting) before proceeding.
+
+Also, check that the check master command works successfully on each node:
+
+```
+su - gitlab-consul
+gitlab-ctl repmgr-check-master || echo 'This node is a standby repmgr node'
+```
+
+This command relies on exit codes to tell Consul whether a particular node is a master
+or secondary. The most important thing here is that this command does not produce errors.
+If there are errors it's most likely due to incorrect `gitlab-consul` database user permissions.
+Check the [Troubleshooting section](#troubleshooting) before proceeding.
+
+#### Configuring the Pgbouncer node
+
+1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul-information), [`CONSUL_PASSWORD_HASH`](#consul-information), and [`PGBOUNCER_PASSWORD_HASH`](#pgbouncer-information) before executing the next step.
+
+1. Edit `/etc/gitlab/gitlab.rb` replacing values noted in the `# START user configuration` section:
+
+ ```ruby
+ # Disable all components except Pgbouncer and Consul agent
+ roles ['pgbouncer_role']
+
+ # Configure Pgbouncer
+ pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
+
+ # Configure Consul agent
+ consul['watchers'] = %w(postgresql)
+
+ # START user configuration
+ # Please set the real values as explained in Required Information section
+ # Replace CONSUL_PASSWORD_HASH with with a generated md5 value
+ # Replace PGBOUNCER_PASSWORD_HASH with with a generated md5 value
+ pgbouncer['users'] = {
+ 'gitlab-consul': {
+ password: 'CONSUL_PASSWORD_HASH'
+ },
+ 'pgbouncer': {
+ password: 'PGBOUNCER_PASSWORD_HASH'
+ }
+ }
+ # Replace placeholders:
+ #
+ # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
+ # with the addresses gathered for CONSUL_SERVER_NODES
+ consul['configuration'] = {
+ retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z)
+ }
+ #
+ # END user configuration
```
+
+ > `pgbouncer_role` was introduced with GitLab 10.3
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+1. Create a `.pgpass` file so Consule is able to
+ reload pgbouncer. Enter the `PGBOUNCER_PASSWORD` twice when asked:
+
+ ```sh
+ gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul
+ ```
+
+##### PGBouncer Checkpoint
+
+1. Ensure the node is talking to the current master:
+
+ ```sh
+ gitlab-ctl pgb-console # You will be prompted for PGBOUNCER_PASSWORD
+ ```
+
+ If there is an error `psql: ERROR: Auth failed` after typing in the
+ password, ensure you previously generated the MD5 password hashes with the correct
+ format. The correct format is to concatenate the password and the username:
+ `PASSWORDUSERNAME`. For example, `Sup3rS3cr3tpgbouncer` would be the text
+ needed to generate an MD5 password hash for the `pgbouncer` user.
+
+1. Once the console prompt is available, run the following queries:
+
+ ```sh
+ show databases ; show clients ;
+ ```
+
+ The output should be similar to the following:
+
+ ```
+ name | host | port | database | force_user | pool_size | reserve_pool | pool_mode | max_connections | current_connections
+ ---------------------+-------------+------+---------------------+------------+-----------+--------------+-----------+-----------------+---------------------
+ gitlabhq_production | MASTER_HOST | 5432 | gitlabhq_production | | 20 | 0 | | 0 | 0
+ pgbouncer | | 6432 | pgbouncer | pgbouncer | 2 | 0 | statement | 0 | 0
+ (2 rows)
+
+ type | user | database | state | addr | port | local_addr | local_port | connect_time | request_time | ptr | link | remote_pid | tls
+ ------+-----------+---------------------+---------+----------------+-------+------------+------------+---------------------+---------------------+-----------+------+------------+-----
+ C | pgbouncer | pgbouncer | active | 127.0.0.1 | 56846 | 127.0.0.1 | 6432 | 2017-08-21 18:09:59 | 2017-08-21 18:10:48 | 0x22b3880 | | 0 |
+ (2 rows)
+ ```
+
+#### Configuring the Application nodes
+
+These will be the nodes running the `gitlab-rails` service. You may have other
+attributes set, but the following need to be set.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ # Disable PostgreSQL on the application node
+ postgresql['enable'] = false
+
+ gitlab_rails['db_host'] = 'PGBOUNCER_NODE'
+ gitlab_rails['db_port'] = 6432
+ gitlab_rails['db_password'] = 'POSTGRESQL_USER_PASSWORD'
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+##### Application node post-configuration
+
+Ensure that all migrations ran:
+
+```sh
+gitlab-rake gitlab:db:configure
+```
+
+> **Note**: If you encounter a `rake aborted!` error stating that PGBouncer is failing to connect to
+PostgreSQL it may be that your PGBouncer node's IP address is missing from
+PostgreSQL's `trust_auth_cidr_addresses` in `gitlab.rb` on your database nodes. See
+[PGBouncer error `ERROR: pgbouncer cannot connect to server`](#pgbouncer-error-error-pgbouncer-cannot-connect-to-server)
+in the Troubleshooting section before proceeding.
+
+##### Ensure GitLab is running
+
+At this point, your GitLab instance should be up and running. Verify you are
+able to login, and create issues and merge requests. If you have troubles check
+the [Troubleshooting section](#troubleshooting).
+
+#### Example configuration
+
+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.
+
+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.
+
+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.
+
+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.
+
+##### Example recommended setup for Consul servers
+
+On each server edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+# Disable all components except Consul
+roles ['consul_role']
+
+consul['configuration'] = {
+ server: true,
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
+}
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+##### Example recommended setup for PostgreSQL servers
+
+###### Primary node
+
+On primary node edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+# Disable all components except PostgreSQL and Repmgr and Consul
+roles ['postgres_role']
+
+# PostgreSQL configuration
+postgresql['listen_address'] = '0.0.0.0'
+postgresql['hot_standby'] = 'on'
+postgresql['wal_level'] = 'replica'
+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
+
+postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
+repmgr['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
+
+consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
+}
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+###### Secondary nodes
+
+On secondary nodes, edit `/etc/gitlab/gitlab.rb` and add all the configuration
+added to primary node, noted above. In addition, append the following
+configuration
+
+```
+# HA setting to specify if a node should attempt to be master on initialization
+repmgr['master_on_initialization'] = false
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+##### Example recommended setup for application server
+
+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_port'] = 6432
+gitlab_rails['db_password'] = 'toomanysecrets'
+gitlab_rails['auto_migrate'] = false
+
+postgresql['enable'] = false
+pgbouncer['enable'] = true
+consul['enable'] = true
+
+# Configure Pgbouncer
+pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
+
+# Configure Consul agent
+consul['watchers'] = %w(postgresql)
+
+pgbouncer['users'] = {
+ 'gitlab-consul': {
+ password: '5e0e3263571e3704ad655076301d6ebe'
+ },
+ 'pgbouncer': {
+ password: '771a8625958a529132abe6f1a4acb19c'
+ }
+}
+
+consul['configuration'] = {
+ retry_join: %w(10.6.0.11 10.6.0.12 10.6.0.13)
+}
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+##### Example recommended setup manual steps
+
+After deploying the configuration follow these steps:
+
+1. On `10.6.0.21`, our primary database
+
+ Enable the `pg_trgm` extension
+
+ ```sh
gitlab-psql -d gitlabhq_production
-
+ ```
+
+ ```
CREATE EXTENSION pg_trgm;
+ ```
+
+1. On `10.6.0.22`, our first standby database
- # Output:
+ Make this node a standby of the primary
- CREATE EXTENSION
+ ```sh
+ gitlab-ctl repmgr standby setup 10.6.0.21
```
-1. Exit the database prompt by typing `\q` and Enter.
-1. Exit the `gitlab-psql` user by running `exit` twice.
-1. Run `sudo gitlab-ctl reconfigure` a final time.
-1. Configure the GitLab application servers with the appropriate details.
- This step is covered in [Configuring GitLab for HA](gitlab.md).
+
+1. On `10.6.0.23`, our second standby database
+
+ Make this node a standby of the primary
+
+ ```sh
+ gitlab-ctl repmgr standby setup 10.6.0.21
+ ```
+
+1. On `10.6.0.31`, our application server
+
+ Set gitlab-consul's pgbouncer password to `toomanysecrets`
+
+ ```sh
+ gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul
+ ```
+
+ Run database migrations
+
+ ```sh
+ gitlab-rake gitlab:db:configure
+ ```
+
+#### Example minimal setup
+
+This example uses 3 postgresql servers, and 1 application node.
+
+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 postgres [failover](#failover-procedure) and [restore](#restore-procedure) procedures in addition to [consul outage recovery](consul.md#outage-recovery) on the same set of machines.
+
+In this example 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.
+
+Here is a list and description of each machine and the assigned IP:
+
+* `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.
+
+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.
+
+##### Example minimal configuration for database servers
+
+##### Primary node
+On primary database node edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+# Disable all components except PostgreSQL, Repmgr, and Consul
+roles ['postgres_role']
+
+# PostgreSQL configuration
+postgresql['listen_address'] = '0.0.0.0'
+postgresql['hot_standby'] = 'on'
+postgresql['wal_level'] = 'replica'
+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
+
+postgresql['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
+repmgr['trust_auth_cidr_addresses'] = %w(10.6.0.0/16)
+
+consul['configuration'] = {
+ server: true,
+ retry_join: %w(10.6.0.21 10.6.0.22 10.6.0.23)
+}
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+###### Secondary nodes
+
+On secondary nodes, edit `/etc/gitlab/gitlab.rb` and add all the information added
+to primary node, noted above. In addition, append the following configuration
+
+```
+# HA setting to specify if a node should attempt to be master on initialization
+repmgr['master_on_initialization'] = false
+```
+
+##### Example minimal configuration for application server
+
+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_port'] = 6432
+gitlab_rails['db_password'] = 'toomanysecrets'
+gitlab_rails['auto_migrate'] = false
+
+postgresql['enable'] = false
+pgbouncer['enable'] = true
+consul['enable'] = true
+
+# Configure Pgbouncer
+pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
+
+# Configure Consul agent
+consul['watchers'] = %w(postgresql)
+
+pgbouncer['users'] = {
+ 'gitlab-consul': {
+ password: '5e0e3263571e3704ad655076301d6ebe'
+ },
+ 'pgbouncer': {
+ password: '771a8625958a529132abe6f1a4acb19c'
+ }
+}
+
+consul['configuration'] = {
+ retry_join: %w(10.6.0.21 10.6.0.22 10.6.0.23)
+}
+```
+
+[Reconfigure Omnibus GitLab][reconfigure GitLab] for the changes to take effect.
+
+##### Example minimal setup manual steps
+
+The manual steps for this configuration are the same as for the [example recommended setup](#example-recommended-setup-manual-steps).
+
+#### Failover procedure
+
+By default, if the master database fails, `repmgrd` should promote one of the
+standby nodes to master automatically, and consul will update pgbouncer with
+the new master.
+
+If you need to failover manually, you have two options:
+
+**Shutdown the current master database**
+
+Run:
+
+```sh
+gitlab-ctl stop postgresql
+```
+
+The automated failover process will see this and failover to one of the
+standby nodes.
+
+**Or perform a manual failover**
+
+1. Ensure the old master node is not still active.
+1. Login to the server that should become the new master and run:
+
+ ```sh
+ gitlab-ctl repmgr standby promote
+ ```
+
+1. If there are any other standby servers in the cluster, have them follow
+ the new master server:
+
+ ```sh
+ gitlab-ctl repmgr standby follow NEW_MASTER
+ ```
+
+#### Restore procedure
+
+If a node fails, it can be removed from the cluster, or added back as a standby
+after it has been restored to service.
+
+- If you want to remove the node from the cluster, on any other node in the
+ cluster, run:
+
+ ```sh
+ gitlab-ctl repmgr standby unregister --node=X
+ ```
+
+ where X is the value of node in `repmgr.conf` on the old server.
+
+ To find this, you can use:
+
+ ```sh
+ awk -F = '$1 == "node" { print $2 }' /var/opt/gitlab/postgresql/repmgr.conf
+ ```
+
+ It will output something like:
+
+ ```
+ 959789412
+ ```
+
+ Then you will use this id to unregister the node:
+
+ ```sh
+ gitlab-ctl repmgr standby unregister --node=959789412
+ ```
+
+- To add the node as a standby server:
+
+ ```sh
+ gitlab-ctl repmgr standby follow NEW_MASTER
+ gitlab-ctl restart repmgrd
+ ```
+
+ CAUTION: **Warning:** When the server is brought back online, and before
+ you switch it to a standby node, repmgr will report that there are two masters.
+ If there are any clients that are still attempting to write to the old master,
+ this will cause a split, and the old master will need to be resynced from
+ scratch by performing a `gitlab-ctl repmgr standby setup NEW_MASTER`.
+
+#### Alternate configurations
+
+##### Database authorization
+
+By default, we give any host on the database network the permission to perform
+repmgr operations using PostgreSQL's `trust` method. If you do not want this
+level of trust, there are alternatives.
+
+You can trust only the specific nodes that will be database clusters, or you
+can require md5 authentication.
+
+##### Trust specific addresses
+
+If you know the IP address, or FQDN of all database and pgbouncer nodes in the
+cluster, you can trust only those nodes.
+
+In `/etc/gitlab/gitlab.rb` on all of the database nodes, set
+`repmgr['trust_auth_cidr_addresses']` to an array of strings containing all of
+the addresses.
+
+If setting to a node's FQDN, they must have a corresponding PTR record in DNS.
+If setting to a node's IP address, specify it as `XXX.XXX.XXX.XXX/32`.
+
+For example:
+
+```ruby
+repmgr['trust_auth_cidr_addresses'] = %w(192.168.1.44/32 db2.example.com)
+```
+
+
+##### MD5 Authentication
+
+If you are running on an untrusted network, repmgr can use md5 authentication
+with a [.pgpass file](https://www.postgresql.org/docs/9.6/static/libpq-pgpass.html)
+to authenticate.
+
+You can specify by IP address, FQDN, or by subnet, using the same format as in
+the previous section:
+
+1. On the current master node, create a password for the `gitlab` and
+ `gitlab_repmgr` user:
+
+ ```sh
+ gitlab-psql -d template1
+ template1=# \password gitlab_repmgr
+ Enter password: ****
+ Confirm password: ****
+ template1=# \password gitlab
+ ```
+
+1. On each database node:
+
+ 1. Edit `/etc/gitlab/gitlab.rb`:
+ 1. Ensure `repmgr['trust_auth_cidr_addresses']` is **not** set
+ 1. Set `postgresql['md5_auth_cidr_addresses']` to the desired value
+ 1. Set `postgresql['sql_replication_user'] = 'gitlab_repmgr'`
+ 1. Reconfigure with `gitlab-ctl reconfigure`
+ 1. Restart postgresql with `gitlab-ctl restart postgresql`
+
+ 1. Create a `.pgpass` file. Enter the `gitlab_repmgr` password twice to
+ when asked:
+
+ ```sh
+ gitlab-ctl write-pgpass --user gitlab_repmgr --hostuser gitlab-psql --database '*'
+ ```
+
+1. On each pgbouncer node, edit `/etc/gitlab/gitlab.rb`:
+ 1. Ensure `gitlab_rails['db_password']` is set to the plaintext password for
+ the `gitlab` database user
+ 1. [Reconfigure GitLab] for the changes to take effect
+
+## Troubleshooting
+
+#### Consul and PostgreSQL changes not taking effect.
+
+Due to the potential impacts, `gitlab-ctl reconfigure` only reloads Consul and PostgreSQL, it will not restart the services. However, not all changes can be activated by reloading.
+
+To restart either service, run `gitlab-ctl restart SERVICE`
+
+For PostgreSQL, it is usually safe to restart the master node by default. Automatic failover defaults to a 1 minute timeout. Provided the database returns before then, nothing else needs to be done. To be safe, you can stop `repmgrd` on the standby nodes first with `gitlab-ctl stop repmgrd`, then start afterwards with `gitlab-ctl start repmgrd`.
+
+On the consul server nodes, it is important to restart the consul service in a controlled fashion. Read our [consul documentation](consul.md#restarting-the-server-cluster) for instructions on how to restart the service.
+
+#### `gitlab-ctl repmgr-check-master` command produces errors
+
+If this command displays errors about database permissions it is likely that something failed during
+install, resulting in the `gitlab-consul` database user getting incorrect permissions. Follow these
+steps to fix the problem:
+
+1. On the master database node, connect to the database prompt - `gitlab-psql -d template1`
+1. Delete the `gitlab-consul` user - `DROP USER "gitlab-consul";`
+1. Exit the database prompt - `\q`
+1. [Reconfigure GitLab] and the user will be re-added with the proper permissions.
+1. Change to the `gitlab-consul` user - `su - gitlab-consul`
+1. Try the check command again - `gitlab-ctl repmgr-check-master`.
+
+Now there should not be errors. If errors still occur then there is another problem.
+
+#### PGBouncer error `ERROR: pgbouncer cannot connect to server`
+
+You may get this error when running `gitlab-rake gitlab:db:configure` or you
+may see the error in the PGBouncer log file.
+
+```
+PG::ConnectionBad: ERROR: pgbouncer cannot connect to server
+```
+
+The problem may be that your PGBouncer node's IP address is not included in the
+`trust_auth_cidr_addresses` setting in `/etc/gitlab/gitlab.rb` on the database nodes.
+
+You can confirm that this is the issue by checking the PostgreSQL log on the master
+database node. If you see the following error then `trust_auth_cidr_addresses`
+is the problem.
+
+```
+2018-03-29_13:59:12.11776 FATAL: no pg_hba.conf entry for host "123.123.123.123", user "pgbouncer", database "gitlabhq_production", SSL off
+```
+
+To fix the problem, add the IP address to `/etc/gitlab/gitlab.rb`.
+
+```
+postgresql['trust_auth_cidr_addresses'] = %w(123.123.123.123/32 <other_cidrs>)
+```
+
+[Reconfigure GitLab] for the changes to take effect.
+
+#### Issues with other components
+
+If you're running into an issue with a component not outlined here, be sure to check the troubleshooting section of their specific documentation page.
+
+- [Consul](consul.md#troubleshooting)
+- [PostgreSQL](http://docs.gitlab.com/omnibus/settings/database.html#troubleshooting)
+- [GitLab application](gitlab.md#troubleshooting)
+
+## Configure using Omnibus
+
+**Note**: We recommend that you follow the instructions here for a full [PostgreSQL cluster](#high-availability-with-gitlab-omnibus-premium-only).
+If you are reading this section due to an old bookmark, you can find that old documentation [in the repository](https://gitlab.com/gitlab-org/gitlab-ce/blob/v10.1.4/doc/administration/high_availability/database.md#configure-using-omnibus).
---
@@ -114,3 +1165,6 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
+1. [Manage the bundled Consul cluster](consul.md)
+
+[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md
new file mode 100644
index 00000000000..d44744f2af8
--- /dev/null
+++ b/doc/administration/high_availability/gitaly.md
@@ -0,0 +1,90 @@
+# Configuring Gitaly for Scaled and High Availability
+
+Gitaly does not yet support full high availability. However, Gitaly is quite
+stable and is in use on GitLab.com. Scaled and highly available GitLab environments
+should consider using Gitaly on a separate node.
+
+See the [Gitaly HA Epic](https://gitlab.com/groups/gitlab-org/-/epics/289) to
+track plans and progress toward high availability support.
+
+This document is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
+environments and [High Availability Architecture](./README.md#high-availability-architecture-examples).
+
+## Running Gitaly on its own server
+
+Starting with GitLab 11.4, Gitaly is a replacement for NFS except
+when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer)
+is used.
+
+NOTE: **Note:** While Gitaly can be used as a replacement for NFS, we do not recommend using EFS as it may impact GitLab's performance. Please review the [relevant documentation](nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
+
+NOTE: **Note:** Gitaly network traffic is unencrypted so we recommend a firewall to
+restrict access to your Gitaly server.
+
+The steps below are the minimum necessary to configure a Gitaly server with
+Omnibus:
+
+1. SSH into the Gitaly server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Do not complete any other steps on the download page.
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
+
+ Gitaly must trigger some callbacks to GitLab via GitLab Shell. As a result,
+ the GitLab Shell secret must be the same between the other GitLab servers and
+ the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gitlab-secrets.json`
+ from an existing GitLab server to the Gitaly server. Without this shared secret,
+ Git operations in GitLab will result in an API error.
+
+ > **NOTE:** In most or all cases the storage paths below end in `repositories` which is
+ different than `path` in `git_data_dirs` of Omnibus installations. Check the
+ directory layout on your Gitaly server to be sure.
+
+ ```ruby
+ # Enable Gitaly
+ gitaly['enable'] = true
+
+ ## Disable all other services
+ sidekiq['enable'] = false
+ gitlab_workhorse['enable'] = false
+ unicorn['enable'] = false
+ postgresql['enable'] = false
+ nginx['enable'] = false
+ prometheus['enable'] = false
+ alertmanager['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ redis_exporter['enable'] = false
+ gitlab_monitor['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.
+ gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
+
+ # Make Gitaly accept connections on all network interfaces. You must use
+ # firewalls to restrict access to this address/port.
+ gitaly['listen_addr'] = "0.0.0.0:8075"
+ gitaly['auth_token'] = 'abc123secret'
+
+ gitaly['storage'] = [
+ { 'name' => 'default', 'path' => '/mnt/gitlab/default/repositories' },
+ { 'name' => 'storage1', 'path' => '/mnt/gitlab/storage1/repositories' },
+ ]
+
+ # To use tls for gitaly you need to add
+ gitaly['tls_listen_addr'] = "0.0.0.0:9999"
+ gitaly['certificate_path'] = "path/to/cert.pem"
+ gitaly['key_path'] = "path/to/key.pem"
+ ```
+
+Again, reconfigure (Omnibus) or restart (source).
+
+Continue configuration of other components by going back to:
+
+- [Scaled Architectures](./README.md#scalable-architecture-examples)
+- [High Availability Architectures](./README.md#high-availability-architecture-examples)
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index d95c3acec54..888426ece5c 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -1,8 +1,4 @@
-# Configuring GitLab for HA
-
-Assuming you have already configured a [database](database.md), [Redis](redis.md), and [NFS](nfs.md), you can
-configure the GitLab application server(s) now. Complete the steps below
-for each GitLab application server in your environment.
+# Configuring GitLab Scaling and High Availability
> **Note:** There is some additional configuration near the bottom for
additional GitLab application servers. It's important to read and understand
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 72341a5c777..d0e0e320019 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -39,7 +39,14 @@ options:
### Improving NFS performance with GitLab
-NOTE: **Note:** This is only available with GitLab 11.9 and up.
+NOTE: **Note:**
+This is only available starting in certain versions of GitLab:
+
+ * 11.5.11
+ * 11.6.11
+ * 11.7.12
+ * 11.8.8
+ * 11.9.0 and up (e.g. 11.10, 11.11, etc.)
If you are using NFS to share Git data, we recommend that you enable a
number of feature flags that will allow GitLab application processes to
diff --git a/doc/administration/high_availability/nfs_host_client_setup.md b/doc/administration/high_availability/nfs_host_client_setup.md
new file mode 100644
index 00000000000..a8bc101dee6
--- /dev/null
+++ b/doc/administration/high_availability/nfs_host_client_setup.md
@@ -0,0 +1,135 @@
+# Configuring NFS for GitLab HA
+
+Setting up NFS for a GitLab HA setup allows all applications nodes in a cluster
+to share the same files and maintain data consistency. Application nodes in an HA
+setup act as clients while the NFS server plays host.
+
+> Note: The instructions provided in this documentation allow for setting a quick
+proof of concept but will leave NFS as potential single point of failure and
+therefore not recommended for use in production. Explore options such as [Pacemaker
+and Corosync](http://clusterlabs.org/) for highly available NFS in production.
+
+Below are instructions for setting up an application node(client) in an HA cluster
+to read from and write to a central NFS server(host).
+
+NOTE: **Note:**
+Using EFS may negatively impact performance. Please review the [relevant documentation](nfs.md#avoid-using-awss-elastic-file-system-efs) for additional details.
+
+## NFS Server Setup
+
+> Follow the instructions below to set up and configure your NFS server.
+
+### Step 1 - Install NFS Server on Host
+
+Installing the nfs-kernel-server package allows you to share directories with the clients running the GitLab application.
+
+```sh
+apt-get update
+apt-get install nfs-kernel-server
+```
+
+### Step 2 - Export Host's Home Directory to Client
+
+In this setup we will share the home directory on the host with the client. Edit the exports file as below to share the host's home directory with the client. If you have multiple clients running GitLab you must enter the client IP addresses in line in the `/etc/exports` file.
+
+```text
+#/etc/exports for one client
+/home <client-ip-address>(rw,sync,no_root_squash,no_subtree_check)
+
+#/etc/exports for three clients
+/home <client-ip-address>(rw,sync,no_root_squash,no_subtree_check) <client-2-ip-address>(rw,sync,no_root_squash,no_subtree_check) <client-3-ip-address>(rw,sync,no_root_squash,no_subtree_check)
+```
+
+Restart the NFS server after making changes to the `exports` file for the changes
+to take effect.
+
+```sh
+systemctl restart nfs-kernel-server
+```
+
+NOTE: **Note:**
+You may need to update your server's firewall. See the [firewall section](#nfs-in-a-firewalled-environment) at the end of this guide.
+
+## Client/ GitLab application node Setup
+
+> Follow the instructions below to connect any GitLab rails application node running
+inside your HA environment to the NFS server configured above.
+
+### Step 1 - Install NFS Common on Client
+
+The nfs-common provides NFS functionality without installing server components which
+we don't need running on the application nodes.
+
+```sh
+apt-get update
+apt-get install nfs-common
+```
+
+### Step 2 - Create Mount Points on Client
+
+Create a directroy on the client that we can mount the shared directory from the host.
+Please note that if your mount point directory contains any files they will be hidden
+once the remote shares are mounted. An empty/new directory on the client is recommended
+for this purpose.
+
+```sh
+mkdir -p /nfs/home
+```
+
+Confirm that the mount point works by mounting it on the client and checking that
+it is mounted with the command below:
+
+```sh
+mount <host_ip_address>:/home
+df -h
+```
+
+### Step 3 - Set up Automatic Mounts on Boot
+
+Edit `/etc/fstab` on client as below to mount the remote shares automatically at boot.
+Note that GitLab requires advisory file locking, which is only supported natively in
+NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used.
+We recommend using version 4 and do not specifically test NFSv3.
+
+```text
+#/etc/fstab
+165.227.159.85:/home /nfs/home nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+```
+
+Reboot the client and confirm that the mount point is mounted automatically.
+
+### Step 4 - Set up GitLab to Use NFS mounts
+
+When using the default Omnibus configuration you will need to share 5 data locations
+between all GitLab cluster nodes. No other locations should be shared. Changing the
+default file locations in `gitlab.rb` on the client allows you to have one main mount
+point and have all the required locations as subdirectories to use the NFS mount for
+git-data.
+
+```text
+git_data_dirs({"default" => {"path" => "/nfs/home/var/opt/gitlab-data/git-data"}})
+gitlab_rails['uploads_directory'] = '/nfs/home/var/opt/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/nfs/home/var/opt/gitlab-data/shared'
+gitlab_ci['builds_directory'] = '/nfs/home/var/opt/gitlab-data/builds'
+```
+
+Save the changes in `gitlab.rb` and run `gitlab-ctl reconfigure`.
+
+## NFS in a Firewalled Environment
+
+If the traffic between your NFS server and NFS client(s) is subject to port filtering
+by a firewall, then you will need to reconfigure that firewall to allow NFS communication.
+
+[This guide from TDLP](http://tldp.org/HOWTO/NFS-HOWTO/security.html#FIREWALLS)
+covers the basics of using NFS in a firewalled environment. Additionally, we encourage you to
+search for and review the specific documentation for your OS/distro and your firewall software.
+
+Example for Ubuntu:
+
+Check that NFS traffic from the client is allowed by the firewall on the host by running
+the command: `sudo ufw status`. If it's being blocked, then you can allow traffic from a specific
+client with the command below.
+
+```sh
+sudo ufw allow from <client-ip-address> to any port nfs
+```
diff --git a/doc/administration/high_availability/pg_ha_architecture.png b/doc/administration/high_availability/pg_ha_architecture.png
new file mode 100644
index 00000000000..ef870f652ae
--- /dev/null
+++ b/doc/administration/high_availability/pg_ha_architecture.png
Binary files differ
diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md
new file mode 100644
index 00000000000..762179cf756
--- /dev/null
+++ b/doc/administration/high_availability/pgbouncer.md
@@ -0,0 +1,132 @@
+# Working with the bundle Pgbouncer service
+
+## Overview
+
+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`.
+
+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.
+
+## Operations
+
+### Running Pgbouncer as part of an HA GitLab installation
+
+See our [HA documentation for PostgreSQL](database.md) for information on running pgbouncer as part of a HA setup
+
+### 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`
+
+1. Generate SQL_USER_PASSWORD_HASH with the command `gitlab-ctl pg-password-md5 gitlab`. We'll also need to enter the plaintext SQL_USER_PASSWORD later
+
+1. On your database node, ensure the following is set in your `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ postgresql['pgbouncer_user_password'] = 'PGBOUNCER_USER_PASSWORD_HASH'
+ postgresql['sql_user_password'] = 'SQL_USER_PASSWORD_HASH'
+ postgresql['listen_address'] = 'XX.XX.XX.Y' # Where XX.XX.XX.Y is the ip address on the node postgresql should listen on
+ postgresql['md5_auth_cidr_addresses'] = %w(AA.AA.AA.B/32) # Where AA.AA.AA.B is the IP address of the pgbouncer node
+ ```
+
+1. Run `gitlab-ctl reconfigure`
+
+ **Note:** If the database was already running, it will need to be restarted after reconfigure by running `gitlab-ctl restart postgresql`.
+
+1. On the node you are running pgbouncer on, make sure the following is set in `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ pgbouncer['enable'] = true
+ pgbouncer['databases'] = {
+ gitlabhq_production: {
+ host: 'DATABASE_HOST',
+ user: 'pgbouncer',
+ password: 'PGBOUNCER_USER_PASSWORD_HASH'
+ }
+ }
+ ```
+
+1. Run `gitlab-ctl reconfigure`
+
+1. On the node running unicorn, make sure the following is set in `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ gitlab_rails['db_host'] = 'PGBOUNCER_HOST'
+ gitlab_rails['db_port'] = '6432'
+ gitlab_rails['db_password'] = 'SQL_USER_PASSWORD'
+ ```
+
+1. Run `gitlab-ctl reconfigure`
+
+1. At this point, your instance should connect to the database through pgbouncer. If you are having issues, see the [Troubleshooting](#troubleshooting) section
+
+### Interacting with pgbouncer
+
+#### 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.
+
+To start a session, run
+
+```shell
+# gitlab-ctl pgb-console
+Password for user pgbouncer:
+psql (9.6.8, server 1.7.2/bouncer)
+Type "help" for help.
+
+pgbouncer=#
+```
+
+The password you will be prompted for is the PGBOUNCER_USER_PASSWORD
+
+To get some basic information about the instance, run
+```shell
+pgbouncer=# show databases; show clients; show servers;
+ name | host | port | database | force_user | pool_size | reserve_pool | pool_mode | max_connections | current_connections
+---------------------+-----------+------+---------------------+------------+-----------+--------------+-----------+-----------------+---------------------
+ gitlabhq_production | 127.0.0.1 | 5432 | gitlabhq_production | | 100 | 5 | | 0 | 1
+ pgbouncer | | 6432 | pgbouncer | pgbouncer | 2 | 0 | statement | 0 | 0
+(2 rows)
+
+ type | user | database | state | addr | port | local_addr | local_port | connect_time | request_time | ptr | link
+| remote_pid | tls
+------+-----------+---------------------+--------+-----------+-------+------------+------------+---------------------+---------------------+-----------+------
++------------+-----
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44590 | 127.0.0.1 | 6432 | 2018-04-24 22:13:10 | 2018-04-24 22:17:10 | 0x12444c0 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44592 | 127.0.0.1 | 6432 | 2018-04-24 22:13:10 | 2018-04-24 22:17:10 | 0x12447c0 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44594 | 127.0.0.1 | 6432 | 2018-04-24 22:13:10 | 2018-04-24 22:17:10 | 0x1244940 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44706 | 127.0.0.1 | 6432 | 2018-04-24 22:14:22 | 2018-04-24 22:16:31 | 0x1244ac0 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44708 | 127.0.0.1 | 6432 | 2018-04-24 22:14:22 | 2018-04-24 22:15:15 | 0x1244c40 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44794 | 127.0.0.1 | 6432 | 2018-04-24 22:15:15 | 2018-04-24 22:15:15 | 0x1244dc0 |
+| 0 |
+ C | gitlab | gitlabhq_production | active | 127.0.0.1 | 44798 | 127.0.0.1 | 6432 | 2018-04-24 22:15:15 | 2018-04-24 22:16:31 | 0x1244f40 |
+| 0 |
+ C | pgbouncer | pgbouncer | active | 127.0.0.1 | 44660 | 127.0.0.1 | 6432 | 2018-04-24 22:13:51 | 2018-04-24 22:17:12 | 0x1244640 |
+| 0 |
+(8 rows)
+
+ type | user | database | state | addr | port | local_addr | local_port | connect_time | request_time | ptr | link | rem
+ote_pid | tls
+------+--------+---------------------+-------+-----------+------+------------+------------+---------------------+---------------------+-----------+------+----
+--------+-----
+ S | gitlab | gitlabhq_production | idle | 127.0.0.1 | 5432 | 127.0.0.1 | 35646 | 2018-04-24 22:15:15 | 2018-04-24 22:17:10 | 0x124dca0 | |
+ 19980 |
+(1 row)
+```
+
+## Troubleshooting
+
+In case you are experiencing any issues connecting through pgbouncer, the first place to check is always the logs:
+
+```shell
+# gitlab-ctl tail pgbouncer
+```
+
+Additionally, you can check the output from `show databases` in the [Administrative console](#administrative-console). In the output, you would expect to see values in the `host` field for the `gitlabhq_production` database. Additionally, `current_connections` should be greater than 1.
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 3daebc4d84b..46ad3ecd9bb 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,6 +1,103 @@
-# Configuring Redis for GitLab HA
+# Configuring Redis for Scaling and High Availability
-> Experimental Redis Sentinel support was [Introduced][ce-1877] in GitLab 8.11.
+## Provide your own Redis instance **[CORE ONLY]**
+
+The following are the requirements for providing your own Redis instance:
+
+- Redis version 2.8 or higher. Version 3.2 or higher is recommend as this is
+ what ships with the GitLab Omnibus package.
+- Standalone Redis or Redis high availability with Sentinel are supported. Redis
+ Cluster is not supported.
+- Managed Redis from cloud providers such as AWS Elasticache will work. If these
+ services support high availability, be sure it is not the Redis Cluster type.
+
+Note the Redis node's IP address or hostname, port, and password (if required).
+These will be necessary when configuring the GitLab application servers later.
+
+## Redis in a Scaled Environment
+
+This section is relevant for [Scaled Architecture](./README.md#scalable-architecture-examples)
+environments including [Basic Scaling](./README.md#basic-scaling) and
+[Full Scaling](./README.md#full-scaling).
+
+### Provide your own Redis instance **[CORE ONLY]**
+
+If you want to use your own deployed Redis instance(s),
+see [Provide your own Redis instance](#provide-your-own-redis-instance-core-only)
+for more details. However, you can use the GitLab Omnibus package to easily
+deploy the bundled Redis.
+
+### Standalone Redis using GitLab Omnibus **[CORE ONLY]**
+
+The GitLab Omnibus package can be used to configure a standalone Redis server.
+In this configuration Redis is not highly available, and represents a single
+point of failure. However, in a scaled environment the objective is to allow
+the environment to handle more users or to increase throughput. Redis itself
+is generally stable and can handle many requests so it is an acceptable
+trade off to have only a single instance. See [Scaling and High Availability](./README.md)
+for an overview of GitLab scaling and high availability options.
+
+The steps below are the minimum necessary to configure a Redis server with
+Omnibus:
+
+1. SSH into the Redis server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Do not complete any other steps on the download page.
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
+
+ ```ruby
+ ## Enable Redis
+ redis['enable'] = true
+
+ ## Disable all other services
+ sidekiq['enable'] = false
+ gitlab_workhorse['enable'] = false
+ unicorn['enable'] = false
+ postgresql['enable'] = false
+ nginx['enable'] = false
+ prometheus['enable'] = false
+ alertmanager['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ gitlab_monitor['enable'] = false
+ gitaly['enable'] = false
+
+ redis['bind'] = '0.0.0.0'
+ redis['port'] = '6379'
+ redis['password'] = 'SECRET_PASSWORD_HERE'
+
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Note the Redis node's IP address or hostname, port, and
+ Redis password. These will be necessary when configuring the GitLab
+ application servers later.
+
+Advanced configuration options are supported and can be added if
+needed.
+
+Continue configuration of other components by going
+[back to Scaled Architectures](./README.md#scalable-architecture-examples)
+
+## Redis with High Availability
+
+This section is relevant for [High Availability Architecture](./README.md#high-availability-architecture-examples)
+environments including [Horizontal](./README.md#horizontal),
+[Hybrid](./README.md#hybrid), and
+[Fully Distributed](./README.md#fully-distributed).
+
+### Provide your own Redis instance **[CORE ONLY]**
+
+If you want to use your own deployed Redis instance(s),
+see [Provide your own Redis instance](#provide-your-own-redis-instance-core-only)
+for more details. However, you can use the GitLab Omnibus package to easily
+deploy the bundled Redis.
+
+### High Availability with GitLab Omnibus **[PREMIUM ONLY]**
+
+> Experimental Redis Sentinel support was [introduced in GitLab 8.11][ce-1877].
Starting with 8.14, Redis Sentinel is no longer experimental.
If you've used it with versions `< 8.14` before, please check the updated
documentation here.
@@ -53,8 +150,6 @@ failure.
Make sure that you read this document once as a whole before configuring the
components below.
-### High Availability with Sentinel
-
> **Notes:**
>
> - Starting with GitLab `8.11`, you can configure a list of Redis Sentinel
@@ -270,10 +365,9 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- # Enable the master role and disable all other services in the machine
- # (you can still enable Sentinel).
- redis_master_role['enable'] = true
-
+ # Specify server role as 'redis_master_role'
+ roles ['redis_master_role']
+
# IP address pointing to a local IP that the other machines can reach to.
# You can also set bind to '0.0.0.0' which listen in all interfaces.
# If you really need to bind to an external accessible IP, make
@@ -287,6 +381,7 @@ The prerequisites for a HA Redis setup are the following:
# Set up password authentication for Redis (use the same password in all nodes).
redis['password'] = 'redis-password-goes-here'
```
+
1. Only the primary GitLab application server should handle migrations. To
prevent database migrations from running on upgrade, add the following
@@ -298,6 +393,10 @@ The prerequisites for a HA Redis setup are the following:
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+> Note: You can specify multiple roles like sentinel and redis as:
+> roles ['redis_sentinel_role', 'redis_master_role']. Read more about high
+> availability roles at https://docs.gitlab.com/omnibus/roles/
+
### Step 2. Configuring the slave Redis instances
1. SSH into the **slave** Redis server.
@@ -310,11 +409,9 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- # Enable the slave role and disable all other services in the machine
- # (you can still enable Sentinel). This will also set automatically
- # `redis['master'] = false`.
- redis_slave_role['enable'] = true
-
+ # Specify server role as 'redis_slave_role'
+ roles ['redis_slave_role']
+
# IP address pointing to a local IP that the other machines can reach to.
# You can also set bind to '0.0.0.0' which listen in all interfaces.
# If you really need to bind to an external accessible IP, make
@@ -336,17 +433,19 @@ The prerequisites for a HA Redis setup are the following:
#redis['master_port'] = 6379
```
-1. To prevent database migrations from running on upgrade, run:
+1. To prevent reconfigure from running automatically on upgrade, run:
```
sudo touch /etc/gitlab/skip-auto-reconfigure
```
- Only the primary GitLab application server should handle migrations.
-
1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
1. Go through the steps again for all the other slave nodes.
+> Note: You can specify multiple roles like sentinel and redis as:
+> roles ['redis_sentinel_role', 'redis_slave_role']. Read more about high
+> availability roles at https://docs.gitlab.com/omnibus/roles/
+
---
These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
@@ -400,13 +499,13 @@ multiple machines with the Sentinel daemon.
be duplicate below):
```ruby
- redis_sentinel_role['enable'] = true
+ roles ['redis_sentinel_role']
# Must be the same in every sentinel node
redis['master_name'] = 'gitlab-redis'
# The same password for Redis authentication you set up for the master node.
- redis['password'] = 'redis-password-goes-here'
+ redis['master_password'] = 'redis-password-goes-here'
# The IP of the master Redis node.
redis['master_ip'] = '10.0.0.1'
@@ -573,8 +672,7 @@ or a failover promotes a different **Master** node.
In `/etc/gitlab/gitlab.rb`:
```ruby
-redis_master_role['enable'] = true
-redis_sentinel_role['enable'] = true
+roles ['redis_sentinel_role', 'redis_master_role']
redis['bind'] = '10.0.0.1'
redis['port'] = 6379
redis['password'] = 'redis-password-goes-here'
@@ -596,8 +694,7 @@ sentinel['quorum'] = 2
In `/etc/gitlab/gitlab.rb`:
```ruby
-redis_slave_role['enable'] = true
-redis_sentinel_role['enable'] = true
+roles ['redis_sentinel_role', 'redis_slave_role']
redis['bind'] = '10.0.0.2'
redis['port'] = 6379
redis['password'] = 'redis-password-goes-here'
@@ -619,8 +716,7 @@ sentinel['quorum'] = 2
In `/etc/gitlab/gitlab.rb`:
```ruby
-redis_slave_role['enable'] = true
-redis_sentinel_role['enable'] = true
+roles ['redis_sentinel_role', 'redis_slave_role']
redis['bind'] = '10.0.0.3'
redis['port'] = 6379
redis['password'] = 'redis-password-goes-here'
@@ -643,7 +739,7 @@ In `/etc/gitlab/gitlab.rb`:
```ruby
redis['master_name'] = 'gitlab-redis'
-redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
gitlab_rails['redis_sentinels'] = [
{'host' => '10.0.0.1', 'port' => 26379},
{'host' => '10.0.0.2', 'port' => 26379},
@@ -764,15 +860,11 @@ Before proceeding with the troubleshooting below, check your firewall rules:
### Troubleshooting Redis replication
You can check if everything is correct by connecting to each server using
-`redis-cli` application, and sending the `INFO` command.
+`redis-cli` application, and sending the `info replication` command as below.
-If authentication was correctly defined, it should fail with:
-`NOAUTH Authentication required` error. Try to authenticate with the
-previous defined password with `AUTH redis-password-goes-here` and
-try the `INFO` command again.
-
-Look for the `# Replication` section where you should see some important
-information like the `role` of the server.
+```
+/opt/gitlab/embedded/bin/redis-cli -a <redis-password> info replication
+```
When connected to a `master` redis, you will see the number of connected
`slaves`, and a list of each with connection details:
@@ -842,7 +934,7 @@ To make sure your configuration is correct:
1. Run in the console:
```ruby
- redis = Redis.new(Gitlab::Redis.params)
+ redis = Redis.new(Gitlab::Redis::SharedState.params)
redis.info
```
diff --git a/doc/administration/img/db_load_balancing_postgres_stats.png b/doc/administration/img/db_load_balancing_postgres_stats.png
new file mode 100644
index 00000000000..8b311616e7b
--- /dev/null
+++ b/doc/administration/img/db_load_balancing_postgres_stats.png
Binary files differ
diff --git a/doc/administration/img/high_availability/active-active-diagram.png b/doc/administration/img/high_availability/active-active-diagram.png
deleted file mode 100644
index 4f5984b88fe..00000000000
--- a/doc/administration/img/high_availability/active-active-diagram.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/high_availability/active-passive-diagram.png b/doc/administration/img/high_availability/active-passive-diagram.png
deleted file mode 100644
index 3b42ce5911c..00000000000
--- a/doc/administration/img/high_availability/active-passive-diagram.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/high_availability/fully-distributed.png b/doc/administration/img/high_availability/fully-distributed.png
new file mode 100644
index 00000000000..ad23207134e
--- /dev/null
+++ b/doc/administration/img/high_availability/fully-distributed.png
Binary files differ
diff --git a/doc/administration/img/high_availability/geo-ha-diagram.png b/doc/administration/img/high_availability/geo-ha-diagram.png
new file mode 100644
index 00000000000..da5d612827c
--- /dev/null
+++ b/doc/administration/img/high_availability/geo-ha-diagram.png
Binary files differ
diff --git a/doc/administration/img/high_availability/horizontal.png b/doc/administration/img/high_availability/horizontal.png
new file mode 100644
index 00000000000..c3bd489d96f
--- /dev/null
+++ b/doc/administration/img/high_availability/horizontal.png
Binary files differ
diff --git a/doc/administration/img/high_availability/hybrid.png b/doc/administration/img/high_availability/hybrid.png
new file mode 100644
index 00000000000..7d4a56bf0ea
--- /dev/null
+++ b/doc/administration/img/high_availability/hybrid.png
Binary files differ
diff --git a/doc/administration/img/instance_review_button.png b/doc/administration/img/instance_review_button.png
new file mode 100644
index 00000000000..b7604d7c7e5
--- /dev/null
+++ b/doc/administration/img/instance_review_button.png
Binary files differ
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index 658b2f55d30..9ac310af248 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -10,6 +10,8 @@ GitLab has several features based on receiving incoming emails:
- [New merge request by email](../user/project/merge_requests/index.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](https://docs.gitlab.com/ee/user/project/service_desk.html): provide e-mail support to
+ your customers through GitLab. **[PREMIUM]**
## Requirements
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 5f368ea8d49..02e88dbd2a6 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -32,8 +32,14 @@ Learn how to install, configure, update, and maintain your GitLab instance.
### Installing GitLab
- [Install](../install/README.md): Requirements, directory structures, and installation methods.
+ - [Database load balancing](database_load_balancing.md): Distribute database queries among multiple database servers. **[STARTER ONLY]**
+ - [Omnibus support for external MySQL DB](https://docs.gitlab.com/omnibus/settings/database.html#using-a-mysql-database-management-server-enterprise-edition-only): Omnibus package supports configuring an external MySQL database. **[STARTER ONLY]**
+ - [Omnibus support for log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only) **[STARTER ONLY]**
- [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability.
- [High Availability on AWS](../university/high-availability/aws/README.md): Set up GitLab HA on Amazon AWS.
+- [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **[PREMIUM ONLY]**
+- [Disaster Recovery](https://docs.gitlab.com/ee/administration/geo/disaster_recovery/index.html): Quickly fail-over to a different site with minimal effort in a disaster situation. **[PREMIUM ONLY]**
+- [Pivotal Tile](https://docs.gitlab.com/ee/install/pivotal/index.html): Deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **[PREMIUM ONLY]**
### Configuring GitLab
@@ -44,8 +50,8 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Global user settings](user_settings.md): Configure instance-wide user permissions.
- [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).
+- [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.
- [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)
@@ -54,6 +60,9 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages.
- [Merge request diffs storage](merge_request_diffs.md): Configure merge requests diffs external storage.
- [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI.
+- [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data. **[STARTER ONLY]**
+- [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md) **[PREMIUM ONLY]**
+- [Upload a license](https://docs.gitlab.com/ee/user/admin_area/license.html): Upload a license to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]**
- [Admin Area](../user/admin_area/index.md): for self-managed instance-wide configuration and maintenance.
#### Customizing GitLab's appearance
@@ -63,6 +72,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [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.
+- [Additional custom email text](https://docs.gitlab.com/ee/user/admin_area/settings/email.html#custom-additional-text-premium-only): Add additional custom text to emails sent from GitLab. **[PREMIUM ONLY]**
### Maintaining GitLab
@@ -96,7 +106,16 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Libravatar](../customization/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. See also other [authentication](../topics/authentication/index.md#gitlab-administrators) topics (for example, enforcing 2FA).
+- [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers.
+ - [Sync LDAP](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER ONLY]**
+ - [Kerberos authentication](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER ONLY]**
+ - See also other [authentication](../topics/authentication/index.md#gitlab-administrators) topics (for example, enforcing 2FA).
+- [Email users](https://docs.gitlab.com/ee/tools/email.html): Email GitLab users from within GitLab. **[STARTER ONLY]**
+- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
+- [Audit logs and events](audit_events.md): View the changes made within the GitLab server for:
+ - Groups and projects. **[STARTER]**
+ - Instances. **[PREMIUM ONLY]**
+- [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], create [issues by email] and
[merge requests by email], and to enable [Service Desk].
@@ -108,6 +127,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
[reply by email]: reply_by_email.md
[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email
[merge requests by email]: ../user/project/merge_requests/index.md#create-new-merge-requests-by-email
+[Service Desk]: https://docs.gitlab.com/ee/user/project/service_desk.html
## Project settings
@@ -116,13 +136,15 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet.
-- [Custom project templates](https://docs.gitlab.com/ee/user/admin_area/custom_project_templates.html): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
+- [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **[PREMIUM ONLY]**
+- [Packages](packages.md): Enable GitLab to act as a Maven repository or NPM registry. **[PREMIUM ONLY]**
### Repository settings
- [Repository checks](repository_checks.md): Periodic Git repository checks.
- [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories.
- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
+- [Limit repository size](https://docs.gitlab.com/ee/user/admin_area/settings/account_and_limit_settings.html): Set a hard limit for your repositories' size. **[STARTER ONLY]**
## Continuous Integration settings
@@ -159,6 +181,10 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
+## Analytics
+
+- [Pseudonymizer](pseudonymizer.md): Export data from GitLab's database to CSV files in a secure way. **[ULTIMATE]**
+
## Troubleshooting
- [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong
diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md
new file mode 100644
index 00000000000..b1244f44e95
--- /dev/null
+++ b/doc/administration/instance_review.md
@@ -0,0 +1,17 @@
+# Instance Review **[CORE ONLY]**
+
+> [Introduced][6995] in [GitLab Core][ee] 11.3.
+
+If you are running a medium size instance of GitLab Core edition you are qualified for a free Instance Review. You can find the button in the User menu.
+
+![Instance Review button](img/instance_review_button.png)
+
+When you click the button you will be redirected to a form with prefilled data obtained from your instance.
+
+Once you submit the data to GitLab Inc. you can see the initial report.
+
+Additionally you will be contacted by our team for further review which should help you to improve your usage of GitLab.
+
+[6995]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6995
+[ee]: https://about.gitlab.com/pricing/
+
diff --git a/doc/administration/maven_packages.md b/doc/administration/maven_packages.md
new file mode 100644
index 00000000000..d8551f64ece
--- /dev/null
+++ b/doc/administration/maven_packages.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'packages.md'
+---
+
+This document was moved to [another location](packages.md).
diff --git a/doc/administration/maven_repository.md b/doc/administration/maven_repository.md
new file mode 100644
index 00000000000..d8551f64ece
--- /dev/null
+++ b/doc/administration/maven_repository.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'packages.md'
+---
+
+This document was moved to [another location](packages.md).
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 81a6e6bca9f..48bd709a2b7 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -49,6 +49,47 @@ The following metrics are available:
| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
| unicorn_workers | Gauge | 11.11 | The number of Unicorn workers |
+## Sidekiq Metrics available for Geo **[PREMIUM]**
+
+Sidekiq jobs may also gather metrics, and these metrics can be accessed if the Sidekiq exporter is enabled (e.g. via
+the `monitoring.sidekiq_exporter` configuration option in `gitlab.yml`.
+
+| Metric | Type | Since | Description | Labels |
+|:-------------------------------------------- |:------- |:----- |:----------- |:------ |
+| geo_db_replication_lag_seconds | Gauge | 10.2 | Database replication lag (seconds) | url
+| geo_repositories | Gauge | 10.2 | Total number of repositories available on primary | url
+| geo_repositories_synced | Gauge | 10.2 | Number of repositories synced on secondary | url
+| geo_repositories_failed | Gauge | 10.2 | Number of repositories failed to sync on secondary | url
+| geo_lfs_objects | Gauge | 10.2 | Total number of LFS objects available on primary | url
+| geo_lfs_objects_synced | Gauge | 10.2 | Number of LFS objects synced on secondary | url
+| geo_lfs_objects_failed | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | url
+| geo_attachments | Gauge | 10.2 | Total number of file attachments available on primary | url
+| geo_attachments_synced | Gauge | 10.2 | Number of attachments synced on secondary | url
+| geo_attachments_failed | Gauge | 10.2 | Number of attachments failed to sync on secondary | url
+| geo_last_event_id | Gauge | 10.2 | Database ID of the latest event log entry on the primary | url
+| geo_last_event_timestamp | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | url
+| geo_cursor_last_event_id | Gauge | 10.2 | Last database ID of the event log processed by the secondary | url
+| geo_cursor_last_event_timestamp | Gauge | 10.2 | Last UNIX timestamp of the event log processed by the secondary | url
+| geo_status_failed_total | Counter | 10.2 | Number of times retrieving the status from the Geo Node failed | url
+| geo_last_successful_status_check_timestamp | Gauge | 10.2 | Last timestamp when the status was successfully updated | url
+| geo_lfs_objects_synced_missing_on_primary | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | url
+| geo_job_artifacts_synced_missing_on_primary | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | url
+| geo_attachments_synced_missing_on_primary | Gauge | 10.7 | Number of attachments marked as synced due to the file missing on the primary | url
+| geo_repositories_checksummed_count | Gauge | 10.7 | Number of repositories checksummed on primary | url
+| geo_repositories_checksum_failed_count | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | url
+| geo_wikis_checksummed_count | Gauge | 10.7 | Number of wikis checksummed on primary | url
+| geo_wikis_checksum_failed_count | Gauge | 10.7 | Number of wikis failed to calculate the checksum on primary | url
+| geo_repositories_verified_count | Gauge | 10.7 | Number of repositories verified on secondary | url
+| geo_repositories_verification_failed_count | Gauge | 10.7 | Number of repositories failed to verify on secondary | url
+| geo_repositories_checksum_mismatch_count | Gauge | 10.7 | Number of repositories that checksum mismatch on secondary | url
+| geo_wikis_verified_count | Gauge | 10.7 | Number of wikis verified on secondary | url
+| geo_wikis_verification_failed_count | Gauge | 10.7 | Number of wikis failed to verify on secondary | url
+| geo_wikis_checksum_mismatch_count | Gauge | 10.7 | Number of wikis that checksum mismatch on secondary | url
+| geo_repositories_checked_count | Gauge | 11.1 | Number of repositories that have been checked via `git fsck` | url
+| geo_repositories_checked_failed_count | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url
+| geo_repositories_retrying_verification_count | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url
+| geo_wikis_retrying_verification_count | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url
+
### Ruby metrics
Some basic Ruby runtime metrics are available:
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index f2ac155a694..ce65d77274b 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -96,13 +96,15 @@ To use an external Prometheus server:
1. Set each bundled service's [exporter](#bundled-software-metrics) to listen on a network address, for example:
- ```ruby
+ ```ruby
gitlab_monitor['listen_address'] = '0.0.0.0'
+ sidekiq['listen_address'] = '0.0.0.0'
gitlab_monitor['listen_port'] = '9168'
- gitaly['prometheus_listen_addr'] = "0.0.0.0:9236"
node_exporter['listen_address'] = '0.0.0.0:9100'
redis_exporter['listen_address'] = '0.0.0.0:9121'
postgres_exporter['listen_address'] = '0.0.0.0:9187'
+ gitaly['prometheus_listen_addr'] = "0.0.0.0:9236"
+ gitlab_workhorse['prometheus_listen_addr'] = "0.0.0.0:9229"
```
1. Install and set up a dedicated Prometheus instance, if necessary, using the [official installation instructions](https://prometheus.io/docs/prometheus/latest/installation/).
@@ -112,6 +114,18 @@ To use an external Prometheus server:
gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '192.168.0.1']
```
+1. To scrape nginx metrics, you'll also need to configure nginx to allow the Prometheus server
+ IP. For example:
+
+ ```ruby
+ nginx['status']['options'] = {
+ "server_tokens" => "off",
+ "access_log" => "off",
+ "allow" => "192.168.0.1",
+ "deny" => "all",
+ }
+ ```
+
1. [Reconfigure GitLab][reconfigure] to apply the changes
1. Edit the Prometheus server's configuration file.
1. Add each node's exporters to the Prometheus server's
@@ -203,6 +217,12 @@ The Postgres exporter allows you to measure various PostgreSQL metrics.
[➔ Read more about the Postgres exporter.](postgres_exporter.md)
+### PgBouncer exporter
+
+The PgBouncer exporter allows you to measure various PgBouncer metrics.
+
+[➔ Read more about the PgBouncer exporter.](pgbouncer_exporter.md)
+
### GitLab monitor exporter
The GitLab monitor exporter allows you to measure various GitLab metrics, pulled from Redis and the database.
diff --git a/doc/administration/monitoring/prometheus/pgbouncer_exporter.md b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md
new file mode 100644
index 00000000000..d76834fdbea
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md
@@ -0,0 +1,34 @@
+# PgBouncer exporter
+
+>**Note:**
+Available since [Omnibus GitLab 11.0][2493]. For installations from source
+you'll have to install and configure it yourself.
+
+The [PgBouncer exporter] allows you to measure various PgBouncer metrics.
+
+To enable the PgBouncer exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ pgbouncer_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect.
+
+Prometheus will now automatically begin collecting performance data from
+the PgBouncer exporter exposed under `localhost:9188`.
+
+The PgBouncer exporter will also be enabled by default if the [pgbouncer_role][postgres roles]
+is enabled.
+
+[← Back to the main Prometheus page](index.md)
+
+[2493]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2493
+[PgBouncer exporter]: https://github.com/stanhu/pgbouncer_exporter
+[postgres roles]: https://docs.gitlab.com/omnibus/roles/#postgres-roles
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/npm_registry.md b/doc/administration/npm_registry.md
new file mode 100644
index 00000000000..d8551f64ece
--- /dev/null
+++ b/doc/administration/npm_registry.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'packages.md'
+---
+
+This document was moved to [another location](packages.md).
diff --git a/doc/administration/operations/cleaning_up_redis_sessions.md b/doc/administration/operations/cleaning_up_redis_sessions.md
index b45ca99fd80..20c19445404 100644
--- a/doc/administration/operations/cleaning_up_redis_sessions.md
+++ b/doc/administration/operations/cleaning_up_redis_sessions.md
@@ -27,7 +27,7 @@ rcli() {
# This example works for Omnibus installations of GitLab 7.3 or newer. For an
# installation from source you will have to change the socket path and the
# path to redis-cli.
- sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket "$@"
+ sudo /opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.shared_state.socket "$@"
}
# test the new shell function; the response should be PONG
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
new file mode 100644
index 00000000000..286b99aceb5
--- /dev/null
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -0,0 +1,218 @@
+# Extra Sidekiq processes **[STARTER ONLY]**
+
+GitLab Enterprise Edition allows one to start an extra set of Sidekiq processes
+besides the default one. These processes can be used to consume a dedicated set
+of queues. This can be used to ensure certain queues always have dedicated
+workers, no matter the number of jobs that need to be processed.
+
+## Starting extra processes via Omnibus GitLab
+
+To enable `sidekiq-cluster`, you must apply the `sidekiq_cluster['enable'] = true`
+setting `/etc/gitlab/gitlab.rb`:
+
+```ruby
+sidekiq_cluster['enable'] = true
+```
+
+You will then specify how many additional processes to create via `sidekiq-cluster`
+as well as which queues for them to handle. This is done via the
+`sidekiq_cluster['queue_groups']` setting. This is an array whose items contain
+which queues to process. Each item in the array will equate to one additional
+sidekiq process.
+
+As an example, to make additional sidekiq processes that process the
+`elastic_indexer` and `mailers` queues, you would apply the following:
+
+```ruby
+sidekiq_cluster['queue_groups'] = [
+ "elastic_indexer",
+ "mailers"
+]
+```
+
+To have an additional sidekiq process handle multiple queues, you simply put a
+comma after the first queue name and then put the next queue name:
+
+```ruby
+sidekiq_cluster['queue_groups'] = [
+ "elastic_indexer,elastic_commit_indexer",
+ "mailers"
+]
+```
+
+Keep in mind, all changes must be followed by reconfiguring your GitLab
+application via `sudo gitlab-ctl reconfigure`.
+
+### Monitoring
+
+Once the Sidekiq processes are added, you can visit the "Background Jobs"
+section under the admin area in GitLab (`/admin/background_jobs`).
+
+![Extra sidekiq processes](img/sidekiq-cluster.png)
+
+### All queues with exceptions
+
+To have the additional sidekiq processes work on every queue EXCEPT the ones
+you list:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq_cluster['negate'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+
+### Limiting concurrency
+
+1. Edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq_cluster['concurrency'] = 25
+ ```
+
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+Keep in mind, this normally would not exceed the number of CPU cores available.
+
+### Modifying the check interval
+
+To modify the check interval for the additional Sidekiq processes:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq_cluster['interval'] = 5
+ ```
+
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+
+This tells the additional processes how often to check for enqueued jobs.
+
+## Starting extra processes via command line
+
+Starting extra Sidekiq processes can be done using the command
+`/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster`. This command
+takes arguments using the following syntax:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster [QUEUE,QUEUE,...] [QUEUE, ...]
+```
+
+Each separate argument denotes a group of queues that have to be processed by a
+Sidekiq process. Multiple queues can be processed by the same process by
+separating them with a comma instead of a space.
+
+Instead of a queue, a queue namespace can also be provided, to have the process
+automatically listen on all queues in that namespace without needing to
+explicitly list all the queue names. For more information about queue namespaces,
+see the relevant section in the
+[Sidekiq style guide](../../development/sidekiq_style_guide.md#queue-namespaces).
+
+For example, say you want to start 2 extra processes: one to process the
+"process_commit" queue, and one to process the "post_receive" queue. This can be
+done as follows:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit post_receive
+```
+
+If you instead want to start one process processing both queues you'd use the
+following syntax:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive
+```
+
+If you want to have one Sidekiq process process the "process_commit" and
+"post_receive" queues, and one process to process the "gitlab_shell" queue,
+you'd use the following:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive gitlab_shell
+```
+
+### Monitoring
+
+The `sidekiq-cluster` command will not terminate once it has started the desired
+amount of Sidekiq processes. Instead, the process will continue running and
+forward any signals to the child processes. This makes it easy to stop all
+Sidekiq processes as you simply send a signal to the `sidekiq-cluster` process,
+instead of having to send it to the individual processes.
+
+If the `sidekiq-cluster` process crashes or receives a `SIGKILL`, the child
+processes will terminate themselves after a few seconds. This ensures you don't
+end up with zombie Sidekiq processes.
+
+All of this makes monitoring the processes fairly easy. Simply hook up
+`sidekiq-cluster` to your supervisor of choice (e.g. runit) and you're good to
+go.
+
+If a child process died the `sidekiq-cluster` command will signal all remaining
+process to terminate, then terminate itself. This removes the need for
+`sidekiq-cluster` to re-implement complex process monitoring/restarting code.
+Instead you should make sure your supervisor restarts the `sidekiq-cluster`
+process whenever necessary.
+
+### PID files
+
+The `sidekiq-cluster` command can store its PID in a file. By default no PID
+file is written, but this can be changed by passing the `--pidfile` option to
+`sidekiq-cluster`. For example:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster --pidfile /var/run/gitlab/sidekiq_cluster.pid process_commit
+```
+
+Keep in mind that the PID file will contain the PID of the `sidekiq-cluster`
+command and not the PID(s) of the started Sidekiq processes.
+
+### Environment
+
+The Rails environment can be set by passing the `--environment` flag to the
+`sidekiq-cluster` command, or by setting `RAILS_ENV` to a non-empty value. The
+default value is "development".
+
+### All queues with exceptions
+
+You're able to run all queues in `sidekiq_queues.yml` file on a single or
+multiple processes with exceptions using the `--negate` flag.
+
+For example, say you want to run a single process for all queues,
+except "process_commit" and "post_receive". You can do so by executing:
+
+```bash
+sidekiq-cluster process_commit,post_receive --negate
+```
+
+For multiple processes of all queues (except "process_commit" and "post_receive"):
+
+```bash
+sidekiq-cluster process_commit,post_receive process_commit,post_receive --negate
+```
+
+### Limiting concurrency
+
+By default, `sidekiq-cluster` will spin up extra Sidekiq processes that use
+one thread per queue up to a maximum of 50. If you wish to change the cap, use
+the `-m N` option. For example, this would cap the maximum number of threads to 1:
+
+```bash
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive -m 1
+```
+
+For each queue group, the concurrency factor will be set to min(number of
+queues, N). Setting the value to 0 will disable the limit.
+
+Note that each thread requires a Redis connection, so adding threads may
+increase Redis latency and potentially cause client timeouts. See the [Sidekiq
+documentation about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis)
+for more details.
+
+## Number of threads
+
+Each process started using `sidekiq-cluster` (whether it be via command line or
+via the gitlab.rb file) starts with a number of threads that equals the number
+of queues, plus one spare thread. For example, a process that handles the
+"process_commit" and "post_receive" queues will use 3 threads in total.
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index f8d58c6ce28..69f110805b7 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -30,6 +30,19 @@ instructions will break installations using older versions of OpenSSH, such as
those included with CentOS 6 as of September 2017. If you want to use this
feature for CentOS 6, follow [the instructions on how to build and install a custom OpenSSH package](#compiling-a-custom-version-of-openssh-for-centos-6) before continuing.
+## Fast lookup is required for Geo **[PREMIUM]**
+
+By default, GitLab manages an `authorized_keys` file, which contains all the
+public SSH keys for users allowed to access GitLab. However, to maintain a
+single source of truth, [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) needs to be configured to perform SSH fingerprint
+lookups via database lookup.
+
+As part of [setting up Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions),
+you will be required to follow the steps outlined below for both the primary and
+secondary nodes, but note that the `Write to "authorized keys" file` checkbox
+only needs to be unchecked on the primary node since it will be reflected
+automatically on the secondary if database replication is working.
+
## Setting up fast lookup via GitLab Shell
GitLab Shell provides a way to authorize SSH users via a fast, indexed lookup
@@ -69,7 +82,7 @@ file will still be scanned. So git SSH performance will still be slow for many
users as long as a large file exists.
You can disable any more writes to the `authorized_keys` file by unchecking
-`Write to "authorized_keys" file` in the Application Settings of your GitLab
+`Write to "authorized_keys" file` in the **Admin Area > Settings > Network > Performance optimization** of your GitLab
installation.
![Write to authorized keys setting](img/write_to_authorized_keys_setting.png)
diff --git a/doc/administration/operations/img/sidekiq-cluster.png b/doc/administration/operations/img/sidekiq-cluster.png
new file mode 100644
index 00000000000..4eb1849010e
--- /dev/null
+++ b/doc/administration/operations/img/sidekiq-cluster.png
Binary files differ
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
index 32f36d68c50..df795a48169 100644
--- a/doc/administration/operations/index.md
+++ b/doc/administration/operations/index.md
@@ -11,6 +11,7 @@ Keep your GitLab instance up and running smoothly.
by GitLab to another file system or another server.
- [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller
to restart Sidekiq.
+- [Extra Sidekiq operations](extra_sidekiq_processes.md): Configure an extra set of Sidekiq processes to ensure certain queues always have dedicated workers, no matter the amount of jobs that need to be processed. **[STARTER ONLY]**
- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
- Speed up SSH operations by [Authorizing SSH users via a fast,
indexed lookup to the GitLab database](fast_ssh_key_lookup.md), and/or
diff --git a/doc/administration/packages.md b/doc/administration/packages.md
new file mode 100644
index 00000000000..5b9a13e3859
--- /dev/null
+++ b/doc/administration/packages.md
@@ -0,0 +1,174 @@
+# GitLab Packages administration **[PREMIUM ONLY]**
+
+GitLab Packages allows organizations to utilize GitLab as a private repository
+for a variety of common package managers. Users are able to build and publish
+packages, which can be easily consumed as a dependency in downstream projects.
+
+The Packages feature allows GitLab to act as a repository for the following:
+
+| Software repository | Description | Available in GitLab version |
+| ------------------- | ----------- | --------------------------- |
+| [Maven Repository](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ |
+| [NPM Registry](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ |
+
+Don't you see your package management system supported yet?
+Please consider contributing
+to GitLab. This [development documentation](https://docs.gitlab.com/ee/development/packages.html) will guide you through the process.
+
+## Enabling the Packages feature
+
+NOTE: **Note:**
+After the Packages feature is enabled, the repositories are available
+for all new projects by default. To enable it for existing projects, users will
+have to explicitly do so in the project's settings.
+
+To enable the Packages feature:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['packages_enabled'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. After the installation is complete, you will have to configure the `packages`
+ section in `config/gitlab.yml`. Set to `true` to enable it:
+
+ ```yaml
+ packages:
+ enabled: true
+ ```
+1. [Restart GitLab] for the changes to take effect.
+
+## Changing the storage path
+
+By default, the packages are stored locally, but you can change the default
+local location or even use object storage.
+
+### Changing the local storage path
+
+The packages for Omnibus GitLab installations are stored under
+`/var/opt/gitlab/gitlab-rails/shared/packages/` and for source
+installations under `shared/packages/` (relative to the git homedir).
+To change the local storage path:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['packages_storage_path'] = "/mnt/packages"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Edit the `packages` section in `config/gitlab.yml`:
+
+ ```yaml
+ packages:
+ enabled: true
+ storage_path: shared/packages
+ ```
+1. [Restart GitLab] for the changes to take effect.
+
+### Using object storage
+
+Instead of relying on the local storage, you can use an object storage to
+upload packages:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines (uncomment where
+ necessary):
+
+ ```ruby
+ gitlab_rails['packages_enabled'] = true
+ gitlab_rails['packages_storage_path'] = "/var/opt/gitlab/gitlab-rails/shared/packages"
+ gitlab_rails['packages_object_store_enabled'] = true
+ gitlab_rails['packages_object_store_remote_directory'] = "packages" # The bucket name.
+ gitlab_rails['packages_object_store_direct_upload'] = false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
+ gitlab_rails['packages_object_store_background_upload'] = true # Temporary option to limit automatic upload (Default: true).
+ gitlab_rails['packages_object_store_proxy_download'] = false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
+ gitlab_rails['packages_object_store_connection'] = {
+ ##
+ ## If the provider is AWS S3, uncomment the following
+ ##
+ #'provider' => 'AWS',
+ #'region' => 'eu-west-1',
+ #'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
+ #'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY',
+ ##
+ ## If the provider is other than AWS (an S3-compatible one), uncomment the following
+ ##
+ #'host' => 's3.amazonaws.com',
+ #'aws_signature_version' => 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ #'endpoint' => 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
+ #'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Edit the `packages` section in `config/gitlab.yml` (uncomment where necessary):
+
+ ```yaml
+ packages:
+ enabled: true
+ ##
+ ## The location where build packages are stored (default: shared/packages).
+ ##
+ #storage_path: shared/packages
+ object_store:
+ enabled: false
+ remote_directory: packages # The bucket name.
+ #direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
+ #background_upload: true # Temporary option to limit automatic upload (Default: true).
+ #proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
+ connection:
+ ##
+ ## If the provider is AWS S3, uncomment the following
+ ##
+ #provider: AWS
+ #region: us-east-1
+ #aws_access_key_id: AWS_ACCESS_KEY_ID
+ #aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ ##
+ ## If the provider is other than AWS (an S3-compatible one), uncomment the following
+ ##
+ #host: 's3.amazonaws.com' # default: s3.amazonaws.com.
+ #aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ #endpoint: 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
+ #path_style: false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+
+### Migrating local packages to object storage
+
+After [configuring the object storage](#using-object-storage), you may use the
+following task to migrate existing packages from the local storage to the remote one.
+The processing will be done in a background worker and requires **no downtime**.
+
+For Omnibus GitLab:
+
+```sh
+sudo gitlab-rake "gitlab:packages:migrate"
+```
+
+For installations from source:
+
+```bash
+RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:packages:migrate
+```
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md
new file mode 100644
index 00000000000..036e1d3fe61
--- /dev/null
+++ b/doc/administration/pseudonymizer.md
@@ -0,0 +1,103 @@
+# Pseudonymizer **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5532) in [GitLab Ultimate][ee] 11.1.
+
+As GitLab's database hosts sensitive information, using it unfiltered for analytics
+implies high security requirements. To help alleviate this constraint, the Pseudonymizer
+service is used to export GitLab's data in a pseudonymized way.
+
+CAUTION: **Warning:**
+This process is not impervious. If the source data is available, it's possible for
+a user to correlate data to the pseudonymized version.
+
+The Pseudonymizer currently uses `HMAC(SHA256)` to mutate fields that shouldn't
+be textually exported. This ensures that:
+
+- the end-user of the data source cannot infer/revert the pseudonymized fields
+- the referential integrity is maintained
+
+## Configuration
+
+To configure the pseudonymizer, you need to:
+
+- Provide a manifest file that describes which fields should be included or
+ pseudonymized ([example `manifest.yml` file](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/config/pseudonymizer.yml)).
+ A default manifest is provided with the GitLab installation. Using a relative file path will be resolved from the Rails root.
+ Alternatively, you can use an absolute file path.
+- Use an object storage and specify the connection parameters in the `pseudonymizer.upload.connection` configuration option.
+
+**For Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['pseudonymizer_manifest'] = 'config/pseudonymizer.yml'
+ gitlab_rails['pseudonymizer_upload_remote_directory'] = 'gitlab-elt' # bucket name
+ gitlab_rails['pseudonymizer_upload_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
+ 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY'
+ }
+ ```
+
+ NOTE: **Note:**
+ If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs.
+
+ ```ruby
+ gitlab_rails['pseudonymizer_upload_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'use_iam_profile' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure)
+ for the changes to take effect.
+
+---
+
+**For installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
+ lines:
+
+ ```yaml
+ pseudonymizer:
+ manifest: config/pseudonymizer.yml
+ upload:
+ remote_directory: 'gitlab-elt' # bucket name
+ connection:
+ provider: AWS
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: eu-central-1
+ ```
+
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source)
+ for the changes to take effect.
+
+## Usage
+
+You can optionally run the pseudonymizer using the following environment variables:
+
+- `PSEUDONYMIZER_OUTPUT_DIR` - where to store the output CSV files (defaults to `/tmp`)
+- `PSEUDONYMIZER_BATCH` - the batch size when querying the DB (defaults to `100000`)
+
+```bash
+## Omnibus
+sudo gitlab-rake gitlab:db:pseudonymizer
+
+## Source
+sudo -u git -H bundle exec rake gitlab:db:pseudonymizer RAILS_ENV=production
+```
+
+This will produce some CSV files that might be very large, so make sure the
+`PSEUDONYMIZER_OUTPUT_DIR` has sufficient space. As a rule of thumb, at least
+10% of the database size is recommended.
+
+After the pseudonymizer has run, the output CSV files should be uploaded to the
+configured object storage and deleted from the local disk.
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/administration/raketasks/geo.md b/doc/administration/raketasks/geo.md
new file mode 100644
index 00000000000..9f3b31442f3
--- /dev/null
+++ b/doc/administration/raketasks/geo.md
@@ -0,0 +1,57 @@
+# Geo Rake Tasks **[PREMIUM ONLY]**
+
+## Git housekeeping
+
+There are few tasks you can run to schedule a git housekeeping to start at the
+next repository sync in a **Secondary node**:
+
+### Incremental Repack
+
+This is equivalent of running `git repack -d` on a _bare_ repository.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake geo:git:housekeeping:incremental_repack
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake geo:git:housekeeping:incremental_repack RAILS_ENV=production
+```
+
+### Full Repack
+
+This is equivalent of running `git repack -d -A --pack-kept-objects` on a
+_bare_ repository which will optionally, write a reachability bitmap index
+when this is enabled in GitLab.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake geo:git:housekeeping:full_repack
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake geo:git:housekeeping:full_repack RAILS_ENV=production
+```
+
+### GC
+
+This is equivalent of running `git gc` on a _bare_ repository, optionally writing
+a reachability bitmap index when this is enabled in GitLab.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake geo:git:housekeeping:gc
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake geo:git:housekeeping:gc RAILS_ENV=production
+```
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index c39fef907db..42a1a1c2e60 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -43,6 +43,9 @@ If you find it necessary, you can run this migration script again to schedule mi
Any error or warning will be logged in Sidekiq's log file.
+NOTE: **Note:**
+If Geo is enabled, each project that is successfully migrated generates an event to replicate the changes on any **secondary** nodes.
+
You only need the `gitlab:storage:migrate_to_hashed` rake task to migrate your repositories, but we have additional
commands below that helps you inspect projects and attachments in both legacy and hashed storage.
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 4e1e363888d..4db3cbb9958 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -47,15 +47,15 @@ Any change in the URL will need to be reflected on disk (when groups / users or
projects are renamed). This can add a lot of load in big installations,
especially if using any type of network based filesystem.
-For GitLab Geo in particular: Geo does work with legacy storage, but in some
+CAUTION: **Caution:**
+For Geo in particular: Geo does work with legacy storage, but in some
edge cases due to race conditions it can lead to errors when a project is
renamed multiple times in short succession, or a project is deleted and
recreated under the same name very quickly. We expect these race events to be
rare, and we have not observed a race condition side-effect happening yet.
-
This pattern also exists in other objects stored in GitLab, like issue
Attachments, GitLab Pages artifacts, Docker Containers for the integrated
-Registry, etc.
+Registry, etc. Hashed storage is a requirement for Geo.
## Hashed Storage
@@ -110,7 +110,7 @@ enabled for all new projects.
To start a migration, enable Hashed Storage for new projects:
-1. Go to **Admin > Settings** and expand the **Repository Storage** section.
+1. Go to **Admin > Settings > Repository** and expand the **Repository Storage** section.
2. Select the **Use hashed storage paths for newly created and renamed projects** checkbox.
Check if the change breaks any existing integration you may have that
@@ -138,7 +138,7 @@ source-based installation.
Similar to the migration, to disable Hashed Storage for new
projects:
-1. Go to **Admin > Settings** and expand the **Repository Storage** section.
+1. Go to **Admin > Settings > Repository** and expand the **Repository Storage** section.
2. Uncheck the **Use hashed storage paths for newly created and renamed projects** checkbox.
To schedule a complete rollback, see the
diff --git a/doc/analytics/README.md b/doc/analytics/README.md
new file mode 100644
index 00000000000..6b63edb5174
--- /dev/null
+++ b/doc/analytics/README.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/index.html#user-contribution-analysis-starter)
diff --git a/doc/analytics/contribution_analytics.md b/doc/analytics/contribution_analytics.md
new file mode 100644
index 00000000000..38d71263bc1
--- /dev/null
+++ b/doc/analytics/contribution_analytics.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/group/contribution_analytics/index.html).
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 7d68d0ae744..07a6201b10b 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -2,6 +2,15 @@
Discussions are set of related notes on snippets, issues, merge requests or commits.
+This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
+
+## Discussions pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
## Issues
### List project issue discussions
@@ -144,7 +153,8 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab
### Add note to existing issue discussion
-Adds a new note to the discussion.
+Adds a new note to the discussion. This can also
+[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment).
```
POST /projects/:id/issues/:issue_iid/discussions/:discussion_id/notes
@@ -643,7 +653,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.
### Add note to existing merge request discussion
-Adds a new note to the discussion.
+Adds a new note to the discussion. This can also
+[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment).
```
POST /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes
diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md
index ec48bf4940b..cf02bbd9c92 100644
--- a/doc/api/graphql/index.md
+++ b/doc/api/graphql/index.md
@@ -29,7 +29,11 @@ curl --data "value=100" --header "PRIVATE-TOKEN: <your_access_token>" https://gi
## Available queries
-A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID.
+A first iteration of a GraphQL API includes the following queries
+
+1. `project` : Within a project it is also possible to fetch a `mergeRequest` by IID.
+
+1. `group` : Only basic group information is currently supported.
## GraphiQL
diff --git a/doc/api/group_level_variables.md b/doc/api/group_level_variables.md
index 3551bfa3f8b..7b00df6d775 100644
--- a/doc/api/group_level_variables.md
+++ b/doc/api/group_level_variables.md
@@ -22,10 +22,12 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
[
{
"key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
"value": "TEST_1"
},
{
"key": "TEST_VARIABLE_2",
+ "variable_type": "env_var",
"value": "TEST_2"
}
]
@@ -51,6 +53,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
```json
{
"key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
"value": "TEST_1"
}
```
@@ -63,12 +66,13 @@ Create a new variable.
POST /groups/:id/variables
```
-| Attribute | Type | required | Description |
-|-------------|---------|----------|-----------------------|
-| `id` | integer/string | yes | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
-| `value` | string | yes | The `value` of a variable |
-| `protected` | boolean | no | Whether the variable is protected |
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-----------------------|
+| `id` | integer/string | yes | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
@@ -78,6 +82,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
{
"key": "NEW_VARIABLE",
"value": "new value",
+ "variable_type": "env_var",
"protected": false
}
```
@@ -90,12 +95,13 @@ Update a group's variable.
PUT /groups/:id/variables/:key
```
-| Attribute | Type | required | Description |
-|-------------|---------|----------|-------------------------|
-| `id` | integer/string | yes | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable |
-| `value` | string | yes | The `value` of a variable |
-| `protected` | boolean | no | Whether the variable is protected |
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-------------------------|
+| `id` | integer/string | yes | The ID of a group or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/variables/NEW_VARIABLE" --form "value=updated value"
@@ -105,6 +111,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
{
"key": "NEW_VARIABLE",
"value": "updated value",
+ "variable_type": "env_var",
"protected": true
}
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ed4b6281acc..7992af15448 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -93,6 +93,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -227,6 +235,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -351,6 +367,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -445,6 +469,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -629,6 +661,14 @@ Parameters:
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon",
"web_url" : "https://gitlab.example.com/root"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 4,
"target_project_id": 4,
"labels": [ ],
@@ -718,6 +758,7 @@ POST /projects/:id/merge_requests
| `target_branch` | string | yes | The target branch |
| `title` | string | yes | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR |
| `target_project_id` | integer | no | The target project (numeric id) |
| `labels` | string | no | Labels for MR as a comma-separated list |
@@ -843,6 +884,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `target_branch` | string | no | The target branch |
| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
+| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR |
@@ -885,6 +927,14 @@ Must include at least one non-required attribute from above.
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1030,6 +1080,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1180,6 +1238,14 @@ Parameters:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1436,6 +1502,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1557,6 +1631,14 @@ Example response:
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
@@ -1698,6 +1780,14 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
"web_url": "https://gitlab.example.com/barrett.krajcik"
},
+ "assignees": [{
+ "name": "Miss Monserrate Beier",
+ "username": "axel.block",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/axel.block"
+ }],
"source_project_id": 3,
"target_project_id": 3,
"labels": [],
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index 3b76c19dc07..90d8c033cd6 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -92,7 +92,7 @@ Parameters:
- `description` (optional) - The description of a milestone
- `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone
-- `state_event` (optional) - The state event of the milestone (close|activate)
+- `state_event` (optional) - The state event of the milestone (close or activate)
## Delete project milestone
diff --git a/doc/api/notes.md b/doc/api/notes.md
index dd4e18b14e6..dfde80c6441 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -2,6 +2,15 @@
Notes are comments on snippets, issues or merge requests.
+This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
+
+## Notes pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
## Issues
### List project issue notes
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 50d9e007ecc..470e55425f8 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -88,6 +88,7 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/
"variables": [
{
"key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
"value": "TEST_1"
}
]
@@ -296,6 +297,7 @@ POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
```sh
curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "key=NEW_VARIABLE" --form "value=new value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables"
@@ -304,6 +306,7 @@ curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "key=N
```json
{
"key": "NEW_VARIABLE",
+ "variable_type": "env_var",
"value": "new value"
}
```
@@ -322,6 +325,7 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
```sh
curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "value=updated value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables/NEW_VARIABLE"
@@ -331,6 +335,7 @@ curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "value=
{
"key": "NEW_VARIABLE",
"value": "updated value"
+ "variable_type": "env_var",
}
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 1a4310ef328..753faec3cc8 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -114,6 +114,7 @@ Example of response
[
{
"key": "RUN_NIGHTLY_BUILD",
+ "variable_type": "env_var",
"value": "true"
},
{
@@ -135,7 +136,7 @@ POST /projects/:id/pipeline
|------------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `ref` | string | yes | Reference to commit |
-| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] |
+| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'variable_type' => 'file', 'value' => 'true' }] |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master"
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index f36e352da67..c831cc52a93 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -161,6 +161,7 @@ Parameters:
| `name` | String | yes | The name of the cluster |
| `domain` | String | no | The [base domain](../user/project/clusters/index.md#base-domain) of 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 |
| `platform_kubernetes_attributes[token]` | String | yes | 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 |
diff --git a/doc/api/project_level_variables.md b/doc/api/project_level_variables.md
index 438bebe62f5..4a6f5624394 100644
--- a/doc/api/project_level_variables.md
+++ b/doc/api/project_level_variables.md
@@ -20,10 +20,12 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
[
{
"key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
"value": "TEST_1"
},
{
"key": "TEST_VARIABLE_2",
+ "variable_type": "env_var",
"value": "TEST_2"
}
]
@@ -49,6 +51,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
```json
{
"key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
"value": "TEST_1"
}
```
@@ -61,12 +64,13 @@ Create a new variable.
POST /projects/:id/variables
```
-| Attribute | Type | required | Description |
-|-------------|---------|----------|-----------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
-| `value` | string | yes | The `value` of a variable |
-| `protected` | boolean | no | Whether the variable is protected |
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-----------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
@@ -76,6 +80,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
{
"key": "NEW_VARIABLE",
"value": "new value",
+ "variable_type": "env_var",
"protected": false
}
```
@@ -88,12 +93,13 @@ Update a project's variable.
PUT /projects/:id/variables/:key
```
-| Attribute | Type | required | Description |
-|-------------|---------|----------|-------------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable |
-| `value` | string | yes | The `value` of a variable |
-| `protected` | boolean | no | Whether the variable is protected |
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-------------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
@@ -103,6 +109,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
{
"key": "NEW_VARIABLE",
"value": "updated value",
+ "variable_type": "env_var",
"protected": true
}
```
diff --git a/doc/api/project_templates.md b/doc/api/project_templates.md
index 3b5b12c8da3..0a94a8d47ae 100644
--- a/doc/api/project_templates.md
+++ b/doc/api/project_templates.md
@@ -15,7 +15,7 @@ templates are also available from this API endpoint.
Support will be added for [Issue and Merge Request templates](../user/project/description_templates.md)
in a future release.
-Support for [Group-level file templates](../user/group/index.md#group-level-file-templates-premium)
+Support for [Group-level file templates](../user/group/index.md#group-file-templates-premium)
**[PREMIUM]** was [added](https://gitlab.com/gitlab-org/gitlab-ee/issues/5987)
in GitLab 11.5
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0a950352ecf..951961e45ff 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -494,7 +494,9 @@ GET /projects/:id
"name": "Diaspora",
"path": "diaspora",
"kind": "group",
- "full_path": "diaspora"
+ "full_path": "diaspora",
+ "avatar_url": "http://localhost:3000/uploads/group/avatar/3/foo.jpg",
+ "web_url": "http://localhost:3000/groups/diaspora"
},
"import_status": "none",
"import_error": null,
@@ -561,6 +563,8 @@ GET /projects/:id
}
```
+**Note**: The `web_url` and `avatar_url` attributes on `namespace` were [introduced][ce-27427] in GitLab 11.11.
+
If the project is a fork, and you provide a valid token to authenticate, the
`forked_from_project` field will appear in the response.
@@ -1587,3 +1591,4 @@ GET /projects/:id/snapshot
[eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium"
[ee-6137]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6137
+[ce-27427]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27427
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 46f7b1d2a25..2d91428d1c1 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -486,6 +486,7 @@ POST /runners
| `locked` | boolean| no | Whether the Runner should be locked for current project |
| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs |
| `tag_list` | Array[String] | no | List of Runner's tags |
+| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
```
diff --git a/doc/api/services.md b/doc/api/services.md
index e8ae7ff78f4..742abccb69e 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -545,7 +545,7 @@ GET /projects/:id/services/jira
Set JIRA service for a project.
> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and
-> `project_url` are replaced by `project_key`, `url`. If you are using an
+> `project_url` are replaced by `url`. If you are using an
> older version, [follow this documentation][old-jira-api].
```
@@ -557,7 +557,7 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. For example, `https://jira.example.com`. |
-| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
+| `api_url` | string | no | The base URL to the JIRA instance API. Web URL value will be used if not set. For example, `https://jira-api.example.com`. |
| `username` | string | yes | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | yes | The password of the user created to be used with GitLab/JIRA. |
| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). |
diff --git a/doc/api/users.md b/doc/api/users.md
index 606003a75e2..d3e67d3d510 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -33,7 +33,7 @@ GET /users
]
```
-You can also search for users by email or username with: `/users?search=John`
+You can also search for users by name or primary email using `?search=`. For example. `/users?search=John`.
In addition, you can lookup users by username:
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ee/index.md b/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
new file mode 100644
index 00000000000..4ce96fcb230
--- /dev/null
+++ b/doc/articles/how_to_configure_ldap_gitlab_ee/index.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html).
diff --git a/doc/ci/README.md b/doc/ci/README.md
index a7ad2018b09..27bde2ac0f1 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -3,76 +3,125 @@ comments: false
description: "Learn how to use GitLab CI/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous Delivery toolset to build, test, and deploy your application."
---
-# GitLab Continuous Integration (GitLab CI/CD)
+# GitLab CI/CD
-GitLab CI/CD is GitLab's built-in tool for software development using continuous methodology:
+GitLab CI/CD is a tool built into GitLab for software development
+through the [continuous methodologies](introduction/index.md#introduction-to-cicd-methodologies):
-- Continuous integration (CI).
-- Continuous delivery and deployment (CD).
-
-Within the [DevOps lifecycle](../README.md#the-entire-devops-lifecycle), GitLab CI/CD spans
-the [Verify (CI)](../README.md#verify) and [Release (CD)](../README.md#release) stages.
+- Continuous Integration (CI)
+- Continuous Delivery (CD)
+- Continuous Deployment (CD)
## Overview
-CI/CD is a vast area, so GitLab provides documentation for all levels of expertise. Consult the following table to find the right documentation for you:
-
-| Level of expertise | Resource |
-|:------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------|
-| New to the concepts of CI and CD | For a high-level overview, read an [introduction to CI/CD with GitLab](introduction/index.md). |
-| Familiar with GitLab CI/CD concepts | After getting familiar with GitLab CI/CD, let us walk you through a simple example in our [getting started guide](quick_start/README.md). |
-| A GitLab CI/CD expert | Jump straight to our [`.gitlab.yml`](yaml/README.md) reference. |
-
-Familiarity with GitLab Runner is also useful because it is responsible for running the jobs in your
-CI/CD pipeline. On GitLab.com, shared Runners are enabled by default so you won't need to set this up to get started.
-
-## CI/CD with Auto DevOps
-
-[Auto DevOps](../topics/autodevops/index.md) is the default minimum-configuration method for
-implementing CI/CD. Auto DevOps:
-
-- Provides simplified setup and execution of CI/CD.
-- Allows GitLab to automatically detect, build, test, deploy, and monitor your applications.
-
-## Manually configured CI/CD
-
-For complete control, you can manually configure GitLab CI/CD.
-
-### Usage
-
-With basic knowledge of how GitLab CI/CD works, the following documentation extends your knowledge
-into more features:
-
-| Topic | Description |
-|:--------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------|
-| [Creating and using CI/CD pipelines](pipelines.md) | Understand, visualize, create, and use CI/CD pipelines. |
-| [CI/CD Variables](variables/README.md) | How environment variables can be configured and made available in pipelines. |
-| [Where variables can be used](variables/where_variables_can_be_used.md) | A deeper look into where and how CI/CD variables can be used. |
-| [User](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions | Learn about the access levels a user can have for performing certain CI actions. |
-| [Configuring GitLab Runners](runners/README.md) | Documentation for configuring [GitLab Runner](https://docs.gitlab.com/runner/). |
-| [Environments and deployments](environments.md) | Deploy the output of jobs into environments for reviewing, staging, and production. |
-| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Learn about the output of jobs. |
-| [Cache dependencies in GitLab CI/CD](caching/index.md) | Discover how to speed up pipelines using caching. |
-| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. |
-| [Pipelines for merge requests](merge_request_pipelines/index.md) | Create pipelines specifically for merge requests. |
-| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. |
-| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. |
-| [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. |
-| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. |
-| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
-| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
-| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes in a per-branch basis. |
-| [Optimizing GitLab for large repositories](large_repositories/index.md) | Useful tips on how to optimize GitLab and GitLab Runner for big repositories. |
-| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. |
-| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
-
-### GitLab Pages
-
-GitLab CI/CD can be used to build and host static websites. For more information, see the
-documentation on [GitLab Pages](../user/project/pages/index.md),
-or dive right into the [CI/CD step-by-step guide for Pages](../user/project/pages/getting_started_part_four.md).
-
-### Examples
+Continuous Integration works by pushing small code chunks to your
+application's code base hosted in a Git repository, and, to every
+push, run a pipeline of scripts to build, test, and validate the
+code changes before merging them into the main branch.
+
+Continuous Delivery and Deployment consist of a step further CI,
+deploying your application to production at every
+push to the default branch of the repository.
+
+These methodologies allow you to catch bugs and errors early in
+the development cycle, ensuring that all the code deployed to
+production complies with the code standards you established for
+your app.
+
+For a complete overview of these methodologies and GitLab CI/CD,
+read the [Introduction to CI/CD with GitLab](introduction/index.md).
+
+## Getting started
+
+GitLab CI/CD is configured by a file called `.gitlab-ci.yml` placed
+at the repository's root. The scripts set in this file are executed
+by the [GitLab Runner](https://docs.gitlab.com/runner/).
+
+To get started with GitLab CI/CD, we recommend you read through
+the following documents:
+
+- [How GitLab CI/CD works](introduction/index.md#how-gitlab-cicd-works).
+- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow).
+- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md).
+
+You can also get started by using one of the
+[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates)
+available through the UI. You can use them by creating a new file,
+choosing a template that suits your application, and adjusting it
+to your needs:
+
+![Use a .gitlab-ci.yml template](img/add_file_template_11_10.png)
+
+For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide.
+
+Once you're familiar with how GitLab CI/CD works, see the
+[`. gitlab-ci.yml` full reference](yaml/README.md)
+for all the attributes you can set and use.
+
+NOTE: **Note:**
+GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#extra-shared-runners-pipeline-minutes-quota).
+
+## Configuration
+
+GitLab CI/CD supports numerous configuration options:
+
+| Configuration | Description |
+|:--------------|:-------------|
+| [Pipelines](pipelines.md) | Structure your CI/CD process through pipelines. |
+| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. |
+| [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). |
+| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
+| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
+| [Schedule pipelines](../user/project/pipelines/schedules.md) | Schedule pipelines to run as often as you need. |
+| [Custom path for `.gitlab-ci.yml`](../user/project/pipelines/settings.md#custom-ci-config-path) | Define a custom path for the CI/CD configuration file. |
+| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.|
+| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
+| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
+| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. |
+| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. |
+| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. |
+| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. |
+
+Note that certain operations can only be performed according to the
+[user](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions.
+
+## Feature set
+
+Use the vast GitLab CI/CD to easily configure it for specific purposes.
+Its feature set is listed on the table below according to DevOps stages.
+
+| Feature | Description |
+|:--------|:------------|
+| **Configure** ||
+| [Auto DevOps](../topics/autodevops/index.md) | Set up your app's entire lifecycle. |
+| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
+|---+---|
+| **Verify** ||
+| [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html) | Quickly determine the performance impact of pending code changes. |
+| [CI services](services/README.md) | Link Docker containers with your base image.|
+| [Code Quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** | Analyze your source code quality. |
+| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
+| [Interactive Web Terminals](interactive_web_terminal/index.md) **[CORE ONLY]** | Open an interactive web terminal to debug the running jobs. |
+| [JUnit tests](junit_test_reports.md) | Identify script failures directly on merge requests. |
+| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
+|---+---|
+| **Release** ||
+| [Auto Deploy](../topics/autodevops/index.md#auto-deploy) | Deploy your application to a production environment in a Kubernetes cluster. |
+| [Building Docker images](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
+| [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) **[PREMIUM]** | Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature. |
+| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. |
+| [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html) **[PREMIUM]** | Deploy your features behind Feature Flags. |
+| [GitLab Pages](../user/project/pages/index.md) | Deploy static websites. |
+| [GitLab Releases](../user/project/releases/index.md) | Add release notes to Git tags. |
+| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes. |
+|---+---|
+| **Secure** ||
+| [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]** | Check your Docker containers for known vulnerabilities.|
+| [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]** | Analyze your dependencies for known vulnerabilities. |
+| [License Management](https://docs.gitlab.com/ee/user/application_security/license_management/) **[ULTIMATE]** | Search your project dependencies for their licenses. |
+| [Security Test reports](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate) **[ULTIMATE]** | Check for app vulnerabilities. |
+
+## Examples
GitLab provides examples of configuring GitLab CI/CD in the form of:
@@ -81,9 +130,10 @@ GitLab provides examples of configuring GitLab CI/CD in the form of:
- [`multi-project-pipelines`](https://gitlab.com/gitlab-examples/multi-project-pipelines) for examples of implementing multi-project pipelines.
- [`review-apps-nginx`](https://gitlab.com/gitlab-examples/review-apps-nginx/) provides an example of using Review Apps.
-### Administration
+## Administration **[CORE ONLY]**
-As a GitLab administrator, you can change the default behavior of GitLab CI/CD for:
+As a GitLab administrator, you can change the default behavior
+of GitLab CI/CD for:
- An [entire GitLab instance](../user/admin_area/settings/continuous_integration.md).
- Specific projects, using [pipelines settings](../user/project/pipelines/settings.md).
@@ -93,33 +143,22 @@ See also:
- [How to enable or disable GitLab CI/CD](enable_or_disable_ci.md).
- Other [CI administration settings](../administration/index.md#continuous-integration-settings).
-### Using Docker
-
-Docker is commonly used with GitLab CI/CD. Learn more about how to to accomplish this with the following
-documentation:
-
-| Topic | Description |
-|:-------------------------------------------------------------------------|:-------------------------------------------------------------------------|
-| [Using Docker images](docker/using_docker_images.md) | Use GitLab and GitLab Runner with Docker to build and test applications. |
-| [Building Docker images with GitLab CI/CD](docker/using_docker_build.md) | Maintain Docker-based projects using GitLab CI/CD. |
-
-Related topics include:
-
-- [Docker integration](docker/README.md).
-- [CI services (linked Docker containers)](services/README.md).
+## References
-## Why GitLab CI/CD?
+### Why GitLab CI/CD?
-The following articles explain reasons to use GitLab CI/CD for your CI/CD infrastructure:
+The following articles explain reasons to use GitLab CI/CD
+for your CI/CD infrastructure:
- [Why we chose GitLab CI for our CI/CD solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
- [Building our web-app on GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation.
-## Breaking changes
+### Breaking changes
-As GitLab CI/CD has evolved, certain breaking changes have been necessary. These are:
+As GitLab CI/CD has evolved, certain breaking changes have
+been necessary. These are:
- [CI variables renaming for GitLab 9.0](variables/deprecated_variables.md#gitlab-90-renamed-variables). Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
diff --git a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
new file mode 100644
index 00000000000..1c8e12d229c
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
@@ -0,0 +1,147 @@
+# Using GitLab CI/CD with a Bitbucket Cloud repository **[PREMIUM]**
+
+GitLab CI/CD can be used with Bitbucket Cloud by creating a
+[CI/CD project](https://docs.gitlab.com/ee/user/project/ci_cd_for_external_repo.html) and connecting
+your Git repository via URL.
+
+1. In GitLab create a **CI/CD for external repo**, select **Repo by URL** and
+ create the project.
+
+ ![Create project](img/external_repository.png)
+
+ GitLab will import the repository and enable [Pull Mirroring][pull-mirroring].
+
+1. In GitLab create a
+ [Personal Access Token](../../user/profile/personal_access_tokens.md)
+ with `api` scope. This will be used to authenticate requests from the web
+ hook that will be created in Bitbucket to notify GitLab of new commits.
+
+1. In Bitbucket from **Settings > Webhooks** create a new web hook to notify
+ GitLab of new commits.
+
+ The web hook URL should be set to the GitLab API to trigger pull mirroring,
+ using the Personal Access Token we just generated for authentication.
+
+ ```
+ https://gitlab.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>/mirror/pull?private_token=<PERSONAL_ACCESS_TOKEN>
+ ```
+
+ The web hook Trigger should be set to 'Repository Push'.
+
+ ![Bitbucket Cloud webhook](img/bitbucket_webhook.png)
+
+ After saving, test the web hook by pushing a change to your Bitbucket
+ repository.
+
+1. In Bitbucket create an **App Password** from **Bitbucket Settings > App
+ Passwords** to authenticate the build status script setting commit build
+ statuses in Bitbucket. Repository write permissions are required.
+
+ ![Bitbucket Cloud webhook](img/bitbucket_app_password.png)
+
+1. In GitLab from **Settings > CI/CD > Environment variables** add variables to allow
+ communication with Bitbucket via the Bitbucket API.
+
+ `BITBUCKET_ACCESS_TOKEN`: the Bitbucket app password created above
+
+ `BITBUCKET_USERNAME`: the username of the Bitbucket account
+
+ `BITBUCKET_NAMESPACE`: set this if your GitLab and Bitbucket namespaces differ
+
+ `BITBUCKET_REPOSITORY`: set this if your GitLab and Bitbucket project names differ
+
+1. In Bitbucket add a script to push the pipeline status to Bitbucket.
+
+ > Note: changes made in GitLab will be overwritten by any changes made
+ upstream in Bitbucket.
+
+ Create a file `build_status` and insert the script below and run
+ `chmod +x build_status` in your terminal to make the script executable.
+
+ ```bash
+ #!/usr/bin/env bash
+
+ # Push GitLab CI/CD build status to Bitbucket Cloud
+
+ if [ -z "$BITBUCKET_ACCESS_TOKEN" ]; then
+ echo "ERROR: BITBUCKET_ACCESS_TOKEN is not set"
+ exit 1
+ fi
+ if [ -z "$BITBUCKET_USERNAME" ]; then
+ echo "ERROR: BITBUCKET_USERNAME is not set"
+ exit 1
+ fi
+ if [ -z "$BITBUCKET_NAMESPACE" ]; then
+ echo "Setting BITBUCKET_NAMESPACE to $CI_PROJECT_NAMESPACE"
+ BITBUCKET_NAMESPACE=$CI_PROJECT_NAMESPACE
+ fi
+ if [ -z "$BITBUCKET_REPOSITORY" ]; then
+ echo "Setting BITBUCKET_REPOSITORY to $CI_PROJECT_NAME"
+ BITBUCKET_REPOSITORY=$CI_PROJECT_NAME
+ fi
+
+ BITBUCKET_API_ROOT="https://api.bitbucket.org/2.0"
+ BITBUCKET_STATUS_API="$BITBUCKET_API_ROOT/repositories/$BITBUCKET_NAMESPACE/$BITBUCKET_REPOSITORY/commit/$CI_COMMIT_SHA/statuses/build"
+ BITBUCKET_KEY="ci/gitlab-ci/$CI_JOB_NAME"
+
+ case "$BUILD_STATUS" in
+ running)
+ BITBUCKET_STATE="INPROGRESS"
+ BITBUCKET_DESCRIPTION="The build is running!"
+ ;;
+ passed)
+ BITBUCKET_STATE="SUCCESSFUL"
+ BITBUCKET_DESCRIPTION="The build passed!"
+ ;;
+ failed)
+ BITBUCKET_STATE="FAILED"
+ BITBUCKET_DESCRIPTION="The build failed."
+ ;;
+ esac
+
+ echo "Pushing status to $BITBUCKET_STATUS_API..."
+ curl --request POST $BITBUCKET_STATUS_API \
+ --user $BITBUCKET_USERNAME:$BITBUCKET_ACCESS_TOKEN \
+ --header "Content-Type:application/json" \
+ --silent \
+ --data "{ \"state\": \"$BITBUCKET_STATE\", \"key\": \"$BITBUCKET_KEY\", \"description\":
+ \"$BITBUCKET_DESCRIPTION\",\"url\": \"$CI_PROJECT_URL/-/jobs/$CI_JOB_ID\" }"
+ ```
+
+1. Still in Bitbucket, create a `.gitlab-ci.yml` file to use the script to push
+ pipeline success and failures to Bitbucket.
+
+ ```
+ stages:
+ - test
+ - ci_status
+
+ unit-tests:
+ script:
+ - echo "Success. Add your tests!"
+
+ success:
+ stage: ci_status
+ before_script:
+ - ""
+ after_script:
+ - ""
+ script:
+ - BUILD_STATUS=passed BUILD_KEY=push ./build_status
+ when: on_success
+
+ failure:
+ stage: ci_status
+ before_script:
+ - ""
+ after_script:
+ - ""
+ script:
+ - BUILD_STATUS=failed BUILD_KEY=push ./build_status
+ when: on_failure
+ ```
+
+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
diff --git a/doc/ci/ci_cd_for_external_repos/github_integration.md b/doc/ci/ci_cd_for_external_repos/github_integration.md
new file mode 100644
index 00000000000..0e2acf957e0
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/github_integration.md
@@ -0,0 +1,111 @@
+# Using GitLab CI/CD with a GitHub repository **[PREMIUM]**
+
+GitLab CI/CD can be used with **GitHub.com** and **GitHub Enterprise** by
+creating a [CI/CD project](https://docs.gitlab.com/ee/user/project/ci_cd_for_external_repo.html) to connect your GitHub repository to
+GitLab.
+
+NOTE: **Note:**
+To use **GitHub Enterprise** with **GitLab.com** you should use the
+manual method.
+
+## Connect with GitHub integration
+
+If the [GitHub integration](../../integration/github.md) has been enabled by your GitLab
+administrator:
+
+NOTE: **Note:**
+Due to a 10-token limitation on the [GitHub OAuth Implementation](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#creating-multiple-tokens-for-oauth-apps),
+if you import more than 10 times, your oldest imported project's token will be
+revoked. See issue [#9147](https://gitlab.com/gitlab-org/gitlab-ee/issues/9147)
+for more information.
+
+1. In GitLab create a **CI/CD for external repo** project and select
+ **GitHub**.
+
+ ![Create project](img/github_omniauth.png)
+
+1. Once authenticated, you will be redirected to a list of your repositories to
+ connect. Click **Connect** to select the repository.
+
+ ![Create project](img/github_repo_list.png)
+
+1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md).
+
+GitLab will import the project, enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter), enable
+[GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html), and create a web hook
+on GitHub to notify GitLab of new commits.
+
+## Connect with Personal Access Token
+
+NOTE: **Note:** Personal access tokens can only be used to connect GitHub.com
+repositories to GitLab.
+
+If you are not using the [GitHub integration](../../integration/github.md), you can
+still 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
+ statuses to GitHub.
+
+ The `repo` and `admin:repo_hook` should be enable to allow GitLab access to
+ your project, update commit statuses, and create a web hook to notify
+ GitLab of new commits.
+
+1. In GitLab create a **CI/CD for external repo** project and select
+ **GitHub**.
+
+ ![Create project](img/github_omniauth.png)
+
+1. Paste the token into the **Personal access token** field and click **List
+ Repositories**. Click **Connect** to select the repository.
+
+1. In GitHub, add a `.gitlab-ci.yml` to [configure GitLab CI/CD](../quick_start/README.md).
+
+GitLab will import the project, enable [Pull Mirroring](../../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter), enable
+[GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html), and create a web hook
+on GitHub to notify GitLab of new commits.
+
+## Connect manually
+
+If the [GitHub integration](../../integration/github.md) is not enabled, or is enabled
+for a different GitHub instance, you GitLab CI/CD can be manually enabled for
+your repository.
+
+1. In GitHub open https://github.com/settings/tokens/new create a **Personal
+ Access Token.** GitLab will use this token to access your repository and
+ push commit statuses.
+
+ Enter a **Token description** and update the scope to allow:
+
+ `repo` so that GitLab can access your project and update commit statuses
+
+1. In GitLab create a **CI/CD project** using the Git URL option and the HTTPS
+ URL for your GitHub repository. If your project is private, use the personal
+ access token you just created for authentication.
+
+ GitLab will automatically configure polling-based pull mirroring.
+
+1. Still in GitLab, enable the [GitHub project integration](https://docs.gitlab.com/ee/user/project/integrations/github.html)
+ from **Settings > Integrations.**
+
+ Check the **Active** checkbox to enable the integration, paste your
+ personal access token and HTTPS repository URL into the form, and **Save.**
+
+1. Still in GitLab create a **Personal Access Token** with `API` scope to
+ authenticate the GitHub web hook notifying GitLab of new commits.
+
+1. In GitHub from **Settings > Webhooks** create a web hook to notify GitLab of
+ new commits.
+
+ The web hook URL should be set to the GitLab API to
+ [trigger pull mirroring](https://docs.gitlab.com/ee/api/projects.html#start-the-pull-mirroring-process-for-a-project-starter),
+ using the GitLab personal access token we just created.
+
+ ```
+ https://gitlab.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>/mirror/pull?private_token=<PERSONAL_ACCESS_TOKEN>
+ ```
+
+ ![Create web hook](img/github_push_webhook.png)
+
+1. In GitHub add a `.gitlab-ci.yml` to configure GitLab CI/CD.
diff --git a/doc/ci/ci_cd_for_external_repos/img/bitbucket_app_password.png b/doc/ci/ci_cd_for_external_repos/img/bitbucket_app_password.png
new file mode 100644
index 00000000000..ac5be3c3058
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/bitbucket_app_password.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/bitbucket_webhook.png b/doc/ci/ci_cd_for_external_repos/img/bitbucket_webhook.png
new file mode 100644
index 00000000000..0a3476d9035
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/bitbucket_webhook.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/ci_cd_for_external_repo.png b/doc/ci/ci_cd_for_external_repos/img/ci_cd_for_external_repo.png
new file mode 100644
index 00000000000..f068688146b
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/ci_cd_for_external_repo.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/external_repository.png b/doc/ci/ci_cd_for_external_repos/img/external_repository.png
new file mode 100644
index 00000000000..b850d91f56b
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/external_repository.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/github_omniauth.png b/doc/ci/ci_cd_for_external_repos/img/github_omniauth.png
new file mode 100644
index 00000000000..71a3a057a41
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/github_omniauth.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png b/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png
new file mode 100644
index 00000000000..3f2059504f5
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/github_push_webhook.png b/doc/ci/ci_cd_for_external_repos/img/github_push_webhook.png
new file mode 100644
index 00000000000..e8c17d664e1
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/github_push_webhook.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png b/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png
new file mode 100644
index 00000000000..73579dd3cf1
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/img/github_repo_list.png
Binary files differ
diff --git a/doc/ci/ci_cd_for_external_repos/index.md b/doc/ci/ci_cd_for_external_repos/index.md
new file mode 100644
index 00000000000..450ffe512dc
--- /dev/null
+++ b/doc/ci/ci_cd_for_external_repos/index.md
@@ -0,0 +1,31 @@
+# GitLab CI/CD for external repositories **[PREMIUM]**
+
+NOTE: **Note:**
+This feature [is available for free](https://about.gitlab.com/2019/03/21/six-more-months-ci-cd-github/) to
+GitLab.com users until September 22nd, 2019.
+
+>[Introduced][ee-4642] in [GitLab Premium][eep] 10.6.
+
+GitLab CI/CD can be used with GitHub or any other Git server.
+Instead of moving your entire project to GitLab, you can connect your
+external repository to get the benefits of GitLab CI/CD.
+
+- [GitHub](github_integration.md)
+- [Bitbucket Cloud](bitbucket_integration.md)
+
+Connecting an external repository will set up [repository mirroring][mirroring]
+and create a lightweight project where issues, merge requests, wiki, and
+snippets disabled. These features
+[can be re-enabled later][settings].
+
+1. From your GitLab dashboard click **New project**
+1. Switch to the **CI/CD for external repo** tab
+1. Choose **GitHub** or **Repo by URL**
+1. The next steps are similar to the [import flow](../../user/project/import/index.md)
+
+![CI/CD for external repository project creation](img/ci_cd_for_external_repo.png)
+
+[ee-4642]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4642
+[eep]: https://about.gitlab.com/pricing/
+[mirroring]: ../../workflow/repository_mirroring.md
+[settings]: ../../user/project/settings/index.md#sharing-and-permissions
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3e52cc786dd..d5e6fbe8113 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -16,11 +16,11 @@ For example:
1. Test your code.
1. Deploy your code into a testing or staging environment before you release it to the public.
-This helps prevent bugs not only in your software, but in the deployment process as well.
+This helps find bugs in your software, and also in the deployment process as well.
GitLab CI/CD is capable of not only testing or building your projects, but also
deploying them in your infrastructure, with the added benefit of giving you a
-way to track your deployments. In other words, you can always know what is
+way to track your deployments. In other words, you will always know what is
currently being deployed or has been deployed on your servers.
It's important to know that:
@@ -31,12 +31,12 @@ It's important to know that:
GitLab:
-- Provides a full history of your deployments per every environment.
+- Provides a full history of your deployments for each environment.
- Keeps track of your deployments, so you always know what is currently being deployed on your
servers.
If you have a deployment service such as [Kubernetes](../user/project/clusters/index.md)
-enabled for your project, you can use it to assist with your deployments, and
+associated with your project, you can use it to assist with your deployments, and
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
## Configuring environments
@@ -46,8 +46,8 @@ Configuring environments involves:
1. Understanding how [pipelines](pipelines.md) work.
1. Defining environments in your project's [`.gitlab-ci.yml`](yaml/README.md) file.
-The rest of this section illustrates how to configure environments and deployments using an example.
-It assumes you have already:
+The rest of this section illustrates how to configure environments and deployments using
+an example scenario. It assumes you have already:
- Created a [project](../gitlab-basics/create-project.md) in GitLab.
- Set up [a Runner](runners/README.md).
@@ -94,9 +94,8 @@ We have defined 3 [stages](yaml/README.md#stages):
- `build`
- `deploy`
-The jobs assigned to these stages will run in this order. If a job fails, then
-the jobs that are assigned to the next stage won't run, rendering the pipeline
-as failed.
+The jobs assigned to these stages will run in this order. If any job fails, then
+the pipeline fails and jobs that are assigned to the next stage won't run.
In our case:
@@ -104,15 +103,15 @@ In our case:
- Then the `build` job.
- Lastly the `deploy_staging` job.
-With this configuration, we ensure that:
+With this configuration, we:
-- The tests pass.
-- Our app is able to be built successfully.
+- Check that the tests pass.
+- Ensure that our app is able to be built successfully.
- Lastly we deploy to the staging server.
NOTE: **Note:**
The `environment` keyword is just a hint for GitLab that this job actually
-deploys to this environment's `name`. It can also have a `url` that is
+deploys to the `name` environment. It can also have a `url` that is
exposed in various places within GitLab. Each time a job that
has an environment specified succeeds, a deployment is recorded, storing
the Git SHA and environment name.
@@ -134,14 +133,13 @@ In summary, with the above `.gitlab-ci.yml` we have achieved the following:
> etc.
> Starting with GitLab 9.3, the environment URL is exposed to the Runner via
-> `$CI_ENVIRONMENT_URL`. The URL would be expanded from `.gitlab-ci.yml`, or if
-> the URL was not defined there, the external URL from the environment would be
-> used.
+> `$CI_ENVIRONMENT_URL`. The URL is expanded from `.gitlab-ci.yml`, or if
+> the URL was not defined there, the external URL from the environment is used.
### Configuring manual deployments
-Converting automatically executed job into jobs requiring to a manual action involves
-adding `when: manual` to the job's configuration.
+Adding `when: manual` to an automatically executed job's configuration converts it to
+a job requiring manual action.
To expand on the [previous example](#defining-environments), the following includes
another job that deploys our app to a production server and is
@@ -187,7 +185,7 @@ deploy_prod:
The `when: manual` action:
-- Exposes a "play" button in GitLab's UI.
+- Exposes a "play" button in GitLab's UI for that job.
- Means the `deploy_prod` job will only be triggered when the "play" button is clicked.
You can find the "play" button in the pipelines, environments, deployments, and jobs views.
@@ -200,8 +198,8 @@ You can find the "play" button in the pipelines, environments, deployments, and
| Deployments | ![Deployments manual action](img/environments_manual_action_deployments.png) |
| Jobs | ![Builds manual action](img/environments_manual_action_jobs.png) |
-Clicking on the play button in any view will trigger the `deploy_prod` job, and the deployment will be recorded under a new
-environment named `production`.
+Clicking on the play button in any view will trigger the `deploy_prod` job, and the
+deployment will be recorded as a new environment named `production`.
NOTE: **Note:**
If your environment's name is `production` (all lowercase),
@@ -209,14 +207,13 @@ it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
### Configuring dynamic environments
-Other environments are good for deploying to stable environments like staging or production.
+Regular environments are good when deploying to "stable" environments like staging or production.
-However, what about environments for branches other than `master`? Dynamic environments can be used to achieve these.
-
-Dynamic environments make it possible to create environments on the fly by
+However, for environments for branches other than `master`, dynamic environments
+can be used. Dynamic environments make it possible to create environments on the fly by
declaring their names dynamically in `.gitlab-ci.yml`.
-Dynamic environments form the basis of [Review apps](review_apps/index.md).
+Dynamic environments are a fundamental part of [Review apps](review_apps/index.md).
#### Allowed variables
@@ -237,10 +234,10 @@ For more information, see [Where variables can be used](variables/where_variable
#### Example configuration
-GitLab Runner exposes various [environment variables](variables/README.md) when a job runs and so
+GitLab Runner exposes various [environment variables](variables/README.md) when a job runs, so
you can use them as environment names.
-In the following example, a job will deploy to all branches except `master`:
+In the following example, the job will deploy to all branches except `master`:
```yaml
deploy_review:
@@ -261,28 +258,33 @@ In this example:
- The job's name is `deploy_review` and it runs on the `deploy` stage.
- We set the `environment` with the `environment:name` as `review/$CI_COMMIT_REF_NAME`.
Since the [environment name](yaml/README.md#environmentname) can contain slashes (`/`), we can
- use this pattern to distinguish between dynamic environments and the regular ones.
-- We tell the job to run [`only`](yaml/README.md#onlyexcept-basic) on branches [`except`](yaml/README.md#onlyexcept-basic) `master`.
+ use this pattern to distinguish between dynamic and regular environments.
+- We tell the job to run [`only`](yaml/README.md#onlyexcept-basic) on branches,
+ [`except`](yaml/README.md#onlyexcept-basic) `master`.
For the value of:
- `environment:name`, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`,
- which takes the value of the branch name.
-- `environment:url`, since `$CI_COMMIT_REF_NAME` itself may also contain `/`, or other characters that
- would be invalid in a domain name or URL, we use `$CI_ENVIRONMENT_SLUG` so that the environment can get a specific and distinct URL for each branch.
+ which receives the value of the branch name.
+- `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME`
+ may contain a `/` or other characters that would be invalid in a domain name or URL,
+ so we use `$CI_ENVIRONMENT_SLUG` to get a "clean" or "safe" URL.
For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
- You could also use `$CI_COMMIT_REF_SLUG` in `environment:url`. For example, `https://$CI_COMMIT_REF_SLUG.example.com`.
- 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 are unlikely and you may prefer environment names to be more closely based on the branch name. The example
- above would give you an URL like `https://100-do-the-thing.example.com`.
+ 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
+ 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
+ of `https://100-do-the-thing.example.com`.
NOTE: **Note:**
-You are not bound to use the same prefix or only slashes in the dynamic
-environments' names (`/`). However, this will enable the [grouping similar environments](#grouping-similar-environments) feature.
+You are not required to use the same prefix or only slashes (`/`) in the dynamic environments'
+names. However, using this format will enable the [grouping similar environments](#grouping-similar-environments)
+feature.
### Complete example
@@ -292,7 +294,7 @@ The configuration in this section provides a full development workflow where you
- Built.
- Deployed as a Review App.
- Deployed to a staging server once the merge request is merged.
-- Finally, manually deployed to the production server.
+- Finally, able to be manually deployed to the production server.
The following combines the previous configuration examples, including:
@@ -348,8 +350,8 @@ deploy_prod:
- master
```
-A more realistic example would include copying files to a location where a
-webserver (for example, NGINX) could then read and serve.
+A more realistic example would also include copying files to a location where a
+webserver (for example, NGINX) could then acess and serve them.
The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
@@ -366,32 +368,33 @@ review_app:
This example requires that NGINX and GitLab Runner are set up on the server this job will run on.
NOTE: **Note:**
-See the [limitations](#limitations) section for some edge cases regarding naming of your branches and Review Apps.
+See the [limitations](#limitations) section for some edge cases regarding the naming of
+your branches and Review Apps.
-The complete example provides the following workflow for developers:
+The complete example provides the following workflow to developers:
- Create a branch locally.
-- Make changes and commit them
+- Make changes and commit them.
- Push the branch to GitLab.
- Create a merge request.
-Behind the scenes, GitLab runner will:
+Behind the scenes, GitLab Runner will:
- Pick up the changes and start running the jobs.
- Run the jobs sequentially as defined in `stages`:
- First, run the tests.
- If the tests succeed, build the app.
- - If the build succeeds, the app will be is deployed to an environment with a name specific to the
+ - If the build succeeds, the app is deployed to an environment with a name specific to the
branch.
So now, every branch:
- Gets its own environment.
-- Is deployed to its own location, with the added benefit of:
+- Is deployed to its own unique location, with the added benefit of:
- Having a [history of deployments](#viewing-deployment-history).
- Being able to [rollback changes](#retrying-and-rolling-back) if needed.
-For more information on using the URL, see [Using the environment URL](#using-the-environment-url).
+For more information, see [Using the environment URL](#using-the-environment-url).
### Protected environments
@@ -401,11 +404,12 @@ For more information, see [Protected environments](environments/protected_enviro
## Working with environments
-Having configured environments, GitLab provides many features to work with them. These are documented below.
+Once environments are configured, GitLab provides many features for working with them,
+as documented below.
### Viewing environments and deployments
-A list of environments and deployment statuses is available on project's **Operations > Environments** page.
+A list of environments and deployment statuses is available on each project's **Operations > Environments** page.
For example:
@@ -416,11 +420,11 @@ This example shows:
- The environment's name with a link to its deployments.
- The last deployment ID number and who performed it.
- The job ID of the last deployment with its respective job name.
-- The commit information of the last deployment such as who committed, to what
+- The commit information of the last deployment, such as who committed it, to what
branch, and the Git SHA of the commit.
- The exact time the last deployment was performed.
-- A button that takes you to the URL that you have defined under the
- `environment` keyword in `.gitlab-ci.yml`.
+- A button that takes you to the URL that you defined under the `environment` keyword
+ in `.gitlab-ci.yml`.
- A button that re-deploys the latest deployment, meaning it runs the job
defined by the environment name for that specific commit.
@@ -432,8 +436,8 @@ deployments, but an environment can have multiple deployments.
> - While you can create environments manually in the web interface, we recommend
> that you define your environments in `.gitlab-ci.yml` first. They will
> be automatically created for you after the first deploy.
-> - The environments page can only be viewed by Reporters and above. For more
-> information on the permissions, see the [permissions documentation](../user/permissions.md).
+> - The environments page can only be viewed by users with [Reporter permission](../user/permissions.md#project-members-permissions)
+> and above. For more information on permissions, see the [permissions documentation](../user/permissions.md).
> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured
> will show up in the **Environment** and **Last deployment** lists.
@@ -442,7 +446,7 @@ deployments, but an environment can have multiple deployments.
GitLab keeps track of your deployments, so you:
- Always know what is currently being deployed on your servers.
-- Can have the full history of your deployments per every environment.
+- Can have the full history of your deployments for every environment.
Clicking on an environment shows the history of its deployments. Here's an example **Environments** page
with multiple deployments:
@@ -460,9 +464,9 @@ To retry or rollback a deployment:
1. Navigate to **Operations > Environments**.
1. Click on the environment.
-1. On the page that lists the deployment history for the environment, click the:
- - **Rollback** button against a previously successful deployment, to roll back to that deployment.
- - **Retry** button against the last deployment, to retry that deployment.
+1. In the deployment history list for the environment, click the:
+ - **Retry** button next to the last deployment, to retry that deployment.
+ - **Rollback** button next to a previously successful deployment, to roll back to that deployment.
NOTE: **Note:**
The defined deployment process in the job's `script` determines whether the rollback succeeds or not.
@@ -470,9 +474,7 @@ The defined deployment process in the job's `script` determines whether the roll
### Using the environment URL
The [environment URL](yaml/README.md#environmenturl) is exposed in a few
-places within GitLab.
-
-These are:
+places within GitLab:
- In a merge request widget as a link:
![Environment URL in merge request](img/environments_mr_review_app.png)
@@ -493,27 +495,28 @@ For example:
#### Going from source files to public pages
With GitLab's [Route Maps](review_apps/index.md#route-maps) you can go directly
-from source files to public pages on the environment set for Review Apps.
+from source files to public pages in the environment set for Review Apps.
### Stopping an environment
Stopping an environment:
-- Moves it from the list of **Available** environments to the list of **Stopped** environments on the [**Environments** page](#viewing-environments-and-deployments).
+- Moves it from the list of **Available** environments to the list of **Stopped**
+ environments on the [**Environments** page](#viewing-environments-and-deployments).
- Executes an [`on_stop` action](yaml/README.md#environmenton_stop), if defined.
This is often used when multiple developers are working on a project at the same time,
each of them pushing to their own branches, causing many dynamic environments to be created.
NOTE: **Note:**
-Starting with GitLab 8.14, dynamic environments will be stopped automatically
+Starting with GitLab 8.14, dynamic environments are stopped automatically
when their associated branch is deleted.
#### Automatically stopping an environment
Environments can be stopped automatically using special configuration.
-Consider the following example where the `deploy_review` calls the `stop_review`
+Consider the following example where the `deploy_review` job calls `stop_review`
to clean up and stop the environment:
```yaml
@@ -542,14 +545,14 @@ stop_review:
action: stop
```
-Setting the [`GIT_STRATEGY`](yaml/README.md#git-strategy) to `none` is necessary on the
-`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't try to check out the code
-after the branch is deleted.
+Setting the [`GIT_STRATEGY`](yaml/README.md#git-strategy) to `none` is necessary in the
+`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't
+try to check out the code after the branch is deleted.
When you have an environment that has a stop action defined (typically when
the environment describes a Review App), GitLab will automatically trigger a
stop action when the associated branch is deleted. The `stop_review` job must
-be in the same `stage` as the `deploy_review` one in order for the environment
+be in the same `stage` as the `deploy_review` job in order for the environment
to automatically stop.
You can read more in the [`.gitlab-ci.yml` reference](yaml/README.md#environmenton_stop).
@@ -562,8 +565,8 @@ As documented in [Configuring dynamic environments](#configuring-dynamic-environ
prepend environment name with a word, followed by a `/`, and finally the branch
name, which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
-In short, environments that are named like `type/foo` are presented under a
-group named `type`.
+In short, environments that are named like `type/foo` are all presented under the same
+group, named `type`.
In our [minimal example](#example-configuration), we named the environments `review/$CI_COMMIT_REF_NAME`
where `$CI_COMMIT_REF_NAME` is the branch name. Here is a snippet of the example:
@@ -588,13 +591,14 @@ exist, you should see something like:
>
> - For the monitoring dashboard to appear, you need to:
> - Enable the [Prometheus integration](../user/project/integrations/prometheus.md).
-> - Configure Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/index.md)
+> - Configure Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/index.md).
> - With GitLab 9.2, all deployments to an environment are shown directly on the monitoring dashboard.
-If you have enabled [Prometheus for monitoring system and response metrics](../user/project/integrations/prometheus.md), you can monitor the performance behavior of your app running in each environment.
+If you have enabled [Prometheus for monitoring system and response metrics](../user/project/integrations/prometheus.md),
+you can monitor the behavior of your app running in each environment.
-Once configured, GitLab will attempt to retrieve [supported performance metrics](../user/project/integrations/prometheus_library/index.md) for any
-environment that has had a successful deployment. If monitoring data was
+Once configured, GitLab will attempt to retrieve [supported performance metrics](../user/project/integrations/prometheus_library/index.md)
+for any environment that has had a successful deployment. If monitoring data was
successfully retrieved, a **Monitoring** button will appear for each environment.
![Environment Detail with Metrics](img/deployments_view.png)
@@ -604,8 +608,8 @@ Clicking on the **Monitoring** button will display a new page showing up to the
after initial deployment.
All deployments to an environment are shown directly on the monitoring dashboard,
-which allows easy correlation between any changes in performance and a new
-version of the app, all without leaving GitLab.
+which allows easy correlation between any changes in performance and new
+versions of the app, all without leaving GitLab.
![Monitoring dashboard](img/environments_monitoring.png)
@@ -617,8 +621,8 @@ If you deploy to your environments with the help of a deployment service (for ex
the [Kubernetes integration](../user/project/clusters/index.md)), GitLab can open
a terminal session to your environment.
-This is a powerful feature that allows you to debug issues without leaving the comfort of your web browser. To
-enable it, just follow the instructions given in the service integration
+This is a powerful feature that allows you to debug issues without leaving the comfort
+of your web browser. To enable it, just follow the instructions given in the service integration
documentation.
Once enabled, your environments will gain a "terminal" button:
@@ -663,8 +667,9 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
### Scoping environments with specs **[PREMIUM]**
-Some GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) features can behave differently for each
-environment. For example, you can [create a secret variable to be injected only into a production environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium).
+Some GitLab [Enterprise Edition](https://about.gitlab.com/pricing/) features can
+behave differently for each environment. For example, you can
+[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
In most cases, these features use the _environment specs_ mechanism, which offers
an efficient way to implement scoping within each environment group.
@@ -696,9 +701,8 @@ In this case, `review/feature-1` spec takes precedence over `review/*` and `*` s
## Limitations
-You are limited to use only the [CI predefined variables](variables/README.md) in the
-`environment: name`. If you try to re-use variables defined inside `script`
-as part of the environment name, it will not work.
+In the `environment: name`, you are limited to only the [predefined environment variables](variables/predefined_variables.md).
+Re-using variables defined inside `script` as part of the environment name will not work.
## Further reading
@@ -707,3 +711,4 @@ Below are some links you may find interesting:
- [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
+- [Deploy Boards for your applications running on Kubernetes](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]**
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index 219af4ced9d..ab5c0e2dbad 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -9,8 +9,8 @@
- Some of them are just for testing.
- Others are for production.
-Because deploy jobs can be raised by different users with different roles, it is important that
-specific environments are "protected" to avoid unauthorized people affecting them.
+Since deploy jobs can be raised by different users with different roles, it is important that
+specific environments are "protected" to prevent unauthorized people from affecting them.
By default, a protected environment does one thing: it ensures that only people
with the right privileges can deploy to it, thus keeping it safe.
@@ -28,14 +28,14 @@ To protect an environment:
1. Navigate to your project's **Settings > CI/CD**.
1. Expand the **Protected Environments** section.
1. From the **Environment** dropdown menu, select the environment you want to protect.
-1. In the **Allowed to Deploy** dropdown menu, select the role, users, or groups you want to have deploy access.
- There are some considerations to have in mind:
- - There are two roles to choose from:
- - **Maintainers**: will allow access to all maintainers in the project.
- - **Developers**: will allow access to all maintainers and all developers in the project.
- - You can only select groups that are associated with the project.
- - Only users that have at least Developer permission level will appear on
- the **Allowed to Deploy** dropdown menu.
+1. In the **Allowed to Deploy** dropdown menu, select the role, users, or groups you
+ want to give deploy access to. Keep in mind that:
+ - There are two roles to choose from:
+ - **Maintainers**: will allow access to all maintainers in the project.
+ - **Developers**: will allow access to all maintainers and all developers in the project.
+ - You can only select groups that are already associated with the project.
+ - Only users that have at least Developer permission level will appear in
+ the **Allowed to Deploy** dropdown menu.
1. Click the **Protect** button.
The protected environment will now appear in the list of protected environments.
@@ -44,5 +44,6 @@ The protected environment will now appear in the list of protected environments.
Maintainers can:
-- Update existing protected environments at any time by changing the access on **Allowed to deploy** dropdown menu.
-- Unprotect a protected environment by clicking the **Unprotect** button of the environment to unprotect.
+- Update existing protected environments at any time by changing the access in the
+ **Allowed to Deploy** dropdown menu.
+- Unprotect a protected environment by clicking the **Unprotect** button for that environment.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 7f686781e3c..6b9f8181d1d 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -18,30 +18,30 @@ Examples are available in several forms. As a collection of:
The following table lists examples for different use cases:
-| Use case | Resource |
-|:-----------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------|
-| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
-| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
-| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** |
-| Container scanning | [Container Scanning with GitLab CI/CD](container_scanning.md). |
-| Dependency scanning | [Dependency Scanning with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html). **[ULTIMATE]** |
-| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). |
-| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](dast.md) **[ULTIMATE]** |
-| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
-| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
-| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. |
-| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
-| JUnit | [JUnit test reports](../junit_test_reports.md). |
-| License management | [Dependencies license management with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/license_management.html) **[ULTIMATE]** |
-| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
-| PHP | [Testing PHP projects](php.md). |
-| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
-| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
-| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
-| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
-| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
-| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](https://docs.gitlab.com/ee/ci/examples/sast.html) **[ULTIMATE]** |
-| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
+| Use case | Resource |
+|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------|
+| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
+| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
+| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** |
+| Container scanning | [Container Scanning with GitLab CI/CD](container_scanning.md). |
+| Dependency scanning | [Dependency Scanning with GitLab CI/CD](dependency_scanning.md). **[ULTIMATE]** |
+| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). |
+| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](dast.md) **[ULTIMATE]** |
+| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
+| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
+| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. |
+| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
+| JUnit | [JUnit test reports](../junit_test_reports.md). |
+| License management | [Dependencies license management with GitLab CI/CD](license_management.md) **[ULTIMATE]** |
+| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
+| PHP | [Testing PHP projects](php.md). |
+| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
+| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
+| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
+| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
+| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
+| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](sast.md) **[ULTIMATE]** |
+| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
### Contributing examples
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index b47038011de..442d0788d37 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -56,7 +56,7 @@ provide a list of URLs to test, please consult
TIP: **Tip:**
For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically
extracted and shown right in the merge request widget.
-[Learn more on Browser Performance Testing in merge requests](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
+[Learn more on Browser Performance Testing in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
## Performance testing on Review Apps
diff --git a/doc/ci/examples/code_quality.md b/doc/ci/examples/code_quality.md
index 3e7d6e7e3f7..186d4527bb6 100644
--- a/doc/ci/examples/code_quality.md
+++ b/doc/ci/examples/code_quality.md
@@ -1,7 +1,7 @@
# Analyze your project's Code Quality
CAUTION: **Caution:**
-The job definition shown below is supported on GitLab 11.5 and later versions.
+The job definition shown below is supported on GitLab 11.11 and later versions.
It also requires the GitLab Runner 11.5 or later.
For earlier versions, use the [previous job definitions](#previous-job-definitions).
@@ -11,27 +11,11 @@ and Docker.
First, you need GitLab Runner with
[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
-generates the expected report:
+Once you set up the Runner, include the CodeQuality template in your CI config:
```yaml
-code_quality:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run
- --env SOURCE_CODE="$PWD"
- --volume "$PWD":/code
- --volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
- artifacts:
- reports:
- codequality: gl-code-quality-report.json
+include:
+ - template: Code-Quality.gitlab-ci.yml
```
The above example will create a `code_quality` job in your CI/CD pipeline which
@@ -54,6 +38,28 @@ While these old job definitions are still maintained they have been deprecated
and may be removed in next major release, GitLab 12.0.
You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+For GitLab 11.5 and earlier, the job should look like:
+
+```yaml
+code_quality:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ artifacts:
+ reports:
+ codequality: gl-code-quality-report.json
+```
+
For GitLab 11.4 and earlier, the job should look like:
```yaml
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index 36fdc29fe65..4d41e424f4a 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -1,119 +1,5 @@
-# Container Scanning with GitLab CI/CD
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
+---
-CAUTION: **Caution:**
-The job definition shown below is supported on GitLab 11.5 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-You can check your Docker images (or more precisely the containers) for known
-vulnerabilities by using [Clair](https://github.com/coreos/clair) and
-[clair-scanner](https://github.com/arminc/clair-scanner), two open source tools
-for Vulnerability Static Analysis for containers.
-
-First, you need GitLab Runner with
-[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
-generates the expected report:
-
-```yaml
-container_scanning:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- ## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
- CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
- CI_APPLICATION_TAG: $CI_COMMIT_SHA
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
- - apk add -U wget ca-certificates
- - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
- - mv clair-scanner_linux_amd64 clair-scanner
- - chmod +x clair-scanner
- - touch clair-whitelist.yml
- - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
- - retries=0
- - echo "Waiting for clair daemon to start"
- - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
- artifacts:
- reports:
- container_scanning: gl-container-scanning-report.json
-```
-
-The above example will create a `container_scanning` job in your CI/CD pipeline, pull
-the image from the [Container Registry](../../user/project/container_registry.md)
-(whose name is defined from the two `CI_APPLICATION_` variables) and scan it
-for possible vulnerabilities. The report will be saved as a
-[Container Scanning report artifact](../yaml/README.md#artifactsreportscontainer_scanning-ultimate)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest Container Scanning artifact available.
-
-If you want to whitelist some specific vulnerabilities, you can do so by defining
-them in a [YAML file](https://github.com/arminc/clair-scanner/blob/master/README.md#example-whitelist-yaml-file),
-in our case its named `clair-whitelist.yml`.
-
-TIP: **Tip:**
-For [GitLab Ultimate][ee] users, this information will
-be automatically extracted and shown right in the merge request widget.
-[Learn more on Container Scanning in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html).
-
-CAUTION: **Caution:**
-Starting with GitLab 11.5, Container Scanning feature is licensed under the name `container_scanning`.
-While the old name `sast_container` is still maintained, it has been deprecated with GitLab 11.5 and
-may be removed in next major release, GitLab 12.0. You are advised to update your current `.gitlab-ci.yml`
-configuration to reflect that change if you are using the `$GITLAB_FEATURES` environment variable.
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, Container Scanning job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-container_scanning:
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- ## Define two new variables based on GitLab's CI/CD predefined variables
- ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
- CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
- CI_APPLICATION_TAG: $CI_COMMIT_SHA
- allow_failure: true
- services:
- - docker:stable-dind
- script:
- - docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
- - apk add -U wget ca-certificates
- - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
- - mv clair-scanner_linux_amd64 clair-scanner
- - chmod +x clair-scanner
- - touch clair-whitelist.yml
- - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
- - retries=0
- - echo "Waiting for clair daemon to start"
- - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
- - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
- artifacts:
- paths: [gl-container-scanning-report.json]
-```
-
-Alternatively the job name could be `sast:container`
-and the artifact name could be `gl-sast-container-report.json`.
-These names have been deprecated with GitLab 11.0
-and may be removed in next major release, GitLab 12.0.
-
-[ee]: https://about.gitlab.com/pricing/
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index ab0ca13d2cf..b676c661267 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -1,102 +1,5 @@
-# Dynamic Application Security Testing with GitLab CI/CD
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dast/index.html'
+---
-CAUTION: **Caution:**
-The job definition shown below is supported on GitLab 11.5 and later versions.
-It also requires the GitLab Runner 11.5 or later.
-For earlier versions, use the [previous job definitions](#previous-job-definitions).
-
-[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_program_analysis)
-is using the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
-to perform an analysis on your running web application.
-Since it is based on [ZAP Baseline](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan)
-DAST will perform passive scanning only;
-it will not actively attack your application.
-
-It can be very useful combined with [Review Apps](../review_apps/index.md).
-
-## Example
-
-First, you need GitLab Runner with
-[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that
-generates the expected report:
-
-```yaml
-dast:
- image: registry.gitlab.com/gitlab-org/security-products/zaproxy
- variables:
- website: "https://example.com"
- allow_failure: true
- script:
- - mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
- - cp /zap/wrk/gl-dast-report.json .
- artifacts:
- reports:
- dast: gl-dast-report.json
-```
-
-The above example will create a `dast` job in your CI/CD pipeline which will run
-the tests on the URL defined in the `website` variable (change it to use your
-own) and scan it for possible vulnerabilities. The report will be saved as a
-[DAST report artifact](../yaml/README.md#artifactsreportsdast-ultimate)
-that you can later download and analyze.
-Due to implementation limitations we always take the latest DAST artifact available.
-
-It's also possible to authenticate the user before performing DAST checks:
-
-```yaml
-dast:
- image: registry.gitlab.com/gitlab-org/security-products/zaproxy
- variables:
- website: "https://example.com"
- login_url: "https://example.com/sign-in"
- username: "john.doe@example.com"
- password: "john-doe-password"
- allow_failure: true
- script:
- - mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t $website
- --auth-url $login_url
- --auth-username $username
- --auth-password $password || true
- - cp /zap/wrk/gl-dast-report.json .
- artifacts:
- reports:
- dast: gl-dast-report.json
-```
-See [zaproxy documentation](https://gitlab.com/gitlab-org/security-products/zaproxy)
-to learn more about authentication settings.
-
-TIP: **Tip:**
-For [GitLab Ultimate][ee] users, this information will
-be automatically extracted and shown right in the merge request widget.
-[Learn more on DAST in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html).
-
-## Previous job definitions
-
-CAUTION: **Caution:**
-Before GitLab 11.5, DAST job and artifact had to be named specifically
-to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
-and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
-
-For GitLab 11.4 and earlier, the job should look like:
-
-```yaml
-dast:
- image: registry.gitlab.com/gitlab-org/security-products/zaproxy
- variables:
- website: "https://example.com"
- allow_failure: true
- script:
- - mkdir /zap/wrk/
- - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
- - cp /zap/wrk/gl-dast-report.json .
- artifacts:
- paths: [gl-dast-report.json]
-```
-
-[ee]: https://about.gitlab.com/pricing/
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
diff --git a/doc/ci/examples/dependency_scanning.md b/doc/ci/examples/dependency_scanning.md
new file mode 100644
index 00000000000..3a8b53b425c
--- /dev/null
+++ b/doc/ci/examples/dependency_scanning.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
diff --git a/doc/ci/examples/license_management.md b/doc/ci/examples/license_management.md
new file mode 100644
index 00000000000..08704425a75
--- /dev/null
+++ b/doc/ci/examples/license_management.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/license_management/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
diff --git a/doc/ci/examples/sast.md b/doc/ci/examples/sast.md
new file mode 100644
index 00000000000..688cc79d0f6
--- /dev/null
+++ b/doc/ci/examples/sast.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/sast/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
diff --git a/doc/ci/examples/sast_docker.md b/doc/ci/examples/sast_docker.md
index 70b269046e5..570898b2d23 100644
--- a/doc/ci/examples/sast_docker.md
+++ b/doc/ci/examples/sast_docker.md
@@ -1,5 +1,5 @@
---
-redirect_to: 'container_scanning.md'
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
---
-This document was moved to [another location](container_scanning.md).
+This document was moved to [another location](../../user/application_security/container_scanning/index.html).
diff --git a/doc/ci/img/add_file_template_11_10.png b/doc/ci/img/add_file_template_11_10.png
new file mode 100644
index 00000000000..ca04d72615b
--- /dev/null
+++ b/doc/ci/img/add_file_template_11_10.png
Binary files differ
diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png
index 45d882b536c..12090434bef 100644
--- a/doc/ci/img/deployments_view.png
+++ b/doc/ci/img/deployments_view.png
Binary files differ
diff --git a/doc/ci/img/environments_available.png b/doc/ci/img/environments_available.png
index 7ab92838ece..48fc6effc2d 100644
--- a/doc/ci/img/environments_available.png
+++ b/doc/ci/img/environments_available.png
Binary files differ
diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png
index 61b7e9fe77c..6a7b7ce5679 100644
--- a/doc/ci/img/environments_mr_review_app.png
+++ b/doc/ci/img/environments_mr_review_app.png
Binary files differ
diff --git a/doc/ci/img/metrics_reports.png b/doc/ci/img/metrics_reports.png
new file mode 100644
index 00000000000..ffd9f6830a2
--- /dev/null
+++ b/doc/ci/img/metrics_reports.png
Binary files differ
diff --git a/doc/ci/img/multi_pipeline_mini_graph.gif b/doc/ci/img/multi_pipeline_mini_graph.gif
new file mode 100644
index 00000000000..de49ba5aa12
--- /dev/null
+++ b/doc/ci/img/multi_pipeline_mini_graph.gif
Binary files differ
diff --git a/doc/ci/img/multi_project_pipeline_graph.png b/doc/ci/img/multi_project_pipeline_graph.png
new file mode 100644
index 00000000000..723a455cb4a
--- /dev/null
+++ b/doc/ci/img/multi_project_pipeline_graph.png
Binary files differ
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 2a4160f62b0..7109b2ec583 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -54,3 +54,8 @@ terminal will block the job from finishing for the duration configured in
close the terminal window.
![finished job with terminal open](img/finished_job_with_terminal_open.png)
+
+## Interactive Web Terminals for the Web IDE **[ULTIMATE ONLY]**
+
+Read the Web IDE docs to learn how to run [Interactive Terminals through the Web IDE](../../user/project/web_ide/index.md).
+
diff --git a/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png b/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png
new file mode 100644
index 00000000000..5089a1088c5
--- /dev/null
+++ b/doc/ci/introduction/img/gitlab_workflow_example_extended_11_11.png
Binary files differ
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index 6055d8c282a..14ea648c00b 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -133,8 +133,8 @@ At the end, if anything goes wrong, you can easily
### Basic CI/CD workflow
-This is a very simple example for how GitLab CI/CD fits in a common
-development workflow.
+Consider the following example for how GitLab CI/CD fits in a
+common development workflow.
Assume that you have discussed a code implementation in an issue
and worked locally on your proposed changes. Once you push your
@@ -154,7 +154,7 @@ Once you're happy with your implementation:
- GitLab CI/CD deploys your changes automatically to a production environment.
- And finally, you and your team can easily roll it back if something goes wrong.
-<img src="img/gitlab_workflow_example_11_9.png" alt="GitLab workflow example" class="image-noshadow">
+![GitLab workflow example](img/gitlab_workflow_example_11_9.png)
GitLab CI/CD is capable of a doing a lot more, but this workflow
exemplifies GitLab's ability to track the entire process,
@@ -162,6 +162,48 @@ without the need of any external tool to deliver your software.
And, most usefully, you can visualize all the steps through
the GitLab UI.
+#### A deeper look into the CI/CD basic workflow
+
+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.
+
+![Deeper look into the basic CI/CD workflow](img/gitlab_workflow_example_extended_11_11.png)
+
+If you look at the image from the left to the right,
+you'll see some of the features available in GitLab
+according to each stage (Verify, Package, Release).
+
+1. **Verify**:
+ - Automatically build and test your application with Continuous Integration.
+ - Analyze your source code quality with [GitLab Code Quality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html). **[STARTER]**
+ - Determine the performance impact of code changes with [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html). **[PREMIUM]**
+ - Perform a series of tests, such as [Container Scanning](https://docs.gitlab.com/ee/ci/examples/container_scanning.html) **[ULTIMATE]**, [Dependency Scanning](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html) **[ULTIMATE]**, and [JUnit tests](../junit_test_reports.md).
+ - Deploy your changes with [Review Apps](../review_apps/index.md) to preview the app changes on every branch.
+1. **Package**:
+ - Store Docker images with [Container Registry](../../user/project/container_registry.md).
+ - Store NPM packages with [NPM Registry](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html). **[PREMIUM]**
+ - Store Maven artifacts with [Maven Repository](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html). **[PREMIUM]**
+1. **Release**:
+ - Continuous Deployment, automatically deploying your app to production.
+ - Continuous Delivery, manually click to deploy your app to production.
+ - Deploy static websites with [GitLab Pages](../../user/project/pages/index.md).
+ - Ship features to only a portion of your pods and let a percentage of your user base to visit the temporarily deployed feature with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html). **[PREMIUM]**
+ - Deploy your features behind [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html). **[PREMIUM]**
+ - Add release notes to any Git tag with [GitLab Releases](../../user/project/releases/index.md).
+ - View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html). **[PREMIUM]**
+ - Deploy your application to a production environment in a Kubernetes cluster with [Auto Deploy](../../topics/autodevops/index.md#auto-deploy).
+
+With GitLab CI/CD you can also:
+
+- Easily set up your app's entire lifecycle with [Auto DevOps](../../topics/autodevops/index.md).
+- Deploy your app to different [environments](../environments.md).
+- 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](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate). **[ULTIMATE]**
+
+To see all CI/CD features, navigate back to the [CI/CD index](../README.md).
+
### Setting up GitLab CI/CD for the first time
To get started with GitLab CI/CD, you need to familiarize yourself
@@ -178,16 +220,3 @@ existing one) for any application.
For a deep view of GitLab's CI/CD configuration options, check the
[`.gitlab-ci.yml` full reference](../yaml/README.md).
-
-### GitLab CI/CD feature set
-
-- Easily set up your app's entire lifecycle with [Auto DevOps](../../topics/autodevops/index.md).
-- Deploy static websites with [GitLab Pages](../../user/project/pages/index.md).
-- Deploy your app to different [environments](../environments.md).
-- Preview changes per merge request with [Review Apps](../review_apps/index.md).
-- Develop secure and private Docker images with [Container Registry](../../user/project/container_registry.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](https://docs.gitlab.com/ee/user/project/merge_requests/#security-reports-ultimate). **[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 d03c0b68daf..799217c9a08 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -71,11 +71,11 @@ merge request widget.
NOTE: **Note:**
If you also want the ability to browse JUnit output files, include the
-[`artifacts:paths`](yaml/README.md#artifactspaths) keyword.
+[`artifacts:paths`](yaml/README.md#artifactspaths) keyword. An example of this is shown in the Ruby example below.
### Ruby example
-Use the following job in `.gitlab-ci.yml`:
+Use the following job in `.gitlab-ci.yml`. This includes the `artifacts:paths` keyword to provide a link to the JUnit output file.
```yaml
## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec
@@ -85,6 +85,8 @@ ruby:
- bundle install
- rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml
artifacts:
+ paths:
+ - rspec.xml
reports:
junit: rspec.xml
```
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index cfe638c0a22..244ccbb92b0 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -72,8 +72,9 @@ done by GitLab, requiring you to do them.
> Introduced in GitLab Runner 11.10.
-`GIT_CLONE_PATH` allows you to control where you clone your sources.
-This can have implications if you heavily use big repositories with fork workflow.
+[`GIT_CLONE_PATH`](../yaml/README.md#custom-build-directories) allows you to
+control where you clone your sources. This can have implications if you
+heavily use big repositories with fork workflow.
Fork workflow from GitLab Runner's perspective is stored as a separate repository
with separate worktree. That means that GitLab Runner cannot optimize the usage
@@ -83,29 +84,31 @@ In such cases, ideally you want to make the GitLab Runner executor be used only
for the given project and not shared across different projects to make this
process more efficient.
-The `GIT_CLONE_PATH` has to be within the `$CI_BUILDS_DIR`. Currently,
-it is impossible to pick any path from disk.
+The [`GIT_CLONE_PATH`](../yaml/README.md#custom-build-directories) has to be
+within the `$CI_BUILDS_DIR`. Currently, it is impossible to pick any path
+from disk.
## Git clean flags
> Introduced in GitLab Runner 11.10.
-`GIT_CLEAN_FLAGS` allows you to control whether or not you require
-the `git clean` command to be executed for each CI job.
-By default, GitLab ensures that you have your worktree on the given SHA,
+[`GIT_CLEAN_FLAGS`](../yaml/README.md#git-clean-flags) allows you to control
+whether or not you require the `git clean` command to be executed for each CI
+job. By default, GitLab ensures that you have your worktree on the given SHA,
and that your repository is clean.
-`GIT_CLEAN_FLAGS` is disabled when set to `none`. On very big repositories, this
-might be desired because `git clean` is disk I/O intensive. Controlling that
-with `GIT_CLEAN_FLAGS: -ffdx -e .build/`, for example, allows you to control and
-disable removal of some directories within the worktree between subsequent runs,
-which can speed-up the incremental builds. This has the biggest effect
-if you re-use existing machines, and have an existing worktree that you can re-use
-for builds.
-
-For exact parameters accepted by `GIT_CLEAN_FLAGS`, see the documentation
-for [git clean](https://git-scm.com/docs/git-clean). The
-available parameters are dependent on Git version.
+[`GIT_CLEAN_FLAGS`](../yaml/README.md#git-clean-flags) is disabled when set
+to `none`. On very big repositories, this might be desired because `git
+clean` is disk I/O intensive. Controlling that with `GIT_CLEAN_FLAGS: -ffdx
+-e .build/`, for example, allows you to control and disable removal of some
+directories within the worktree between subsequent runs, which can speed-up
+the incremental builds. This has the biggest effect if you re-use existing
+machines, and have an existing worktree that you can re-use for builds.
+
+For exact parameters accepted by
+[`GIT_CLEAN_FLAGS`](../yaml/README.md#git-clean-flags), see the documentation
+for [git clean](https://git-scm.com/docs/git-clean). The available parameters
+are dependent on Git version.
## Fork-based workflow
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index 4f61e97bd8a..3c26a38e3de 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -1,5 +1,11 @@
# Pipelines for merge requests
+NOTE: **Note**:
+As of GitLab 11.10, pipelines for merge requests require GitLab Runner 11.9
+or higher due to the [recent refspecs
+changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/25504).
+Anything lower will cause the pipeline to fail.
+
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/15310) in GitLab 11.6.
Usually, when you create a new merge request, a pipeline runs with the
@@ -61,14 +67,14 @@ when a merge request was created or updated. For example:
![Merge request page](img/merge_request.png)
-## Combined ref pipelines **[PREMIUM]**
+## Pipelines for Merged Results **[PREMIUM]**
-> [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
It's possible for your source and target branches to diverge, which can result
in the scenario that source branch's pipeline was green, the target's pipeline was green,
but the combined output fails. By having your merge request pipeline automatically
-create a new ref that contains the merge result of the source and target branch
+create a new ref that contains the merge result of the source and target branch
(then running a pipeline on that ref), we can better test that the combined result
is also valid.
@@ -87,7 +93,7 @@ The detached state serves to warn you that you are working in a situation
subjected to merge problems, and helps to highlight that you should
get out of WIP status or resolve merge conflicts as soon as possible.
-### Enabling combined ref pipelines
+### Enabling Pipelines for Merged Results
This feature disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186). It can be enabled at the project level:
@@ -97,7 +103,7 @@ This feature disabled by default until we resolve issues with [contention handli
![Merge request pipeline config](img/merge_request_pipeline_config.png)
-### Combined ref pipeline's limitations
+### Pipelines for Merged Result's limitations
- This feature requires [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
- This feature requires [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
diff --git a/doc/ci/metrics_reports.md b/doc/ci/metrics_reports.md
new file mode 100644
index 00000000000..83a7094faaa
--- /dev/null
+++ b/doc/ci/metrics_reports.md
@@ -0,0 +1,40 @@
+# Metrics Reports **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9788) in [GitLab Premium](https://about.gitlab.com/pricing) 11.10.
+Requires GitLab Runner 11.10 and above.
+
+## Overview
+
+GitLab provides a lot of great reporting tools for [merge requests](../user/project/merge_requests/index.md) - [JUnit reports](./junit_test_reports.md), [codequality](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html), performance tests, etc. While JUnit is a great open framework for tests that "pass" or "fail", it is also important to see other types of metrics from a given change.
+
+You can configure your job to use custom Metrics Reports, and GitLab will display a report on the merge request so that it's easier and faster to identify changes without having to check the entire log.
+
+![Metrics Reports](./img/metrics_reports.png)
+
+## Use cases
+
+Consider the following examples of data that can utilize Metrics Reports:
+
+1. Memory usage
+1. Load testing results
+1. Code complexity
+1. Code coverage stats
+
+## How it works
+
+Metrics are read from the metrics report (default: `metrics.txt`). They are parsed and displayed in the MR widget.
+
+## How to set it up
+
+Add a job that creates a [metrics report](yaml/README.md#artifactsreportsmetrics-premium) (default filename: `metrics.txt`). The file should conform to the [OpenMetrics](https://openmetrics.io/) format.
+
+For example:
+
+```yaml
+metrics:
+ script:
+ - echo 'metric_name metric_value' > metrics.txt
+ artifacts:
+ reports:
+ metrics: metrics.txt
+```
diff --git a/doc/ci/multi_project_pipeline_graphs.md b/doc/ci/multi_project_pipeline_graphs.md
new file mode 100644
index 00000000000..af10e5b6126
--- /dev/null
+++ b/doc/ci/multi_project_pipeline_graphs.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'multi_project_pipelines.md'
+---
+
+This document was moved to [another location](multi_project_pipelines.md).
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
new file mode 100644
index 00000000000..556059c01b6
--- /dev/null
+++ b/doc/ci/multi_project_pipelines.md
@@ -0,0 +1,152 @@
+# Multi-project pipelines **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/2121) in
+[GitLab Premium 9.3](https://about.gitlab.com/2017/06/22/gitlab-9-3-released/#multi-project-pipeline-graphs).
+
+When you set up [GitLab CI/CD](README.md) across multiple projects, you can visualize
+the entire pipeline, including all cross-project inter-dependencies.
+
+## Overview
+
+GitLab CI/CD is a powerful continuous integration tool that works not only per project, but also across projects. When you
+configure GitLab CI for your project, you can visualize the stages
+of your [jobs](pipelines.md#configuring-pipelines) on a [pipeline graph](pipelines.md#visualizing-pipelines).
+
+![Multi-project pipeline graph](img/multi_project_pipeline_graph.png)
+
+In the Merge Request Widget, multi-project pipeline mini-graphs are displayed,
+and when hovering or tapping (on touchscreen devices) they will expand and be shown adjacent to each other.
+
+![Multi-project mini graph](img/multi_pipeline_mini_graph.gif)
+
+Multi-project pipelines are useful for larger products that require cross-project inter-dependencies, such as those
+adopting a [microservices architecture](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/).
+
+## Use cases
+
+Let's assume you deploy your web app from different projects in GitLab:
+
+- One for the free version, which has its own pipeline that builds and tests your app
+- One for the paid version add-ons, which also pass through builds and tests
+- One for the documentation, which also builds, tests, and deploys with an SSG
+
+With Multi-Project Pipelines, you can visualize the entire pipeline, including all stages of builds and tests for the three projects.
+
+## Triggering multi-project pipelines through API
+
+When you use the [`CI_JOB_TOKEN` to trigger pipelines](triggers/README.md#ci-job-token), GitLab
+recognizes the source of the job token, and thus internally ties these pipelines
+together, allowing you to visualize their relationships on pipeline graphs.
+
+These relationships are displayed in the pipeline graph by showing inbound and
+outbound connections for upstream and downstream pipeline dependencies.
+
+## Creating multi-project pipelines from `.gitlab-ci.yml`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
+
+### Triggering a downstream pipeline using a bridge job
+
+Before GitLab 11.8, it was necessary to implement a pipeline job that was
+responsible for making the API request [to trigger a pipeline](#triggering-multi-project-pipelines-through-api)
+in a different project.
+
+In GitLab 11.8, GitLab provides a new CI/CD configuration syntax to make this
+task easier, and avoid needing GitLab Runner for triggering cross-project
+pipelines. The following illustrates configuring a bridge job:
+
+```yaml
+rspec:
+ stage: test
+ script: bundle exec rspec
+
+staging:
+ variables:
+ ENVIRONMENT: staging
+ stage: deploy
+ trigger: my/deployment
+```
+
+In the example above, as soon as `rspec` job succeeds in the `test` stage,
+the `staging` bridge job is going to be started. The initial status of this
+job will be `pending`. GitLab will create a downstream pipeline in the
+`my/deployment` project and, as soon as the pipeline gets created, the
+`staging` job will succeed. `my/deployment` is a full path to that project.
+
+The user that created the upstream pipeline needs to have access rights to the
+downstream project (`my/deployment` in this case). If a downstream project can
+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-ce/issues/39640).
+
+NOTE: **Note:**
+Bridge jobs do not support every configuration entry that a user can use
+in the case of regular jobs. Bridge jobs will not to be picked by a Runner,
+thus there is no point in adding support for `script`, for example. If a user
+tries to use unsupported configuration syntax, YAML validation will fail upon
+pipeline creation.
+
+### Specifying a downstream pipeline branch
+
+It is possible to specify a branch name that a downstream pipeline will use:
+
+```yaml
+rspec:
+ stage: test
+ script: bundle exec rspec
+
+staging:
+ stage: deploy
+ trigger:
+ project: my/deployment
+ branch: stable-11-2
+```
+
+Use a `project` keyword to specify full path to a downstream project. Use
+a `branch` keyword to specify a branch name.
+
+GitLab will use a commit that is currently on the HEAD of the branch when
+creating a downstream pipeline.
+
+### Passing variables to a downstream pipeline
+
+Sometimes you might want to pass variables to a downstream pipeline.
+You can do that using the `variables` keyword, just like you would when
+defining a regular job.
+
+```yaml
+rspec:
+ stage: test
+ script: bundle exec rspec
+
+staging:
+ variables:
+ ENVIRONMENT: staging
+ stage: deploy
+ trigger: my/deployment
+```
+
+The `ENVIRONMENT` variable will be passed to every job defined in a downstream
+pipeline. It will be available as an environment variable when GitLab Runner picks a job.
+
+### Limitations
+
+Because bridge jobs are a little different to regular jobs, it is not
+possible to use exactly the same configuration syntax here, as one would
+normally do when defining a regular job that will be picked by a runner.
+
+Some features are not implemented yet. For example, support for environments.
+
+[Configuration keywords](yaml/README.md) available for bridge jobs are:
+
+- `trigger` (to define a downstream pipeline trigger)
+- `stage`
+- `allow_failure`
+- `only` and `except`
+- `when`
+- `extends`
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 2ffa3d4edc7..342c2ab972a 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -19,7 +19,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](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository that GitLab pulls from](../workflow/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**.
@@ -220,7 +220,7 @@ For information on adding pipeline badges to projects, see [Pipeline badges](../
Pipelines for different projects can be combined and visualized together.
-For more information, see [Multi-project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipelines.html).
+For more information, see [Multi-project pipelines](multi_project_pipelines.md).
## Working with pipelines
@@ -266,6 +266,9 @@ Clicking on an individual job will show you its job trace, and allow you to:
- Retry the job.
- Erase the job trace.
+NOTE: **Note:**
+To prevent jobs from being bypassed or run out of order, canceled jobs can only be retried when the whole pipeline they belong to is retried.
+
### Seeing the failure reason for jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782) in GitLab 10.7.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 65886400c64..015f1c0dc0f 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -127,7 +127,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](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository where GitLab pulls from](../../workflow/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/review_apps/index.md b/doc/ci/review_apps/index.md
index 2fc1e14f02e..1a71c5fd258 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -3,12 +3,10 @@
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/21971) in GitLab 8.12. Further additions were made in GitLab 8.13 and 8.14.
> - Inspired by [Heroku's Review Apps](https://devcenter.heroku.com/articles/github-integration-review-apps), which itself was inspired by [Fourchette](https://github.com/rainforestapp/fourchette).
-For a video introduction to Review Apps, see [8.14 Webcast: Review Apps & Time Tracking Beta (EE) - GitLab Release](https://www.youtube.com/watch?v=CteZol_7pxo).
-
-## Overview
-
Review Apps are a collaboration tool that takes the hard work out of providing an environment to showcase product changes.
+## Introduction
+
Review Apps:
- Provide an automatic live preview of changes made in a feature branch by spinning up a dynamic environment for your merge requests.
@@ -18,103 +16,60 @@ Review Apps:
![Review Apps Workflow](img/continuous-delivery-review-apps.svg)
-Reviewing anything, from performance to interface changes, becomes much easier with a live environment and so Review Apps can make a large impact on your development flow.
+In the above example:
-## What are Review Apps?
+- A Review App is built every time a commit is pushed to`topic branch`.
+- The reviewer fails two reviews before passing the third review.
+- Once the review as passed, `topic branch` is merged into `master` where it's deploy to staging.
+- After been approved in staging, the changes that were merged into `master` are deployed in to production.
-A Review App is a mapping of a branch with an [environment](../environments.md). The following is an example of a merge request with an environment set dynamically.
+### How Review Apps work
+
+A Review App is a mapping of a branch with an [environment](../environments.md).
+Access to the Review App is made available as a link on the [merge request](../../user/project/merge_requests.md) relevant to the branch.
+
+The following is an example of a merge request with an environment set dynamically.
![Review App in merge request](img/review_apps_preview_in_mr.png)
-In this example, you can see a branch was:
+In this example, a branch was:
- Successfully built.
- Deployed under a dynamic environment that can be reached by clicking on the **View app** button.
-## How do Review Apps work?
-
-The basis of Review Apps in GitLab is [dynamic environments](../environments.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch.
-
-Access to the Review App is made available as a link on the [merge request](../../user/project/merge_requests.md) relevant to the branch. Review Apps enable you to review all changes proposed by the merge request in live environment.
-
-## Use cases
-
-Some supported use cases include the:
-
-- Simple case of deploying a simple static HTML website.
-- More complicated case of an application that uses a database. Deploying a branch on a temporary instance and booting up this instance with all required software and services automatically on the fly is not a trivial task. However, it is possible, especially if you use Docker or a configuration management tool like Chef, Puppet, Ansible, or Salt.
-
-Review Apps usually make sense with web applications, but you can use them any way you'd like.
-
-## Implementing Review Apps
-
-Implementing Review Apps depends on your:
-
-- Technology stack.
-- Deployment process.
-
-### Prerequisite Knowledge
-
-To get a better understanding of Review Apps, review documentation on how environments and deployments work. Before you implement your own Review Apps:
-
-1. Learn about [environments](../environments.md) and their role in the development workflow.
-1. Learn about [CI variables](../variables/README.md) and how they can be used in your CI jobs.
-1. Explore the [`environment` syntax](../yaml/README.md#environment) as defined in `.gitlab-ci.yml`. This will become a primary reference.
-1. Additionally, find out about [manual actions](../environments.md#configuring-manual-deployments) and how you can use them to deploy to critical environments like production with the push of a button.
-1. Follow the [example tutorials](#examples). These will guide you through setting up infrastructure and using Review Apps.
-
-### Configuring dynamic environments
-
-Configuring Review Apps dynamic environments depends on your technology stack and infrastructure.
-
-For more information, see [dynamic environments](../environments.md#configuring-dynamic-environments) documentation to understand how to define and create them.
-
-### Creating and destroying Review Apps
-
-Creating and destroying Review Apps is defined in `.gitlab-ci.yml` at a job level under the `environment` keyword.
+## Configuring Review Apps
-For more information, see [Introduction to environments and deployments](../environments.md).
+Review Apps are built on [dynamic environments](../environments.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch.
-### Adding Review Apps to your workflow
-
-The process of adding Review Apps in your workflow is as follows:
+The process of configuring Review Apps is as follows:
1. Set up the infrastructure to host and deploy the Review Apps.
1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a Runner to do deployment.
1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches.
1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps.
-After adding Review Apps to your workflow, you follow the branched Git flow. That is:
+### Examples
-1. Push a branch and let the Runner deploy the Review App based on the `script` definition of the dynamic environment job.
-1. Wait for the Runner to build and deploy your web application.
-1. Click on the link that provided in the merge request related to the branch to see the changes live.
-
-## Limitations
-
-Check the [environments limitations](../environments.md#limitations).
-
-## Examples
-
-The following are example projects that use Review Apps with:
+The following are example projects that demonstrate Review App configuration:
- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx).
- [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift).
See also the video [Demo: Cloud Native Development with GitLab](https://www.youtube.com/watch?v=jfIyQEwrocw), which includes a Review Apps example.
-## Route Maps
+### Route Maps
-> Introduced in GitLab 8.17. In GitLab 11.5 the file links
-are surfaced to the merge request widget.
+> Introduced in GitLab 8.17. In GitLab 11.5, the file links are available in the merge request widget.
Route Maps allows you to go directly from source files
to public pages on the [environment](../environments.md) defined for
-Review Apps. Once set up, the review app link in the merge request
+Review Apps.
+
+Once set up, the review app link in the merge request
widget can take you directly to the pages changed, making it easier
and faster to preview proposed modifications.
-All you need to do is to tell GitLab how the paths of files
+Configuring Route Maps involves telling GitLab how the paths of files
in your repository map to paths of pages on your website using a Route Map.
Once set, GitLab will display **View on ...** buttons, which will take you
to the pages changed directly from merge requests.
@@ -123,9 +78,9 @@ To set up a route map, add a a file inside the repository at `.gitlab/route-map.
which contains a YAML array that maps `source` paths (in the repository) to `public`
paths (on the website).
-### Route Maps example
+#### Route Maps example
-Below there's an example of a route map for [Middleman](https://middlemanapp.com),
+The following is an example of a route map for [Middleman](https://middlemanapp.com),
a static site generator (SSG) used to build [GitLab's website](https://about.gitlab.com),
deployed from its [project on GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com):
@@ -147,18 +102,17 @@ deployed from its [project on GitLab.com](https://gitlab.com/gitlab-com/www-gitl
public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
```
-Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys:
+Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, there is a hash map with two keys:
- `source`
- - a string, starting and ending with `'`, for an exact match
- - a regular expression, starting and ending with `/`, for a pattern match
- - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
- - Can include capture groups denoted by `()` that can be referred to in the `public` path.
- - Slashes (`/`) can, but don't have to, be escaped as `\/`.
- - Literal periods (`.`) should be escaped as `\.`.
-- `public`
- - a string, starting and ending with `'`.
- - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`.
+ - A string, starting and ending with `'`, for an exact match.
+ - A regular expression, starting and ending with `/`, for a pattern match:
+ - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
+ - Can include capture groups denoted by `()` that can be referred to in the `public` path.
+ - Slashes (`/`) can, but don't have to, be escaped as `\/`.
+ - Literal periods (`.`) should be escaped as `\.`.
+- `public`, a string starting and ending with `'`.
+ - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`.
The public path for a source path is determined by finding the first
`source` expression that matches it, and returning the corresponding
@@ -171,12 +125,12 @@ will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`,
and will result in a public path of `index.html`, instead of
`index.html.haml`.
-Once you have the route mapping set up, it will be exposed in a few places:
+Once you have the route mapping set up, it will take effect in the following locations:
-- In the merge request widget. The **View app** button will take you to the
- environment URL you have set up in `.gitlab-ci.yml`. The dropdown will render
- the first 5 matched items from the route map, but you can filter them if more
- than 5 are available.
+- In the merge request widget. The:
+ - **View app** button will take you to the environment URL set in `.gitlab-ci.yml`.
+ - Dropdown will list the first 5 matched items from the route map, but you can filter them if more
+ than 5 are available.
![View app file list in merge request widget](img/view_on_mr_widget.png)
@@ -187,3 +141,15 @@ Once you have the route mapping set up, it will be exposed in a few places:
- In the blob file view.
!["View on env" button in file view](img/view_on_env_blob.png)
+
+## Working with Review Apps
+
+After adding Review Apps to your workflow, you follow the branched Git flow. That is:
+
+1. Push a branch and let the Runner deploy the Review App based on the `script` definition of the dynamic environment job.
+1. Wait for the Runner to build and deploy your web application.
+1. Click on the link that provided in the merge request related to the branch to see the changes live.
+
+## Limitations
+
+Review App limitations are the same as [environments limitations](../environments.md#limitations).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 0e72bdddee7..ad80b5d8818 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -23,6 +23,63 @@ attackers can impersonate the user that exposed their trigger token publicly in
their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#gitlab-cicd-environment-variables)
to protect trigger tokens.
+### CI job token
+
+You can use the `CI_JOB_TOKEN` [variable][predef] (used to authenticate
+with the [GitLab Container Registry][registry]) in the following cases.
+
+#### When used with multi-project pipelines **[PREMIUM]**
+
+> **Note**:
+The use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced][ee-2017]
+in [GitLab Premium][ee] 9.3.
+
+This way of triggering can only be used when invoked inside `.gitlab-ci.yml`,
+and it creates a dependent pipeline relation visible on the
+[pipeline graph](../multi_project_pipelines.md#overview). For example:
+
+```yaml
+build_docs:
+ stage: deploy
+ script:
+ - curl --request POST --form "token=$CI_JOB_TOKEN" --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline
+ only:
+ - tags
+```
+
+Pipelines triggered that way also expose a special variable:
+`CI_PIPELINE_SOURCE=pipeline`.
+
+Read more about the [pipelines trigger API][trigapi].
+
+#### When a pipeline depends on the artifacts of another pipeline **[PREMIUM]**
+
+> The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced][ee-2346]
+ in [GitLab Premium][ee] 9.5.
+
+With the introduction of dependencies between different projects, one of
+them may need to access artifacts created by a previous one. This process
+must be granted for authorized accesses, and it can be done using the
+`CI_JOB_TOKEN` variable that identifies a specific job. For example:
+
+```yaml
+build_submodule:
+ image: debian
+ stage: test
+ script:
+ - apt update && apt install -y unzip
+ - curl --location --output artifacts.zip "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test&job_token=$CI_JOB_TOKEN"
+ - unzip artifacts.zip
+ only:
+ - tags
+```
+
+This allows you to use that for multi-project pipelines and download artifacts
+from any project to which you have access as this follows the same principles
+with the [permission model][permissions].
+
+Read more about the [jobs API](../../api/jobs.md#download-the-artifacts-archive).
+
## Adding a new trigger
You can add a new trigger by going to your project's
@@ -225,7 +282,10 @@ removed with one of the future versions of GitLab. You are advised to
[take ownership](#taking-ownership-of-a-trigger) of any legacy triggers.
[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
+[ee-2346]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2346
[ee]: https://about.gitlab.com/pricing/
[variables]: ../variables/README.md
[predef]: ../variables/README.md#predefined-environment-variables
[registry]: ../../user/project/container_registry.md
+[permissions]: ../../user/permissions.md#job-permissions
+[trigapi]: ../../api/pipeline_triggers.md
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 61d1a904f76..1ba22070abe 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -52,6 +52,33 @@ 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
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46806) in GitLab 11.11.
+
+There are two types of variables supported by GitLab:
+
+- `env_var`: the runner will create 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.
+
+#### Masked variables
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/13784) in GitLab 11.10
+
+By default, variables will be created as masked variables.
+This means that the value of the variable will be hidden in job logs,
+though it must match certain requirements to do so:
+
+- The value must be in a single line.
+- The value must not have escape characters.
+- The value must not use variables.
+- The value must not have any whitespace.
+- The value must be at least 8 characters long.
+
+If the value does not meet the requirements above, then the CI variable will fail to save.
+In order to save, either alter the value to meet the masking requirements
+or disable **Masked** for the variable.
+
## Getting started
To get started with environment variables in the scope of GitLab
@@ -104,7 +131,10 @@ let's say you want to output `HELLO WORLD` for a `TEST` variable.
You can either set the variable directly in the `.gitlab-ci.yml`
file or through the UI.
-#### Via [`.gitlab-ci.yml`](../yaml/README.md#variables)
+#### Via `.gitlab-ci.yml`
+
+To create a new custom `env_var` variable via [`.gitlab-ci.yml`](../yaml/README.md#variables), define their variable/value pair under
+`variables`:
```yaml
variables:
@@ -116,11 +146,13 @@ For a deeper look into them, see [`.gitlab-ci.yml` defined variables](#gitlab-ci
#### Via the UI
From the UI, navigate to your project's **Settings > CI/CD** and
-expand **Environment variables**. Create a new variable by naming
-it in the field **Input variable key**, and define its value in the
+expand **Variables**. Create a new variable by choosing its **type**, naming
+it in the field **Input variable key**, and defining its value in the
**Input variable value** field:
-![CI/CD settings - new variable](img/new_custom_variable_example.png)
+![CI/CD settings - new variable](img/new_custom_variables_example.png)
+
+You'll also see the option to mask and/or protect your variables.
Once you've set the variables, call them from the `.gitlab-ci.yml` file:
@@ -129,19 +161,14 @@ test_variable:
stage: test
script:
- echo $CI_JOB_STAGE # calls a predefined variable
- - echo $TEST # calls a custom variable
+ - echo $TEST # calls a custom variable of type `env_var`
+ - echo $GREETING # calls a custom variable of type `file` that contains the path to the temp file
+ - cat $GREETING # the temp file itself contains the variable value
```
The output will be:
-![Output custom variable](img/custom_variable_output.png)
-
-CAUTION: **Important:**
-Be aware that variables are not masked, and their values can be shown
-in the job logs if explicitly asked to do so. If your project is public or
-internal, you can set the pipelines private from your [project's Pipelines
-settings](../../user/project/pipelines/settings.md#visibility-of-pipelines).
-Follow the discussion in issue [#13784][ce-13784] for masking the variables.
+![Output custom variable](img/custom_variables_output.png)
### Syntax of environment variables in job scripts
@@ -288,7 +315,7 @@ use for storing things like passwords, SSH keys, and credentials.
Group-level variables can be added by:
1. Navigating to your group's **Settings > CI/CD** page.
-1. Inputing variable keys and values in the **Environment variables** section.
+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.
@@ -339,6 +366,25 @@ Protected variables can be added by going to your project's
Once you set them, they will be available for all subsequent pipelines.
+### Limiting environment scopes of environment variables **[PREMIUM]**
+
+> [Introduced][ee-2112] in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
+
+You can limit the environment scope of a variable by
+[defining which environments][envs] it can be available for.
+
+Wildcards can be used, and the default environment scope is `*` which means
+any jobs will have this variable, not matter if an environment is defined or
+not.
+
+For example, if the environment scope is `production`, then only the jobs
+having the environment `production` defined would have this specific variable.
+Wildcards (`*`) can be used along with the environment name, therefore if the
+environment scope is `review/*` then any jobs with environment names starting
+with `review/` would have that particular variable.
+
+To learn more about about scoping environments, see [Scoping environments with specs](../environments.md#scoping-environments-with-specs-premium).
+
### Deployment environment variables
> Introduced in GitLab 8.15.
@@ -379,8 +425,7 @@ For instance, suppose you added a
[custom variable `$TEST`](#creating-a-custom-environment-variable)
as exemplified above and you want to override it in a manual pipeline.
Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**.
-Choose the branch you want to run the pipeline for, then add a new variable
-pair through the UI:
+Choose the branch you want to run the pipeline for, then add a new variable through the UI:
![Override variable value](img/override_variable_manual_pipeline.png)
@@ -425,8 +470,9 @@ Below you can find supported syntax reference:
1. Equality matching using a string
> Example: `$VARIABLE == "some value"`
+ > Example: `$VARIABLE != "some value"` _(added in 11.11)_
- You can use equality operator `==` to compare a variable content to a
+ You can use equality operator `==` or `!=` to compare a variable content to a
string. We support both, double quotes and single quotes to define a string
value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'`
are supported. `"some value" == $VARIABLE` is correct too.
@@ -434,22 +480,26 @@ Below you can find supported syntax reference:
1. Checking for an undefined value
> Example: `$VARIABLE == null`
+ > Example: `$VARIABLE != null` _(added in 11.11)_
It sometimes happens that you want to check whether a variable is defined
or not. To do that, you can compare a variable to `null` keyword, like
`$VARIABLE == null`. This expression is going to evaluate to truth if
- variable is not defined.
+ variable is not defined when `==` is used, or to falsey if `!=` is used.
1. Checking for an empty variable
> Example: `$VARIABLE == ""`
+ > Example: `$VARIABLE != ""` _(added in 11.11)_
If you want to check whether a variable is defined, but is empty, you can
- simply compare it against an empty string, like `$VAR == ''`.
+ simply compare it against an empty string, like `$VAR == ''` or non-empty
+ string `$VARIABLE != ""`.
1. Comparing two variables
> Example: `$VARIABLE_1 == $VARIABLE_2`
+ > Example: `$VARIABLE_1 != $VARIABLE_2` _(added in 11.11)_
It is possible to compare two variables. This is going to compare values
of these variables.
@@ -468,9 +518,11 @@ Below you can find supported syntax reference:
1. Pattern matching _(added in 11.0)_
> Example: `$VARIABLE =~ /^content.*/`
+ > Example: `$VARIABLE_1 !~ /^content.*/` _(added in 11.11)_
It is possible perform pattern matching against a variable and regular
- expression. Expression like this evaluates to truth if matches are found.
+ expression. Expression like this evaluates to truth if matches are found
+ when using `=~`. It evaluates to truth if matches are not found when `!~` is used.
Pattern matching is case-sensitive by default. Use `i` flag modifier, like
`/pattern/i` to make a pattern case-insensitive.
@@ -644,13 +696,15 @@ MIIFQzCCBCugAwIBAgIRAL/ElDjuf15xwja1ZnCocWAwDQYJKoZIhvcNAQELBQAw'
...
```
+[ee-2112]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2112
[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables"
-[eep]: https://about.gitlab.com/pricing/ "Available only in GitLab Premium"
[envs]: ../environments.md
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
[shellexecutors]: https://docs.gitlab.com/runner/executors/
[triggered]: ../triggers/README.md
+[trigger-job-token]: ../triggers/README.md#ci-job-token
[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
[registry]: ../../user/project/container_registry.md
[dependent-repositories]: ../../user/project/new_ci_build_permissions_model.md#dependent-repositories
+[get-job-artifacts]: ../../api/jobs.html#get-job-artifacts
diff --git a/doc/ci/variables/img/custom_variable_output.png b/doc/ci/variables/img/custom_variable_output.png
deleted file mode 100644
index 50f3bceff9a..00000000000
--- a/doc/ci/variables/img/custom_variable_output.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/variables/img/custom_variables_output.png b/doc/ci/variables/img/custom_variables_output.png
new file mode 100644
index 00000000000..29f5c63b3d9
--- /dev/null
+++ b/doc/ci/variables/img/custom_variables_output.png
Binary files differ
diff --git a/doc/ci/variables/img/new_custom_variable_example.png b/doc/ci/variables/img/new_custom_variable_example.png
deleted file mode 100644
index d169c5f1806..00000000000
--- a/doc/ci/variables/img/new_custom_variable_example.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/variables/img/new_custom_variables_example.png b/doc/ci/variables/img/new_custom_variables_example.png
new file mode 100644
index 00000000000..4b78e0ff587
--- /dev/null
+++ b/doc/ci/variables/img/new_custom_variables_example.png
Binary files differ
diff --git a/doc/ci/variables/img/override_variable_manual_pipeline.png b/doc/ci/variables/img/override_variable_manual_pipeline.png
index 3bcd354e096..3c8c59720cf 100644
--- a/doc/ci/variables/img/override_variable_manual_pipeline.png
+++ b/doc/ci/variables/img/override_variable_manual_pipeline.png
Binary files differ
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 846c539daab..4e902c042e6 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -49,23 +49,23 @@ future GitLab releases.**
| `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) |
-| `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) |
-| `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) |
-| `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`) |
-| `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`) |
-| `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`) |
-| `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) |
-| `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) |
-| `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) |
-| `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) |
-| `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) |
-| `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) |
-| `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) |
-| `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) |
-| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of usernames of assignees for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). [Multiple assignees for merge requests](https://gitlab.com/gitlab-org/gitlab-ee/issues/2004) is scheduled for a future release |
-| `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) |
-| `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) |
+| `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 and the merge request is created. |
+| `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 and the merge request is created. |
+| `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_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 |
@@ -84,6 +84,7 @@ future GitLab releases.**
| `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_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 |
@@ -111,3 +112,7 @@ future GitLab releases.**
| `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 |
+
+[gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token
+[registry]: ../../user/project/container_registry.md
+[dependent-repositories]: ../../user/project/new_ci_build_permissions_model.md#dependent-repositories
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 091ddcb0bae..0470cf52654 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -112,3 +112,22 @@ They are:
- Not supported:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- In the `only` and `except` [variables expressions](README.md#environment-variables-expressions).
+
+## Variables with an environment scope
+
+Variables defined with an environment scope are supported. Given that
+there is a variable `$STAGING_SECRET` defined in a scope of
+`review/staging/*`, the following job that is using dynamic environments
+is going to be created, based on the matching variable expression:
+
+```yaml
+my-job:
+ stage: staging
+ environment:
+ name: review/$CI_JOB_STAGE/deploy
+ script:
+ - 'deploy staging'
+ only:
+ variables:
+ - $STAGING_SECRET == 'something'
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 36a0bf10416..dca2d953286 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -19,7 +19,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-ce`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml).
NOTE: **Note:**
-If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter),
+If you have a [mirrored repository where GitLab pulls from](../../workflow/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**.
@@ -56,7 +56,7 @@ independently from each other.
Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
-project namespace. For example, `http://gitlab.example.com/gitlab-org/project-123/-/ci/lint`.
+project namespace. For example, `https://gitlab.example.com/gitlab-org/project-123/-/ci/lint`.
### Unavailable names for jobs
@@ -101,7 +101,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`, and `artifacts:reports:performance`. |
+| [`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`. |
| [`dependencies`](#dependencies) | Other jobs that a job depends on so that you can pass artifacts between them. |
| [`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. |
@@ -1457,7 +1457,7 @@ dashboards.
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html)
+The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html)
as artifacts.
The collected Dependency Scanning report will be uploaded to GitLab as an artifact and will
@@ -1468,7 +1468,7 @@ dashboards.
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-The `container_scanning` report collects [Container Scanning vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html)
+The `container_scanning` report collects [Container Scanning vulnerabilities](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html)
as artifacts.
The collected Container Scanning report will be uploaded to GitLab as an artifact and will
@@ -1479,7 +1479,7 @@ dashboards.
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-The `dast` report collects [DAST vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html)
+The `dast` report collects [DAST vulnerabilities](https://docs.gitlab.com/ee/user/application_security/dast/index.html)
as artifacts.
The collected DAST report will be uploaded to GitLab as an artifact and will
@@ -1507,6 +1507,14 @@ as artifacts.
The collected Performance report will be uploaded to GitLab as an artifact and will
be automatically shown in merge requests.
+##### `artifacts:reports:metrics` **[PREMIUM]**
+
+The `metrics` report collects [Metrics](../../ci/metrics_reports.md)
+as artifacts.
+
+The collected Metrics report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests.
+
### `dependencies`
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
@@ -1702,7 +1710,7 @@ test:
from `trigger` definition is started by GitLab, a downstream pipeline gets
created.
-Learn more about [multi-project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipelines.html#creating-cross-project-pipelines-from-gitlab-ci-yml).
+Learn more about [multi-project pipelines](../multi_project_pipelines.md#creating-multi-project-pipelines-from-gitlab-ciyml).
#### Simple `trigger` syntax
@@ -2061,27 +2069,27 @@ from another project:
```yaml
include:
- template: Bash.gitlab-ci.yml
- - project: /group/my-project
+ - project: group/my-project
file: /templates/docker-workflow.yml
```
-The `/templates/docker-workflow.yml` present in `/group/my-project` includes two local files
-of the `/group/my-project`:
+The `/templates/docker-workflow.yml` present in `group/my-project` includes two local files
+of the `group/my-project`:
```yaml
include:
- - local: : /templates/docker-build.yml
- - local: : /templates/docker-testing.yml
+ - local: /templates/docker-build.yml
+ - local: /templates/docker-testing.yml
```
-Our `/templates/docker-build.yml` present in `/group/my-project` adds a `docker-build` job:
+Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job:
```yaml
docker-build:
script: docker build -t my-image .
```
-Our second `/templates/docker-test.yml` present in `/group/my-project` adds a `docker-test` job:
+Our second `/templates/docker-test.yml` present in `group/my-project` adds a `docker-test` job:
```yaml
docker-test:
@@ -2472,18 +2480,18 @@ Use [`stage`](#stage) instead.
## Custom build directories
-> [Introduced][https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1267] in Gitlab Runner 11.10
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1267) in Gitlab Runner 11.10
NOTE: **Note:**
This can only be used when `custom_build_dir` is enabled in the [Runner's
configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnerscustom_build_dir-section).
This is the default configuration for `docker` and `kubernetes` executor.
-By default, GitLab Runner clones the repository in a unique subpath of the `$CI_BUILDS_DIR` directory.
-However, sometimes your project might require the code in a specific directory,
-but sometimes your project might require to have the code in a specific directory,
-like Go projects, for example. In that case, you can specify the `GIT_CLONE_PATH` variable
-to tell the Runner in which directory to clone the repository:
+By default, GitLab Runner clones the repository in a unique subpath of the
+`$CI_BUILDS_DIR` directory. However, your project might require the code in a
+specific directory (Go projects, for example). In that case, you can specify
+the `GIT_CLONE_PATH` variable to tell the Runner in which directory to clone the
+repository:
```yml
variables:
diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md
new file mode 100644
index 00000000000..01c31728c21
--- /dev/null
+++ b/doc/customization/issue_and_merge_request_template.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/project/description_templates.html#setting-a-default-template-for-issues-and-merge-requests--starter'
+---
+
+This document was moved to [description_templates](https://docs.gitlab.com/ee/user/project/description_templates.html#setting-a-default-template-for-issues-and-merge-requests--starter).
diff --git a/doc/development/README.md b/doc/development/README.md
index 5a33c46c620..83a1145c020 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -38,6 +38,7 @@ description: 'Learn how to contribute to GitLab.'
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags.md)
+- [Licensed feature availability](licensed_feature_availability.md)
- [View sent emails or preview mailers](emails.md)
- [Shell commands](shell_commands.md) in the GitLab codebase
- [`Gemfile` guidelines](gemfile.md)
@@ -48,6 +49,7 @@ description: 'Learn how to contribute to GitLab.'
- [How to dump production data to staging](db_dump.md)
- [Working with the GitHub importer](github_importer.md)
- [Import/Export development documentation](import_export.md)
+- [Elasticsearch integration docs](elasticsearch.md)
- [Working with Merge Request diffs](diffs.md)
- [Kubernetes integration guidelines](kubernetes.md)
- [Permissions](permissions.md)
@@ -55,6 +57,7 @@ description: 'Learn how to contribute to GitLab.'
- [Guidelines for reusing abstractions](reusing_abstractions.md)
- [DeclarativePolicy framework](policies.md)
- [How Git object deduplication works in GitLab](git_object_deduplication.md)
+- [Geo development](geo.md)
## Performance guides
@@ -101,6 +104,10 @@ description: 'Learn how to contribute to GitLab.'
- [Query Count Limits](query_count_limits.md)
- [Database helper modules](database_helpers.md)
+## Integration guides
+
+- [Jira Connect app](integrations/jira_connect.md)
+
## Testing guides
- [Testing standards and style guidelines](testing_guide/index.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 115c8cfb9ff..8d1879e2944 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -210,12 +210,156 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
-<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&amp;h=797">
+```mermaid
+graph TB
+
+ HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX(NGINX)
+ SSH --TCP 22 --> GitLabShell(GitLab Shell)
+ SMTP(SMTP Gateway)
+ Geo(GitLab Geo Node) -- TCP 22, 80, 443 --> NGINX
+
+ subgraph GitLab
+ GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
+ GitLabShell --> Gitaly
+ GitLabShell --> Redis
+ Unicorn --> PgBouncer(PgBouncer)
+ Unicorn --> Redis
+ Unicorn --> Gitaly
+ Redis --> Sidekiq
+ Sidekiq("Sidekiq (GitLab Rails, ES Indexer)") --> PgBouncer
+ GitLabWorkhorse(GitLab Workhorse) --> Unicorn
+ GitLabWorkhorse --> Redis
+ GitLabWorkhorse --> Gitaly
+ Gitaly --> Redis
+ NGINX --> GitLabWorkhorse
+ NGINX -- TCP 8090 --> GitLabPages(GitLab Pages)
+ NGINX --> Grafana(Grafana)
+ Grafana -- TCP 9090 --> Prometheus(Prometheus)
+ Prometheus -- TCP 80, 443 --> Unicorn
+ RedisExporter(Redis Exporter) --> Redis
+ Prometheus -- TCP 9121 --> RedisExporter
+ PostgreSQLExporter(PostgreSQL Exporter) --> PostgreSQL
+ PgBouncerExporter(PgBouncer Exporter) --> PgBouncer
+ Prometheus -- TCP 9187 --> PostgreSQLExporter
+ Prometheus -- TCP 9100 --> NodeExporter(Node Exporter)
+ Prometheus -- TCP 9168 --> GitLabMonitor(GitLab Monitor)
+ Prometheus -- TCP 9127 --> PgBouncerExporter
+ GitLabMonitor --> PostgreSQL
+ GitLabMonitor --> GitLabShell
+ GitLabMonitor --> Sidekiq
+ PgBouncer --> Consul(Consul)
+ PostgreSQL --> Consul
+ PgBouncer --> PostgreSQL
+ NGINX --> Registry(Registry)
+ Unicorn --> Registry
+ NGINX --> Mattermost(Mattermost)
+ Mattermost --- Unicorn
+ Prometheus --> Alertmanager
+ Migrations --> PostgreSQL
+ Runner(Runner) --> NGINX
+ Unicorn -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 80, 443 --> Sentry
+ Unicorn -- TCP 80, 443 --> Sentry
+ Sidekiq -- UDP 6831 --> Jaeger
+ Unicorn -- UDP 6831 --> Jaeger
+ Gitaly -- UDP 6831 --> Jaeger
+ GitLabShell -- UDP 6831 --> Jaeger
+ GitLabWorkhorse -- UDP 6831 --> Jaeger
+ Alertmanager -- TCP 25 --> SMTP
+ Sidekiq -- TCP 25 --> SMTP
+ Unicorn -- TCP 25 --> SMTP
+ Unicorn -- TCP 369 --> LDAP
+ Sidekiq -- TCP 369 --> LDAP
+ Unicorn -- TCP 443 --> ObjectStorage("Object Storage")
+ Sidekiq -- TCP 443 --> ObjectStorage
+ GitLabWorkhorse -- TCP 443 --> ObjectStorage
+ Registry -- TCP 443 --> ObjectStorage
+ Geo -- TCP 5432 --> PostgreSQL
+ end
+
+ HTTPK8s(HTTP/HTTPS) -- TCP 80, 443 --> LoadBalancerK8s(Load Balancer)
+ LoadBalancerK8s -- TCP 80, 443 --> nginx-ingressK8s
+ subgraph Kubernetes
+ PrometheusK8s(Prometheus)
+ TillerK8s(Tiller)
+ nginx-ingressK8s(NGINX Ingress)
+ Cert-ManagerK8s(Cert-Manager)
+ GitLabRunnerK8s(GitLab Runner)
+ GitLabRunnerK8s --> NGINX
+ JupyterHubK8s(JupyterHub)
+ nginx-ingressK8s --> JupyterHubK8s
+ KnativeK8s(Knative)
+ end
+
+classDef defaultoff stroke-dasharray: 5, 5
+class ElasticSearch defaultoff
+class Grafana defaultoff
+class PrometheusK8s defaultoff
+class TillerK8s defaultoff
+class nginx-ingressK8s defaultoff
+class Cert-ManagerK8s defaultoff
+class GitLabRunnerK8s defaultoff
+class JupyterHubK8s defaultoff
+class KnativeK8s defaultoff
+class HTTPK8s defaultoff
+class LoadBalancerK8s defaultoff
+class Sentry defaultoff
+class Jaeger defaultoff
+class Alertmanager defaultoff
+class SMTP defaultoff
+class ObjectStorage defaultoff
+class Geo defaultoff
+```
-_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
+**Legend**:
+
+* ✅ - Automatically configured
+* ⚙ - Requires additional configuration
+* ⤓ - Additional software/service required
+* ❌ - Not available
+
+| Component | Description | [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html) | [GitLab chart](https://docs.gitlab.com/charts/) | [Minikube Minimal](https://docs.gitlab.com/charts/development/minikube/#deploying-gitlab-with-minimal-settings) | [GitLab.com](https://gitlab.com) | CE/EE |
+| --------- | ----------- |:--------------------:|:------------------:|:-----:|:--------:|:--------:|
+| NGINX | Routes requests to appropriate components, terminates SSL | [✅](https://docs.gitlab.com/omnibus/settings/nginx.html) | [✅](https://docs.gitlab.com/charts/charts/nginx/index.html) | [⚙](https://docs.gitlab.com/charts/charts/nginx/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) |CE & EE |
+| Unicorn (GitLab Rails) | Handles requests for the web interface and API | [✅](https://docs.gitlab.com/omnibus/settings/unicorn.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#unicorn) | CE & EE |
+| Sidekiq | Background jobs processor | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#sidekiq) | CE & EE |
+| Gitaly | Git RPC service for handling all git calls made by GitLab | [✅](https://docs.gitlab.com/ee/administration/gitaly/) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Workhorse | Smart reverse proxy, handles large HTTP requests | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Shell | Handles `git` over SSH sessions | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Pages | Hosts static websites | [⚙](https://docs.gitlab.com/ee/administration/pages/) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#gitlab-pages) | CE & EE |
+| Registry | Container registry, allows pushing and pulling of images | [⚙](https://docs.gitlab.com/ee/administration/container_registry.html#container-registry-domain-configuration) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/ee/user/project/container_registry.html#build-and-push-images) | CE & EE |
+| Redis | Caching service | [✅](https://docs.gitlab.com/omnibus/settings/redis.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| PostgreSQL | Database | [✅](https://docs.gitlab.com/omnibus/settings/database.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#postgresql) | CE & EE |
+| PgBouncer | Database connection pooling, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | EE Only |
+| Consul | Database node discovery, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/consul.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#consul) | EE Only |
+| Prometheus | Time-series database, metrics collection, and query service | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [⚙](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#prometheus) | CE & EE |
+| Prometheus Alertmanager | Deduplicates, groups, and routes alerts from Prometheus | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| Grafana | Metrics dashboard | [⚙](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [✅](https://dashboards.gitlab.com/d/RZmbBr7mk/gitlab-triage?refresh=30s) | CE & EE |
+| Redis Exporter | Prometheus endpoint with Redis metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| PostgreSQL Exporter | Prometheus endpoint with PostgreSQL metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| PgBouncer Exporter | Prometheus endpoint with PgBouncer metrics | [⚙](https://docs.gitlab.com/ee/administration/monitoring/prometheus/pgbouncer_exporter.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| GitLab Monitor | Tracks a variety of GitLab metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_monitor_exporter.html) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| Mattermost | Open-source Slack alternative | [⚙](https://docs.gitlab.com/omnibus/gitlab-mattermost/) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#manual-configuration), [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost.html) | CE & EE |
+| Minio | Object storage service | [⤓](https://min.io/download) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [❌](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#storage-architecture) | CE & EE |
+| Runner | Executes GitLab CI jobs | [⤓](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/runner/) | [⚙](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) | CE & EE |
+| Migrations | Database migrations | [✅](https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | CE & EE |
+| Certificate Management | TLS Settings, Let's Encrypt | [✅](https://docs.gitlab.com/omnibus/settings/ssl.html) | [✅](https://docs.gitlab.com/charts/installation/tls.html) | [⚙](https://docs.gitlab.com/charts/installation/tls.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#secrets-management) | CE & EE |
+| GitLab Geo Node | Geographically distributed GitLab nodes | [⚙](https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions) | [❌](https://gitlab.com/charts/gitlab/issues/8) | [❌](https://gitlab.com/charts/gitlab/issues/8) | ✅ | EE Only |
+| LDAP Authentication | Authenticate users against centralized LDAP directory | [⤓](https://docs.gitlab.com/ee/administration/auth/ldap.html) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [❌](https://about.gitlab.com/pricing/#gitlab-com) | CE & EE |
+| Outbound email (SMTP) | Send email messages to users | [⤓](https://docs.gitlab.com/omnibus/settings/smtp.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| Inbound email (SMTP) | Receive messages to update issues | [⤓](https://docs.gitlab.com/ee/administration/incoming_email.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| ElasticSearch | Improved search within GitLab | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | EE Only |
+| Sentry: GitLab instance | Tracking errors generated by the GitLab instance | [⤓](https://docs.gitlab.com/omnibus/settings/configuration.html#error-reporting-and-logging-with-sentry) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [✅](https://about.gitlab.com/handbook/support/workflows/services/gitlab_com/500_errors.html#searching-sentry) | CE & EE |
+| Jaeger: GitLab instance | View traces generated by the GitLab instance | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | CE & EE |
+| Sentry: deployed apps | Error tracking for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | CE & EE |
+| Jaeger: deployed apps | Distributed tracing for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | EE Only |
+| Kubernetes cluster apps | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | CE & EE |
A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
+We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/).
+
The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository.
When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects.
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index b1a32e0ed26..c4e5995714d 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -23,11 +23,6 @@ one of the [Merge request coaches][team].
If you need assistance with security scans or comments, feel free to include the
Security Team (`@gitlab-com/gl-security`) in the review.
-The `danger-review` CI job will randomly pick a reviewer and a maintainer for
-each area of the codebase that your merge request seems to touch. It only makes
-recommendations - feel free to override it if you think someone else is a better
-fit!
-
Depending on the areas your merge request touches, it must be **approved** by one
or more [maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#maintainer):
@@ -37,6 +32,26 @@ widget. Reviewers can add their approval by [approving additionally](https://doc
Getting your merge request **merged** also requires a maintainer. If it requires
more than one approval, the last maintainer to review and approve it will also merge it.
+### Reviewer roulette
+
+The `danger-review` CI job will randomly pick a reviewer and a maintainer for
+each area of the codebase that your merge request seems to touch. It only makes
+recommendations - feel free to override it if you think someone else is a better
+fit!
+
+It picks reviewers and maintainers from the list at the
+[engineering projects](https://about.gitlab.com/handbook/engineering/projects/)
+page, with these behaviours:
+
+1. It will not pick people whose [GitLab status](../user/profile/#current-status)
+ contains the string 'OOO'.
+2. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
+ are three times as likely to be picked as other reviewers.
+3. It always picks the same reviewers and maintainers for the same
+ branch name (unless their OOO status changes, as in point 1). It
+ removes leading `ce-` and `ee-`, and trailing `-ce` and `-ee`, so
+ that it can be stable for backport branches.
+
### Approval guidelines
As described in the section on the responsibility of the maintainer below, you
@@ -132,14 +147,6 @@ If a developer who happens to also be a maintainer was involved in a merge reque
as a domain expert and/or reviewer, it is recommended that they are not also picked
as the maintainer to ultimately approve and merge it.
-Try to review in a timely manner; doing so allows everyone involved in the merge
-request to iterate faster as the context is fresh in memory. Further, this
-improves contributors' experiences significantly. Reviewers should aim to review
-within two working days from the date they were assigned the merge request. If
-you don't think you'll be able to review a merge request within that time, let
-the author know as soon as possible. When the author of the merge request has not
-heard anything after two days, a new reviewer should be assigned.
-
Maintainers should check before merging if the merge request is approved by the
required approvers.
@@ -220,6 +227,37 @@ It is responsibility of the author of a merge request that the merge request is
Developers who have capacity can regularly check the list of [merge requests to review](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=opened&label_name%5B%5D=ready%20for%20review) and assign any merge request they want to review.
+### Review turnaround time
+
+Since [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization),
+reviewers are expected to review assigned merge requests in a timely manner,
+even when this may negatively impact their other tasks and priorities.
+Doing so allows everyone involved in the merge request to iterate faster as the
+context is fresh in memory, improves contributors' experiences significantly,
+and gives authors more time to address feedback and iterate on their work before
+the [feature freeze](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md#feature-freeze-on-the-7th-for-the-release-on-the-22nd).
+
+A turnaround time of two working days is usually acceptable, since engineers
+will typically have other things to work on while they're waiting for review,
+but don't hesitate to ask the author if it's unclear what time frame would be
+acceptable, how urgent the review is, or how significant the blockage. Authors
+are also encouraged to provide this information up-front to reviewers, but are
+expected to be mindful of the [guidelines on when to ask for review on MRs that
+are intended to go in before the feature freeze](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md#between-the-1st-and-the-7th),
+and realistic in their expectations if these were not followed.
+
+If you don't think you'll be able to review a merge request within a reasonable
+time frame, let the author know as soon as possible and try to help them find
+another reviewer or maintainer who will be able to, so that they can be unblocked
+and get on with their work quickly. Of course, if you are out of office and have
+[communicated](https://about.gitlab.com/handbook/paid-time-off/#communicating-your-time-off)
+this through your GitLab.com Status, authors are expected to realize this and
+find a different reviewer themselves.
+
+When a merge request author feels like they have been blocked for longer than
+is reasonable, they are free to remind the reviewer through Slack or assign
+another reviewer.
+
### Reviewing code
Understand why the change is necessary (fixes a bug, improves the user
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 827b3d7681a..0e1ab8663ed 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -340,13 +340,14 @@ addressed.
In order to track things that can be improved in GitLab's codebase,
we use the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
-For user experience improvements, we use the ~"UX debt" label.
+For missed user experience requirements, we use the ~"UX debt" label.
These labels should be added to issues that describe things that can be improved,
shortcuts that have been taken, features that need additional attention, and all
other things that have been left behind due to high velocity of development.
For example, code that needs refactoring should use the ~"technical debt" label,
-user experience refinements should use the ~"UX debt" label.
+something that didn't ship according to our Design System guidelines should
+use the ~"UX debt" label.
Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 5e310092a6e..8a4aa5dfa7f 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -155,7 +155,7 @@ the contribution acceptance criteria below:
restarting the failing CI job, rebasing from master to bring in updates that
may resolve the failure, or if it has not been fixed yet, ask a developer to
help you fix the test.
-1. The MR initially contains a a few logically organized commits.
+1. The MR initially contains a few logically organized commits.
1. The changes can merge without problems. If not, you should rebase if you're the
only one working on your feature branch, otherwise merge `master`.
1. Only one specific issue is fixed or one specific feature is implemented. Do not
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 4a2ff64807e..52a4bb27817 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -16,13 +16,13 @@ In addition to this page, the following resources to help craft and contribute d
- [Markdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/) - A reference for the markdown implementation used by GitLab's documentation site and about.gitlab.com.
- [Site architecture](site_architecture/index.md) - How docs.gitlab.com is built.
-## Source and rendered locations
+## Source files and rendered web locations
Documentation for GitLab Community Edition (CE) and Enterprise Edition (EE), along with GitLab Runner and Omnibus, is published to [docs.gitlab.com](https://docs.gitlab.com). The documentation for CE and EE is also published within the application at `/help` on the domain of the GitLab instance.
At `/help`, only content for your current edition and version is included, whereas multiple versions' content is available at docs.gitlab.com.
-The source of the documentation is maintained in the following repository locations:
+The source of the documentation exists within the codebase of each GitLab application in the following repository locations:
| Project | Path |
| --- | --- |
@@ -48,87 +48,11 @@ as its markdown rendering engine. See the [GitLab Markdown Guide](https://about.
Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is missing, you are welcome to suggest one via a merge request.
-## Documentation types and organization
-
-The documentation is structured based on the GitLab UI structure itself,
-separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
-[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development).
-
-Organize docs by product area and subject, not type. For example, do not create groupings of similar media types
-(e.g. indexes of all articles, videos, etc.).
-
-Similarly, we do not use glossaries or FAQs. Such grouping of content by type makes
-it difficult to browse for the information you need and difficult to maintain up-to-date content.
-Instead, organize content by its subject (e.g. everything related to CI goes together)
-and cross-link between any related content.
-
-### Location and naming of files
-
-Our goal is to have a clear hierarchical structure with meaningful URLs
-like `docs.gitlab.com/user/project/merge_requests/`. With this pattern,
-you can immediately tell that you are navigating to user-related documentation
-about project features; specifically about merge requests. Our site's paths match
-those of our repository, so the clear structure also makes documentation easier to update.
-
-In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation,
-all docs should be linked at least from its higher-level index page if not also from other relevant locations.
-
-The table below shows what kind of documentation goes where.
-
-| Directory | What belongs here |
-|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
-| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
-| `doc/api/` | API related documentation. |
-| `doc/development/` | Documentation related to the development of GitLab. Related process and style guides should go here. |
-| `doc/legal/` | Legal documents about contributing to GitLab. |
-| `doc/install/` | Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
-| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
-| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
-
-**Rules and best practices:**
-
-1. When you create a new directory, always start with an `index.md` file.
- Do not use another file name and **do not** create `README.md` files.
-1. **Do not** use special chars and spaces, or capital letters in file names,
- directory names, branch names, and anything that generates a path.
-1. When creating a new document and it has more than one word in its name,
- make sure to use underscores instead of spaces or dashes (`-`). For example,
- a proper naming would be `import_projects_from_github.md`. The same rule
- applies to images.
-1. For image files, do not exceed 100KB.
-1. We do not yet support embedded videos. Please link out.
-1. There are four main directories, `user`, `administration`, `api` and `development`.
-1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
- `profile/`, `dashboard/` and `admin_area/`.
- 1. `doc/user/project/` should contain all project related documentation.
- 1. `doc/user/group/` should contain all group related documentation.
- 1. `doc/user/profile/` should contain all profile related documentation.
- Every page you would navigate under `/profile` should have its own document,
- i.e. `account.md`, `applications.md`, `emails.md`, etc.
- 1. `doc/user/dashboard/` should contain all dashboard related documentation.
- 1. `doc/user/admin_area/` should contain all admin related documentation
- describing what can be achieved by accessing GitLab's admin interface
- (_not to be confused with `doc/administration` where server access is
- required_).
- 1. Every category under `/admin/application_settings` should have its
- own document located at `doc/user/admin_area/settings/`. For example,
- the **Visibility and Access Controls** category should have a document
- located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
-1. The `doc/topics/` directory holds topic-related technical content. Create
- `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
- General user- and admin- related documentation, should be placed accordingly.
-1. The directories `/workflow/`, `/university/`, and `/articles/` have
-been **deprecated** and the majority their docs have been moved to their correct location
-in small iterations.
-
-If you are unsure where a document or a content addition should live, this should
-not stop you from authoring and contributing. You can use your best judgment and
-then ask the reviewer of your MR to confirm your decision, and/or ask a technical writer
-at any stage in the process. The techncial writing team will review all documentation
-changes, regardless, and can move content if there is a better place for it.
-
-### Changing document location
+## Folder structure and files
+
+See the [Structure](styleguide.md#structure) section of the [Documentation Style Guide](styleguide.md).
+
+## Changing document location
Changing a document's location requires specific steps to be followed to ensure that
users can seamlessly access the new doc page, whether they are accesing content
@@ -186,7 +110,7 @@ Things to note:
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the "Change documentation location" MR description template.
-#### Alternative redirection method
+### Alternative redirection method
Alternatively to the method described above, you can simply replace the content
of the old file with a frontmatter containing a redirect link:
@@ -204,7 +128,7 @@ This redirection method will not provide a redirect fallback on GitLab `/help`.
it, make sure to add a link to the new page on the doc, otherwise it's a dead end for users that
land on the doc via `/help`.
-#### Redirections for pages with Disqus comments
+### Redirections for pages with Disqus comments
If the documentation page being relocated already has any Disqus comments,
we need to preserve the Disqus thread.
@@ -389,7 +313,7 @@ to merge changes that will break `master` from a merge request with a successful
## Docs site architecture
See the [Docs site architecture](site_architecture/index.md) page to learn
-how we build and deploy the site at [docs.gitlab.com](https://docs.gitlab.com), and
+how we build and deploy the site at [docs.gitlab.com](https://docs.gitlab.com) and
to review all the assets and libraries in use.
### Global navigation
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 1cefe48b4c1..5caca846cc9 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -4,64 +4,190 @@ description: 'Writing styles, markup, formatting, and other standards for GitLab
# Documentation Style Guide
-The documentation style guide defines the markup structure used in
-GitLab documentation. Check the
-[documentation guidelines](index.md) for general development instructions.
+This document defines the standards for GitLab's documentation content and files.
-See the GitLab handbook for the [writing style guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
+For broader information about the documentation, see the [Documentation guidelines](index.md).
For programmatic help adhering to the guidelines, see [linting](index.md#linting).
-## Files
+See the GitLab handbook for further [writing style guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines)
+that apply to all GitLab content, not just documentation.
-See the [Documentation types and organization](index.md#documentation-types-and-organization) section
-on the index page for information on how to structure and name files across the GitLab documentation.
+## Documentation is the single source of truth (SSOT)
-DANGER: **Attention:**
-**Do not** use capital letters, spaces, or special chars in file names,
-branch names, directory names, headings, or in anything that generates a path.
+### Why a single source of truth
-NOTE: **Note:**
-**Do not** create new `README.md` files, name them `index.md` instead. There's
-a test that will fail if it spots a new `README.md` file.
-
-### Markdown
+The documentation is the SSOT for all information related to the implementation, usage, and troubleshooting of GitLab products and features. It evolves continually, in keeping with new products and features, and with improvements for clarity, accuracy, and completeness.
-The [documentation website](https://docs.gitlab.com) uses GitLab Kramdown as its Markdown rendering engine [as of October 2018](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108). For a complete Kramdown reference, see the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
+This policy prevents information silos, ensuring that it remains easy to find information about GitLab products.
-The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown)
-gem will support all [GFM markup](../../user/markdown.md) in the future. For now,
-use regular markdown markup, following the rules in the linked style guide.
+It also informs decisions about the kinds of content we include in our documentation.
-Note that Kramdown-specific markup (e.g., `{:.class}`) will not render properly in on GitLab instances in [`/help`](index.md#gitlab-help).
+The documentation is a continually evolving SSOT for all information related to the implementation, usage, and troubleshooting of GitLab products and features.
-## Content
+### All information
-These guidelines help toward the goal of having every user's search of documentation
-yield a useful result, and ensuring content is helpful and easy to consume.
+Include problem-solving actions that may address rare cases or be considered 'risky', so long as proper context is provided in the form of fully detailed warnings and caveats. This kind of content should be included as it could be helpful to others and, when properly explained, its benefits outweigh the risks. If you think you have found an exception to this rule, contact the Technical Writing team.
-### Subject matter
+We will add all troubleshooting information to the documentation, no matter how unlikely a user is to encounter a situation.
+For the Troubleshooting sections, people in GitLab Support can merge additions themselves.
-[The documentation is the single source of truth (SSOT)](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) for any and all helpful information and processes needed to learn about, implement, use, and troubleshoot GitLab features. Note that this includes problem-solving actions that may address rare cases or be considered 'risky', so long as proper context is provided. See the SSOT link for more detail.
+### All media types
-### Media types and sources
-
-Include any media types/sources, if relevant to readers. You can freely include or link presentations, diagrams, videos, etc.; no matter who it was originally composed for, if it is helpful to any of our audiences, we can include it.
+Include any media types/sources if the content is relevant to readers. You can freely include or link presentations, diagrams, videos, etc.; no matter who it was originally composed for, if it is helpful to any of our audiences, we can include it.
- If you use an image that has a separate source file (for example, a vector or diagram format), link the image to the source file so that it may be reused or updated by anyone.
- Do not copy and paste content from other sources unless it is a limited quotation with the source cited. Typically it is better to either rephrase relevant information in your own words or link out to the other source.
-### Structure across documents
+### No special types
+
+In the software industry, it is a best practice to organize documentatioin in different types. For example, [Divio recommends](https://www.divio.com/blog/documentation/):
+
+1. Tutorials
+2. How-to guides
+3. Explanation
+4. Reference (for example, a glossary)
+
+At GitLab, we have so many product changes in our monthly releases that we can't afford to continually update multiple types of information.
+If we have multiple types, the information will become outdated. Therefore, we have a [single template](structure.md) for documentation.
+
+We currently do not distinguish specific document types, although we are open to reconsidering this policy
+once the documentation has reached a future stage of maturity and quality. If you are reading this, then despite our
+continual improvement efforts, that point hasn't been reached.
+
+### Link instead of summarize
+
+There is a temptation to summarize the information on another page.
+This will cause the information to live in two places.
+Instead, link to the SSOT and explain why it is important to consume the information.
+
+### Organize by topic, not by type
+
+Beyond top-level audience-type folders (e.g. `administration`), we organize content by topic, not by type, so that it can be located as easily as possible within the single-source-of-truth (SSOT) section for the subject matter.
+
+For example, do not create groupings of similar media types (e.g. glossaries, FAQs, or sets of all articles or videos).
+
+Such grouping of content by type makes
+it difficult to browse for the information you need and difficult to maintain up-to-date content.
+Instead, organize content by its subject (e.g. everything related to CI goes together)
+and cross-link between any related content.
+
+### Docs-first methodology
+
+We employ a **docs-first methodology** to help ensure that the docs remain a complete and trusted resource, and to make communicating about the use of GitLab more efficient.
+
+* If the answer to a question exists in documentation, share the link to the docs instead of rephrasing the information.
+* When you encounter new information not available in GitLab’s documentation (for example, when working on a support case or testing a feature), your first step should be to create a merge request to add this information to the docs. You can then share the MR in order to communicate this information.
+
+New information that would be useful toward the future usage or troubleshooting of GitLab should not be written directly in a forum or other messaging system, but added to a docs MR and then referenced, as described above. Note that among any other doc changes, you can always add a Troubleshooting section to a doc if none exists, or un-comment and use the placeholder Troubleshooting section included as part of our [doc template](structure.md#template-for-new-docs), if present.
+
+The more we reflexively add useful information to the docs, the more (and more successfully) the docs will be used to efficiently accomplish tasks and solve problems.
-- Place files in the correct directory per the [Documentation directory structure](index.md#documentation-types-and-organization) guidelines.
-- To avoid duplication, do not include the same information in multiple places. Instead, choose one 'single source of truth (SSOT)' location and link from other relevant locations.
-- When referencing other GitLab products and features, link to their respective docs.
-- When referencing third-party products or technologies, link out to their external sites, documentation, and resources.
+If you have questions when considering, authoring, or editing docs, ask the Technical Writing team on Slack in `#docs` or in GitLab by mentioning the writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages). Otherwise, forge ahead with your best effort. It does not need to be perfect; the team is happy to review and improve upon your content. Please review the [Documentation guidelines](index.md) before you begin your first documentation MR.
+
+Having a knowledge base is any form that is separate from the documentation would be against the docs-first methodology because the content would overlap with the documentation.
+
+## Markdown
+
+All GitLab documentation is written using [Markdown](https://en.wikipedia.org/wiki/Markdown).
+
+The [documentation website](https://docs.gitlab.com) uses GitLab Kramdown as its Markdown rendering engine. For a complete Kramdown reference, see the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/).
+
+The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown)
+Ruby gem will support all [GFM markup](../../user/markdown.md) in the future. That is,
+all markup that is supported for display in the GitLab application itself. For now,
+use regular Markdown markup, following the rules in the linked style guide.
+
+Note that Kramdown-specific markup (e.g., `{:.class}`) will not render properly on GitLab instances under [`/help`](index.md#gitlab-help).
+
+## Structure
+
+### Organize by topic, not by type
+
+Because we want documentation to be a SSOT, we should [organize by topic, not by type](#organize-by-topic-not-by-type).
+
+### Folder structure overview
+
+The documentation is separated by top-level audience folders [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user),
+[`administration`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`development`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development) (contributing) folders.
+
+Beyond that, we primarily follow the structure of the GitLab user interface or API.
+
+Our goal is to have a clear hierarchical structure with meaningful URLs
+like `docs.gitlab.com/user/project/merge_requests/`. With this pattern,
+you can immediately tell that you are navigating to user-related documentation
+about Project features; specifically about Merge Requests. Our site's paths match
+those of our repository, so the clear structure also makes documentation easier to update.
+
+The table below shows what kind of documentation goes where.
+
+| Directory | What belongs here |
+|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------|
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here, including usage of the `/admin` interface. |
+| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface exist under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab, whether contributing code or docs. Related process and style guides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/` | Contains instructions for installing GitLab. |
+| `doc/update/` | Contains instructions for updating GitLab. |
+| `doc/topics/` | Indexes per topic (`doc/topics/topic-name/index.md`): all resources for that topic. |
+
+### Working with directories and files
+
+1. When you create a new directory, always start with an `index.md` file.
+ Do not use another file name and **do not** create `README.md` files.
+1. **Do not** use special characters and spaces, or capital letters in file names,
+ directory names, branch names, and anything that generates a path.
+1. When creating a new document and it has more than one word in its name,
+ make sure to use underscores instead of spaces or dashes (`-`). For example,
+ a proper naming would be `import_projects_from_github.md`. The same rule
+ applies to images.
+1. For image files, do not exceed 100KB.
+1. We do not yet support embedded videos. Please link out.
+1. There are four main directories, `user`, `administration`, `api` and `development`.
+1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
+ `profile/`, `dashboard/` and `admin_area/`.
+ 1. `doc/user/project/` should contain all project related documentation.
+ 1. `doc/user/group/` should contain all group related documentation.
+ 1. `doc/user/profile/` should contain all profile related documentation.
+ Every page you would navigate under `/profile` should have its own document,
+ i.e. `account.md`, `applications.md`, `emails.md`, etc.
+ 1. `doc/user/dashboard/` should contain all dashboard related documentation.
+ 1. `doc/user/admin_area/` should contain all admin related documentation
+ describing what can be achieved by accessing GitLab's admin interface
+ (_not to be confused with `doc/administration` where server access is
+ required_).
+ 1. Every category under `/admin/application_settings` should have its
+ own document located at `doc/user/admin_area/settings/`. For example,
+ the **Visibility and Access Controls** category should have a document
+ located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
+1. The `doc/topics/` directory holds topic-related technical content. Create
+ `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
+ General user- and admin- related documentation, should be placed accordingly.
+1. The directories `/workflow/`, `/university/`, and `/articles/` have
+been **deprecated** and the majority their docs have been moved to their correct location
+in small iterations.
+
+If you are unsure where a document or a content addition should live, this should
+not stop you from authoring and contributing. You can use your best judgment and
+then ask the reviewer of your MR to confirm your decision, and/or ask a technical writer
+at any stage in the process. The techncial writing team will review all documentation
+changes, regardless, and can move content if there is a better place for it.
+
+### Avoid duplication
+
+Do not include the same information in multiple places. [Link to a SSOT instead.](#link-instead-of-summarize)
+
+### References across documents
+
+- Give each folder an index.md page that introduces the topic, introduces the pages within, and links to the pages within (including to the index pages of any next-level subpaths).
+- To ensure discoverability, ensure each new or renamed doc is linked from its higher-level index page and other related pages.
+- When making reference to other GitLab products and features, link to their respective docs, at least on first mention.
+- When making reference to third-party products or technologies, link out to their external sites, documentation, and resources.
### Structure within documents
-- Include any and all applicable subsections as described on the [structure and template](structure.md) page,
-- To ensure discoverability, link to each doc from its higher-level index page and other related pages.
+- Include any and all applicable subsections as described on the [structure and template](structure.md) page.
- Structure content in alphabetical order in tables, lists, etc., unless there is
a logical reason not to (for example, when mirroring the UI or an otherwise ordered sequence).
@@ -95,7 +221,7 @@ Include any media types/sources, if relevant to readers. You can freely include
- List item 2
```
-### Tables overlapping the ToC
+### Tables overlapping the TOC
By default, all tables have a width of 100% on docs.gitlab.com.
In a few cases, the table will overlap the table of contents (ToC).
@@ -236,16 +362,23 @@ For other punctuation rules, please refer to the
E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`,
write `Read more about [GitLab Issue Boards](LINK)`.
-### Links to confidential issues
+### Links requiring permissions
+
+Don't link directly to:
+
+- [Confidential issues](../../user/project/issues/confidential_issues.md).
+- Project features that require [special permissions](../../user/permissions.md) to view.
-Don't link directly to [confidential issues](../../user/project/issues/confidential_issues.md). These will fail for:
+These will fail for:
- Those without sufficient permissions.
- Automated link checkers.
Instead:
-- Mention in the text that the information is contained in a confidential issue. This will reduce confusion.
+- To reduce confusion, mention in the text that the information is either:
+ - Contained in a confidential issue.
+ - Requires special permission to a project to view.
- Provide a link in back ticks (`` ` ``) so that those with access to the issue can easily navigate to it.
Example:
@@ -471,41 +604,49 @@ The following are recommended verbs for specific uses.
## GitLab versions and tiers
-- Every piece of documentation that comes with a new feature should declare the
- GitLab version that feature got introduced. Right below the heading add a
- blockquote:
+Tagged and released versions of GitLab documentation are available:
+
+- In the [documentation archives](https://docs.gitlab.com/archives/).
+- At the `/help` URL for any GitLab installation.
+
+The version introducing a new feature is added to the top of the topic in the documentation to provide
+a helpful link back to how the feature was developed.
+
+### Text for documentation requiring version text
+
+- For features that need to declare the GitLab version that the feature was introduced. Text similar
+ to the following should be added immediately below the heading as a blockquote:
```md
- > Introduced in GitLab 8.3.
+ > Introduced in GitLab 11.3.
```
-- Whenever possible, every feature should have a link to the issue, MR or epic
- (in that order) that introduced it. The above quote would be then transformed to:
+- Whenever possible, version text should have a link to the issue, merge request, or epic that introduced the feature.
+ An issue is preferred over a merge request, and a merge request is preferred over an epic. For example:
```md
- > [Introduced](<link-to-issue>) in GitLab 8.3.
+ > [Introduced](<link-to-issue>) in GitLab 11.3.
```
-- If the feature is only available in GitLab Enterprise Edition, don't forget to mention
+- If the feature is only available in GitLab Enterprise Edition, mention
the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers)
the feature is available in:
```md
- > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+ > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
```
-### Early versions of EE
+### Removing version text
-If the feature was created before GitLab 9.2 (before [different EE tiers were introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1851)):
+Over time, version text will reference a progressively older version of GitLab. In cases where version text
+refers to versions of GitLab four or more major versions back, consider removing the text.
-- Declare it as "Introduced in GitLab Enterprise Edition X.Y".
-- Note which tier the feature is available in.
+For example, if the current major version is 11.x, version text referencing versions of GitLab 7.x
+and older are candidates for removal.
-For example:
-
-```md
-> [Introduced](<link-to-issue>) in GitLab Enterprise Edition 9.0. Available in [GitLab Premium](https://about.gitlab.com/pricing/).
-```
+NOTE: **Note:**
+This guidance applies to any text that mentions a GitLab version, not just "Introduced in... " text.
+Other text includes deprecation notices and version-specific how-to information.
## Product badges
@@ -525,6 +666,12 @@ keyword "only":
- For GitLab Premium: `**[PREMIUM ONLY]**`.
- For GitLab Ultimate: `**[ULTIMATE ONLY]**`.
+For GitLab.com only tiers (when the feature is not available for self-hosted instances):
+
+- For GitLab Bronze and higher tiers: `**[BRONZE ONLY]**`.
+- For GitLab Silver and higher tiers: `**[SILVER ONLY]**`.
+- For GitLab Gold: `**[GOLD ONLY]**`.
+
The tier should be ideally added to headers, so that the full badge will be displayed.
However, it can be also mentioned from paragraphs, list items, and table cells. For these cases,
the tier mention will be represented by an orange question mark that will show the tiers on hover.
@@ -533,6 +680,7 @@ For example:
- `**[STARTER]**` renders as **[STARTER]**
- `**[STARTER ONLY]**` renders as **[STARTER ONLY]**
+- `**[SILVER ONLY]**` renders as **[SILVER ONLY]**
The absence of tiers' mentions mean that the feature is available in GitLab Core,
GitLab.com Free, and all higher tiers.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index c5a1d915be6..9853b38b8e9 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -922,7 +922,7 @@ import mixin from 'ee_else_ce/path/mixin';
mixins: [mixin]
}
```
-
+
- Computed Properties/methods and getters only used in the child import still need a counterpart in CE
- For store modules, we will need a CE counterpart too.
@@ -933,47 +933,27 @@ import mixin from 'ee_else_ce/path/mixin';
- Since we are using the async loading to check which component to load, we'd still use the component's name, check [this example](#child-component-only-used-in-ee).
* **EE extra HTML**
- - For the templates that have extra HTML in EE we will use the `ifEE` mixin with the `v-if` directive.
- - You can either use the `template` tag as a wrapper or directly in the element, if there is only one element to be rendered in EE:
-
-```html
- <template v-if="ifEE">
- <p>Several</p>
- <p>non wrapper</p>
- <p>elements</p>
- <p>that are rendered</p>
- <p>in EE only</p>
- </template>
-```
-
-
-```html
- <ul v-if="ifEE">
- <li>One wrapped</li>
- <li>element</li>
- <li>that is rendered</li>
- <li>in EE only</li>
- </template>
-```
+ - For the templates that have extra HTML in EE we should move it into a new component and use the `ee_else_ce` dynamic import
### Non Vue Files
For regular JS files, the approach is similar.
1. We will keep using the [`ee_else_ce`](https://docs.gitlab.com/ee/development/ee_features.html#javascript-code-in-assetsjavascripts) helper, this means that EE only code should be inside the `ee/` folder.
1. An EE file should be created with the EE only code, and it should extend the CE counterpart.
-1. For code inside functions that can't be extended, we will use an `if` statement with the `ifEE` helper
+ 1. For code inside functions that can't be extended, the code should be moved into a new file and we should use `ee_else_ce` helper:
##### Example:
```javascript
-import { ifEE } from '~/lib/utils/common_utils'
-if (ifEE) {
- $('.js-import-git-toggle-button').on('click', () => {
- const $projectMirror = $('#project_mirror');
+ import eeCode from 'ee_else_ce/ee_code';
- $projectMirror.attr('disabled', !$projectMirror.attr('disabled'));
- });
-}
+ function test() {
+ const test = 'a';
+
+ eeCode();
+
+ return test;
+ }
```
## SCSS code in `assets/stylesheets`
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
new file mode 100644
index 00000000000..0c9e7908713
--- /dev/null
+++ b/doc/development/elasticsearch.md
@@ -0,0 +1,166 @@
+# Elasticsearch knowledge **[STARTER ONLY]**
+
+This area is to maintain a compendium of useful information when working with elasticsearch.
+
+Information on how to enable ElasticSearch and perform the initial indexing is kept in https://docs.gitlab.com/ee/integration/elasticsearch.html#enabling-elasticsearch
+
+## Initial installation on OS X
+
+It is recommended to use the Docker image. After installing docker you can immediately spin up an instance with
+
+```
+docker run --name elastic56 -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:5.6.12
+```
+
+and use `docker stop elastic56` and `docker start elastic56` to stop/start it.
+
+### Installing on the host
+
+We currently only support Elasticsearch [5.6 to 6.x](https://docs.gitlab.com/ee/integration/elasticsearch.html#requirements)
+
+Version 5.6 is available on homebrew and is the recommended version to use in order to test compatibility.
+
+```
+brew install elasticsearch@5.6
+```
+
+There is no need to install any plugins
+
+## New repo indexer (beta)
+
+If you're interested on working with the new beta repo indexer, all you need to do is:
+
+- git clone git@gitlab.com:gitlab-org/gitlab-elasticsearch-indexer.git
+- make
+- make install
+
+this adds `gitlab-elasticsearch-indexer` to `$GOPATH/bin`, please make sure that is in your `$PATH`. After that GitLab will find it and you'll be able to enable it in the admin settings area.
+
+**note:** `make` will not recompile the executable unless you do `make clean` beforehand
+
+## Helpful rake tasks
+
+- `gitlab:elastic:test:index_size`: Tells you how much space the current index is using, as well as how many documents are in the index.
+- `gitlab:elastic:test:index_size_change`: Outputs index size, reindexes, and outputs index size again. Useful when testing improvements to indexing size.
+
+Additionally, if you need large repos or multiple forks for testing, please consider [following these instructions](https://docs.gitlab.com/ee/development/rake_tasks.html#extra-project-seed-options)
+
+## How does it work?
+
+The ElasticSearch integration depends on an external indexer. We ship a [ruby indexer](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/bin/elastic_repo_indexer) by default but are also working on an [indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). The user must trigger the initial indexing via a rake task, but after this is done GitLab itself will trigger reindexing when required via `after_` callbacks on create, update, and destroy that are inherited from [/ee/app/models/concerns/elastic/application_search.rb](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/concerns/elastic/application_search.rb).
+
+All indexing after the initial one is done via `ElasticIndexerWorker` (sidekiq jobs).
+
+Search queries are generated by the concerns found in [ee/app/models/concerns/elastic](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/models/concerns/elastic). These concerns are also in charge of access control, and have been a historic source of security bugs so please pay close attention to them!
+
+## Existing Analyzers/Tokenizers/Filters
+These are all defined in https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/elasticsearch/git/model.rb
+
+### Analyzers
+#### `path_analyzer`
+Used when indexing blobs' paths. Uses the `path_tokenizer` and the `lowercase` and `asciifolding` filters.
+
+Please see the `path_tokenizer` explanation below for an example.
+
+#### `sha_analyzer`
+Used in blobs and commits. Uses the `sha_tokenizer` and the `lowercase` and `asciifolding` filters.
+
+Please see the `sha_tokenizer` explanation later below for an example.
+
+#### `code_analyzer`
+Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the filters: `code`, `edgeNGram_filter`, `lowercase`, and `asciifolding`
+
+The `whitespace` tokenizer was selected in order to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` in order to be properly searched.
+
+Please see the `code` filter for an explanation on how tokens are split.
+
+#### `code_search_analyzer`
+Not directly used for indexing, but rather used to transform a search input. Uses the `whitespace` tokenizer and the `lowercase` and `asciifolding` filters.
+
+### Tokenizers
+#### `sha_tokenizer`
+This is a custom tokenizer that uses the [`edgeNGram` tokenizer](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-edgengram-tokenizer.html) to allow SHAs to be searcheable by any sub-set of it (minimum of 5 chars).
+
+example:
+
+`240c29dc7e` becomes:
+- `240c2`
+- `240c29`
+- `240c29d`
+- `240c29dc`
+- `240c29dc7`
+- `240c29dc7e`
+
+#### `path_tokenizer`
+This is a custom tokenizer that uses the [`path_hierarchy` tokenizer](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-pathhierarchy-tokenizer.html) with `reverse: true` in order to allow searches to find paths no matter how much or how little of the path is given as input.
+
+example:
+
+`'/some/path/application.js'` becomes:
+- `'/some/path/application.js'`
+- `'some/path/application.js'`
+- `'path/application.js'`
+- `'application.js'`
+
+### Filters
+#### `code`
+Uses a [Pattern Capture token filter](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-pattern-capture-tokenfilter.html) to split tokens into more easily searched versions of themselves.
+
+Patterns:
+- `"(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)"`: captures CamelCased and lowedCameCased strings as separate tokens
+- `"(\\d+)"`: extracts digits
+- `"(?=([\\p{Lu}]+[\\p{L}]+))"`: captures CamelCased strings recursively. Ex: `ThisIsATest` => `[ThisIsATest, IsATest, ATest, Test]`
+- `'"((?:\\"|[^"]|\\")*)"'`: captures terms inside quotes, removing the quotes
+- `"'((?:\\'|[^']|\\')*)'"`: same as above, for single-quotes
+- `'\.([^.]+)(?=\.|\s|\Z)'`: separate terms with periods in-between
+- `'\/?([^\/]+)(?=\/|\b)'`: separate path terms `like/this/one`
+
+#### `edgeNGram_filter`
+Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-edgengram-tokenfilter.html) to allow inputs with only parts of a token to find the token. For example it would turn `glasses` into permutations starting with `gl` and ending with `glasses`, which would allow a search for "`glass`" to find the original token `glasses`
+
+## Gotchas
+
+- Searches can have their own analyzers. Remember to check when editing analyzers
+- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches
+
+## Troubleshooting
+
+### Getting "flood stage disk watermark [95%] exceeded"
+
+You might get an error such as
+
+```
+[2018-10-31T15:54:19,762][WARN ][o.e.c.r.a.DiskThresholdMonitor] [pval5Ct]
+ flood stage disk watermark [95%] exceeded on
+ [pval5Ct7SieH90t5MykM5w][pval5Ct][/usr/local/var/lib/elasticsearch/nodes/0] free: 56.2gb[3%],
+ all indices on this node will be marked read-only
+```
+
+This is because you've exceeded the disk space threshold - it thinks you don't have enough disk space left, based on the default 95% threshold.
+
+In addition, the `read_only_allow_delete` setting will be set to `true`. It will block indexing, `forcemerge`, etc
+
+```
+curl "http://localhost:9200/gitlab-development/_settings?pretty"
+```
+
+Add this to your `elasticsearch.yml` file:
+
+```
+# turn off the disk allocator
+cluster.routing.allocation.disk.threshold_enabled: false
+```
+
+_or_
+
+```
+# set your own limits
+cluster.routing.allocation.disk.threshold_enabled: true
+cluster.routing.allocation.disk.watermark.flood_stage: 5gb # ES 6.x only
+cluster.routing.allocation.disk.watermark.low: 15gb
+cluster.routing.allocation.disk.watermark.high: 10gb
+```
+
+Restart ElasticSearch, and the `read_only_allow_delete` will clear on it's own.
+
+_from "Disk-based Shard Allocation | Elasticsearch Reference" [5.6](https://www.elastic.co/guide/en/elasticsearch/reference/5.6/disk-allocator.html#disk-allocator) and [6.x](https://www.elastic.co/guide/en/elasticsearch/reference/6.x/disk-allocator.html)_
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
new file mode 100644
index 00000000000..77f064a21a9
--- /dev/null
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -0,0 +1,27 @@
+# Frontend FAQ
+
+## Rules of Frontend FAQ
+
+1. **You talk about Frontend FAQ.**
+ Please share links to it whenever applicable, so more eyes catch when content
+ gets outdated.
+2. **Keep it short and simple.**
+ Whenever an answer needs more than two sentences it does not belong here.
+3. **Provide background when possible.**
+ Linking to relevant source code, issue / epic, or other documentation helps
+ to understand the answer.
+4. **If you see something, do something.**
+ Please remove or update any content that is outdated as soon as you see it.
+
+## FAQ
+
+### How do I find the Rails route for a page?
+
+The easiest way is to type the following in the browser while on the page in
+question:
+
+```javascript
+document.body.dataset.page
+```
+
+Find here the [source code setting the attribute](https://gitlab.com/gitlab-org/gitlab-ce/blob/cc5095edfce2b4d4083a4fb1cdc7c0a1898b9921/app/views/layouts/application.html.haml#L4).
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index d5cc28dc00c..36d5e4ab96b 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -73,6 +73,10 @@ How we use UI components.
How we use Snowplow to track custom events.
+## Frontend FAQ
+
+Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful information.
+
---
## Style Guides
diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md
index 548d72bea93..36880dd746d 100644
--- a/doc/development/fe_guide/style_guide_scss.md
+++ b/doc/development/fe_guide/style_guide_scss.md
@@ -16,10 +16,12 @@ New utility classes should be added to [`utilities.scss`](https://gitlab.com/git
**Background color**: `.bg-variant-shade` e.g. `.bg-warning-400`
**Text color**: `.text-variant-shade` e.g. `.text-success-500`
+
- variant is one of 'primary', 'secondary', 'success', 'warning', 'error'
- shade is on of the shades listed on [colors](https://design.gitlab.com/foundations/colors/)
**Font size**: `.text-size` e.g. `.text-2`
+
- **size** is number from 1-6 from our [Type scale](https://design.gitlab.com/foundations/typography)
### Naming
diff --git a/doc/development/geo.md b/doc/development/geo.md
new file mode 100644
index 00000000000..c8e6a86eb52
--- /dev/null
+++ b/doc/development/geo.md
@@ -0,0 +1,463 @@
+# Geo (development) **[PREMIUM ONLY]**
+
+Geo connects GitLab instances together. One GitLab instance is
+designated as a **primary** node and can be run with multiple
+**secondary** nodes. Geo orchestrates quite a few components that can be seen on
+the diagram below and are described in more detail within this document.
+
+![Geo Architecture Diagram](../administration/geo/replication/img/geo_architecture.png)
+
+## Replication layer
+
+Geo handles replication for different components:
+- [Database](#database-replication): includes the entire application, except cache and jobs.
+- [Git repositories](#repository-replication): includes both projects and wikis.
+- [Uploaded blobs](#uploads-replication): includes anything from images attached on issues
+to raw logs and assets from CI.
+
+With the exception of the Database replication, on a *secondary* node, everything is coordinated
+by the [Geo Log Cursor](#geo-log-cursor).
+
+### Geo Log Cursor daemon
+
+The [Geo Log Cursor daemon](#geo-log-cursor-daemon) is a separate process running on
+each **secondary** node. It monitors the [Geo Event Log](#geo-event-log)
+for new events and creates background jobs for each specific event type.
+
+For example when a repository is updated, the Geo **primary** node creates
+a Geo event with an associated repository updated event. The Geo Log Cursor daemon
+picks the event up and schedules a `Geo::ProjectSyncWorker` job which will
+use the `Geo::RepositorySyncService` and `Geo::WikiSyncService` classes
+to update the repository and the wiki respectively.
+
+The Geo Log Cursor daemon can operate in High Availability mode automatically.
+The daemon will try to acquire a lock from time to time and once acquired, it
+will behave as the *active* daemon.
+
+Any additional running daemons on the same node, will be in standby
+mode, ready to resume work if the *active* daemon releases its lock.
+
+We use the [`ExclusiveLease`](https://www.rubydoc.info/github/gitlabhq/gitlabhq/Gitlab/ExclusiveLease) lock type with a small TTL, that is renewed at every
+pooling cycle. That allows us to implement this global lock with a timeout.
+
+At the end of the pooling cycle, if the daemon can't renew and/or reacquire
+the lock, it switches to standby mode.
+
+### Database replication
+
+Geo uses [streaming replication](#streaming-replication) to replicate
+the database from the **primary** to the **secondary** nodes. This
+replication gives the **secondary** nodes access to all the data saved
+in the database. So users can log in on the **secondary** and read all
+the issues, merge requests, etc. on the **secondary** node.
+
+### Repository replication
+
+Geo also replicates repositories. Each **secondary** node keeps track of
+the state of every repository in the [tracking database](#tracking-database).
+
+There are a few ways a repository gets replicated by the:
+
+- [Repository Sync worker](#repository-sync-worker).
+- [Geo Log Cursor](#geo-log-cursor).
+
+#### Project Registry
+
+The `Geo::ProjectRegistry` class defines the model used to track the
+state of repository replication. For each project in the main
+database, one record in the tracking database is kept.
+
+It records the following about repositories:
+
+- The last time they were synced.
+- The last time they were successfully synced.
+- If they need to be resynced.
+- When a retry should be attempted.
+- The number of retries.
+- If and when they were verified.
+
+It also stores these attributes for project wikis in dedicated columns.
+
+#### Repository Sync worker
+
+The `Geo::RepositorySyncWorker` class runs periodically in the
+background and it searches the `Geo::ProjectRegistry` model for
+projects that need updating. Those projects can be:
+
+- Unsynced: Projects that have never been synced on the **secondary**
+ node and so do not exist yet.
+- Updated recently: Projects that have a `last_repository_updated_at`
+ timestamp that is more recent than the `last_repository_successful_sync_at`
+ timestamp in the `Geo::ProjectRegistry` model.
+- Manual: The admin can manually flag a repository to resync in the
+ [Geo admin panel](https://docs.gitlab.com/ee/user/admin_area/geo_nodes.html).
+
+When we fail to fetch a repository on the secondary `RETRIES_BEFORE_REDOWNLOAD`
+times, Geo does a so-called _redownload_. It will do a clean clone
+into the `@geo-temporary` directory in the root of the storage. When
+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.
+
+#### File Registry
+
+Similar to the [Project Registry](#project-registry), there is a
+`Geo::FileRegistry` 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.
+
+#### File Download Dispatch worker
+
+Also similar to the [Repository Sync worker](#repository-sync-worker),
+there is a `Geo::FileDownloadDispatchWorker` class that is run
+periodically to sync all uploads that aren't synced to the Geo
+**secondary** node yet.
+
+Files are copied via HTTP(s) and initiated via the
+`/api/v4/geo/transfers/:type/:id` endpoint,
+e.g. `/api/v4/geo/transfers/lfs/123`.
+
+## Authentication
+
+To authenticate file transfers, each `GeoNode` record has two fields:
+
+- A public access key (`access_key` field).
+- A secret access key (`secret_access_key` field).
+
+The **secondary** node authenticates itself via a [JWT request](https://jwt.io/).
+When the **secondary** node wishes to download a file, it sends an
+HTTP request with the `Authorization` header:
+
+```
+Authorization: GL-Geo <access_key>:<JWT payload>
+```
+
+The **primary** node uses the `access_key` field to look up the
+corresponding **secondary** node and decrypts the JWT payload,
+which contains additional information to identify the file
+request. This ensures that the **secondary** node downloads the right
+file for the right database ID. For example, for an LFS object, the
+request must also include the SHA256 sum of the file. An example JWT
+payload looks like:
+
+```
+{ "data": { sha256: "31806bb23580caab78040f8c45d329f5016b0115" }, iat: "1234567890" }
+```
+
+If the requested file matches the requested SHA256 sum, then the Geo
+**primary** node sends data via the [X-Sendfile](https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/)
+feature, which allows NGINX to handle the file transfer without tying
+up Rails or Workhorse.
+
+NOTE: **Note:**
+JWT requires synchronized clocks between the machines
+involved, otherwise it may fail with an encryption error.
+
+## Git Push to Geo secondary
+
+The Git Push Proxy exists as a functionality built inside the `gitlab-shell` component.
+It is active on a **secondary** node only. It allows the user that has cloned a repository
+from the secondary node to push to the same URL.
+
+Git `push` requests directed to a **secondary** node will be sent over to the **primary** node,
+while `pull` requests will continue to be served by the **secondary** node for maximum efficiency.
+
+HTTPS and SSH requests are handled differently:
+
+- With HTTPS, we will give the user a `HTTP 302 Redirect` pointing to the project on the **primary** node.
+The git client is wise enough to understand that status code and process the redirection.
+- With SSH, because there is no equivalent way to perform a redirect, we have to proxy the request.
+This is done inside [`gitlab-shell`](https://gitlab.com/gitlab-org/gitlab-shell), by first translating the request
+to the HTTP protocol, and then proxying it to the **primary** node.
+
+The [`gitlab-shell`](https://gitlab.com/gitlab-org/gitlab-shell) daemon knows when to proxy based on the response
+from `/api/v4/allowed`. A special `HTTP 300` status code is returned and we execute a "custom action",
+specified in the response body. The response contains additional data that allows the proxied `push` operation
+to happen on the **primary** node.
+
+## Using the Tracking Database
+
+Along with the main database that is replicated, a Geo **secondary**
+node has its own separate [Tracking database](#tracking-database).
+
+The tracking database contains the state of the **secondary** node.
+
+Any database migration that needs to be run as part of an upgrade
+needs to be applied to the tracking database on each **secondary** node.
+
+### Configuration
+
+The database configuration is set in [`config/database_geo.yml`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/config/database_geo.yml.postgresql).
+The directory [`ee/db/geo`](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/db/geo)
+contains the schema and migrations for this database.
+
+To write a migration for the database, use the `GeoMigrationGenerator`:
+
+```
+rails g geo_migration [args] [options]
+```
+
+To migrate the tracking database, run:
+
+```
+bundle exec rake geo:db:migrate
+```
+
+### Foreign Data Wrapper
+
+The use of [FDW](#fdw) was introduced in GitLab 10.1.
+
+This is useful for the [Geo Log Cursor](#geo-log-cursor) and improves
+the performance of some synchronization operations.
+
+While FDW is available in older versions of PostgreSQL, we needed to
+raise the minimum required version to 9.6 as this includes many
+performance improvements to the FDW implementation.
+
+#### Refeshing the Foreign Tables
+
+Whenever the database schema changes on the **primary** node, the
+**secondary** node will need to refresh its foreign tables by running
+the following:
+
+```sh
+bundle exec rake geo:db:refresh_foreign_tables
+```
+
+Failure to do this will prevent the **secondary** node from
+functioning properly. The **secondary** node will generate error
+messages, as the following PostgreSQL error:
+
+```
+ERROR: relation "gitlab_secondary.ci_job_artifacts" does not exist at character 323
+STATEMENT: SELECT a.attname, format_type(a.atttypid, a.atttypmod),
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attrelid = '"gitlab_secondary"."ci_job_artifacts"'::regclass
+ AND a.attnum > 0 AND NOT a.attisdropped
+ ORDER BY a.attnum
+```
+
+## Finders
+
+Geo uses [Finders](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/app/finders),
+which are classes take care of the heavy lifting of looking up
+projects/attachments/etc. in the tracking database and main database.
+
+### Finders Performance
+
+The Finders need to compare data from the main database with data in
+the tracking database. For example, counting the number of synced
+projects normally involves retrieving the project IDs from one
+database and checking their state in the other database. This is slow
+and requires a lot of memory.
+
+To overcome this, the Finders use [FDW](#fdw), or Foreign Data
+Wrappers. This allows a regular `JOIN` between the main database and
+the tracking database.
+
+## Redis
+
+Redis on the **secondary** node works the same as on the **primary**
+node. It is used for caching, storing sessions, and other persistent
+data.
+
+Redis data replication between **primary** and **secondary** node is
+not used, so sessions etc. aren't shared between nodes.
+
+## Object Storage
+
+GitLab can optionally use Object Storage to store data it would
+otherwise store on disk. These things can be:
+
+ - LFS Objects
+ - CI Job Artifacts
+ - Uploads
+
+Objects that are stored in object storage, are not handled by Geo. Geo
+ignores items in object storage. Either:
+
+- The object storage layer should take care of its own geographical
+ replication.
+- All secondary nodes should use the same storage node.
+
+## Verification
+
+### Repository verification
+
+Repositories are verified with a checksum.
+
+The **primary** node calculates a checksum on the repository. It
+basically hashes all Git refs together and stores that hash in the
+`project_repository_states` table of the database.
+
+The **secondary** node does the same to calculate the hash of its
+clone, and compares the hash with the value the **primary** node
+calculated. If there is a mismatch, Geo will mark this as a mismatch
+and the administrator can see this in the [Geo admin panel](https://docs.gitlab.com/ee/user/admin_area/geo_nodes.html).
+
+## Glossary
+
+### Primary node
+
+A **primary** node is the single node in a Geo setup that read-write
+capabilities. It's the single source of truth and the Geo
+**secondary** nodes replicate their data from there.
+
+In a Geo setup, there can only be one **primary** node. All
+**secondary** nodes connect to that **primary**.
+
+### Secondary node
+
+A **secondary** node is a read-only replica of the **primary** node
+running in a different geographical location.
+
+### Streaming replication
+
+Geo depends on the streaming replication feature of PostgreSQL. It
+completely replicates the database data and the database schema. The
+database replica is a read-only copy.
+
+Streaming replication depends on the Write Ahead Logs, or WAL. Those
+logs are copied over to the replica and replayed there.
+
+Since streaming replication also replicates the schema, the database
+migration do not need to run on the secondary nodes.
+
+### Tracking database
+
+A database on each Geo **secondary** node that keeps state for the node
+on which it resides. Read more in [Using the Tracking database](#using-the-tracking-database).
+
+### FDW
+
+Foreign Data Wrapper, or FDW, is a feature built-in in PostgreSQL. It
+allows data to be queried from different data sources. In Geo, it's
+used to query data from different PostgreSQL instances.
+
+## Geo Event Log
+
+The Geo **primary** stores events in the `geo_event_log` table. Each
+entry in the log contains a specific type of event. These type of
+events include:
+
+ - Repository Deleted event
+ - Repository Renamed event
+ - Repositories Changed event
+ - Repository Created event
+ - Hashed Storage Migrated event
+ - Lfs Object Deleted event
+ - Hashed Storage Attachments event
+ - Job Artifact Deleted event
+ - Upload Deleted event
+
+### Geo Log Cursor
+
+The process running on the **secondary** node that looks for new
+`Geo::EventLog` rows.
+
+## Code features
+
+### `Gitlab::Geo` utilities
+
+Small utility methods related to Geo go into the
+[`ee/lib/gitlab/geo.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/gitlab/geo.rb)
+file.
+
+Many of these methods are cached using the `RequestStore` class, to
+reduce the performance impact of using the methods throughout the
+codebase.
+
+#### Current node
+
+The class method `.current_node` returns the `GeoNode` record for the
+current node.
+
+We use the `host`, `port`, and `relative_url_root` values from
+`gitlab.yml` and search in the database to identify which node we are
+in (see `GeoNode.current_node`).
+
+#### Primary or secondary
+
+To determine whether the current node is a **primary** node or a
+**secondary** node use the `.primary?` and `.secondary?` class
+methods.
+
+It is possible for these methods to both return `false` on a node when
+the node is not enabled. See [Enablement](#enablement).
+
+#### Geo Database configured?
+
+There is also an additional gotcha when dealing with things that
+happen during initialization time. In a few places, we use the
+`Gitlab::Geo.geo_database_configured?` method to check if the node has
+the tracking database, which only exists on the **secondary**
+node. This overcomes race conditions that could happen during
+bootstrapping of a new node.
+
+#### Enablement
+
+We consider Geo feature enabled when the user has a valid license with the
+feature included, and they have at least one node defined at the Geo Nodes
+screen.
+
+See `Gitlab::Geo.enabled?` and `Gitlab::Geo.license_allows?` methods.
+
+#### Read-only
+
+All Geo **secondary** nodes are read-only.
+
+The general principle of a [read-only database](verifying_database_capabilities.md#read-only-database)
+applies to all Geo **secondary** nodes. So the
+`Gitlab::Database.read_only?` method will always return `true` on a
+**secondary** node.
+
+When some write actions are not allowed because the node is a
+**secondary**, consider adding the `Gitlab::Database.read_only?` or
+`Gitlab::Database.read_write?` guard, instead of `Gitlab::Geo.secondary?`.
+
+The database itself will already be read-only in a replicated setup,
+so we don't need to take any extra step for that.
+
+## History of communication channel
+
+The communication channel has changed since first iteration, you can
+check here historic decisions and why we moved to new implementations.
+
+### Custom code (GitLab 8.6 and earlier)
+
+In GitLab versions before 8.6, custom code is used to handle
+notification from **primary** node to **secondary** nodes by HTTP
+requests.
+
+### System hooks (GitLab 8.7 to 9.5)
+
+Later, it was decided to move away from custom code and begin using
+system hooks. More people were using them, so
+many would benefit from improvements made to this communication layer.
+
+There is a specific **internal** endpoint in our API code (Grape),
+that receives all requests from this System Hooks:
+`/api/v4/geo/receive_events`.
+
+We switch and filter from each event by the `event_name` field.
+
+### Geo Log Cursor (GitLab 10.0 and up)
+
+Since GitLab 10.0, [System Webhooks](#system-hooks-gitlab-87-to-95) are no longer
+used and Geo Log Cursor is used instead. The Log Cursor traverses the
+`Geo::EventLog` rows to see if there are changes since the last time
+the log was checked and will handle repository updates, deletes,
+changes, and renames.
+
+The table is within the replicated database. This has two advantages over the
+old method:
+
+- Replication is synchronous and we preserve the order of events.
+- Replication of the events happen at the same time as the changes in the
+ database.
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index bfde26dbe4a..4ef20d5fd74 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -63,6 +63,8 @@ If your test-suite is failing with Gitaly issues, as a first step, try running:
rm -rf tmp/tests/gitaly
```
+During rspec tests, the Gitaly instance will write logs to `gitlab/log/gitaly-test.log`.
+
## Legacy Rugged code
While Gitaly can handle all Git access, many of GitLab customers still
@@ -80,6 +82,8 @@ most commonly-used RPCs can be enabled via feature flags:
* `rugged_get_tree_entries`
* `rugged_tree_entry`
* `rugged_commit_is_ancestor`
+* `rugged_commit_tree_entry`
+* `rugged_list_commits_by_oid`
A convenience Rake task can be used to enable or disable these flags
all together. To enable:
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index 6dcade3bb51..b9dc3797e5b 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -93,7 +93,7 @@ become available, you will be able to share job templates like this
Dependencies should be kept to the minimum. The introduction of a new
dependency should be argued in the merge request, as per our [Approval
-Guidelines](../code_review.html#approval-guidelines). Both [License
+Guidelines](../code_review.md#approval-guidelines). Both [License
Management](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html)
**[ULTIMATE]** and [Dependency
Scanning](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html)
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 223585ebb55..38d56322de8 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -300,7 +300,7 @@ file in. Once the changes are on master, they will be picked up by
[Crowdin](http://translate.gitlab.com) and be presented for translation.
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
-and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
+and regenerate it using the same command.
### Validating PO files
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 131e3edf35e..ac04a21b37a 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -13,14 +13,17 @@ are very appreciative of the work done by translators and proofreaders!
- Lyubomir Vasilev - [Crowdin](https://crowdin.com/profile/lyubomirv)
- Catalan
- David Planella - [GitLab](https://gitlab.com/dplanella), [Crowdin](https://crowdin.com/profile/dplanella)
-- Chinese Simplified
- - Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
-- Chinese Traditional
+- Chinese Simplified 简体中文
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
+ - Victor Wu - [GitLab](https://gitlab.com/victorwuky), [Crowdin](https://crowdin.com/profile/victorwu)
+ - Xiaogang Wen - [GitLab](https://gitlab.com/xiaogang_gitlab), [Crowdin](https://crowdin.com/profile/xiaogang_gitlab)
+- Chinese Traditional 繁體中文
- Weizhe Ding - [GitLab](https://gitlab.com/d.weizhe), [Crowdin](https://crowdin.com/profile/d.weizhe)
- Yi-Jyun Pan - [GitLab](https://gitlab.com/pan93412), [Crowdin](https://crowdin.com/profile/pan93412)
-- Chinese Traditional, Hong Kong
- - Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
+ - Victor Wu - [GitLab](https://gitlab.com/victorwuky), [Crowdin](https://crowdin.com/profile/victorwu)
+- Chinese Traditional, Hong Kong 繁體中文 (香港)
+ - Victor Wu - [GitLab](https://gitlab.com/victorwuky), [Crowdin](https://crowdin.com/profile/victorwu)
+ - Ivan Ip - [GitLab](https://gitlab.com/lifehome), [Crowdin](https://crowdin.com/profile/lifehome)
- Czech
- Proofreaders needed.
- Danish
@@ -40,6 +43,7 @@ are very appreciative of the work done by translators and proofreaders!
- Pedro Garcia - [GitLab](https://gitlab.com/pedgarrod), [Crowdin](https://crowdin.com/profile/breaking_pitt)
- German
- Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah)
+ - Katrin Leinweber - [GitLab](https://gitlab.com/katrinleinweber/), [Crowdin](https://crowdin.com/profile/katrinleinweber)
- Greek
- Proofreaders needed.
- Hebrew
@@ -56,7 +60,6 @@ are very appreciative of the work done by translators and proofreaders!
- Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [Crowdin](https://crowdin.com/profile/hiroponz)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- - Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie)
- Jeongwhan Choi - [GitLab](https://gitlab.com/jeongwhanchoi), [Crowdin](https://crowdin.com/profile/jeongwhanchoi)
- Mongolian
diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md
new file mode 100644
index 00000000000..5bf43d320c6
--- /dev/null
+++ b/doc/development/integrations/jira_connect.md
@@ -0,0 +1,41 @@
+# Setting up a development environment
+
+The following are required to install and test the app:
+
+1. A Jira Cloud instance
+
+ Atlassian provides free instances for development and testing. [Click here to sign up](http://go.atlassian.com/cloud-dev).
+
+1. A GitLab instance available over the internet
+
+ For the app to work, Jira Cloud should be able to connect to the GitLab instance through the internet.
+
+ To easily expose your local development environment, you can use tools like [serveo](https://serveo.net) or [ngrok](https://ngrok.com).
+ These also take care of SSL for you because Jira requires all connections to the app host to be over SSL.
+
+> This feature is currently behind the `:jira_connect_app` feature flag
+
+# Installing the app in Jira
+
+1. Enable Jira development mode to install apps that are not from the Atlassian Marketplace
+
+ 1. Navigate to **Jira settings** (cog icon) > **Apps** > **Manage apps**.
+ 1. Scroll to the bottom of the **Manage apps** page and click **Settings**.
+ 1. Select **Enable development mode** and click **Apply**.
+
+1. Install the app
+
+ 1. Navigate to Jira, then choose **Jira settings** (cog icon) > **Apps** > **Manage apps**.
+ 1. Click **Upload app**.
+ 1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance.
+
+ For example:
+ ```
+ https://xxxx.serveo.net/-/jira_connect/app_descriptor.json
+ ```
+ 1. Click **Upload**.
+
+ If the install was successful, you should see the **GitLab for Jira** app under **Manage apps**.
+ You can also click **Getting Started** to open the configuration page rendered from your GitLab instance.
+
+ _Note that any changes to the app descriptor requires you to uninstall then reinstall the app._
diff --git a/doc/development/licensed_feature_availability.md b/doc/development/licensed_feature_availability.md
new file mode 100644
index 00000000000..1657d73e0c9
--- /dev/null
+++ b/doc/development/licensed_feature_availability.md
@@ -0,0 +1,37 @@
+# Licensed feature availability **[STARTER]**
+
+As of GitLab 9.4, we've been supporting a simplified version of licensed
+feature availability checks via `ee/app/models/license.rb`, both for
+on-premise or GitLab.com plans and features.
+
+## Restricting features scoped by namespaces or projects
+
+GitLab.com plans are persisted on user groups and namespaces, therefore, if you're adding a
+feature such as [Related issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html) or
+[Service desk](https://docs.gitlab.com/ee/user/project/service_desk.html),
+it should be restricted on namespace scope.
+
+1. Add the feature symbol on `EES_FEATURES`, `EEP_FEATURES` or `EEU_FEATURES` constants in
+ `ee/app/models/license.rb`. Note on `ee/app/models/ee/namespace.rb` that _Bronze_ GitLab.com
+ features maps to on-premise _EES_, _Silver_ to _EEP_ and _Gold_ to _EEU_.
+2. Check using:
+
+```ruby
+project.feature_available?(:feature_symbol)
+```
+
+## Restricting global features (instance)
+
+However, for features such as [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html) and
+[Load balancing](https://docs.gitlab.com/ee/administration/database_load_balancing.html), which cannot be restricted
+to only a subset of projects or namespaces, the check will be made directly in
+the instance license.
+
+1. Add the feature symbol on `EES_FEATURES`, `EEP_FEATURES` or `EEU_FEATURES` constants in
+ `ee/app/models/license.rb`.
+2. Add the same feature symbol to `GLOBAL_FEATURES`
+3. Check using:
+
+```ruby
+License.feature_available?(:feature_symbol)
+```
diff --git a/doc/development/packages.md b/doc/development/packages.md
new file mode 100644
index 00000000000..a3b891d7834
--- /dev/null
+++ b/doc/development/packages.md
@@ -0,0 +1,68 @@
+# Packages **[PREMIUM]**
+
+This document will guide you through adding another [package management system](https://docs.gitlab.com/ee/administration/packages.html) support to GitLab.
+
+See already supported package types in [Packages documentation](https://docs.gitlab.com/ee/administration/packages.html)
+
+Since GitLab packages' UI is pretty generic, it is possible to add 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-ee/merge_requests/8673).
+- [Maven repository](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6607).
+- [Instance level endpoint for Maven repository](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8757)
+
+## General information
+
+The existing database model requires the following:
+
+- Every package belongs to a project.
+- Every package file belongs to a package.
+- A package can have one or more package files.
+- The package model is based on storing information about the package and its version.
+
+## API endpoints
+
+Package systems work with GitLab via API. For example `ee/lib/api/npm_packages.rb`
+implements API endpoints to work with NPM clients. So, the first thing to do is to
+add a new `ee/lib/api/your_name_packages.rb` file with API endpoints that are
+necessary to make the package system client to work. Usually that means having
+endpoints like:
+
+- GET package information.
+- GET package file content.
+- PUT upload package.
+
+Since the packages belong to a project, it's expected to have project-level endpoint
+for uploading and downloading them. For example:
+
+```
+GET https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/
+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](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html#package-naming-convention)
+
+## Configuration
+
+GitLab has a `packages` section in its configuration file (`gitlab.rb`).
+It applies to all package systems supported by GitLab. Usually you don't need
+to add anything there.
+
+Packages can be configured to use object storage, therefore your code must support it.
+
+## Database
+
+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`
+or add files to existing record. `PackageFile` should be able to store all file-related
+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.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index dcb32c89f65..27fc3231218 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -12,6 +12,40 @@ The `setup` task is an alias for `gitlab:setup`.
This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
+### Seeding issues for all or a given project
+
+You can seed issues for all or a given project with the `gitlab:seed:issues`
+task:
+
+```shell
+# All projects
+bin/rake gitlab:seed:issues
+
+# A specific project
+bin/rake "gitlab:seed:issues[group-path/project-path]"
+```
+
+By default, this seeds an average of 2 issues per week for the last 5 weeks per
+project.
+
+#### Seeding issues for Insights charts **[ULTIMATE]**
+
+You can seed issues specifically for working with the
+[Insights charts](https://docs.gitlab.com/ee/user/group/insights/index.html) with the
+`gitlab:seed:insights:issues` task:
+
+```shell
+# All projects
+bin/rake gitlab:seed:insights:issues
+
+# A specific project
+bin/rake "gitlab:seed:insights:issues[group-path/project-path]"
+```
+
+By default, this seeds an average of 10 issues per week for the last 52 weeks
+per project. All issues will also be randomly labeled with team, type, severity,
+and priority.
+
### Automation
If you're very sure that you want to **wipe the current database** and refill
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index 51fe19c3d9e..fc7aaedca29 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -99,13 +99,13 @@ subgraph gitlab-qa pipeline
1. When packages are ready, and available in the registry, a final step in the
[Omnibus GitLab][omnibus-gitlab] pipeline, triggers a new
- [GitLab QA pipeline][gitlab-qa-pipelines]. It also waits for a resulting status.
+ GitLab QA pipeline (those with access can view them at `https://gitlab.com/gitlab-org/gitlab-qa/pipelines`). It also waits for a resulting status.
1. GitLab QA pulls images from the registry, spins-up containers and runs tests
against a test environment that has been just orchestrated by the `gitlab-qa`
tool.
-1. The result of the [GitLab QA pipeline][gitlab-qa-pipelines] is being
+1. The result of the GitLab QA pipeline is being
propagated upstream, through Omnibus, back to the CE / EE merge request.
#### Using the `review-qa-all` jobs
@@ -146,7 +146,6 @@ you can find an issue you would like to work on in
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
-[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
[quality-nightly-pipelines]: https://gitlab.com/gitlab-org/quality/nightly/pipelines
[quality-staging-pipelines]: https://gitlab.com/gitlab-org/quality/staging/pipelines
diff --git a/doc/development/testing_guide/img/review_apps_cicd_architecture.png b/doc/development/testing_guide/img/review_apps_cicd_architecture.png
index 87e472076f3..1ee28d3db91 100644
--- a/doc/development/testing_guide/img/review_apps_cicd_architecture.png
+++ b/doc/development/testing_guide/img/review_apps_cicd_architecture.png
Binary files differ
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 55ca502f84a..7ad33f05f77 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -6,7 +6,7 @@ Review Apps are automatically deployed by each pipeline, both in
## How does it work?
-### CD/CD architecture diagram
+### CI/CD architecture diagram
![Review Apps CI/CD architecture](img/review_apps_cicd_architecture.png)
@@ -14,23 +14,29 @@ Review Apps are automatically deployed by each pipeline, both in
<summary>Show mermaid source</summary>
<pre>
graph TD
- B1 -.->|2. once gitlab:assets:compile is done,<br />triggers a CNG-mirror pipeline and wait for it to be done| A2
- C1 -.->|2. once review-build-cng is done,<br />Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline| A3
-
-subgraph gitlab-ce/ee `test` stage
- A1[gitlab:assets:compile]
- B1[review-build-cng] -->|1. wait for| A1
- C1[review-deploy] -->|1. wait for| B1
- D1[review-qa-smoke] -->|1. wait for| C1
- D1[review-qa-smoke] -.->|2. once review-deploy is done| E1>gitlab-qa runs the smoke<br/>suite against the Review App]
+ build-qa-image -.->|once the `prepare` stage is done| gitlab:assets:compile
+ review-build-cng -->|triggers a CNG-mirror pipeline and wait for it to be done| CNG-mirror
+ review-build-cng -.->|once the `test` stage is done| review-deploy
+ review-deploy -.->|once the `review` stage is done| review-qa-smoke
+
+subgraph 1. gitlab-ce/ee `prepare` stage
+ build-qa-image
end
-subgraph CNG-mirror pipeline
- A2>Cloud Native images are built];
+subgraph 2. gitlab-ce/ee `test` stage
+ gitlab:assets:compile -->|plays dependent job once done| review-build-cng
end
-subgraph GCP `gitlab-review-apps` project
- A3>"Cloud Native images are deployed to the<br />`review-apps-ce` or `review-apps-ee` Kubernetes (GKE) cluster"];
+subgraph 3. gitlab-ce/ee `review` stage
+ review-deploy["review-deploy<br /><br />Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br /><br />Cloud Native images are deployed to the `review-apps-ce` or `review-apps-ee`<br />Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
+ end
+
+subgraph 4. gitlab-ce/ee `qa` stage
+ review-qa-smoke[review-qa-smoke<br /><br />gitlab-qa runs the smoke suite against the Review App.]
+ end
+
+subgraph CNG-mirror pipeline
+ CNG-mirror>Cloud Native images are built];
end
</pre>
</details>
@@ -38,40 +44,37 @@ subgraph GCP `gitlab-review-apps` project
### Detailed explanation
1. On every [pipeline][gitlab-pipeline] during the `test` stage, the
- [`review-build-cng`][review-build-cng] and
- [`review-deploy`][review-deploy] jobs are automatically started.
- - The [`review-deploy`][review-deploy] job waits for the
- [`review-build-cng`][review-build-cng] job to finish.
- - The [`review-build-cng`][review-build-cng] job waits for the
- [`gitlab:assets:compile`][gitlab:assets:compile] job to finish since the
- [`CNG-mirror`][cng-mirror] pipeline triggered in the following step depends on it.
-1. Once the [`gitlab:assets:compile`][gitlab:assets:compile] job is done,
- [`review-build-cng`][review-build-cng] [triggers a pipeline][cng-pipeline]
- in the [`CNG-mirror`][cng-mirror] project.
- - The [`CNG-mirror`][cng-pipeline] pipeline creates the Docker images of
- each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
- based on the commit from the [GitLab pipeline][gitlab-pipeline] and store
- them in its [registry][cng-mirror-registry].
- - We use the [`CNG-mirror`][cng-mirror] project so that the `CNG`, (**C**loud
- **N**ative **G**itLab), project's registry is not overloaded with a
- lot of transient Docker images.
-1. Once the [`review-build-cng`][review-build-cng] job is done, the
- [`review-deploy`][review-deploy] job deploys the Review App using
- [the official GitLab Helm chart][helm-chart] to the
- [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee]
- Kubernetes cluster on GCP.
- - The actual scripts used to deploy the Review App can be found at
- [`scripts/review_apps/review-apps.sh`][review-apps.sh].
- - These scripts are basically
- [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
- default CNG images are overridden with the images built and stored in the
- [`CNG-mirror` project's registry][cng-mirror-registry].
- - Since we're using [the official GitLab Helm chart][helm-chart], this means
- you get a dedicated environment for your branch that's very close to what
- it would look in production.
+ [`gitlab:assets:compile`][gitlab:assets:compile] job is automatically started.
+ - Once it's done, it starts the [`review-build-cng`][review-build-cng]
+ manual job since the [`CNG-mirror`][cng-mirror] pipeline triggered in the
+ following step depends on it.
+1. The [`review-build-cng`][review-build-cng] job [triggers a pipeline][cng-mirror-pipeline]
+ in the [`CNG-mirror`][cng-mirror] project.
+ - The [`CNG-mirror`][cng-mirror-pipeline] pipeline creates the Docker images of
+ each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
+ based on the commit from the [GitLab pipeline][gitlab-pipeline] and stores
+ them in its [registry][cng-mirror-registry].
+ - We use the [`CNG-mirror`][cng-mirror] project so that the `CNG`, (**C**loud
+ **N**ative **G**itLab), project's registry is not overloaded with a
+ lot of transient Docker images.
+ - Note that the official CNG images are built by the `cloud-native-image`
+ job, which runs only for tags, and triggers itself a [`CNG`][cng] pipeline.
+1. Once the `test` stage is done, the [`review-deploy`][review-deploy] job
+ deploys the Review App using [the official GitLab Helm chart][helm-chart] to
+ the [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee]
+ Kubernetes cluster on GCP.
+ - The actual scripts used to deploy the Review App can be found at
+ [`scripts/review_apps/review-apps.sh`][review-apps.sh].
+ - These scripts are basically
+ [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the
+ default CNG images are overridden with the images built and stored in the
+ [`CNG-mirror` project's registry][cng-mirror-registry].
+ - Since we're using [the official GitLab Helm chart][helm-chart], this means
+ you get a dedicated environment for your branch that's very close to what
+ it would look in production.
1. Once the [`review-deploy`][review-deploy] job succeeds, you should be able to
- use your Review App thanks to the direct link to it from the MR widget. To log
- into the Review App, see "Log into my Review App?" below.
+ use your Review App thanks to the direct link to it from the MR widget. To log
+ into the Review App, see "Log into my Review App?" below.
**Additional notes:**
@@ -82,71 +85,69 @@ subgraph GCP `gitlab-review-apps` project
- If the Review App deployment fails, you can simply retry it (there's no need
to run the [`review-stop`][gitlab-ci-yml] job first).
- The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to
- stop a Review App manually, and is also started by GitLab once a branch is
- deleted.
-- Review Apps are cleaned up regularly using a pipeline schedule that runs
+ stop a Review App manually, and is also started by GitLab once a merge
+ request's branch is deleted after being merged.
+- Review Apps are cleaned up regularly via a pipeline schedule that runs
the [`schedule:review-cleanup`][gitlab-ci-yml] job.
## QA runs
-On every [pipeline][gitlab-pipeline] during the `test` stage, the
-`review-qa-smoke` job is automatically started: it runs the QA smoke suite.
-You can also manually start the `review-qa-all`: it runs the QA full suite.
+On every [pipeline][gitlab-pipeline] in the `qa` stage (which comes after the
+`review` stage), the `review-qa-smoke` job is automatically started and it runs
+the QA smoke suite.
-Note that both jobs first wait for the `review-deploy` job to be finished.
+You can also manually start the `review-qa-all`: it runs the full QA suite.
## Performance Metrics
-On every [pipeline][gitlab-pipeline] during the `test` stage, the
+On every [pipeline][gitlab-pipeline] in the `qa` stage, the
`review-performance` job is automatically started: this job does basic
-browser performance testing using [Sitespeed.io Container](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html) .
+browser performance testing using a
+[Sitespeed.io Container](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
-This job waits for the `review-deploy` job to be finished.
+## How to:
-## How to?
-
-### Log into my Review App?
+### Log into my Review App
The default username is `root` and its password can be found in the 1Password
secure note named **gitlab-{ce,ee} Review App's root password**.
-### Enable a feature flag for my Review App?
+### Enable a feature flag for my Review App
1. Open your Review App and log in as documented above.
1. Create a personal access token.
1. Enable the feature flag using the [Feature flag API](../../api/features.md).
-### Find my Review App slug?
+### Find my Review App slug
1. Open the `review-deploy` job.
1. Look for `Checking for previous deployment of review-*`.
1. For instance for `Checking for previous deployment of review-qa-raise-e-12chm0`,
- your Review App slug would be `review-qa-raise-e-12chm0` in this case.
+ your Review App slug would be `review-qa-raise-e-12chm0` in this case.
-### Run a Rails console?
+### Run a Rails console
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
- , e.g. `review-29951-issu-id2qax`.
-1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`.
-1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`.
+ , e.g. `review-qa-raise-e-12chm0`.
+1. Find and open the `task-runner` Deployment, e.g. `review-qa-raise-e-12chm0-task-runner`.
+1. Click on the Pod in the "Managed pods" section, e.g. `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
- default command or
- - Run `kubectl exec --namespace review-apps-ce review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console`
- and
- - Replace `review-apps-ce` with `review-apps-ee` if the Review App
- is running EE, and
- - Replace `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`
- with your Pod's name.
+ default command or
+ - Run `kubectl exec --namespace review-apps-ce review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console` and
+ - Replace `review-apps-ce` with `review-apps-ee` if the Review App
+ is running EE, and
+ - Replace `review-qa-raise-e-12chm0-task-runner-d5455cc8-2lsvz`
+ with your Pod's name.
-### Dig into a Pod's logs?
+### Dig into a Pod's logs
-1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps)
- , e.g. `review-1979-1-mul-dnvlhv`.
+1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
+ e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `migrations` Deployment, e.g.
- `review-1979-1-mul-dnvlhv-migrations.1`.
+ `review-qa-raise-e-12chm0-migrations.1`.
1. Click on the Pod in the "Managed pods" section, e.g.
- `review-1979-1-mul-dnvlhv-migrations.1-nqwtx`.
+ `review-qa-raise-e-12chm0-migrations.1-nqwtx`.
1. Click on the `Container logs` link.
## Frequently Asked Questions
@@ -182,7 +183,8 @@ find a way to limit it to only us.**
[review-build-cng]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511623
[review-deploy]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511624
[cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror
-[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657
+[cng]: https://gitlab.com/gitlab-org/build/CNG
+[cng-mirror-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657
[cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry
[helm-chart]: https://gitlab.com/charts/gitlab/
[review-apps-ce]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 352651fe91b..1fa6e38ea5a 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -4,6 +4,13 @@
_This diagram demonstrates the relative priority of each test type we use. `e2e` stands for end-to-end._
+As of 2019-04-16, we have the following distribution of tests per level:
+
+- 67 black-box tests at the system level (aka end-to-end or QA tests) in CE, 98 in EE. This represents 0.3% of all the CE tests (0.3% in EE).
+- 5,457 white-box tests at the system level (aka system or feature tests) in CE, 6,585 in EE. This represents 24.6% of all the CE tests (20.3% in EE).
+- 8,298 integration tests in CE, 10,633 in EE: 0.3% of all the CE tests (0.3% in EE). This represents 37.2% of all the CE tests (32.8% in EE).
+- 8,403 unit tests in CE, 15,090 in EE: 0.3% of all the CE tests (0.3% in EE). This represents 37.8% of all the CE tests (46.6% in EE).
+
## Unit tests
Formal definition: <https://en.wikipedia.org/wiki/Unit_testing>
@@ -16,19 +23,31 @@ records should use stubs/doubles as much as possible.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
+| `app/assets/javascripts/` | `spec/javascripts/`, `spec/frontend/` | Karma & Jest | More details in the [Frontend Testing guide](frontend_testing.md) section. |
| `app/finders/` | `spec/finders/` | RSpec | |
+| `app/graphql/` | `spec/graphql/` | RSpec | |
| `app/helpers/` | `spec/helpers/` | RSpec | |
-| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
+| `app/models/` | `spec/models/` | RSpec | |
| `app/policies/` | `spec/policies/` | RSpec | |
| `app/presenters/` | `spec/presenters/` | RSpec | |
-| `app/routing/` | `spec/routing/` | RSpec | |
| `app/serializers/` | `spec/serializers/` | RSpec | |
| `app/services/` | `spec/services/` | RSpec | |
-| `app/tasks/` | `spec/tasks/` | RSpec | |
| `app/uploaders/` | `spec/uploaders/` | RSpec | |
+| `app/validators/` | `spec/validators/` | RSpec | |
| `app/views/` | `spec/views/` | RSpec | |
| `app/workers/` | `spec/workers/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Frontend Testing guide](frontend_testing.md) section. |
+| `bin/` | `spec/bin/` | RSpec | |
+| `config/` | `spec/config/` | RSpec | |
+| `config/initializers/` | `spec/initializers/` | RSpec | |
+| `config/routes.rb`, `config/routes/` | `spec/routing/` | RSpec | |
+| `config/puma.example.development.rb`, `config/unicorn.rb.example` | `spec/rack_servers/` | RSpec | |
+| `db/` | `spec/db/` | RSpec | |
+| `db/{post_,}migrate/` | `spec/migrations/` | RSpec | More details at [`spec/migrations/README.md`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md). |
+| `Gemfile` | `spec/dependencies/`, `spec/sidekiq/` | RSpec | |
+| `lib/` | `spec/lib/` | RSpec | |
+| `lib/tasks/` | `spec/tasks/` | RSpec | |
+| `rubocop/` | `spec/rubocop/` | RSpec | |
+| `spec/factories` | `spec/factories_spec.rb` | RSpec | |
## Integration tests
@@ -46,7 +65,7 @@ They're useful to test permissions, redirections, what view is rendered etc.
| `app/mailers/` | `spec/mailers/` | RSpec | |
| `lib/api/` | `spec/requests/api/` | RSpec | |
| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
-| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [Karma JavaScript test suite](frontend_testing.md#karma-test-suite) section. |
+| `app/assets/javascripts/` | `spec/javascripts/`, `spec/frontend/` | Karma & Jest | More details in the [Frontend Testing guide](frontend_testing.md) section. |
### About controller tests
diff --git a/doc/getting-started/subscription.md b/doc/getting-started/subscription.md
new file mode 100644
index 00000000000..65999183d4a
--- /dev/null
+++ b/doc/getting-started/subscription.md
@@ -0,0 +1,3 @@
+---
+redirect_to: '../subscriptions/index.md'
+--- \ No newline at end of file
diff --git a/doc/git_hooks/git_hooks.md b/doc/git_hooks/git_hooks.md
new file mode 100644
index 00000000000..9b8ad1578a0
--- /dev/null
+++ b/doc/git_hooks/git_hooks.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/push_rules/push_rules.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/push_rules/push_rules.html)
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 4e15f7cfd49..aa008d6f768 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,18 +2,34 @@
comments: false
---
-# GitLab basics
-
-Step-by-step guides on the basics of working with Git and GitLab.
-
-- [Command line basics](command-line-commands.md)
-- [Start using Git on the command line](start-using-git.md)
-- [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Create a project](create-project.md)
-- [Create a group](../user/group/index.md#create-a-new-group)
-- [Create a branch](create-branch.md)
-- [Fork a project](fork-project.md)
-- [Add a file](add-file.md)
-- [Add an image](add-image.md)
-- [Create an issue](../user/project/issues/create_new_issue.md)
-- [Create a merge request](add-merge-request.md)
+# GitLab basics guides
+
+This section provides resources to help you start with GitLab by focusing on basic functionality.
+
+This documentation is split into the following groups:
+
+- [GitLab-specific functionality](#gitlab-basics), for basic GitLab features.
+- [General Git functionality](#git-basics), for working with Git in conjunction with GitLab.
+
+## GitLab basics
+
+The following are guides to basic GitLab functionality:
+
+- [Create and add your SSH Keys](create-your-ssh-keys.md), for enabling Git over SSH.
+- [Create a project](create-project.md), to start using GitLab.
+- [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.
+- [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.
+- [Add an image](add-image.md), to add new images to a project's repository.
+- [Create an issue](../user/project/issues/create_new_issue.md), to start collaborating within a project.
+- [Create a merge request](add-merge-request.md), to request changes made in a branch be merged into a project's repository.
+
+## Git basics
+
+If you're unfamiliar with the command line, these resources will help:
+
+- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface.
+- [Start using Git on the command line](start-using-git.md), for some simple Git commands.
+
+More Git resources are available at GitLab's [Git documentation](../topics/git/index.md).
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 3e99496d531..785e2ffb650 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -16,6 +16,7 @@ To create a project in GitLab:
- [Import a project](../user/project/import/index.md) from a different repository,
if enabled on your GitLab instance. Contact your GitLab admin if this
is unavailable.
+ - Run [CI/CD pipelines for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html). **[PREMIUM]**
## Blank projects
@@ -57,7 +58,7 @@ Built-in templates are project templates that are:
To use a built-in template on the **New project** page:
-1. On the **Create from template** tab.
+1. On the **Create from template** tab, select the **Built-in** tab.
1. From the list of available built-in templates, click the:
- **Preview** button to look at the template source itself.
- **Use template** button to start creating the project.
diff --git a/doc/gitlab-geo/README.md b/doc/gitlab-geo/README.md
new file mode 100644
index 00000000000..30d21db7de5
--- /dev/null
+++ b/doc/gitlab-geo/README.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/index.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/index.md).
diff --git a/doc/gitlab-geo/after_setup.md b/doc/gitlab-geo/after_setup.md
new file mode 100644
index 00000000000..c8a7b9d1096
--- /dev/null
+++ b/doc/gitlab-geo/after_setup.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/using_a_geo_server.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/using_a_geo_server.md).
diff --git a/doc/gitlab-geo/bring-primary-back.md b/doc/gitlab-geo/bring-primary-back.md
new file mode 100644
index 00000000000..8c43f4d805f
--- /dev/null
+++ b/doc/gitlab-geo/bring-primary-back.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/disaster_recovery/bring_primary_back.md'
+---
+
+This document was moved to [another location](../administration/geo/disaster_recovery/bring_primary_back.md).
diff --git a/doc/gitlab-geo/configuration.md b/doc/gitlab-geo/configuration.md
new file mode 100644
index 00000000000..b46a2caea4a
--- /dev/null
+++ b/doc/gitlab-geo/configuration.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/configuration.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/configuration.md).
diff --git a/doc/gitlab-geo/configuration_source.md b/doc/gitlab-geo/configuration_source.md
new file mode 100644
index 00000000000..f1aab86fadc
--- /dev/null
+++ b/doc/gitlab-geo/configuration_source.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/configuration_source.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/configuration_source.md).
diff --git a/doc/gitlab-geo/database.md b/doc/gitlab-geo/database.md
new file mode 100644
index 00000000000..b4156dc4ec6
--- /dev/null
+++ b/doc/gitlab-geo/database.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/database.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/database.md).
diff --git a/doc/gitlab-geo/database_source.md b/doc/gitlab-geo/database_source.md
new file mode 100644
index 00000000000..3392d0f02c0
--- /dev/null
+++ b/doc/gitlab-geo/database_source.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/database_source.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/database_source.md).
diff --git a/doc/gitlab-geo/disaster-recovery.md b/doc/gitlab-geo/disaster-recovery.md
new file mode 100644
index 00000000000..d42e815a879
--- /dev/null
+++ b/doc/gitlab-geo/disaster-recovery.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/disaster_recovery/index.md'
+---
+
+This document was moved to [another location](../administration/geo/disaster_recovery/index.md).
diff --git a/doc/gitlab-geo/docker_registry.md b/doc/gitlab-geo/docker_registry.md
new file mode 100644
index 00000000000..26a708f6845
--- /dev/null
+++ b/doc/gitlab-geo/docker_registry.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/docker_registry.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/docker_registry.md).
diff --git a/doc/gitlab-geo/faq.md b/doc/gitlab-geo/faq.md
new file mode 100644
index 00000000000..f1952bc7e4c
--- /dev/null
+++ b/doc/gitlab-geo/faq.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/faq.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/faq.md).
diff --git a/doc/gitlab-geo/ha.md b/doc/gitlab-geo/ha.md
new file mode 100644
index 00000000000..23ed11eaf09
--- /dev/null
+++ b/doc/gitlab-geo/ha.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/high_availability.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/high_availability.md).
diff --git a/doc/gitlab-geo/object_storage.md b/doc/gitlab-geo/object_storage.md
new file mode 100644
index 00000000000..1f29b7b7e8c
--- /dev/null
+++ b/doc/gitlab-geo/object_storage.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/object_storage.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/object_storage.md).
diff --git a/doc/gitlab-geo/planned-failover.md b/doc/gitlab-geo/planned-failover.md
new file mode 100644
index 00000000000..720b6bc9424
--- /dev/null
+++ b/doc/gitlab-geo/planned-failover.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/disaster_recovery/planned_failover.md'
+---
+
+This document was moved to [another location](../administration/geo/disaster_recovery/planned_failover.md).
diff --git a/doc/gitlab-geo/security-review.md b/doc/gitlab-geo/security-review.md
new file mode 100644
index 00000000000..a0a5b0e536c
--- /dev/null
+++ b/doc/gitlab-geo/security-review.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/security_review.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/security_review.md).
diff --git a/doc/gitlab-geo/ssh.md b/doc/gitlab-geo/ssh.md
new file mode 100644
index 00000000000..4f8db687850
--- /dev/null
+++ b/doc/gitlab-geo/ssh.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/operations/fast_ssh_key_lookup.md'
+---
+
+This document was moved to [another location](../administration/operations/fast_ssh_key_lookup.md).
diff --git a/doc/gitlab-geo/troubleshooting.md b/doc/gitlab-geo/troubleshooting.md
new file mode 100644
index 00000000000..25fe1372c69
--- /dev/null
+++ b/doc/gitlab-geo/troubleshooting.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/troubleshooting.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/troubleshooting.md).
diff --git a/doc/gitlab-geo/tuning.md b/doc/gitlab-geo/tuning.md
new file mode 100644
index 00000000000..84ac40f99db
--- /dev/null
+++ b/doc/gitlab-geo/tuning.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/tuning.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/tuning.md).
diff --git a/doc/gitlab-geo/updating_the_geo_nodes.md b/doc/gitlab-geo/updating_the_geo_nodes.md
new file mode 100644
index 00000000000..28234ec02ed
--- /dev/null
+++ b/doc/gitlab-geo/updating_the_geo_nodes.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/updating_the_geo_nodes.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/updating_the_geo_nodes.md).
diff --git a/doc/gitlab-geo/using_a_geo_server.md b/doc/gitlab-geo/using_a_geo_server.md
new file mode 100644
index 00000000000..c8a7b9d1096
--- /dev/null
+++ b/doc/gitlab-geo/using_a_geo_server.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../administration/geo/replication/using_a_geo_server.md'
+---
+
+This document was moved to [another location](../administration/geo/replication/using_a_geo_server.md).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 61f544deabe..6c1ba7fee95 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -95,7 +95,7 @@ Make sure you have the right version of Git installed:
# Install Git
sudo apt-get install -y git-core
-# Make sure Git is version 2.18.0 or higher
+# Make sure Git is version 2.21.0 or higher
git --version
```
@@ -110,9 +110,9 @@ sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libs
# Download and compile from source
cd /tmp
-curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz
-echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz
-cd git-2.18.0/
+curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz
+echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz
+cd git-2.21.0/
./configure
make prefix=/usr/local all
@@ -311,7 +311,7 @@ the guide here.
sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
# Disable Redis listening on TCP by setting 'port' to 0
-sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
+sudo sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
# Enable Redis socket for default Debian / Ubuntu path
echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
@@ -320,9 +320,9 @@ echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf
# Create the directory which contains the socket
-mkdir /var/run/redis
-chown redis:redis /var/run/redis
-chmod 755 /var/run/redis
+sudo mkdir -p /var/run/redis
+sudo chown redis:redis /var/run/redis
+sudo chmod 755 /var/run/redis
# Persist the directory which contains the socket, if applicable
if [ -d /etc/tmpfiles.d ]; then
@@ -384,7 +384,7 @@ sudo chmod -R u+rwX tmp/pids/
sudo chmod -R u+rwX tmp/sockets/
# Create the public/uploads/ directory
-sudo -u git -H mkdir public/uploads/
+sudo -u git -H mkdir -p public/uploads/
# Make sure only the GitLab user has access to the public/uploads/ directory
# now that files in public/uploads are served by gitlab-workhorse
@@ -540,6 +540,7 @@ sudo -u git -H make
```sh
# Fetch Gitaly source with Git and compile with Go
+cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production
```
@@ -567,6 +568,7 @@ For more information about configuring Gitaly see
### Initialize Database and Activate Advanced Features
```sh
+cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
diff --git a/doc/install/ldap.md b/doc/install/ldap.md
new file mode 100644
index 00000000000..a19f0342b65
--- /dev/null
+++ b/doc/install/ldap.md
@@ -0,0 +1,7 @@
+---
+redirect_to: '../administration/auth/ldap.md'
+---
+
+# GitLab LDAP integration
+
+This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
diff --git a/doc/install/pivotal/index.md b/doc/install/pivotal/index.md
new file mode 100644
index 00000000000..896e01ad975
--- /dev/null
+++ b/doc/install/pivotal/index.md
@@ -0,0 +1,45 @@
+# GitLab Pivotal Tile **[PREMIUM ONLY]**
+
+> Introduced in [GitLab Premium][eep] 8.2.
+
+Easily deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for
+[Pivotal Cloud Foundry][pcf].
+
+## Overview
+
+Enterprise admins want their development toolkit to be more customizable, more
+integrated, and more secure. With Pivotal Cloud Foundry, GitLab is installed and
+scales easily in a highly available environment.
+
+The upgrades are pain-free and well tested. All it takes is upload the new tile
+and click a button to begin the upgrade process.
+
+## Use cases
+
+- You want a highly available deployment with minimal effort. Scale horizontally
+ as your user base grows.
+
+## Features
+
+The GitLab Pivotal Tile is based on [GitLab Premium][eep] and includes nearly all of its features. The features in Premium but _not_ supported on the Tile are:
+
+* PostgreSQL
+* Pages
+* Geo
+* Registry
+* Mattermost
+* Subgroups
+* Elasticsearch
+* Service Desk
+* OAuth & Kerberos Authentication
+
+## Installing GitLab with Pivotal
+
+The product information and installation documentation is hosted on Pivotal's
+website:
+
+- [Product page](https://network.pivotal.io/products/p-gitlab/)
+- [Documentation](https://docs.pivotal.io/partners/gitlab/index.html)
+
+[eep]: https://about.gitlab.com/pricing/
+[pcf]: https://pivotal.io/platform
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 9a6c2ce1976..672723aaf12 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -87,7 +87,7 @@ if your available memory changes. We also recommend [configuring the kernel's sw
to a low value like `10` to make the most of your RAM while still having the swap
available when needed.
-Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database
@@ -105,8 +105,10 @@ features of GitLab work with MySQL/MariaDB:
1. MySQL support for subgroups was [dropped with GitLab 9.3][post].
See [issue #30472][30472] for more information.
-1. Geo does [not support MySQL](https://docs.gitlab.com/ee/administration/geo/replication/database.html#mysql-replication). This means no supported Disaster Recovery solution if using MySQL. **[PREMIUM ONLY]**
+1. Geo does [not support MySQL](https://docs.gitlab.com/ee/administration/geo/replication/database.html). This means no supported Disaster Recovery solution if using MySQL. **[PREMIUM ONLY]**
1. [Zero downtime migrations](../update/README.md#upgrading-without-downtime) do not work with MySQL.
+1. [Database load balancing](https://docs.gitlab.com/ee/administration/database_load_balancing.html) is
+ supported only for PostgreSQL. **[PREMIUM ONLY]**
1. GitLab [optimizes the loading of dashboard events](https://gitlab.com/gitlab-org/gitlab-ce/issues/31806) using [PostgreSQL LATERAL JOINs](https://blog.heapanalytics.com/postgresqls-powerful-new-join-type-lateral/).
1. In general, SQL optimized for PostgreSQL may run much slower in MySQL due to
differences in query planners. For example, subqueries that work well in PostgreSQL
@@ -141,7 +143,17 @@ On some systems you may need to install an additional package (e.g.
#### Additional requirements for GitLab Geo
-If you are using [GitLab Geo](https://docs.gitlab.com/ee/development/geo.html), the [tracking database](https://docs.gitlab.com/ee/development/geo.html#geo-tracking-database) also requires the `postgres_fdw` extension.
+If you are using [GitLab Geo](https://docs.gitlab.com/ee/development/geo.html):
+
+- We strongly recommend running Omnibus-managed instances as they are actively
+ developed and tested. We aim to be compatible with most external (not managed
+ by Omnibus) databases (for example, AWS RDS) but we do not guarantee
+ compatibility.
+- The
+ [tracking database](https://docs.gitlab.com/ee/development/geo.html#geo-tracking-database)
+ requires the
+ [postgres_fdw](https://www.postgresql.org/docs/9.6/static/postgres-fdw.html)
+ extension.
```
CREATE EXTENSION postgres_fdw;
@@ -212,5 +224,5 @@ Support is only provided for the current minor version of the major version you
Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
-Note: We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
+NOTE: **Note:** We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
in the future because we have features such as Issue Boards which require JavaScript extensively.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index a539933f223..1fea6a32c28 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,7 +15,9 @@ See the documentation below for details on how to configure these services.
- [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
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
new file mode 100644
index 00000000000..7cef664bc98
--- /dev/null
+++ b/doc/integration/elasticsearch.md
@@ -0,0 +1,584 @@
+# Elasticsearch integration **[STARTER ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/109 "Elasticsearch Merge Request") in GitLab [Starter](https://about.gitlab.com/pricing/) 8.4. Support
+> for [Amazon Elasticsearch](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg.html) was [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1305) in GitLab
+> [Starter](https://about.gitlab.com/pricing/) 9.0.
+
+This document describes how to set up Elasticsearch with GitLab. Once enabled,
+you'll have the benefit of fast search response times and the advantage of two
+special searches:
+
+- [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html)
+- [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html)
+
+## Version Requirements
+<!-- Please remember to update ee/lib/system_check/app/elasticsearch_check.rb if this changes -->
+
+| GitLab version | Elasticsearch version |
+| -------------- | --------------------- |
+| GitLab Enterprise Edition 8.4 - 8.17 | Elasticsearch 2.4 with [Delete By Query Plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/2.4/plugins-delete-by-query.html) installed |
+| GitLab Enterprise Edition 9.0 - 11.4 | Elasticsearch 5.1 - 5.5 |
+| GitLab Enterprise Edition 11.5+ | Elasticsearch 5.6 - 6.x |
+
+## Installing Elasticsearch
+
+Elasticsearch is _not_ included in the Omnibus packages. You will have to
+install it yourself whether you are using the Omnibus package or installed
+GitLab from source. Providing detailed information on installing Elasticsearch
+is out of the scope of this document.
+
+Once the data is added to the database or repository and [Elasticsearch is
+enabled in the admin area](#enabling-elasticsearch) the search index will be
+updated automatically. Elasticsearch can be installed on the same machine as
+GitLab or on a separate server, or you can use the [Amazon Elasticsearch](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg.html)
+service.
+
+You can follow the steps as described in the [official web site](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html "Elasticsearch installation documentation") or
+use the packages that are available for your OS.
+
+## Elasticsearch repository indexer (beta)
+
+In order to improve elasticsearch indexing performance, GitLab has made available a [new indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer).
+This will replace the included Ruby indexer in the future but should be considered beta software for now, so there may be some bugs.
+
+If you would like to use it, please follow the instructions below.
+
+### Installation
+
+First, we need to install some dependencies, then we'll build and install
+the indexer itself.
+
+#### Dependencies
+
+This project relies on [ICU](http://site.icu-project.org/) for text encoding,
+therefore we need to ensure the development packages for your platform are
+installed before running `make`.
+
+##### Debian / Ubuntu
+
+To install on Debian or Ubuntu, run:
+
+```sh
+sudo apt install libicu-dev
+```
+
+##### CentOS / RHEL
+
+To install on CentOS or RHEL, run:
+
+```sh
+sudo yum install libicu-devel
+```
+
+##### Mac OSX
+
+To install on macOS, run:
+
+```sh
+brew install icu4c
+export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig:$PKG_CONFIG_PATH"
+```
+
+#### Building and installing
+
+To build and install the indexer, run:
+
+```sh
+git clone https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer.git
+cd /gitlab-elasticsearch-indexer
+make
+sudo make install
+```
+
+The `gitlab-elasticsearch-indexer` will be installed to `/usr/local/bin`.
+
+You can change the installation path with the `PREFIX` env variable.
+Please remember to pass the `-E` flag to `sudo` if you do so.
+
+Example:
+
+```sh
+PREFIX=/usr sudo -E make install
+```
+
+Once installed, enable it under your instance's elasticsearch settings explained [below](#enabling-elasticsearch).
+
+## System Requirements
+
+Elasticsearch requires additional resources in excess of those documented in the
+[GitLab system requirements](../install/requirements.md). These will vary by
+installation size, but you should ensure **at least** an additional **8 GiB of RAM**
+for each Elasticsearch node, per the [official guidelines](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html).
+
+Keep in mind, this is the **minimum requirements** as per Elasticsearch. For
+production instances, they recommend considerably more resources.
+
+Storage requirements also vary based on the installation side, but as a rule of
+thumb, you should allocate the total size of your production database, **plus**
+two-thirds of the total size of your git repositories. Efforts to reduce this
+total are being tracked in this epic: [gitlab-org&153](https://gitlab.com/groups/gitlab-org/-/epics/153).
+
+## Enabling Elasticsearch
+
+In order to enable Elasticsearch, you need to have admin access. Go to
+**Admin > Settings > Integrations** and find the "Elasticsearch" section.
+
+The following Elasticsearch settings are available:
+
+| Parameter | Description |
+| --------- | ----------- |
+| `Elasticsearch indexing` | Enables/disables Elasticsearch indexing. You may want to enable indexing but disable search in order to give the index time to be fully completed, for example. Also, keep in mind that this option doesn't have any impact on existing data, this only enables/disables background indexer which tracks data changes. So by enabling this you will not get your existing data indexed, use special rake task for that as explained in [Adding GitLab's data to the Elasticsearch index](#adding-gitlabs-data-to-the-elasticsearch-index). |
+| `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). |
+| `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. |
+| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). |
+| `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects).
+| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. |
+| `AWS Region` | The AWS region your Elasticsearch service is located in. |
+| `AWS Access Key` | The AWS access key. |
+| `AWS Secret Access Key` | The AWS secret access key. |
+
+### Limiting namespaces and projects
+
+If you select `Limit namespaces and projects that can be indexed`, more options will become available
+![limit namespaces and projects options](img/limit_namespaces_projects_options.png)
+
+You can select namespaces and projects to index exclusively. Please note that if the namespace is a group it will include
+any sub-groups and projects belonging to those sub-groups to be indexed as well.
+
+You can filter the selection dropdown by writing part of the namespace or project name you're interested in.
+![limit namespace filter](img/limit_namespace_filter.png)
+
+NOTE: **Note**:
+If no namespaces or projects are selected, no Elasticsearch indexing will take place.
+
+CAUTION: **Warning**:
+If you have already indexed your instance, you will have to regenerate the index in order to delete all existing data
+for filtering to work correctly. To do this run the rake tasks `gitlab:elastic:create_empty_index` and
+`gitlab:elastic:clear_index_status` Afterwards, removing a namespace or a projeect from the list will delete the data
+from the Elasticsearch index as expected.
+
+## Disabling Elasticsearch
+
+To disable the Elasticsearch integration:
+
+1. Navigate to the **Admin > Settings > Integrations**
+1. Find the 'Elasticsearch' section and uncheck 'Search with Elasticsearch enabled'
+ and 'Elasticsearch indexing'
+1. Click **Save** for the changes to take effect
+1. [Optional] Delete the existing index by running the command `sudo gitlab-rake gitlab:elastic:delete_index`
+
+## Adding GitLab's data to the Elasticsearch index
+
+### Indexing small instances (database size less than 500 MiB, size of repos less than 5 GiB)
+
+Configure Elasticsearch's host and port in **Admin > Settings**. Then index the data using one of the following commands:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index
+
+# Installations from source
+bundle exec rake gitlab:elastic:index RAILS_ENV=production
+```
+
+After it completes the indexing process, [enable Elasticsearch searching](elasticsearch.md#enabling-elasticsearch).
+
+### Indexing large instances
+
+WARNING: **Warning**:
+Performing asynchronous indexing, as this will describe, will generate a lot of sidekiq jobs.
+Make sure to prepare for this task by either [Horizontally Scaling](../administration/high_availability/README.md#basic-scaling)
+or creating [extra sidekiq processes](../administration/operations/extra_sidekiq_processes.md)
+
+NOTE: **Note**:
+After indexing the repositories asynchronously, you **MUST** index the database to be able to search.
+
+Configure Elasticsearch's host and port in **Admin > Settings > Integrations**. Then create empty indexes using one of the following commands:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:create_empty_index
+
+# Installations from source
+bundle exec rake gitlab:elastic:create_empty_index RAILS_ENV=production
+```
+
+Indexing large Git repositories can take a while. To speed up the process, you
+can temporarily disable auto-refreshing and replicating. In our experience, you can expect a 20%
+decrease in indexing time. We'll enable them when indexing is done. This step is optional!
+
+```bash
+curl --request PUT localhost:9200/gitlab-production/_settings --data '{
+ "index" : {
+ "refresh_interval" : "-1",
+ "number_of_replicas" : 0
+ } }'
+```
+
+Then enable Elasticsearch indexing and run repository indexing tasks:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories_async
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories_async RAILS_ENV=production
+```
+
+This enqueues a number of Sidekiq jobs to index your existing repositories.
+You can view the jobs in the admin panel (they are placed in the `elastic_batch_project_indexer`)
+queue), or you can query indexing status using a rake task:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories_status
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories_status RAILS_ENV=production
+
+Indexing is 65.55% complete (6555/10000 projects)
+```
+
+By default, one job is created for every 300 projects. For large numbers of
+projects, you may wish to increase the batch size, by setting the `BATCH`
+environment variable.
+
+You can also run the initial indexing synchronously - this is most useful if
+you have a small number of projects or need finer-grained control over indexing
+than Sidekiq permits:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production
+```
+
+It might take a while depending on how big your Git repositories are.
+
+If you want to run several tasks in parallel (probably in separate terminal
+windows) you can provide the `ID_FROM` and `ID_TO` parameters:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000 RAILS_ENV=production
+```
+
+Where `ID_FROM` and `ID_TO` are project IDs. Both parameters are optional.
+As an example, if you have 3,000 repositories and you want to run three separate indexing tasks, you might run:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories ID_TO=1000
+sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=1001 ID_TO=2000
+sudo gitlab-rake gitlab:elastic:index_repositories ID_FROM=2001
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_TO=1000
+bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_FROM=1001 ID_TO=2000
+bundle exec rake gitlab:elastic:index_repositories RAILS_ENV=production ID_FROM=2001
+```
+
+Sometimes your repository index process `gitlab:elastic:index_repositories` or
+`gitlab:elastic:index_repositories_async` can get interrupted. This may happen
+for many reasons, but it's always safe to run the indexing job again - it will
+skip those repositories that have already been indexed.
+
+As the indexer stores the last commit SHA of every indexed repository in the
+database, you can run the indexer with the special parameter `UPDATE_INDEX` and
+it will check every project repository again to make sure that every commit in
+that repository is indexed, it can be useful in case if your index is outdated:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_repositories UPDATE_INDEX=true ID_TO=1000
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_repositories UPDATE_INDEX=true ID_TO=1000 RAILS_ENV=production
+```
+
+You can also use the `gitlab:elastic:clear_index_status` Rake task to force the
+indexer to "forget" all progress, so retrying the indexing process from the
+start.
+
+To index all wikis:
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_wikis
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_wikis RAILS_ENV=production
+```
+
+The wiki indexer also supports the `ID_FROM` and `ID_TO` parameters if you want
+to limit a project set.
+
+Index all database entities (Keep in mind it can take a while, so consider using `screen` or `tmux`):
+
+```sh
+# Omnibus installations
+sudo gitlab-rake gitlab:elastic:index_database
+
+# Installations from source
+bundle exec rake gitlab:elastic:index_database RAILS_ENV=production
+```
+
+Enable replication and refreshing again after indexing (only if you previously disabled it):
+
+```bash
+curl --request PUT localhost:9200/gitlab-production/_settings --data '{
+ "index" : {
+ "number_of_replicas" : 1,
+ "refresh_interval" : "1s"
+ } }'
+```
+
+A force merge should be called after enabling the refreshing above:
+
+```bash
+curl --request POST 'http://localhost:9200/_forcemerge?max_num_segments=5'
+```
+
+Enable Elasticsearch search in **Admin > Settings > Integrations**. That's it. Enjoy it!
+
+## GitLab Elasticsearch Rake Tasks
+
+There are several rake tasks available to you via the command line:
+
+- [sudo gitlab-rake gitlab:elastic:index](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This is a wrapper task. It does the following:
+ - `sudo gitlab-rake gitlab:elastic:create_empty_index`
+ - `sudo gitlab-rake gitlab:elastic:clear_index_status`
+ - `sudo gitlab-rake gitlab:elastic:index_wikis`
+ - `sudo gitlab-rake gitlab:elastic:index_database`
+ - `sudo gitlab-rake gitlab:elastic:index_repositories`
+- [sudo gitlab-rake gitlab:elastic:index_repositories_async](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This iterates over all projects and places them in batches. It then sends these batches to the background via sidekiq jobs to be indexed.
+- [sudo gitlab-rake gitlab:elastic:index_repositories_status](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100.
+- [sudo gitlab-rake gitlab:elastic:index_repositories](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This iterates over all projects and places them in batches. It then performs indexing on said batches synchronously.
+- [sudo gitlab-rake gitlab:elastic:index_wikis](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Iterates over every project, determines if said project contains wiki data, and then indexes the blobs (content) of said wiki data.
+- [sudo gitlab-rake gitlab:elastic:index_database](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This is a [rake multitask](https://www.rubydoc.info/github/ruby/rake/Rake/MultiTask). It does the following:
+ - `sudo gitlab-rake gitlab:elastic:index_projects`
+ - `sudo gitlab-rake gitlab:elastic:index_issues`
+ - `sudo gitlab-rake gitlab:elastic:index_merge_requests`
+ - `sudo gitlab-rake gitlab:elastic:index_snippets`
+ - `sudo gitlab-rake gitlab:elastic:index_notes`
+ - `sudo gitlab-rake gitlab:elastic:index_milestones`
+- [sudo gitlab-rake gitlab:elastic:create_empty_index](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This generates an empty index on the Elasticsearch side.
+- [sudo gitlab-rake gitlab:elastic:clear_index_status](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This deletes all instances of IndexStatus for all projects.
+- [sudo gitlab-rake gitlab:elastic:delete_index](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - This removes the GitLab index on the Elasticsearch instance.
+- [sudo gitlab-rake gitlab:elastic:recreate_index](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Does the same thing as `sudo gitlab-rake gitlab:elastic:create_empty_index`
+- [sudo gitlab-rake gitlab:elastic:add_feature_visibility_levels_to_project](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Adds visibility information to the indices for projects.
+- [sudo gitlab-rake gitlab:elastic:index_projects](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes projects data.
+- [sudo gitlab-rake gitlab:elastic:index_issues](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes issues data.
+- [sudo gitlab-rake gitlab:elastic:index_merge_requests](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes merge requests data.
+- [sudo gitlab-rake gitlab:elastic:index_snippets](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes the snippets data.
+- [sudo gitlab-rake gitlab:elastic:index_notes](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes the notes data.
+- [sudo gitlab-rake gitlab:elastic:index_milestones](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/tasks/gitlab/elastic.rake)
+ - Performs an Elasticsearch import that indexes the milestones data.
+
+### Environment Variables
+
+In addition to the rake tasks, there are some environment variables that can be used to modify the process:
+
+| Environment Variable | Data Type | What it does |
+| -------------------- |:---------:| ---------------------------------------------------------------------------- |
+| `BATCH` | Integer | Modifies the size of the indexing batch (default 300 projects). |
+| `UPDATE_INDEX` | Boolean | Tells the indexer to overwrite any existing index data (true/false). |
+| `ID_TO` | Integer | Tells the indexer to only index projects less than or equal to the value. |
+| `ID_FROM` | Integer | Tells the indexer to only index projects greater than or equal to the value. |
+
+### Batching
+
+The ability to apply batching makes the indexer run more efficiently. The default
+size of a batch is 300 projects, which may or may not be ideal for your setup.
+Depending on the resources available to your GitLab instance (sidekiq) and your
+Elasticsearch instance (RAM, CPU), you may be able to increase or decrease the
+batch size for more efficiency.
+
+- The larger the batch size is, the less sidekiq jobs and indexing requests get created.
+- The larger the batch size is, the more time and RAM it takes to process.
+- The smaller the batch size, the more sidekiq jobs, and indexing requests get created.
+- The smaller the batch size, the more CPU gets utilized.
+
+Finding the ideal size can be tricky, and will vary from GitLab instance to GitLab instance.
+Generally speaking, if the default is not ideal for you, try reducing it to somewhere in
+the 50-150 range (for bigger sized repos) or 450-600 range (for many small-sized repos).
+
+Example use:
+
+```sh
+sudo gitlab-rake gitlab:elastic:index_repositories_async BATCH=50
+```
+
+### Indexing a specific project
+
+Because the `ID_TO` and `ID_FROM` environment variables use the `or equal to` comparison, you can index only one project by using both these variables with the same project ID number:
+
+```sh
+root@git:~# sudo gitlab-rake gitlab:elastic:index_repositories ID_TO=5 ID_FROM=5
+Indexing project repositories...I, [2019-03-04T21:27:03.083410 #3384] INFO -- : Indexing GitLab User / test (ID=33)...
+I, [2019-03-04T21:27:05.215266 #3384] INFO -- : Indexing GitLab User / test (ID=33) is done!
+```
+
+## Elasticsearch Index Scopes
+
+When performing a search, the GitLab index will use the following scopes:
+
+| Scope Name | What it searches |
+| ---------------- | ---------------------- |
+| `commits` | Commit data |
+| `projects` | Project data (default) |
+| `blobs` | Code |
+| `issues` | Issue data |
+| `merge_requests` | Merge Request data |
+| `milestones` | Milestone data |
+| `notes` | Note data |
+| `snippets` | Snippet data |
+| `wiki_blobs` | Wiki contents |
+
+## Tuning
+
+### Deleted documents
+
+Whenever a change or deletion is made to an indexed GitLab object (a merge request description is changed, a file is deleted from the master branch in a repository, a project is deleted, etc), a document in the index is deleted. However, since these are "soft" deletes, the overall number of "deleted documents", and therefore wasted space, increases. Elasticsearch does intelligent merging of segments in order to remove these deleted documents. However, depending on the amount and type of activity in your GitLab installation, it's possible to see as much as 50% wasted space in the index.
+
+In general, we recommend simply letting Elasticseach merge and reclaim space automatically, with the default settings. From [Lucene's Handling of Deleted Documents](https://www.elastic.co/blog/lucenes-handling-of-deleted-documents "Lucene's Handling of Deleted Documents"), _"Overall, besides perhaps decreasing the maximum segment size, it is best to leave Lucene's defaults as-is and not fret too much about when deletes are reclaimed."_
+
+However, some larger installations may wish to tune the merge policy settings:
+
+- Consider reducing the `index.merge.policy.max_merged_segment` size from the default 5 GB to maybe 2 GB or 3 GB. Merging only happens when a segment has at least 50% deletions. Smaller segment sizes will allow merging to happen more frequently.
+
+ ```bash
+ curl --request PUT http://localhost:9200/gitlab-production/_settings --data '{
+ "index" : {
+ "merge.policy.max_merged_segment": "2gb"
+ }
+ }'
+ ```
+
+- You can also adjust `index.merge.policy.reclaim_deletes_weight`, which controls how aggressively deletions are targeted. But this can lead to costly merge decisions, so we recommend not changing this unless you understand the tradeoffs.
+
+ ```bash
+ curl --request PUT http://localhost:9200/gitlab-production/_settings --data '{
+ "index" : {
+ "merge.policy.reclaim_deletes_weight": "3.0"
+ }
+ }'
+ ```
+
+- Do not do a [force merge](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html "Force Merge") to remove deleted documents. A warning in the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html "Force Merge") states that this can lead to very large segments that may never get reclaimed, and can also cause significant performance or availability issues.
+
+## Troubleshooting
+
+Here are some common pitfalls and how to overcome them:
+
+- **How can I verify my GitLab instance is using Elasticsearch?**
+
+ The easiest method is via the rails console (`sudo gitlab-rails console`) by running the following:
+
+ ```ruby
+ u = User.find_by_username('your-username')
+ s = SearchService.new(u, {:search => 'search_term'})
+ pp s.search_objects.class.name
+ ```
+
+ If you see `Elasticsearch::Model::Response::Records`, you are using Elasticsearch.
+
+- **I updated GitLab and now I can't find anything**
+
+ We continuously make updates to our indexing strategies and aim to support
+ newer versions of Elasticsearch. When indexing changes are made, it may
+ be necessary for you to [reindex](#adding-gitlabs-data-to-the-elasticsearch-index) after updating GitLab.
+
+- **I indexed all the repositories but I can't find anything**
+
+ Make sure you indexed all the database data [as stated above](#adding-gitlabs-data-to-the-elasticsearch-index).
+
+ Beyond that, check via the [Elasticsearch Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html) to see if the data shows up on the Elasticsearch side.
+
+ If it shows up via the [Elasticsearch Search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html), check that it shows up via the rails console (`sudo gitlab-rails console`):
+
+ ```ruby
+ u = User.find_by_username('your-username')
+ s = SearchService.new(u, {:search => 'search_term', :scope => ‘blobs’})
+ pp s.search_objects.to_a
+ ```
+
+ See [Elasticsearch Index Scopes](elasticsearch.md#elasticsearch-index-scopes) for more information on searching for specific types of data.
+
+- **I indexed all the repositories but then switched elastic search servers and now I can't find anything**
+
+ You will need to re-run all the rake tasks to re-index the database, repositories, and wikis.
+
+- **The indexing process is taking a very long time**
+
+ The more data present in your GitLab instance, the longer the indexing process takes. You might want to try adjusting the BATCH sizes for asynchronous indexing to help speed up the process.
+
+- **No new data is added to the Elasticsearch index when I push code**
+
+ When performing the initial indexing of blobs, we lock all projects until the project finishes indexing. It could
+ happen that an error during the process causes one or multiple projects to remain locked. In order to unlock them,
+ run the `gitlab:elastic:clear_locked_projects` rake task.
+
+- **"Can't specify parent if no parent field has been configured"**
+
+ If you enabled Elasticsearch before GitLab 8.12 and have not rebuilt indexes you will get
+ exception in lots of different cases:
+
+ ```text
+ Elasticsearch::Transport::Transport::Errors::BadRequest([400] {
+ "error": {
+ "root_cause": [{
+ "type": "illegal_argument_exception",
+ "reason": "Can't specify parent if no parent field has been configured"
+ }],
+ "type": "illegal_argument_exception",
+ "reason": "Can't specify parent if no parent field has been configured"
+ },
+ "status": 400
+ }):
+ ```
+
+ This is because we changed the index mapping in GitLab 8.12 and the old indexes should be removed and built from scratch again,
+ see details in the [8-11-to-8-12 update guide](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/8.11-to-8.12.md#11-elasticsearch-index-update-if-you-currently-use-elasticsearch).
+
+- Exception `Elasticsearch::Transport::Transport::Errors::BadRequest`
+
+ If you have this exception (just like in the case above but the actual message is different) please check if you have the correct Elasticsearch version and you met the other [requirements](#system-requirements).
+ There is also an easy way to check it automatically with `sudo gitlab-rake gitlab:check` command.
+
+- Exception `Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge`
+
+ ```text
+ [413] {"Message":"Request size exceeded 10485760 bytes"}
+ ```
+
+ This exception is seen when your Elasticsearch cluster is configured to reject
+ requests above a certain size (10MiB in this case). This corresponds to the
+ `http.max_content_length` setting in `elasticsearch.yml`. Increase it to a
+ larger size and restart your Elasticsearch cluster.
+
+ AWS has [fixed limits](http://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.
+
diff --git a/doc/integration/github.md b/doc/integration/github.md
index bee68688ace..e145afbdd5e 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -19,7 +19,7 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
1. Provide the required details.
- Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com`
+ - Homepage URL: The URL of your GitLab installation. For example, `https://gitlab.example.com`.
- Application description: Fill this in if you wish.
- Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth/github/callback`. Please make sure the port is included if your GitLab instance is not configured on default port.
![Register OAuth App](img/github_register_app.png)
diff --git a/doc/integration/img/jenkins_gitlab_plugin_config.png b/doc/integration/img/jenkins_gitlab_plugin_config.png
new file mode 100644
index 00000000000..3e1f83e96dc
--- /dev/null
+++ b/doc/integration/img/jenkins_gitlab_plugin_config.png
Binary files differ
diff --git a/doc/integration/img/jenkins_gitlab_service.png b/doc/integration/img/jenkins_gitlab_service.png
new file mode 100644
index 00000000000..682a5ae8ee2
--- /dev/null
+++ b/doc/integration/img/jenkins_gitlab_service.png
Binary files differ
diff --git a/doc/integration/img/jenkins_gitlab_service_settings.png b/doc/integration/img/jenkins_gitlab_service_settings.png
new file mode 100644
index 00000000000..5a12e9cb39a
--- /dev/null
+++ b/doc/integration/img/jenkins_gitlab_service_settings.png
Binary files differ
diff --git a/doc/integration/img/jenkins_project.png b/doc/integration/img/jenkins_project.png
new file mode 100644
index 00000000000..126b05c8879
--- /dev/null
+++ b/doc/integration/img/jenkins_project.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_gl_setup_1.png b/doc/integration/img/jira_dev_panel_gl_setup_1.png
new file mode 100644
index 00000000000..75279877c93
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_gl_setup_1.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_jira_setup_1.png b/doc/integration/img/jira_dev_panel_jira_setup_1.png
new file mode 100644
index 00000000000..5c0f594cc1d
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_jira_setup_1.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_jira_setup_2.png b/doc/integration/img/jira_dev_panel_jira_setup_2.png
new file mode 100644
index 00000000000..a4778a00dd5
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_jira_setup_2.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_jira_setup_3.png b/doc/integration/img/jira_dev_panel_jira_setup_3.png
new file mode 100644
index 00000000000..4049a65f56b
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_jira_setup_3.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_jira_setup_4.png b/doc/integration/img/jira_dev_panel_jira_setup_4.png
new file mode 100644
index 00000000000..81d84cb173d
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_jira_setup_4.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_jira_setup_5.png b/doc/integration/img/jira_dev_panel_jira_setup_5.png
new file mode 100644
index 00000000000..73dc867d301
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_jira_setup_5.png
Binary files differ
diff --git a/doc/integration/img/jira_dev_panel_manual_refresh.png b/doc/integration/img/jira_dev_panel_manual_refresh.png
new file mode 100644
index 00000000000..dc92d533bde
--- /dev/null
+++ b/doc/integration/img/jira_dev_panel_manual_refresh.png
Binary files differ
diff --git a/doc/integration/img/limit_namespace_filter.png b/doc/integration/img/limit_namespace_filter.png
new file mode 100644
index 00000000000..88f5caa41db
--- /dev/null
+++ b/doc/integration/img/limit_namespace_filter.png
Binary files differ
diff --git a/doc/integration/img/limit_namespaces_projects_options.png b/doc/integration/img/limit_namespaces_projects_options.png
new file mode 100644
index 00000000000..488341f7e92
--- /dev/null
+++ b/doc/integration/img/limit_namespaces_projects_options.png
Binary files differ
diff --git a/doc/integration/img/salesforce_app_details.png b/doc/integration/img/salesforce_app_details.png
new file mode 100644
index 00000000000..00e66f07282
--- /dev/null
+++ b/doc/integration/img/salesforce_app_details.png
Binary files differ
diff --git a/doc/integration/img/salesforce_app_secret_details.png b/doc/integration/img/salesforce_app_secret_details.png
new file mode 100644
index 00000000000..fad2a4a1f97
--- /dev/null
+++ b/doc/integration/img/salesforce_app_secret_details.png
Binary files differ
diff --git a/doc/integration/img/salesforce_oauth_app_details.png b/doc/integration/img/salesforce_oauth_app_details.png
new file mode 100644
index 00000000000..a5fb680cca6
--- /dev/null
+++ b/doc/integration/img/salesforce_oauth_app_details.png
Binary files differ
diff --git a/doc/integration/img/ultra_auth_credentials.png b/doc/integration/img/ultra_auth_credentials.png
new file mode 100644
index 00000000000..cff98a4b056
--- /dev/null
+++ b/doc/integration/img/ultra_auth_credentials.png
Binary files differ
diff --git a/doc/integration/img/ultra_auth_edit_callback_url.png b/doc/integration/img/ultra_auth_edit_callback_url.png
new file mode 100644
index 00000000000..b7548122c5e
--- /dev/null
+++ b/doc/integration/img/ultra_auth_edit_callback_url.png
Binary files differ
diff --git a/doc/integration/img/ultra_auth_edit_callback_url_highlighted.png b/doc/integration/img/ultra_auth_edit_callback_url_highlighted.png
new file mode 100644
index 00000000000..4abf224756c
--- /dev/null
+++ b/doc/integration/img/ultra_auth_edit_callback_url_highlighted.png
Binary files differ
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
new file mode 100644
index 00000000000..f60333aa07c
--- /dev/null
+++ b/doc/integration/jenkins.md
@@ -0,0 +1,129 @@
+# Jenkins CI service **[STARTER]**
+
+>**Note:**
+In GitLab 8.3, Jenkins integration using the
+[GitLab Hook Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Hook+Plugin)
+was deprecated in favor of the
+[GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin).
+The deprecated integration has been renamed to [Jenkins CI (Deprecated)](jenkins_deprecated.md) in the
+project service settings. We may remove this in a future release and recommend
+using the new 'Jenkins CI' project service instead which is described in this
+document.
+
+## Overview
+
+[Jenkins](https://jenkins.io/) is a great Continuous Integration tool, similar to our built-in
+[GitLab CI](../ci/README.md).
+
+GitLab's Jenkins integration allows you to trigger a Jenkins build when you
+push code to a repository, or when a merge request is created. Additionally,
+it shows the pipeline status on merge requests widgets and on the project's home page.
+
+## Use cases
+
+- Suppose you are new to GitLab, and want to keep using Jenkins until you prepare
+your projects to build with [GitLab CI/CD](../ci/README.md). You set up the
+integration between GitLab and Jenkins, then you migrate to GitLab CI later. While
+you organize yourself and your team to onboard GitLab, you keep your pipelines
+running with Jenkins, but view the results in your project's repository in GitLab.
+- Your team uses [Jenkins Plugins](https://plugins.jenkins.io/) for other proceedings,
+therefore, you opt for keep using Jenkins to build your apps. Show the results of your
+pipelines directly in GitLab.
+
+## Requirements
+
+* [Jenkins GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin)
+* [Jenkins Git Plugin](https://wiki.jenkins.io/display/JENKINS/Git+Plugin)
+* Git clone access for Jenkins from the GitLab repository
+* GitLab API access to report build status
+
+## Configure GitLab users
+
+Create a user or choose an existing user that Jenkins will use to interact
+through the GitLab API. This user will need to be a global Admin or added
+as a member to each Group/Project. Developer permission is required for reporting
+build status. This is because a successful build status can trigger a merge
+when 'Merge when pipeline succeeds' feature is used. Some features of the GitLab
+Plugin may require additional privileges. For example, there is an option to
+accept a merge request if the build is successful. Using this feature would
+require developer, maintainer or owner-level permission.
+
+Copy the private API token from **Profile Settings -> Account**. You will need this
+when configuring the Jenkins server later.
+
+## Configure the Jenkins server
+
+Install [Jenkins GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin)
+and [Jenkins Git Plugin](https://wiki.jenkins.io/display/JENKINS/Git+Plugin).
+
+Go to Manage Jenkins -> Configure System and scroll down to the 'GitLab' section.
+Enter the GitLab server URL in the 'GitLab host URL' field and paste the API token
+copied earlier in the 'API Token' field.
+
+For more information, see GitLab Plugin documentation about
+[Jenkins-to-GitLab authentication](https://github.com/jenkinsci/gitlab-plugin#jenkins-to-gitlab-authentication)
+
+![Jenkins GitLab plugin configuration](img/jenkins_gitlab_plugin_config.png)
+
+## Configure a Jenkins project
+
+Follow the GitLab Plugin documentation about [Jenkins Job Configuration](https://github.com/jenkinsci/gitlab-plugin#jenkins-job-configuration).
+
+NOTE: **Note:**
+Be sure to include the steps about [Build status configuration](https://github.com/jenkinsci/gitlab-plugin#build-status-configuration).
+The 'Publish build status to GitLab' post-build step is required to view
+Jenkins build status in GitLab Merge Requests.
+
+## Configure a GitLab project
+
+Create a new GitLab project or choose an existing one. Then, go to **Integrations ->
+Jenkins CI**.
+
+Check the 'Active' box. Select whether you want GitLab to trigger a build
+on push, Merge Request creation, tag push, or any combination of these. We
+recommend unchecking 'Merge Request events' unless you have a specific use-case
+that requires re-building a commit when a merge request is created. With 'Push
+events' selected, GitLab will build the latest commit on each push and the build
+status will be displayed in the merge request.
+
+Enter the Jenkins URL and Project name. The project name should be URL-friendly
+where spaces are replaced with underscores. To be safe, copy the project name
+from the URL bar of your browser while viewing the Jenkins project.
+
+Optionally, enter a username and password if your Jenkins server requires
+authentication.
+
+![GitLab service settings](img/jenkins_gitlab_service_settings.png)
+
+## Plugin functional overview
+
+GitLab does not contain a database table listing commits. Commits are always
+read from the repository directly. Therefore, it is not possible to retain the
+build status of a commit in GitLab. This is overcome by requesting build
+information from the integrated CI tool. The CI tool is responsible for creating
+and storing build status for Commits and Merge Requests.
+
+### Steps required to implement a similar integration
+
+>**Note:**
+All steps are implemented using AJAX requests on the merge request page.
+
+1. In order to display the build status in a merge request you must create a project service in GitLab.
+2. Your project service will do a (JSON) query to a URL of the CI tool with the SHA1 of the commit.
+3. The project service builds this URL and payload based on project service settings and knowledge of the CI tool.
+4. The response is parsed to give a response in GitLab (success/failed/pending).
+
+## Troubleshooting
+
+### Error in merge requests - "Could not connect to the CI server"
+
+This integration relies on Jenkins reporting the build status back to GitLab via
+the [Commit Status API](../api/commits.md#commit-status).
+
+The error 'Could not connect to the CI server' usually means that GitLab did not
+receive a build status update via the API. Either Jenkins was not properly
+configured or there was an error reporting the status via the API.
+
+1. [Configure the Jenkins server](#configure-the-jenkins-server) for GitLab API access
+2. [Configure a Jenkins project](#configure-a-jenkins-project), including the
+ 'Publish build status to GitLab' post-build action.
diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md
new file mode 100644
index 00000000000..8001c5dbd83
--- /dev/null
+++ b/doc/integration/jenkins_deprecated.md
@@ -0,0 +1,53 @@
+# Jenkins CI (deprecated) service
+
+>**Note:** In GitLab 8.3, Jenkins integration using the
+[GitLab Hook Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Hook+Plugin)
+was deprecated in favor of the
+[GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin).
+Please use documentation for the new [Jenkins CI service](jenkins.md).
+
+Integration includes:
+
+* Trigger Jenkins build after push to repo
+* Show build status on Merge Request page
+
+Requirements:
+
+* [Jenkins GitLab Hook plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Hook+Plugin)
+* git clone access for Jenkins from GitLab repo (via ssh key)
+
+## Jenkins
+
+1. Install [GitLab Hook plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Hook+Plugin)
+2. Set up jenkins project
+
+![screen](img/jenkins_project.png)
+
+## GitLab
+
+In GitLab, perform the following steps.
+
+### Read access to repository
+
+Jenkins needs read access to the GitLab repository. We already specified a
+private key to use in Jenkins, now we need to add a public one to the GitLab
+project. For that case we will need a Deploy key. Read the documentation on
+[how to set up a Deploy key](../ssh/README.md#deploy-keys).
+
+### Jenkins service
+
+Now navigate to GitLab services page and activate Jenkins
+
+![screen](img/jenkins_gitlab_service.png)
+
+Done! Now when you push to GitLab - it will create a build for Jenkins.
+And also you will be able to see merge request build status with a link to the Jenkins build.
+
+### Multi-project Configuration
+
+The GitLab Hook plugin in Jenkins supports the automatic creation of a project
+for each feature branch. After configuration GitLab will trigger feature branch
+builds and a corresponding project will be created in Jenkins.
+
+Configure the GitLab Hook plugin in Jenkins. Go to 'Manage Jenkins' and then
+'Configure System'. Find the 'GitLab Web Hook' section and configure as shown below.
diff --git a/doc/integration/jira_development_panel.md b/doc/integration/jira_development_panel.md
new file mode 100644
index 00000000000..703736eeb3c
--- /dev/null
+++ b/doc/integration/jira_development_panel.md
@@ -0,0 +1,141 @@
+# GitLab Jira development panel integration **[PREMIUM]**
+
+> [Introduced][ee-2381] in [GitLab Premium][eep] 10.0.
+
+Complementary to our [existing Jira][existing-jira] project integration, you're now able to integrate
+GitLab projects with [Jira Development Panel][jira-development-panel]. Both can be used
+simultaneously. This works with self-hosted GitLab or GitLab.com integrated with self-hosted Jira
+or cloud Jira.
+
+By doing this you can easily access related GitLab merge requests, branches, and commits directly from a Jira issue.
+
+This integration connects all GitLab projects within a top-level group or a personal namespace to projects in the Jira instance.
+A top-level GitLab group is one that does not have any parent group itself. All the projects of that top-level group,
+as well as projects of the top-level group's subgroups nesting down, are connected. Alternatively, you can specify
+a GitLab personal namespace in the Jira configuration, which will then connect the projects in that personal namespace to Jira.
+
+NOTE: **Note**:
+Note this is different from the [existing Jira][existing-jira] project integration, where the mapping
+is one GitLab project to the entire Jira instance.
+
+We recommend that a GitLab group admin
+or instance admin (in the case of self-hosted GitLab) set up the integration,
+in order to simplify administration.
+
+TIP: **Tip:**
+Create and use a single-purpose "jira" user in GitLab, so that removing
+regular users won't impact your integration.
+
+## Requirements
+
+### Self-hosted GitLab
+
+If you are using self-hosted GitLab, make sure your GitLab instance is accessible by Jira.
+
+- If you are connecting to Jira Cloud, make sure your instance is accessible via the internet.
+- If you are using Jira Server, make sure your instance is accessible however your network is set up.
+
+### GitLab.com
+
+There are no special requirements if you are using GitLab.com.
+
+## GitLab Configuration
+
+1. In GitLab, create a new application in order to allow Jira to connect with your GitLab account
+
+ While logged-in, go to `Settings -> Applications`. (Click your profile avatar at
+ the top right, choose `Settings`, and then navigate to `Applications` from the left
+ navigation menu.) Use the form to create a new application.
+
+ Enter a useful name for the `Name` field.
+
+ For the `Redirect URI` field, enter `https://<your-gitlab-instance-domain>/login/oauth/callback`,
+ replacing `<your-gitlab-instance-domain>` appropriately. So for example, if you are using GitLab.com,
+ this would be `https://gitlab.com/login/oauth/callback`.
+
+ NOTE: **Note**:
+ If using a GitLab version earlier than 11.3 the `Redirect URI` value should be `https://<your-gitlab-instance-domain>/-/jira/login/oauth/callback`.
+
+ ![GitLab Application setup](img/jira_dev_panel_gl_setup_1.png)
+ - Check `api` in the Scopes section.
+
+2. Click `Save application`. You will see the generated 'Application Id' and 'Secret' values.
+ Copy these values that you will use on the Jira configuration side.
+
+## Jira Configuration
+
+1. In Jira, from the gear menu at the top right, go to `Applications`. Navigate to `DVCS accounts`
+ from the left navigation menu. Click `Link GitHub account` to start creating a new integration.
+ (We are pretending to be GitHub in this integration until there is further platform support from Jira.)
+
+ ![Jira DVCS from Dashboard](img/jira_dev_panel_jira_setup_1.png)
+
+2. Complete the form
+
+ Select GitHub Enterprise for the `Host` field.
+
+ For the `Team or User Account` field, enter the relative path of a top-level GitLab group that you have access to,
+ or the relative path of your personal namespace.
+
+ ![Creation of Jira DVCS integration](img/jira_dev_panel_jira_setup_2.png)
+
+ For the `Host URL` field, enter `https://<your-gitlab-instance-domain>/`,
+ replacing `<your-gitlab-instance-domain>` appropriately. So for example, if you are using GitLab.com,
+ this would be `https://gitlab.com/`.
+
+ NOTE: **Note**:
+ If using a GitLab version earlier than 11.3 the `Host URL` value should be `https://<your-gitlab-instance-domain>/-/jira`
+
+ For the `Client ID` field, use the `Application ID` value from the previous section.
+
+ For the `Client Secret` field, use the `Secret` value from the previous section.
+
+ Ensure that the rest of the checkboxes are checked.
+
+3. Click `Add` to complete and create the integration.
+
+ Jira takes up to a few minutes to know about (import behind the scenes) all the commits and branches
+ for all the projects in the GitLab group you specified in the previous step. These are refreshed
+ every 60 minutes.
+
+ > **Note:**
+ > In the future, we plan on implementating real-time integration. If you need
+ > to refresh the data manually, you can do this from the `Applications -> DVCS
+ > accounts` screen where you initially set up the integration:
+ >
+ > ![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
+
+To connect additional GitLab projects from other GitLab top-level groups (or personal namespaces), repeat the above
+steps with additional Jira DVCS accounts.
+
+You may now refer any Jira issue by its ID in branch names, commit messages and merge request names on GitLab's side,
+and you will be able to see the linked `branches`, `commits`, and `merge requests` when entering a Jira issue
+(inside the Jira issue, merge requests will be called "pull requests").
+
+![Branch, Commit and Pull Requests links on Jira issue](img/jira_dev_panel_jira_setup_3.png)
+
+Click the links to see your GitLab repository data.
+
+![GitLab commits details on a Jira issue](img/jira_dev_panel_jira_setup_4.png)
+
+![GitLab merge requests details on a Jira issue](img/jira_dev_panel_jira_setup_5.png)
+
+## Limitations
+
+- This integration is currently not supported on GitLab instances under a [relative url][relative-url] (e.g. `http://example.com/gitlab`).
+
+## Changelog
+
+### 11.10
+
+- [Instance admins can now setup integration for all namespaces](https://gitlab.com/gitlab-org/gitlab-ee/issues/8902)
+
+### 11.1
+
+- [Support GitLab subgroups in Jira development panel](https://gitlab.com/gitlab-org/gitlab-ee/issues/3561)
+
+[existing-jira]: ../user/project/integrations/jira.md
+[jira-development-panel]: https://confluence.atlassian.com/adminjiraserver070/integrating-with-development-tools-776637096.html#Integratingwithdevelopmenttools-Developmentpanelonissues
+[eep]: https://about.gitlab.com/pricing/
+[ee-2381]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2381
+[relative-url]: https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab
diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md
new file mode 100644
index 00000000000..44117755b83
--- /dev/null
+++ b/doc/integration/kerberos.md
@@ -0,0 +1,315 @@
+# Kerberos integration **[STARTER ONLY]**
+
+GitLab can integrate with [Kerberos][kerb] as an authentication mechanism.
+
+## Overview
+
+[Kerberos][kerb] is a secure method for authenticating a request for a service in a
+computer network. Kerberos was developed in the Athena Project at the
+[Massachusetts Institute of Technology (MIT)][mit]. The name is taken from Greek
+mythology; Kerberos was a three-headed dog who guarded the gates of Hades.
+
+## Use-cases
+
+- GitLab can be configured to allow your users to sign with their Kerberos credentials.
+- You can use Kerberos to [prevent][why-kerb] anyone from intercepting or eavesdropping on the transmitted password.
+
+## Configuration
+
+For GitLab to offer Kerberos token-based authentication, perform the
+following prerequisites. You still need to configure your system for
+Kerberos usage, such as specifying realms. GitLab will make use of the
+system's Kerberos settings.
+
+### GitLab keytab
+
+1. Create a Kerberos Service Principal for the HTTP service on your GitLab server.
+ If your GitLab server is `gitlab.example.com` and your Kerberos realm
+ `EXAMPLE.COM`, create a Service Principal `HTTP/gitlab.example.com@EXAMPLE.COM`
+ in your Kerberos database.
+1. Create a keytab on the GitLab server for the above Service Principal, e.g.
+ `/etc/http.keytab`.
+
+The keytab is a sensitive file and must be readable by the GitLab user. Set
+ownership and protect the file appropriately:
+
+```
+sudo chown git /etc/http.keytab
+sudo chmod 0600 /etc/http.keytab
+```
+
+### Configure GitLab
+
+**Installations from source**
+
+>**Note:**
+For source installations, make sure the `kerberos` gem group
+[has been installed](../install/installation.md#install-gems).
+
+1. Edit the kerberos section of [gitlab.yml] to enable Kerberos ticket-based
+ authentication. In most cases, you only need to enable Kerberos and specify
+ the location of the keytab:
+
+ ```yaml
+ omniauth:
+ enabled: true
+ allow_single_sign_on: ['kerberos']
+
+ kerberos:
+ # Allow the HTTP Negotiate authentication method for Git clients
+ enabled: true
+
+ # Kerberos 5 keytab file. The keytab file must be readable by the GitLab user,
+ # and should be different from other keytabs in the system.
+ # (default: use default keytab from Krb5 config)
+ keytab: /etc/http.keytab
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+
+---
+
+**Omnibus package installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['omniauth_enabled'] = true
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['kerberos']
+
+ gitlab_rails['kerberos_enabled'] = true
+ gitlab_rails['kerberos_keytab'] = "/etc/http.keytab"
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
+
+GitLab will now offer the `negotiate` authentication method for signing in and
+HTTP Git access, enabling Git clients that support this authentication protocol
+to authenticate with Kerberos tokens.
+
+## Creating and linking Kerberos accounts
+
+The Administrative user can navigate to **Admin > Users > Example User > Identities**
+and attach a Kerberos account. Existing GitLab users can go to **Profile > Account**
+and attach a Kerberos account. If you want to allow users without a GitLab
+account to login, you should enable the option `allow_single_sign_on` as
+described in the [Configure GitLab](#configure-gitlab) section. Then, the first
+time a user signs in with Kerberos credentials, GitLab will create a new GitLab
+user associated with the email, which is built from the Kerberos username and
+realm. User accounts will be created automatically when authentication was
+successful.
+
+## Linking Kerberos and LDAP accounts together
+
+If your users log in with Kerberos, but you also have [LDAP integration](../administration/auth/ldap.md)
+enabled, then your users will be automatically linked to their LDAP accounts on
+first login. For this to work, some prerequisites must be met:
+
+The Kerberos username must match the LDAP user's UID. You can choose which LDAP
+attribute is used as the UID in GitLab's [LDAP configuration](../administration/auth/ldap.md#configuration)
+but for Active Directory, this should be `sAMAccountName`.
+
+The Kerberos realm must match the domain part of the LDAP user's Distinguished
+Name. For instance, if the Kerberos realm is `AD.EXAMPLE.COM`, then the LDAP
+user's Distinguished Name should end in `dc=ad,dc=example,dc=com`.
+
+Taken together, these rules mean that linking will only work if your users'
+Kerberos usernames are of the form `foo@AD.EXAMPLE.COM` and their
+LDAP Distinguished Names look like `sAMAccountName=foo,dc=ad,dc=example,dc=com`.
+
+## HTTP Git access
+
+A linked Kerberos account enables you to `git pull` and `git push` using your
+Kerberos account, as well as your standard GitLab credentials.
+
+GitLab users with a linked Kerberos account can also `git pull` and `git push`
+using Kerberos tokens, i.e., without having to send their password with each
+operation.
+
+### HTTP Git access with Kerberos token (passwordless authentication)
+
+#### Support for Git before 2.4
+
+Until Git version 2.4, the `git` command uses only the `negotiate` authentication
+method if the HTTP server offers it, even if this method fails (such as when
+the client does not have a Kerberos token). It is thus not possible to fall back
+to username/password (also known as `basic`) authentication if Kerberos
+authentication fails.
+
+For GitLab users to be able to use either `basic` or `negotiate` authentication
+with older Git versions, it is possible to offer Kerberos ticket-based
+authentication on a different port (e.g. 8443) while the standard port will
+keep offering only `basic` authentication.
+
+**For source installations with HTTPS**
+
+1. Edit the NGINX configuration file for GitLab
+ (e.g., `/etc/nginx/sites-available/gitlab-ssl`) and configure NGINX to
+ listen to port `8443` in addition to the standard HTTPS port:
+
+ ```conf
+ server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl default_server;
+ listen 0.0.0.0:8443 ssl;
+ listen [::]:8443 ipv6only=on ssl;
+ ```
+
+1. Update the Kerberos section of [gitlab.yml]:
+
+ ```yaml
+ kerberos:
+ # Dedicated port: Git before 2.4 does not fall back to Basic authentication if Negotiate fails.
+ # To support both Basic and Negotiate methods with older versions of Git, configure
+ # nginx to proxy GitLab on an extra port (e.g. 8443) and uncomment the following lines
+ # to dedicate this port to Kerberos authentication. (default: false)
+ use_dedicated_port: true
+ port: 8443
+ https: true
+ ```
+
+1. [Restart GitLab] and NGINX for the changes to take effect.
+
+---
+
+**For Omnibus package installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['kerberos_use_dedicated_port'] = true
+ gitlab_rails['kerberos_port'] = 8443
+ gitlab_rails['kerberos_https'] = true
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+---
+
+After this change, all Git remote URLs will have to be updated to
+`https://gitlab.example.com:8443/mygroup/myproject.git` in order to use
+Kerberos ticket-based authentication.
+
+## Upgrading from password-based to ticket-based Kerberos sign-ins
+
+Prior to GitLab 8.10 Enterprise Edition, users had to submit their
+Kerberos username and password to GitLab when signing in. We will
+remove support for password-based Kerberos sign-ins in a future
+release, so we recommend that you upgrade to ticket-based sign-ins.
+
+Depending on your existing GitLab configuration, the 'Sign in with:
+Kerberos Spnego' button may already be visible on your GitLab sign-in
+page. If not, then add the settings [described above](#configuration).
+
+Once you have verified that the 'Kerberos Spnego' button works
+without entering any passwords, you can proceed to disable
+password-based Kerberos sign-ins. To do this you need only need to
+remove the OmniAuth provider named `kerberos` from your `gitlab.yml` /
+`gitlab.rb` file.
+
+**For installations from source**
+
+1. Edit [gitlab.yml] and remove the `- { name: 'kerberos' }` line under omniauth
+ providers:
+
+ ```yaml
+ omniauth:
+ # ...
+ providers:
+ - { name: 'kerberos' } # <-- remove this line
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+
+---
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and remove the `{ "name" => "kerberos" }` line
+ under `gitlab_rails['omniauth_providers']`:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ { "name" => "kerberos" } # <-- remove this entry
+ ]
+ ```
+
+1. [Reconfigure GitLab] for the changes to take effect.
+
+## Support for Active Directory Kerberos environments
+
+When using Kerberos ticket-based authentication in an Active Directory domain,
+it may be necessary to increase the maximum header size allowed by NGINX,
+as extensions to the Kerberos protocol may result in HTTP authentication headers
+larger than the default size of 8kB. Configure `large_client_header_buffers`
+to a larger value in [the NGINX configuration][nginx].
+
+## Troubleshooting
+
+### Unsupported GSSAPI mechanism
+
+With Kerberos SPNEGO authentication, the browser is expected to send a list of
+mechanisms it supports to GitLab. If it doesn't support any of the mechanisms
+GitLab supports, authentication will fail with a message like this in the log:
+
+```
+OmniauthKerberosSpnegoController: failed to process Negotiate/Kerberos authentication: gss_accept_sec_context did not return GSS_S_COMPLETE: An unsupported mechanism was requested Unknown error
+```
+
+This is usually seen when the browser is unable to contact the Kerberos server
+directly. It will fall back to an unsupported mechanism known as
+[`IAKERB`](https://k5wiki.kerberos.org/wiki/Projects/IAKERB), which tries to use
+the GitLab server as an intermediary to the Kerberos server.
+
+If you're experiencing this error, ensure there is connectivity between the
+client machine and the Kerberos server - this is a prerequisite! Traffic may be
+blocked by a firewall, or the DNS records may be incorrect.
+
+Another failure mode occurs when the forward and reverse DNS records for the
+GitLab server do not match. Often, Windows clients will work in this case, while
+Linux clients will fail. They use reverse DNS while detecting the Kerberos
+realm. If they get the wrong realm, then ordinary Kerberos mechanisms will fail,
+so the client will fall back to attempting to negotiate `IAKERB`, leading to the
+above error message.
+
+To fix this, ensure that the forward and reverse DNS for your GitLab server
+match. So for instance, if you acces GitLab as `gitlab.example.com`, resolving
+to IP address `1.2.3.4`, then `4.3.2.1.in-addr.arpa` must be a PTR record for
+`gitlab.example.com`.
+
+Finally, it's possible that the browser or client machine lack Kerberos support
+completely. Ensure that the Kerberos libraries are installed and that you can
+authenticate to other Kerberos services.
+
+### HTTP Basic: Access denied when cloning
+
+```sh
+remote: HTTP Basic: Access denied
+fatal: Authentication failed for '<KRB5 path>'
+```
+
+If you are using Git v2.11 or newer and see the above error when cloning, you can
+set the `http.emptyAuth` Git option to `true` to fix this:
+
+```
+git config --global http.emptyAuth true
+```
+
+See also: [Git v2.11 release notes](https://github.com/git/git/blob/master/Documentation/RelNotes/2.11.0.txt#L482-L486)
+
+## Helpful links
+
+- <https://help.ubuntu.com/community/Kerberos>
+- <http://blog.manula.org/2012/04/setting-up-kerberos-server-with-debian.html>
+- <http://www.roguelynn.com/words/explain-like-im-5-kerberos/>
+
+[gitlab.yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source
+[reconfigure gitlab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[nginx]: http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers
+[kerb]: https://web.mit.edu/kerberos/
+[mit]: http://web.mit.edu/
+[why-kerb]: http://web.mit.edu/sipb/doc/working/guide/guide/node20.html
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 2932c884d04..a13e9f73f48 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -33,6 +33,9 @@ contains some settings that are common for all providers.
- [Authentiq](../administration/auth/authentiq.md)
- [OAuth2Generic](oauth2_generic.md)
- [JWT](../administration/auth/jwt.md)
+- [OpenID Connect](../administration/auth/oidc.md)
+- [UltraAuth](ultra_auth.md)
+- [SalesForce](salesforce.md)
## Initial OmniAuth Configuration
diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md
new file mode 100644
index 00000000000..18d42486fd6
--- /dev/null
+++ b/doc/integration/salesforce.md
@@ -0,0 +1,79 @@
+# SalesForce OmniAuth Provider
+
+You can integrate your GitLab instance with [SalesForce](https://www.salesforce.com/) to enable users to login to your GitLab instance with their SalesForce account.
+
+## Create SalesForce Application
+
+To enable SalesForce OmniAuth provider, you must use SalesForce's credentials for your GitLab instance.
+To get the credentials (a pair of Client ID and Client Secret), you must register an application on UltraAuth.
+
+1. Sign in to [SalesForce](https://www.salesforce.com/).
+
+1. Navigate to **Platform Tools/Apps** and click on **New Connected App**.
+
+1. Fill in the application details into the following fields:
+ - **Connected App Name** and **API Name**: Set to any value but consider something like `<Organization>'s GitLab`, `<Your Name>'s GitLab`, or something else that is descriptive.
+ - **Description**: Description for the application.
+
+ ![SalesForce App Details](img/salesforce_app_details.png)
+1. Select **API (Enable OAuth Settings)** and click on **Enable OAuth Settings**.
+1. Fill in the application details into the following fields:
+ - **Callback URL**: The call callback URL. For example, `https://gitlab.example.com/users/auth/salesforce/callback`.
+ - **Selected OAuth Scopes**: Move **Access your basic information (id, profile, email, address, phone)** and **Allow access to your unique identifier (openid)** to the right column.
+
+ ![SalesForce Oauth App Details](img/salesforce_oauth_app_details.png)
+1. Click **Save**.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "salesforce",
+ "app_id" => "SALESFORCE_CLIENT_ID",
+ "app_secret" => "SALESFORCE_CLIENT_SECRET"
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```
+ - { name: 'salesforce',
+ app_id: 'SALESFORCE_CLIENT_ID',
+ app_secret: 'SALESFORCE_CLIENT_SECRET'
+ }
+ ```
+1. Change `SALESFORCE_CLIENT_ID` to the Consumer Key from the SalesForce connected application page.
+1. Change `SALESFORCE_CLIENT_SECRET` to the Client Secret from the SalesForce connected application page.
+ ![SalesForce App Secret Details](img/salesforce_app_secret_details.png)
+
+1. Save the configuration file.
+1. [Reconfigure GitLab]( ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure ) or [restart GitLab]( ../administration/restart_gitlab.md#installations-from-source ) for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page, there should now be a SalesForce icon below the regular sign in form.
+Click the icon to begin the authentication process. SalesForce will ask the user to sign in and authorize the GitLab application.
+If everything goes well, the user will be returned to GitLab and will be signed in.
+
+NOTE: **Note:**
+GitLab requires the email address of each new user. Once the user is logged in using SalesForce, GitLab will redirect the user to the profile page where they will have to provide the email and verify the email.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 8ee07a7fcdc..15d9d8c9c74 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -1,5 +1,7 @@
# SAML OmniAuth Provider
+> This topic is for SAML on self-managed GitLab instances. For SAML on GitLab.com, see [SAML SSO for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html).
+
NOTE: **Note:**
You need to [enable OmniAuth](omniauth.md) in order to use this.
@@ -10,115 +12,115 @@ Microsoft ADFS to authenticate users.
First configure SAML 2.0 support in GitLab, then register the GitLab application
in your SAML IdP:
-1. Make sure GitLab is configured with HTTPS.
- See [Using HTTPS](../install/installation.md#using-https) for instructions.
-
-1. On your GitLab server, open the configuration file.
+1. Make sure GitLab is configured with HTTPS.
+ See [Using HTTPS](../install/installation.md#using-https) for instructions.
+
+1. On your GitLab server, open the configuration file.
- For omnibus package:
+ For omnibus package:
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
- ```
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
- For installations from source:
+ For installations from source:
- ```sh
- cd /home/git/gitlab
+ ```sh
+ cd /home/git/gitlab
- sudo -u git -H editor config/gitlab.yml
- ```
+ sudo -u git -H editor config/gitlab.yml
+ ```
-1. To allow your users to use SAML to sign up without having to manually create
- an account first, don't forget to add the following values to your configuration:
+1. To allow your users to use SAML to sign up without having to manually create
+ an account first, don't forget to add the following values to your configuration:
- For omnibus package:
+ For omnibus package:
- ```ruby
- gitlab_rails['omniauth_enabled'] = true
- gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
- gitlab_rails['omniauth_block_auto_created_users'] = false
- ```
+ ```ruby
+ gitlab_rails['omniauth_enabled'] = true
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
+ ```
- For installations from source:
+ For installations from source:
- ```yaml
- omniauth:
- enabled: true
- allow_single_sign_on: ["saml"]
- block_auto_created_users: false
- ```
+ ```yaml
+ omniauth:
+ enabled: true
+ allow_single_sign_on: ["saml"]
+ block_auto_created_users: false
+ ```
-1. You can also automatically link SAML users with existing GitLab users if their
- email addresses match by adding the following setting:
+1. You can also automatically link SAML users with existing GitLab users if their
+ email addresses match by adding the following setting:
- For omnibus package:
+ For omnibus package:
- ```ruby
- gitlab_rails['omniauth_auto_link_saml_user'] = true
- ```
+ ```ruby
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
+ ```
- For installations from source:
+ For installations from source:
- ```yaml
- auto_link_saml_user: true
- ```
+ ```yaml
+ auto_link_saml_user: true
+ ```
-1. Add the provider configuration:
+1. Add the provider configuration:
- For omnibus package:
+ For omnibus package:
- ```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ]
- ```
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
- For installations from source:
-
- ```yaml
- omniauth:
- providers:
- - {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ```
+ For installations from source:
-1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
- of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
- installation to generate the correct value).
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
-1. Change the values of `idp_cert_fingerprint`, `idp_sso_target_url`,
- `name_identifier_format` to match your IdP. If a fingerprint is used it must
- be a SHA1 fingerprint; check
- [the omniauth-saml documentation](https://github.com/omniauth/omniauth-saml)
- for more details on these options.
+1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+ of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+ installation to generate the correct value).
-1. Change the value of `issuer` to a unique name, which will identify the application
- to the IdP.
+1. Change the values of `idp_cert_fingerprint`, `idp_sso_target_url`,
+ `name_identifier_format` to match your IdP. If a fingerprint is used it must
+ be a SHA1 fingerprint; check
+ [the omniauth-saml documentation](https://github.com/omniauth/omniauth-saml)
+ for more details on these options.
-1. For the changes to take effect, you must [reconfigure][] GitLab if you installed via Omnibus or [restart GitLab][] if you installed from source.
+1. Change the value of `issuer` to a unique name, which will identify the application
+ to the IdP.
-1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
- in `issuer`.
+1. For the changes to take effect, you must [reconfigure][] GitLab if you installed via Omnibus or [restart GitLab][] if you installed from source.
+
+1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
+ in `issuer`.
To ease configuration, most IdP accept a metadata URL for the application to provide
configuration information to the IdP. To build the metadata URL for GitLab, append
@@ -185,6 +187,78 @@ tell GitLab which groups are external via the `external_groups:` element:
} }
```
+## Required groups
+
+>**Note:**
+This setting is only available on GitLab 10.2 EE and above.
+
+This setting works like `External Groups` setting. Just like there, your IdP has to
+pass Group Information to GitLab, you have to tell GitLab where to look or the
+groups SAML response, and which group membership should be requisite for logging in.
+When `required_groups` is not set or it is empty, anyone with proper authentication
+will be able to use the service.
+
+Example:
+
+```yaml
+{ name: 'saml',
+ label: 'Our SAML Provider',
+ groups_attribute: 'Groups',
+ required_groups: ['Developers', 'Managers', 'Admins'],
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ } }
+```
+
+## Admin Groups
+
+>**Note:**
+This setting is only available on GitLab 8.8 EE and above.
+
+This setting works very similarly to the `External Groups` setting. The requirements
+are the same, your IdP needs to pass Group information to GitLab, you need to tell
+GitLab where to look for the groups in the SAML response, and which group should be
+considered `admin groups`.
+
+```yaml
+{ name: 'saml',
+ label: 'Our SAML Provider',
+ groups_attribute: 'Groups',
+ admin_groups: ['Managers', 'Admins'],
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ } }
+```
+
+## Auditor Groups
+
+>**Note:**
+This setting is only available on GitLab 11.4 EE and above.
+
+This setting also follows the requirements documented for the `External Groups` setting. GitLab uses the Group information provided by your IdP to determine if a user should be assigned the `auditor` role.
+
+```yaml
+{ name: 'saml',
+ label: 'Our SAML Provider',
+ groups_attribute: 'Groups',
+ auditor_groups: ['Auditors', 'Security'],
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ } }
+```
+
## Bypass two factor authentication
If you want some SAML authentication methods to count as 2FA on a per session basis, you can register them in the
@@ -194,28 +268,28 @@ If you want some SAML authentication methods to count as 2FA on a per session ba
1. Edit `/etc/gitlab/gitlab.rb`:
- ```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
- upstream_two_factor_authn_contexts:
- %w(
- urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
- )
-
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ]
- ```
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ %w(
+ urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
+ )
+
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
1. Save the file and [reconfigure][] GitLab for the changes to take effect.
@@ -225,27 +299,27 @@ If you want some SAML authentication methods to count as 2FA on a per session ba
1. Edit `config/gitlab.yml`:
- ```yaml
- omniauth:
- providers:
- - {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
- upstream_two_factor_authn_contexts:
- [
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
- ]
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ```
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ [
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
+ ]
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ```
1. Save the file and [restart GitLab][] for the changes ot take effect
diff --git a/doc/integration/ultra_auth.md b/doc/integration/ultra_auth.md
new file mode 100644
index 00000000000..139cca456aa
--- /dev/null
+++ b/doc/integration/ultra_auth.md
@@ -0,0 +1,78 @@
+# UltraAuth OmniAuth Provider
+
+You can integrate your GitLab instance with [UltraAuth](https://ultraauth.com) to enable users to perform secure biometric authentication to your GitLab instance with your UltraAuth account. Users have to perform the biometric authentication using their mobile device with fingerprint sensor.
+
+## Create UltraAuth Application
+
+To enable UltraAuth OmniAuth provider, you must use UltraAuth's credentials for your GitLab instance.
+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. Scroll down the page that is displayed to locate the **Client ID** and **Client Secret**.
+ Keep this page open as you continue configuration.
+ ![UltraAuth Credentials: OPENID_CLIENT_ID and OPENID_CLIENT_SECRET](img/ultra_auth_credentials.png)
+1. Click on "Edit Callback URL" link.
+ ![Edit UltraAuth Callback URL](img/ultra_auth_edit_callback_url_highlighted.png)
+1. The callback URL will be `http(s)://<your_domain>/users/auth/ultraauth/callback`
+ ![UltraAuth Callback URL](img/ultra_auth_edit_callback_url.png)
+1. Select **Register application**.
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "ultraauth",
+ "app_id" => "OPENID_CLIENT_ID",
+ "app_secret" => "OPENID_CLIENT_SECRET",
+ "args" => {
+ "client_options" => {
+ "redirect_uri" => "https://example.com/users/auth/ultraauth/callback"
+ }
+ }
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```
+ - { name: 'ultraauth',
+ app_id: 'OPENID_CLIENT_ID',
+ app_secret: 'OPENID_CLIENT_SECRET',
+ args: {
+ client_options: {
+ redirect_uri: 'https://example.com/users/auth/ultraauth/callback'
+ }
+ }
+ }
+ ```
+ __Replace `https://example.com/users/auth/ultraauth/callback` with your application's Callback URL.__
+1. Change `OPENID_CLIENT_ID` to the Client ID from the UltraAuth application page.
+1. Change `OPENID_CLIENT_SECRET` to the Client Secret from the UltraAuth application page.
+1. Save the configuration file.
+1. [Reconfigure GitLab]( ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure ) or [restart GitLab]( ../administration/restart_gitlab.md#installations-from-source ) for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page, there should now be a UltraAuth icon below the regular sign in form.
+Click the icon to begin the authentication process. UltraAuth will ask the user to sign in and authorize the GitLab application.
+If everything goes well, the user will be returned to GitLab and will be signed in.
+
+**Note:** GitLab requires the email address of each new user. Once the user is logged in using UltraAuth, GitLab will redirect the user to the profile page where they will have to provide the email and verify the email.
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index e5fc7a3c85f..7f08188bd65 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -1,3 +1,29 @@
----
-redirect_to: 'README.md'
----
+# Corporate contributor license agreement
+
+You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
+
+1. Definitions.
+
+ "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
+
+4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
+
+5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
+
+6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+
+7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
+
+8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
+
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index e5fc7a3c85f..59803aea080 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -1,3 +1,25 @@
----
-redirect_to: 'README.md'
----
+# Individual contributor license agreement
+
+You accept and agree to the following terms and conditions for Your present and future Contributions submitted to GitLab B.V.. Except for the license granted herein to GitLab B.V. and recipients of software distributed by GitLab B.V., You reserve all right, title, and interest in and to Your Contributions.
+
+1. Definitions.
+
+ "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
+
+2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
+
+3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
+
+4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to GitLab B.V., or that your employer has executed a separate Corporate CLA with GitLab B.V..
+
+5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
+
+6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+
+7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [insert_name_here]".
+
+8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
+
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/license/README.md b/doc/license/README.md
new file mode 100644
index 00000000000..4cc387ba95f
--- /dev/null
+++ b/doc/license/README.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/admin_area/license.html'
+---
+
+This document was moved to [user/admin_area/license](https://docs.gitlab.com/ee/user/admin_area/license.html).
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
new file mode 100644
index 00000000000..7654023f266
--- /dev/null
+++ b/doc/push_rules/push_rules.md
@@ -0,0 +1,156 @@
+# Push Rules **[STARTER]**
+
+Gain additional control over pushes to your repository.
+
+## Overview
+
+GitLab already offers [protected branches][protected-branches], but there are
+cases when you need some specific rules like preventing git tag removal or
+enforcing a special format for commit messages.
+
+Push rules are essentially [pre-receive Git hooks][hooks] that are easy to
+enable in a user-friendly interface. They are defined globally if you are an
+admin or per project so you can have different rules applied to different
+projects depending on your needs.
+
+## Use cases
+
+Every push rule could have its own use case, but let's consider some examples.
+
+### Commit messages with a specific reference
+
+Let's assume you have the following requirements for your workflow:
+
+- every commit should reference a JIRA issue, for example: `Refactored css. Fixes JIRA-123.`
+- users should not be able to remove git tags with `git push`
+
+All you need to do is write a simple regular expression that requires the mention
+of a JIRA issue in the commit message, like `JIRA\-\d+`.
+
+Now when a user tries to push a commit with a message `Bugfix`, their push will
+be declined. Only pushing commits with messages like `Bugfix according to JIRA-123`
+will be accepted.
+
+### Restrict branch names
+
+Let's assume there's a strictly policy for branch names in your company, and
+you want the branches to start with a certain name because you have different
+GitLab CI jobs (`feature`, `hotfix`, `docker`, `android`, etc.) that rely on the
+branch name.
+
+Your developers however, don't always remember that policy, so they push
+various branches and CI pipelines do not work as expected. By restricting the
+branch names globally in Push Rules, you can now sleep without the anxiety
+of your developers' mistakes. Every branch that doesn't match your push rule
+will get rejected.
+
+## Enabling push rules
+
+>**Note:**
+GitLab administrators can set push rules globally under
+**Admin area > Push Rules** that all new projects will inherit. You can later
+override them in a project's settings.
+
+1. Navigate to your project's **Settings > Repository** and expand **Push Rules**
+1. Set the rule you want
+1. Click **Save Push Rules** for the changes to take effect
+
+The following options are available.
+
+| Push rule | GitLab version | Description |
+| --------- | :------------: | ----------- |
+| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove git tags with `git push`. Tags will still be able to be deleted through the web UI. |
+| Check whether author is a GitLab user | **Starter** 7.10 | Restrict commits by author (email) to existing GitLab users. |
+| Check whether committer is the current authenticated user | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
+| Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. |
+| Prevent committing secrets to Git | **Starter** 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
+| Restrict by commit message | **Starter** 7.10 | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
+| Restrict by commit message (negative match)| **Starter** 11.1 | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
+| Restrict by branch name | **Starter** 9.3 | Only branch names that match this regular expression are allowed to be pushed. Leave empty to allow any branch name. |
+| Restrict by commit author's email | **Starter** 7.10 | Only commit author's email that match this regular expression are allowed to be pushed. Leave empty to allow any email. |
+| Prohibited file names | **Starter** 7.10 | Any committed filenames that match this regular expression are not allowed to be pushed. Leave empty to allow any filenames. |
+| Maximum file size | **Starter** 7.12 | Pushes that contain added or updated files that exceed this file size (in MB) are rejected. Set to 0 to allow files of any size. |
+
+>**Tip:**
+GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular expressions in push rules. You can check your regular expressions at <https://regex-golang.appspot.com>.
+
+## Prevent pushing secrets to the repository
+
+> [Introduced][ee-385] in [GitLab Starter][ee] 8.12.
+
+You can turn on a predefined blacklist of files which won't be allowed to be
+pushed to a repository.
+
+By selecting the checkbox *Prevent committing secrets to Git*, GitLab prevents
+pushes to the repository when a file matches a regular expression as read from
+[`files_blacklist.yml`][list] (make sure you are at the right branch
+as your GitLab version when viewing this file).
+
+NOTE: **Note**:
+Files already committed won't get restricted by this push rule.
+
+Below is an example list of what will be rejected by these regular expressions:
+
+```shell
+#####################
+# AWS CLI credential blobs
+#####################
+.aws/credentials
+aws/credentials
+homefolder/aws/credentials
+
+#####################
+# Private RSA SSH keys
+#####################
+/ssh/id_rsa
+/.ssh/personal_rsa
+/config/server_rsa
+id_rsa
+.id_rsa
+
+#####################
+# Private DSA SSH keys
+#####################
+/ssh/id_dsa
+/.ssh/personal_dsa
+/config/server_dsa
+id_dsa
+.id_dsa
+
+#####################
+# Private ed25519 SSH keys
+#####################
+/ssh/id_ed25519
+/.ssh/personal_ed25519
+/config/server_ed25519
+id_ed25519
+.id_ed25519
+
+#####################
+# Private ECDSA SSH keys
+#####################
+/ssh/id_ecdsa
+/.ssh/personal_ecdsa
+/config/server_ecdsa
+id_ecdsa
+.id_ecdsa
+
+#####################
+# Any file with .pem or .key extensions
+#####################
+*.pem
+*.key
+
+#####################
+# Any file ending with _history or .history extension
+#####################
+pry.history
+bash_history
+```
+
+[protected-branches]: ../user/project/protected_branches.md
+[signing-commits]: ../user/project/repository/gpg_signed_commits/index.md
+[ee-385]: https://gitlab.com/gitlab-org/gitlab-ee/issues/385
+[list]: https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/lib/gitlab/checks/files_blacklist.yml
+[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index e70a009323e..f5c788af578 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -23,6 +23,16 @@ sudo gitlab-rake gitlab:cleanup:repos
bundle exec rake gitlab:cleanup:repos RAILS_ENV=production
```
+Remove old repository copies from repositories moved to another storage.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:cleanup:moved
+
+# installation from source
+bundle exec rake gitlab:cleanup:moved RAILS_ENV=production
+```
+
Clean up local project upload files if they don't exist in GitLab database. The
task attempts to fix the file if it can find its project, otherwise it moves the
file to a lost and found directory.
diff --git a/doc/subscriptions/billing_table.png b/doc/subscriptions/billing_table.png
new file mode 100644
index 00000000000..acd1b6193ec
--- /dev/null
+++ b/doc/subscriptions/billing_table.png
Binary files differ
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
new file mode 100644
index 00000000000..b6d16536fee
--- /dev/null
+++ b/doc/subscriptions/index.md
@@ -0,0 +1,103 @@
+# Subscription setup and management
+
+This page will help get you started with your new subscription or manage an existing one, whether you have subscribed to GitLab.com or self-managed GitLab.
+
+To subscribe, upgrade, or read more about the types of subscriptions, please see [Subscribe to GitLab](../README.md#subscribe-to-gitlab) on the GitLab Documentation landing page.
+
+## Set up GitLab
+
+Learn how GitLab helps you in the stages of the DevOps lifecycle by learning more [about the GitLab product](https://about.gitlab.com/product/), [GitLab features](https://about.gitlab.com/features/), and [GitLab Documentation](../README.md).
+
+### Self-managed: Install GitLab
+
+Take a look at [installing GitLab](https://about.gitlab.com/install/) and our [administrator documentation](../administration/index.md). Then, follow the instructions below under [Your subscription](#your-subscription) to apply your license file.
+
+### GitLab.com: Create a user and group
+
+Start with creating a user account for yourself using our [sign up page](https://gitlab.com/users/sign_in#register-pane).
+
+[GitLab groups](../user/group/index.md) help assemble related projects together allowing you to grant members access to several projects at once. A group is not required if you plan on having [projects](../user/project/) inside a personal namespace.
+
+## Your subscription
+
+You can view and manage subscriptions through our [Customers portal](https://customers.gitlab.com/). Information on applying your subscription is below.
+
+Please also see our [subscription FAQ](https://about.gitlab.com/pricing/licensing-faq/)
+
+### View subscription and seats
+
+To view and manage the subscriptions you have purchased and the number of seats associated with the subscription, please visit and log into the [Customers’ Portal](https://customers.gitlab.com/subscriptions). For more information, please see our [subscription FAQ](https://about.gitlab.com/pricing/licensing-faq/) and [pricing page](https://about.gitlab.com/pricing/), which includes information on our [true-up pricing policy](https://about.gitlab.com/handbook/product/pricing/#true-up-pricing) when adding more users than at the time of purchase.
+
+Please note that this account may have the same email, but is a _separate_ login from your GitLab.com account. If the two accounts are linked together, then you can use the "sign in with GitLab.com account" link underneath the `Sign In` button.
+
+### Change billing information
+
+In the customers portal, go to the `My Account` page, then revise the `Account Details` information and click on the `Update Account` button.
+
+Future purchases will use the information in this section. The email listed in this section is used for the Customers Portal login and for license related email communication.
+
+### Self-managed: Apply your license file
+
+After purchase, the license file is sent to the email address tied to the Customers portal account, which needs to be [uploaded to the GitLab instance](https://docs.gitlab.com/ee/user/admin_area/license.html#uploading-your-license).
+
+### Link your GitLab.com account with your Customers Portal account
+
+NOTE: **Note:** This is *required* for GitLab.com subscriptions.
+
+Once signed into the customers portal, if your account is not already linked, you should be prompted to link your account with a "Link my GitLab Account" button.
+
+You can also go to the [My Account](https://customers.gitlab.com/customers/edit) page to add or change the GitLab.com account link.
+
+### Change the linked GitLab.com account for your Customers Portal account
+
+To change which GitLab.com account is associated with a Customers Portal account, please follow these steps:
+
+1. Log into the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. In a separate browser tab, visit [GitLab.com](https://gitlab.com) to ensure you are not logged in, or if you are, log out.
+1. Back on the Customers Portal page, click [My Account](https://customers.gitlab.com/customers/edit) in the top menu.
+1. Under `Your GitLab.com account`, click the `Change linked account` button.
+1. Have the user you want associated log in to their [GitLab.com](https://gitlab.com) account.
+
+### GitLab.com: Associate your namespace with your subscription
+
+Once your GitLab.com account is linked, you can go to your [Subscriptions](https://customers.gitlab.com/subscriptions) page to choose or change the namespace your subscription applies to.
+
+Please note that you need to be a group owner to associate a group to your subscription.
+
+### Confirm or upgrade your GitLab.com subscription details within GitLab
+
+To see the status of your GitLab.com subscription, you can click on the Billings
+section of the relevant namespace:
+
+* For individuals, this is located at https://gitlab.com/profile/billings under
+in your Settings,
+* For groups, this is located under the group's Settings dropdown, under Billing.
+
+For groups, you can see details of your subscription - including your current
+plan - in the included table:
+
+![Billing table](billing_table.png)
+
+| Field | Description |
+| ------ | ------ |
+| Seats in subscription | If this is a paid plan, this represents the number of seats you've paid to support in your group. |
+| Seats currently in use | The number of active seats currently in use. |
+| Max seats used | The highest number of seats you've used. If this exceeds the seats in subscription, you may owe an additional fee for the additional users. |
+| Seats owed | If your max seats used exceeds the seats in your subscription, you'll owe an additional fee for the users you've added. |
+| Subscription start date | The date your subscription started. If this is for a Free plan, this is the date you transitioned off your group's paid plan. |
+| Subscription end date | The date your current subscription will end. This does not apply to Free plans. |
+
+## Need help?
+
+[GitLab's Documentation](https://docs.gitlab.com/) offers a wide range of topics covering the use and administration of GitLab.
+
+We also encourage all users to search our project trackers for known issues and existing feature requests in:
+
+- [GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/issues/) for features included in all tiers, and
+- [GitLab EE](https://gitlab.com/gitlab-org/gitlab-ee/issues/) for paid-tier features.
+
+These issues are the best avenue for getting updates on specific product plans and for communicating directly with the relevant GitLab team members.
+
+### Contacting Support
+
+Learn more about the tiers of [GitLab Support](https://about.gitlab.com/support/) or [submit a request via the Support Portal](https://support.gitlab.com/hc/en-us/requests/new).
diff --git a/doc/tools/email.md b/doc/tools/email.md
new file mode 100644
index 00000000000..ab39206ffa4
--- /dev/null
+++ b/doc/tools/email.md
@@ -0,0 +1,38 @@
+# Email from GitLab **[STARTER ONLY]**
+
+As a GitLab administrator you can email GitLab users from within GitLab.
+
+## Overview
+
+GitLab provides a simple tool to email all users or users of a chosen group or
+project right from the admin area. Users will receive the email to their primary
+email address.
+
+## Use-cases
+
+- Notify your users about a new project, a new feature, or a new product launch.
+- Notify your users about a new deployment, or that will be downtime expected
+ for a particular reason.
+
+## Sending emails to users from within GitLab
+
+1. Go to the admin area using the wrench icon in the top right corner and
+ navigate to **Overview > Users > Send email to users**.
+
+ ![admin users](email1.png)
+
+1. Compose an email and choose where it will be sent (all users or users of a
+ chosen group or project):
+
+ ![compose an email](email2.png)
+
+## Unsubscribing from emails
+
+User can choose to unsubscribe from receiving emails from GitLab by following
+the unsubscribe link from the email. Unsubscribing is unauthenticated in order
+to keep the simplicity of this feature.
+
+On unsubscribe, user will receive an email notifying that unsubscribe happened.
+The endpoint that provides the unsubscribe option is rate-limited.
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/tools/email1.png b/doc/tools/email1.png
new file mode 100644
index 00000000000..e79ccc3e9a9
--- /dev/null
+++ b/doc/tools/email1.png
Binary files differ
diff --git a/doc/tools/email2.png b/doc/tools/email2.png
new file mode 100644
index 00000000000..d073c0e42da
--- /dev/null
+++ b/doc/tools/email2.png
Binary files differ
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 0fc7f57feab..0531627b7b3 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -30,6 +30,8 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md)
- [CAS OmniAuth Provider](../../integration/cas.md)
- [SAML OmniAuth Provider](../../integration/saml.md)
+ - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html)
+ - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html)
- [Okta SSO provider](../../administration/auth/okta.md)
- [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 1df492ba82c..2884458a44c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -179,7 +179,7 @@ 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](https://docs.gitlab.com/ee/ci/variables/index.html#limiting-environment-scopes-of-variables-premium).
+[based on the environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium).
The following table is an example of how the three different clusters would
be configured.
@@ -377,8 +377,8 @@ analysis on the current code and checks for potential security issues. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
-Any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html).
+Any security warnings are also shown in the merge request widget. Read more how
+[SAST works](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
NOTE: **Note:**
The Auto SAST stage will be skipped on licenses other than Ultimate.
@@ -396,8 +396,8 @@ to run analysis on the project dependencies and checks for potential security is
report is created, it's uploaded as an artifact which you can later download and
check out.
-Any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html).
+Any security warnings are also shown in the merge request widget. Read more about
+[Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
NOTE: **Note:**
The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate.
@@ -415,8 +415,8 @@ to search the project dependencies for their license. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
-Any licenses are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html).
+Any licenses are also shown in the merge request widget. Read more how
+[License Management works](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
NOTE: **Note:**
The Auto License Management stage will be skipped on licenses other than Ultimate.
@@ -431,8 +431,8 @@ Docker image and checks for potential security issues. Once the report is
created, it's uploaded as an artifact which you can later download and
check out.
-Any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html).
+Any security warnings are also shown in the merge request widget. Read more how
+[Container Scanning works](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
NOTE: **Note:**
The Auto Container Scanning stage will be skipped on licenses other than Ultimate.
@@ -460,7 +460,7 @@ for example after the merge request is merged, the Review App will automatically
be deleted.
Review apps are deployed using the
-[auto-deploy-app](https://gitlab.com/charts/auto-deploy-app) chart with
+[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
Helm. The app will be deployed into the [Kubernetes
namespace](../../user/project/clusters/index.md#deployment-variables)
for the environment.
@@ -487,8 +487,8 @@ to perform an analysis on the current code and checks for potential security
issues. Once the report is created, it's uploaded as an artifact which you can
later download and check out.
-Any security warnings are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html).
+Any security warnings are also shown in the merge request widget. Read how
+[DAST works](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
NOTE: **Note:**
The Auto DAST stage will be skipped on licenses other than Ultimate.
@@ -528,7 +528,7 @@ You can make use of [environment variables](#environment-variables) to automatic
scale your pod replicas.
Apps are deployed using the
-[auto-deploy-app](https://gitlab.com/charts/auto-deploy-app) chart with
+[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
Helm. The app will be deployed into the [Kubernetes
namespace](../../user/project/clusters/index.md#deployment-variables)
for the environment.
@@ -667,7 +667,7 @@ repo or by specifying a project variable:
- **Bundled chart** - If your project has a `./chart` directory with a `Chart.yaml`
file in it, Auto DevOps will detect the chart and use it instead of the [default
- one](https://gitlab.com/charts/auto-deploy-app).
+ one](https://gitlab.com/gitlab-org/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
- **Project variable** - Create a [project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart.
@@ -735,8 +735,11 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| **Variable** | **Description** |
| ------------ | --------------- |
| `AUTO_DEVOPS_DOMAIN` | The [Auto DevOps domain](#auto-devops-base-domain). By default, set automatically by the [Auto DevOps setting](#enablingdisabling-auto-devops). This variable is deprecated and [is scheduled to be removed](https://gitlab.com/gitlab-org/gitlab-ce/issues/56959). Use `KUBE_INGRESS_BASE_DOMAIN` instead. |
-| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/charts/auto-deploy-app). |
+| `AUTO_DEVOPS_CHART` | The Helm Chart used to deploy your apps; defaults to the one [provided by GitLab](https://gitlab.com/gitlab-org/charts/auto-deploy-app). |
| `AUTO_DEVOPS_CHART_REPOSITORY` | The Helm Chart repository used to search for charts; defaults to `https://charts.gitlab.io`. |
+| `AUTO_DEVOPS_CHART_REPOSITORY_NAME` | From Gitlab 11.11, this variable can be used to set the name of the helm repository; defaults to "gitlab" |
+| `AUTO_DEVOPS_CHART_REPOSITORY_USERNAME` | From Gitlab 11.11, this variable can be 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, this variable can be used to set a password to connect to the helm repository. Defaults to no credentials. (Also set AUTO_DEVOPS_CHART_REPOSITORY_USERNAME) |
| `REPLICAS` | The number of replicas to deploy; defaults to 1. |
| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. |
| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 |
@@ -767,6 +770,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
+| `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. |
+| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. |
TIP: **Tip:**
Set up the replica variables using a
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index d1729d70158..71651fcf421 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -78,6 +78,33 @@ git push
In case you're running an older version of Git (< 2.9), consider upgrading
to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]).
+## `ssh_exchange_identification` error
+
+Users may experience the following error when attempting to push or pull
+using Git over SSH:
+
+```
+Please make sure you have the correct access rights
+and the repository exists.
+...
+ssh_exchange_identification: read: Connection reset by peer
+fatal: Could not read from remote repository.
+```
+
+This error usually indicates that SSH daemon's `MaxStartups` value is throttling
+SSH connections. This setting specifies the maximum number of unauthenticated
+connections to the SSH daemon. This affects users with proper authentication
+credentials (SSH keys) because every connection is 'unauthenticated' in the
+beginning. The default value is `10`.
+
+Increase `MaxStartups` by adding or modifying the value in `/etc/ssh/sshd_config`:
+
+```
+MaxStartups 100
+```
+
+Restart SSHD for the change to take effect.
+
## Timeout during git push/pull
If pulling/pushing from/to your repository ends up taking more than 50 seconds,
diff --git a/doc/update/10.0-ce-to-ee.md b/doc/update/10.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.1-ce-to-ee.md b/doc/update/10.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.2-ce-to-ee.md b/doc/update/10.2-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.2-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.3-ce-to-ee.md b/doc/update/10.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.4-ce-to-ee.md b/doc/update/10.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.5-ce-to-ee.md b/doc/update/10.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.6-ce-to-ee.md b/doc/update/10.6-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.6-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.7-ce-to-ee.md b/doc/update/10.7-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.7-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/10.8-ce-to-ee.md b/doc/update/10.8-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/10.8-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.0-ce-to-ee.md b/doc/update/11.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.1-ce-to-ee.md b/doc/update/11.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.2-ce-to-ee.md b/doc/update/11.2-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.2-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.3-ce-to-ee.md b/doc/update/11.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.4-ce-to-ee.md b/doc/update/11.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.5-ce-to-ee.md b/doc/update/11.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.6-ce-to-ee.md b/doc/update/11.6-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.6-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.7-ce-to-ee.md b/doc/update/11.7-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.7-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/11.8-ce-to-ee.md b/doc/update/11.8-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/11.8-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.0-ce-to-ee.md b/doc/update/6.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.1-ce-to-ee.md b/doc/update/6.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.2-ce-to-ee.md b/doc/update/6.2-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.2-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.3-ce-to-ee.md b/doc/update/6.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.4-ce-to-ee.md b/doc/update/6.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.5-ce-to-ee.md b/doc/update/6.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.6-ce-to-ee.md b/doc/update/6.6-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.6-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.7-ce-to-ee.md b/doc/update/6.7-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.7-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.8-ce-to-ee.md b/doc/update/6.8-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.8-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/6.9-ce-to-ee.md b/doc/update/6.9-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/6.9-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.0-ce-to-ee.md b/doc/update/7.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.1-ce-to-ee.md b/doc/update/7.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.10-ce-to-ee.md b/doc/update/7.10-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.10-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.11-ce-to-ee.md b/doc/update/7.11-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.11-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.12-ce-to-ee.md b/doc/update/7.12-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.12-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.13-ce-to-ee.md b/doc/update/7.13-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.13-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.14-ce-to-ee.md b/doc/update/7.14-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.14-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.3-ce-to-ee.md b/doc/update/7.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.4-ce-to-ee.md b/doc/update/7.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.5-ce-to-ee.md b/doc/update/7.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.6-ce-to-ee.md b/doc/update/7.6-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.6-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.7-ce-to-ee.md b/doc/update/7.7-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.7-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.8-ce-to-ee.md b/doc/update/7.8-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.8-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/7.9-ce-to-ee.md b/doc/update/7.9-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/7.9-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.0-ce-to-ee.md b/doc/update/8.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.1-ce-to-ee.md b/doc/update/8.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.10-ce-to-ee.md b/doc/update/8.10-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.10-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.11-ce-to-ee.md b/doc/update/8.11-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.11-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.12-ce-to-ee.md b/doc/update/8.12-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.12-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.13-ce-to-ee.md b/doc/update/8.13-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.13-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.14-ce-to-ee.md b/doc/update/8.14-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.14-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.15-ce-to-ee.md b/doc/update/8.15-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.15-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.16-ce-to-ee.md b/doc/update/8.16-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.16-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.17-ce-to-ee.md b/doc/update/8.17-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.17-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.2-ce-to-ee.md b/doc/update/8.2-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.2-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.3-ce-to-ee.md b/doc/update/8.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.4-ce-to-ee.md b/doc/update/8.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.5-ce-to-ee.md b/doc/update/8.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.6-ce-to-ee.md b/doc/update/8.6-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.6-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.7-ce-to-ee.md b/doc/update/8.7-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.7-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.8-ce-to-ee.md b/doc/update/8.8-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.8-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/8.9-ce-to-ee.md b/doc/update/8.9-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/8.9-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.0-ce-to-ee.md b/doc/update/9.0-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.0-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.1-ce-to-ee.md b/doc/update/9.1-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.1-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.2-ce-to-ee.md b/doc/update/9.2-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.2-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.3-ce-to-ee.md b/doc/update/9.3-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.3-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.4-ce-to-ee.md b/doc/update/9.4-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.4-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/9.5-ce-to-ee.md b/doc/update/9.5-ce-to-ee.md
new file mode 100644
index 00000000000..10c9e21fa81
--- /dev/null
+++ b/doc/update/9.5-ce-to-ee.md
@@ -0,0 +1,5 @@
+---
+redirect_to: upgrading_from_ce_to_ee.md
+---
+
+This document was moved to [another location](upgrading_from_ce_to_ee.md).
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index b7f7d71689d..e607dbceeea 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -41,7 +41,7 @@ Debian/Ubuntu:
sudo apt-get install pgloader
```
-For other distributions, follow the instructions in PostrgreSQL's
+For other distributions, follow the instructions in PostgreSQL's
[download page](https://www.postgresql.org/download/) to add their repository
and then install `pgloader`.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 2e8380aa5d8..f90bff662e2 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -106,14 +106,20 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
-### 8. Start application
+### 8. Install/Update `gitlab-elasticsearch-indexer` (optional) **[STARTER ONLY]**
+
+If you're interested in using GitLab's new [elasticsearch repository indexer](https://docs.gitlab.com/ee/integration/elasticsearch.html#elasticsearch-repository-indexer-beta) (currently in beta)
+please follow the instructions on the document linked above and enable the
+indexer usage in the GitLab admin settings.
+
+### 9. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
-### 9. Check application status
+### 10. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md
index 0d1ecab5f8e..e74a5c00f7e 100644
--- a/doc/update/upgrading_from_ce_to_ee.md
+++ b/doc/update/upgrading_from_ce_to_ee.md
@@ -9,6 +9,10 @@ Community Edition to Enterprise Edition. These documents can be found in the
[`doc/update` directory of Enterprise Edition's source
code][old-ee-upgrade-docs].
+If you want to upgrade the version only, for example 11.8 to 11.9, *without* changing the
+GitLab edition you are using (Community or Enterprise), see the
+[Upgrading from source](upgrading_from_source.md) documentation.
+
## General upgrading steps
This guide assumes you have a correctly configured and tested installation of
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index b0c9aaa1eed..fea89669831 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -16,6 +16,9 @@ If the highest number stable branch is unclear please check the
[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
guide links by version.
+If you are changing from GitLab Community Edition to GitLab Enterprise Edition, see
+the [Upgrading from CE to EE](upgrading_from_ce_to_ee.md) documentation.
+
## Guidelines for all versions
This section contains all the steps necessary to upgrade Community Edition or
diff --git a/doc/user/admin_area/geo_nodes.md b/doc/user/admin_area/geo_nodes.md
new file mode 100644
index 00000000000..776ab139c64
--- /dev/null
+++ b/doc/user/admin_area/geo_nodes.md
@@ -0,0 +1,70 @@
+# Geo nodes admin area **[PREMIUM ONLY]**
+
+For more information about setting up GitLab Geo, read the
+[Geo documentation](https://docs.gitlab.com/ee/administration/geo/replication/index.html).
+
+When you're done, you can navigate to **Admin area > Geo** (`/admin/geo/nodes`).
+
+## Common settings
+
+All Geo nodes have the following settings:
+
+| Setting | Description |
+| --------| ----------- |
+| Primary | This marks a Geo Node as **primary** node. There can be only one **primary** node; make sure that you first add the **primary** node and then all the others. |
+| Name | The unique identifier for the Geo node. Must match the setting `gitlab_rails[geo_node_name]` in `/etc/gitlab/gitlab.rb`. The setting defaults to `external_url` with a trailing slash. |
+| URL | The instance's user-facing URL. |
+
+The node you're reading from is indicated with a green `Current node` label, and
+the **primary** node is given a blue `Primary` label. Remember that you can only make
+changes on the **primary** node!
+
+## **Secondary** node settings
+
+**Secondary** nodes have a number of additional settings available:
+
+| Setting | Description |
+|---------------------------|-------------|
+| Selective synchronization | Enable Geo [selective sync](https://docs.gitlab.com/ee/administration/geo/replication/configuration.html#selective-synchronization) for this **secondary** node. |
+| Repository sync capacity | Number of concurrent requests this **secondary** node will make to the **primary** node when backfilling repositories. |
+| File sync capacity | Number of concurrent requests this **secondary** node will make to the **primary** node when backfilling files. |
+
+## Geo backfill
+
+**Secondary** nodes are notified of changes to repositories and files by the **primary** node,
+and will always attempt to synchronize those changes as quickly as possible.
+
+Backfill is the act of populating the **secondary** node with repositories and files that
+existed *before* the **secondary** node was added to the database. Since there may be
+extremely large numbers of repositories and files, it's infeasible to attempt to
+download them all at once, so GitLab places an upper limit on the concurrency of
+these operations.
+
+How long the backfill takes is a function of the maximum concurrency, but higher
+values place more strain on the **primary** node. From [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3107),
+the limits are configurable. If your **primary** node has lots of surplus capacity,
+you can increase the values to complete backfill in a shorter time. If it's
+under heavy load and backfill is reducing its availability for normal requests,
+you can decrease them.
+
+## Using a different URL for synchronization
+
+The **primary** node's Internal URL is used by **secondary** nodes to contact it
+(to sync repositories, for example). The name Internal URL distinguishes it from
+[External URL](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-the-external-url-for-gitlab)
+which is used by users. Internal URL does not need to be a private address.
+
+Internal URL defaults to External URL, but you can customize it under
+**Admin area > Geo Nodes**.
+
+## Multiple secondary nodes behind a load balancer
+
+In GitLab 11.11, **secondary** nodes can use identical external URLs as long as
+a unique `name` is set for each Geo node. The `gitlab.rb` setting
+`gitlab_rails[geo_node_name]` must:
+
+- Be set for each GitLab instance that runs `unicorn`, `sidekiq`, or `geo_logcursor`.
+- Match a Geo node name.
+
+The load balancer must use sticky sessions in order to avoid authentication
+failures and cross site request errors.
diff --git a/doc/user/admin_area/img/admin_wrench.png b/doc/user/admin_area/img/admin_wrench.png
new file mode 100644
index 00000000000..17eee143e87
--- /dev/null
+++ b/doc/user/admin_area/img/admin_wrench.png
Binary files differ
diff --git a/doc/user/admin_area/img/license_admin_area.png b/doc/user/admin_area/img/license_admin_area.png
new file mode 100644
index 00000000000..b5662b81c5e
--- /dev/null
+++ b/doc/user/admin_area/img/license_admin_area.png
Binary files differ
diff --git a/doc/user/admin_area/img/license_details.png b/doc/user/admin_area/img/license_details.png
new file mode 100644
index 00000000000..2085bb437ad
--- /dev/null
+++ b/doc/user/admin_area/img/license_details.png
Binary files differ
diff --git a/doc/user/admin_area/img/license_no_license_message.png b/doc/user/admin_area/img/license_no_license_message.png
new file mode 100644
index 00000000000..87b397f7905
--- /dev/null
+++ b/doc/user/admin_area/img/license_no_license_message.png
Binary files differ
diff --git a/doc/user/admin_area/img/license_upload.png b/doc/user/admin_area/img/license_upload.png
new file mode 100644
index 00000000000..29d55175a2d
--- /dev/null
+++ b/doc/user/admin_area/img/license_upload.png
Binary files differ
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 00cea22e4e1..dd4e96c1f4a 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -14,16 +14,63 @@ Only admin users can access the Admin Area.
The Admin Area is made up of the following sections:
-| Section | Description |
-|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Overview | View your GitLab Dashboard, and maintain projects, users, groups, jobs, runners, and Gitaly servers. |
-| Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. |
-| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
-| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. |
-| Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. |
-| Abuse Reports | Manage [abuse reports](abuse_reports.md) submitted by your users. |
-| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). |
-| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. |
-| Labels | Create and maintain [labels](labels.md) for your GitLab instance. |
-| Appearance | Customize [GitLab's appearance](../../customization/index.md). |
-| Settings | Modify the [settings](settings/index.md) for your GitLab instance. |
+| Section | Description |
+|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Overview | View your GitLab [Dashboard](#admin-dashboard), and administer [projects](#administer-projects), users, groups, jobs, runners, and Gitaly servers. |
+| Monitoring | View GitLab system information, and information on background jobs, logs, [health checks](monitoring/health_check.md), request profiles, and audit logs. |
+| Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
+| System Hooks | Configure [system hooks](../../system_hooks/system_hooks.md) for many events. |
+| Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. |
+| Abuse Reports | Manage [abuse reports](abuse_reports.md) submitted by your users. |
+| License **[STARTER ONLY]** | Upload, display, and remove [licenses](license.md). |
+| Push Rules **[STARTER]** | Configure pre-defined git [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) for projects. |
+| Geo **[PREMIUM ONLY]** | Configure and maintain [Geo nodes](geo_nodes.md). |
+| Deploy Keys | Create instance-wide [SSH deploy keys](../../ssh/README.md#deploy-keys). |
+| Service Templates | Create [service templates](../project/integrations/services_templates.md) for projects. |
+| Labels | Create and maintain [labels](labels.md) for your GitLab instance. |
+| Appearance | Customize [GitLab's appearance](../../customization/index.md). |
+| Settings | Modify the [settings](settings/index.md) for your GitLab instance. |
+
+## Admin Dashboard
+
+The Dashboard provides statistics and system information about the GitLab instance.
+
+To access the Dashboard, either:
+
+- Click the Admin Area icon (the wrench icon).
+- Visit `/admin` on your self-managed instance.
+
+The Dashboard is the default view of the Admin Area, and is made up of the following sections:
+
+| Section | Description |
+|------------|---------------|
+| Projects | The total number of projects, up to 10 of the latest projects, and the option of creating a new project. |
+| Users | The total number of users, up to 10 of the latest users, and the option of creating a new user. |
+| Groups | The total number of groups, up to 10 of the latest groups, and the option of creating a new group. |
+| Statistics | Totals of all elements of the GitLab instance. |
+| Features | All features available on the GitLab instance. Enabled features are marked with a green circle icon, and disabled features are marked with a power icon. |
+| Components | The major components of GitLab and the version number of each. A link to the Gitaly Servers is also included. |
+
+## Administer Projects
+
+You can administer all projects in the GitLab instance from the Admin Area's Projects page.
+
+To access the Projects page, go to **Admin Area > Overview > Projects**.
+
+Click the **All**, **Private**, **Internal**, or **Public** tab to list only projects of that
+criteria.
+
+By default, all projects are listed, in reverse order of when they were last updated. For each
+project, the name, namespace, description, and size is listed, also options to **Edit** or
+**Delete** it.
+
+Sort projects by **Name**, **Last created**, **Oldest created**, **Last updated**, **Oldest
+updated**, **Owner**, and choose to hide or show archived projects.
+
+In the **Filter by name** field, type the project name you want to find, and GitLab will filter
+them as you type.
+
+Select from the **Namespace** dropdown to filter only projects in that namespace.
+
+You can combine the filter options. For example, click the **Public** tab, and enter `score` in
+the **Filter by name...** input box to list only public projects with `score` in their name.
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
new file mode 100644
index 00000000000..45f986d480f
--- /dev/null
+++ b/doc/user/admin_area/license.md
@@ -0,0 +1,79 @@
+# Activate all GitLab Enterprise Edition functionality with a license **[STARTER ONLY]**
+
+To activate all GitLab Enterprise Edition (EE) functionality, you need to upload
+a license. Once you've received your license from GitLab Inc., you can upload it
+by **signing into your GitLab instance as an admin**.
+
+The license has the form of a base64 encoded ASCII text with a `.gitlab-license`
+extension and can be obtained when you [purchase one][pricing] or when you sign
+up for a [free trial].
+
+NOTE: **Note:**
+As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an
+uploaded license will only have the Core features active. A trial license will
+activate all Ultimate features, but after
+[the trial expires](#what-happens-when-your-license-expires), some functionality
+will be locked.
+
+## Uploading your license
+
+The very first time you visit your GitLab EE installation signed in as an admin,
+you should see a note urging you to upload a license with a link that takes you
+straight to the License admin area.
+
+Otherwise, you can:
+
+1. Navigate manually to the **Admin Area** by clicking the wrench icon in the menu bar.
+
+ ![Admin area icon](img/admin_wrench.png)
+
+1. And then going to the **License** tab and click on **Upload New License**.
+
+ ![License admin area](img/license_admin_area.png)
+
+1. If you've received a `.gitlab-license` file, you should have already downloaded
+ it in your local machine. You can then upload it directly by choosing the
+ license file and clicking the **Upload license** button. In the image below,
+ you can see that the selected license file is named `GitLab.gitlab-license`.
+
+ ![Upload license](img/license_upload.png)
+
+ If you've received your license as plain text, you need to select the
+ "Enter license key" option, copy the license, paste it into the "License key"
+ field and click **Upload license**.
+
+---
+
+Once the license is uploaded, all GitLab Enterprise Edition functionality
+will be active until the end of the license period. When that period ends, the
+instance will [fall back](#what-happens-when-your-license-expires) to Core-only
+functionality.
+
+You can review the license details at any time in the License section of the
+Admin Area.
+
+![License details](img/license_details.png)
+
+## Notification before the license expires
+
+One month before the license expires, a message informing when the expiration
+is due to, will start appearing to GitLab admins. Make sure that you update your
+license, otherwise you will miss all the paid features if it expires.
+
+## What happens when your license expires
+
+In case your license expires, GitLab will lock down some features like Git pushes,
+issue creation, etc., and a message to inform of the expired license will be
+presented to all admins.
+
+In order to get back all the previous functionality, a new license must be uploaded.
+To fall back to having only the Core features active, you'll need to delete the
+expired license(s).
+
+## License history
+
+It's possible to upload and view more than one license,
+but only the latest license will be used as the active license.
+
+[free trial]: https://about.gitlab.com/free-trial/
+[pricing]: https://about.gitlab.com/pricing/
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
new file mode 100644
index 00000000000..4a5bfb6b677
--- /dev/null
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -0,0 +1,53 @@
+# Account and limit settings
+
+## Repository size limit **[STARTER]**
+
+> [Introduced][ee-740] in [GitLab Enterprise Edition 8.12][ee-8.12].
+
+Repositories within your GitLab instance can grow quickly, especially if you are
+using LFS. Their size can grow exponentially and eat up your storage device quite
+fast.
+
+In order to avoid this from happening, you can set a hard limit for your
+repositories' size. This limit can be set globally, per group, or per project,
+with per project limits taking the highest priority.
+
+There are numerous cases where you'll need to set up a limit for repository size.
+For instance, consider the following workflow:
+
+1. Your team develops apps which demand large files to be stored in
+ the application repository.
+1. Although you have enabled [Git LFS](../../../workflow/lfs/manage_large_binaries_with_git_lfs.html#git-lfs)
+ to your project, your storage has grown significantly.
+1. Before you blow your storage limit up, you set up a limit of 10 GB
+ per repository.
+
+### How it works
+
+Only a GitLab administrator can set those limits. Setting the limit to `0` means
+there are no restrictions.
+
+These settings can be found within:
+
+- Each project's settings.
+- A group's settings.
+- The **Size limit per repository (MB)** field in the **Account and limit** section of a GitLab instance's
+ settings by navigating to either:
+ - **Admin Area > Settings > General**.
+ - The path `/admin/application_settings`.
+
+The very first push of a new project cannot be checked for size as of now, so
+the first push will allow you to upload more than the limit dictates, but every
+subsequent push will be denied. LFS objects, however, can be checked on first
+push and **will** be rejected if the sum of their sizes exceeds the maximum
+allowed repository size.
+
+For more manually purging the files, read the docs on
+[reducing the repository size using Git][repo-size].
+
+> **Note:**
+> For GitLab.com, the repository size limit is 10 GB.
+
+[ee-740]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740
+[repo-size]: ../../project/repository/reducing_the_repo_size_using_git.md
+[ee-8.12]: https://about.gitlab.com/2016/09/22/gitlab-8-12-released/#limit-project-size-ee
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 22011cc6967..834a1e2423a 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -10,8 +10,8 @@ You can find it in the admin area, under **Settings > Continuous Integration and
To enable (or disable) [Auto DevOps](../../../topics/autodevops/index.md)
for all projects:
-1. Go to **Admin area > Settings > Continuous Integration and Deployment**.
-1. Check (or uncheck to disable) the box that says "Default to Auto DevOps pipeline for all projects".
+1. Go to **Admin area > Settings > Continuous Integration and Deployment**
+1. Check (or uncheck to disable) the box that says "Default to Auto DevOps pipeline for all projects"
1. Optionally, set up the [Auto DevOps base domain](../../../topics/autodevops/index.md#auto-devops-base-domain)
which is going to be used for Auto Deploy and Auto Review Apps.
1. Hit **Save changes** for the changes to take effect.
@@ -24,9 +24,9 @@ If you want to disable it for a specific project, you can do so in
## Maximum artifacts size **[CORE ONLY]**
-The maximum size of the [job artifacts][art-yml] can be set in the Admin area
-of your GitLab instance. The value is in *MB* and the default is 100MB per job;
-on GitLab.com it's [set to 1G](../../gitlab_com/index.md#gitlab-cicd).
+The maximum size of the [job artifacts](../../../administration/job_artifacts.md)
+can be set in the Admin area of your GitLab instance. The value is in *MB* and
+the default is 100MB per job; on GitLab.com it's [set to 1G](../../gitlab_com/index.md#gitlab-cicd).
To change it:
@@ -50,6 +50,79 @@ This setting is set per job and can be overridden in
[`.gitlab-ci.yml`](../../../ci/yaml/README.md#artifactsexpire_in).
To disable the expiration, set it to `0`. The default unit is in seconds.
+## Shared Runners pipeline minutes quota **[STARTER ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1078)
+in GitLab Starter 8.16.
+
+If you have enabled shared Runners for your GitLab instance, you can limit their
+usage by setting a maximum number of pipeline minutes that a group can use on
+shared Runners per month. Setting this to `0` (default value) will grant
+unlimited pipeline minutes. While build limits are stored as minutes, the
+counting is done in seconds. Usage resets on the first day of each month.
+On GitLab.com, the quota is calculated based on your
+[subscription plan](https://about.gitlab.com/pricing/#gitlab-com).
+
+To change the pipelines minutes quota:
+
+1. Go to **Admin area > Settings > Continuous Integration and Deployment**
+1. Set the pipeline minutes quota limit.
+1. Hit **Save changes** for the changes to take effect
+
+---
+
+While the setting in the Admin area has a global effect, as an admin you can
+also change each group's pipeline minutes quota to override the global value.
+
+1. Navigate to the **Groups** admin area and hit the **Edit** button for the
+ group you wish to change the pipeline minutes quota.
+1. Set the pipeline minutes quota to the desired value
+1. Hit **Save changes** for the changes to take effect.
+
+Once saved, you can see the build quota in the group admin view.
+The quota can also be viewed in the project admin view if shared Runners
+are enabled.
+
+![Project admin info](img/admin_project_quota_view.png)
+
+When the pipeline minutes quota for a group is set to a value different than 0,
+the **Pipelines quota** page is available to the group page settings list.
+You can see there an overview of the pipeline minutes quota of all projects of
+the group.
+
+![Group pipelines quota](img/group_pipelines_quota.png)
+
+
+## Extra Shared Runners pipeline minutes quota
+
+NOTE: **Note:**
+Only available on GitLab.com.
+
+If you have a Group with a [paid plan](https://about.gitlab.com/pricing/#gitlab-com) on GitLab.com,
+then you can purchase additional CI minutes so your pipelines will not be blocked after you have
+used all your CI minutes from your main quota.
+
+In order to purchase additional minutes, you should follow these steps:
+
+1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**.
+
+ ![Buy additional minutes](img/buy_btn.png)
+
+1. Locate the subscription card that is linked to your group on GitLab.com,
+click on **Buy more CI minutes**, and complete the details about the transaction.
+
+ ![Buy additional minutes](img/buy_minutes_card.png)
+
+1. Once we have processed your payment, the extra CI minutes
+will be synced to your Group and you can visualize it from the
+**Group > Settings > Pipelines quota** page:
+
+ ![Additional minutes](img/additional_minutes.png)
+
+NOTE: **Important note**: If you have some minutes used over your default quota, these minutes will
+be deducted from your Additional Minutes quota immediately after your purchase of additional
+minutes.
+
## Archive jobs **[CORE ONLY]**
Archiving jobs is useful for reducing the CI/CD footprint on the system by
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 50c318a4969..01a98cf15dc 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -4,6 +4,22 @@
The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
+## Custom additional text **[PREMIUM ONLY]**
+
+>[Introduced][ee-5031] in [GitLab Premium][eep] 10.7.
+
+The additional text will appear at the bottom of any email and can be used for
+legal/auditing/compliance reasons.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+1. Under the **Email** section, change the **Additional text** field.
+1. Hit **Save** for the changes to take effect.
+
+![Admin email settings](img/email_settings.png)
+
+[ee-5031]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5031
+[eep]: https://about.gitlab.com/pricing/
+
## Custom hostname for private commit emails
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
diff --git a/doc/user/admin_area/settings/external_authorization.md b/doc/user/admin_area/settings/external_authorization.md
new file mode 100644
index 00000000000..06e00e02f3d
--- /dev/null
+++ b/doc/user/admin_area/settings/external_authorization.md
@@ -0,0 +1,112 @@
+# External authorization control **[CORE ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4216) in
+> [GitLab Premium](https://about.gitlab.com/pricing) 10.6.
+> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27056) to
+> [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
+
+In highly controlled environments, it may be necessary for access policy to be
+controlled by an external service that permits access based on project
+classification and user access. GitLab provides a way to check project
+authorization with your own defined service.
+
+## Overview
+
+Once the external service is configured and enabled, when a project is accessed,
+a request is made to the external service with the user information and project
+classification label assigned to the project. When the service replies with a
+known response, the result is cached for 6 hours.
+
+If the external authorization is enabled, GitLab will further block pages and
+functionality that render cross-project data. That includes:
+
+- most pages under Dashboard (Activity, Milestones, Snippets, Assigned merge
+ requests, Assigned issues, Todos)
+- under a specific group (Activity, Contribution analytics, Issues, Issue boards,
+ Labels, Milestones, Merge requests)
+- Global and Group search will be disabled
+
+This is to prevent performing to many requests at once to the external
+authorization service.
+
+Whenever access is granted or denied this is logged in a logfile called
+`external-policy-access-control.log`.
+Read more about logs GitLab keeps in the [omnibus documentation][omnibus-log-docs].
+
+## Configuration
+
+The external authorization service can be enabled by an admin on the GitLab's
+admin area under the settings page:
+
+![Enable external authorization service](img/external_authorization_service_settings.png)
+
+The available required properties are:
+
+- **Service URL**: The URL to make authorization requests to. When leaving the
+ URL blank, cross project features will remain available while still being able
+ to specify classification labels for projects.
+- **External authorization request timeout**: The timeout after which an
+ authorization request is aborted. When a request times out, access is denied
+ to the user.
+- **Client authentication certificate**: The certificate to use to authenticate
+ with the external authorization service.
+- **Client authentication key**: Private key for the certificate when
+ authentication is required for the external authorization service, this is
+ encrypted when stored.
+- **Client authentication key password**: Passphrase to use for the private key when authenticating with the external service this is encrypted when stored.
+- **Default classification label**: The classification label to use when
+ requesting authorization if no specific label is defined on the project
+
+When using TLS Authentication with a self signed certificate, the CA certificate
+needs to be trused by the openssl installation. When using GitLab installed using
+Omnibus, learn to install a custom CA in the
+[omnibus documentation][omnibus-ssl-docs]. Alternatively learn where to install
+custom certificates using `openssl version -d`.
+
+## How it works
+
+When GitLab requests access, it will send a JSON POST request to the external
+service with this body:
+
+```json
+{
+ "user_identifier": "jane@acme.org",
+ "project_classification_label": "project-label",
+ "user_ldap_dn": "CN=Jane Doe,CN=admin,DC=acme"
+}
+```
+
+The `user_ldap_dn` is optional and is only sent when the user is logged in
+through LDAP.
+
+When the external authorization service responds with a status code 200, the
+user is granted access. When the external service responds with a status code
+401 or 403, the user is denied access. In any case, the request is cached for 6 hours.
+
+When denying access, a `reason` can be optionally specified in the JSON body:
+
+```json
+{
+ "reason": "You are not allowed access to this project."
+}
+```
+
+Any other status code than 200, 401 or 403 will also deny access to the user, but the
+response will not be cached.
+
+If the service times out (after 500ms), a message "External Policy Server did
+not respond" will be displayed.
+
+## Classification labels
+
+You can use your own classification label in the project's
+**Settings > General > General project settings** page in the "Classification
+label" box. When no classification label is specified on a project, the default
+label defined in the [global settings](#configuration) will be used.
+
+The label will be shown on all project pages in the upper right corner.
+
+![classification label on project page](img/classification_label_on_project_page.png)
+
+[omnibus-ssl-docs]: https://docs.gitlab.com/omnibus/settings/ssl.html
+[omnibus-log-docs]: https://docs.gitlab.com/omnibus/settings/logs.html
diff --git a/doc/user/admin_area/settings/img/additional_minutes.png b/doc/user/admin_area/settings/img/additional_minutes.png
new file mode 100644
index 00000000000..d148ed79b92
--- /dev/null
+++ b/doc/user/admin_area/settings/img/additional_minutes.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_group_edit.png b/doc/user/admin_area/settings/img/admin_area_group_edit.png
new file mode 100644
index 00000000000..c9bd2f10b36
--- /dev/null
+++ b/doc/user/admin_area/settings/img/admin_area_group_edit.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_groups.png b/doc/user/admin_area/settings/img/admin_area_groups.png
new file mode 100644
index 00000000000..ebdee0eafdc
--- /dev/null
+++ b/doc/user/admin_area/settings/img/admin_area_groups.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_project_quota_view.png b/doc/user/admin_area/settings/img/admin_project_quota_view.png
new file mode 100644
index 00000000000..8320be860da
--- /dev/null
+++ b/doc/user/admin_area/settings/img/admin_project_quota_view.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/buy_btn.png b/doc/user/admin_area/settings/img/buy_btn.png
new file mode 100644
index 00000000000..0cc88b8a48f
--- /dev/null
+++ b/doc/user/admin_area/settings/img/buy_btn.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/buy_minutes_card.png b/doc/user/admin_area/settings/img/buy_minutes_card.png
new file mode 100644
index 00000000000..cf4ad34ead7
--- /dev/null
+++ b/doc/user/admin_area/settings/img/buy_minutes_card.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png b/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png
new file mode 100644
index 00000000000..269a3cf1fbc
--- /dev/null
+++ b/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/classification_label_on_project_page.png b/doc/user/admin_area/settings/img/classification_label_on_project_page.png
new file mode 100644
index 00000000000..4aedb332cec
--- /dev/null
+++ b/doc/user/admin_area/settings/img/classification_label_on_project_page.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/email_settings.png b/doc/user/admin_area/settings/img/email_settings.png
new file mode 100644
index 00000000000..ed0a80d10ce
--- /dev/null
+++ b/doc/user/admin_area/settings/img/email_settings.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/external_authorization_service_settings.png b/doc/user/admin_area/settings/img/external_authorization_service_settings.png
new file mode 100644
index 00000000000..9b8658fd1a1
--- /dev/null
+++ b/doc/user/admin_area/settings/img/external_authorization_service_settings.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/file_template_admin_area.png b/doc/user/admin_area/settings/img/file_template_admin_area.png
new file mode 100644
index 00000000000..269d997e1d9
--- /dev/null
+++ b/doc/user/admin_area/settings/img/file_template_admin_area.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/file_template_user_dropdown.png b/doc/user/admin_area/settings/img/file_template_user_dropdown.png
new file mode 100644
index 00000000000..8c9eb49f6c9
--- /dev/null
+++ b/doc/user/admin_area/settings/img/file_template_user_dropdown.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/group_pipelines_quota.png b/doc/user/admin_area/settings/img/group_pipelines_quota.png
new file mode 100644
index 00000000000..d94b609ad6f
--- /dev/null
+++ b/doc/user/admin_area/settings/img/group_pipelines_quota.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/group_quota_view.png b/doc/user/admin_area/settings/img/group_quota_view.png
new file mode 100644
index 00000000000..791bfd868e0
--- /dev/null
+++ b/doc/user/admin_area/settings/img/group_quota_view.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/group_settings.png b/doc/user/admin_area/settings/img/group_settings.png
new file mode 100644
index 00000000000..a849d9cfdc1
--- /dev/null
+++ b/doc/user/admin_area/settings/img/group_settings.png
Binary files differ
diff --git a/doc/user/admin_area/settings/img/mirror_settings.png b/doc/user/admin_area/settings/img/mirror_settings.png
new file mode 100644
index 00000000000..090db6808a7
--- /dev/null
+++ b/doc/user/admin_area/settings/img/mirror_settings.png
Binary files differ
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index 8358fe64f18..0e697b9e445 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -6,6 +6,7 @@ instance like sign-up restrictions, account limits and quota, metrics, etc.
Navigate to it by going to **Admin area > Settings**. Some of the settings
include:
+- [Account and limit settings](account_and_limit_settings.md) **[STARTER]**
- [Continuous Integration and Deployment](continuous_integration.md)
- [Email](email.md)
- [Sign up restrictions](sign_up_restrictions.md)
@@ -13,6 +14,7 @@ include:
- [Third party offers](third_party_offers.md)
- [Usage statistics](usage_statistics.md)
- [Visibility and access controls](visibility_and_access_controls.md)
+- [Custom templates repository](instance_template_repository.md) **[PREMIUM]**
NOTE: **Note:**
You can change the [first day of the week](../../profile/preferences.md) for the entire GitLab instance
diff --git a/doc/user/admin_area/settings/instance_template_repository.md b/doc/user/admin_area/settings/instance_template_repository.md
new file mode 100644
index 00000000000..4010008f694
--- /dev/null
+++ b/doc/user/admin_area/settings/instance_template_repository.md
@@ -0,0 +1,63 @@
+# Instance template repository **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5986) in
+> [GitLab Premium](https://about.gitlab.com/pricing) 11.3.
+
+## Overview
+
+In hosted systems, enterprises often have a need to share their own templates
+across teams. This feature allows an administrator to pick a project to be the
+instance-wide collection of file templates. These templates are then exposed to
+all users [via the web editor](../../project/repository/web_editor.md#template-dropdowns)
+while the project remains secure.
+
+## Configuration
+
+As an administrator, navigate to **Admin area > Settings > Templates** and
+select the project to serve as the custom template repository.
+
+![File templates in the admin area](img/file_template_admin_area.png)
+
+Once a project has been selected, you can add custom templates to the repository,
+and they will appear in the appropriate places in the
+[frontend](../../project/repository/web_editor.md#template-dropdowns) and
+[API](../../../api/settings.md).
+
+Templates must be added to a specific subdirectory in the repository,
+corresponding to the kind of template. The following types of custom templates
+are supported:
+
+| Type | Directory | Extension |
+| :---------------: | :-----------: | :-----------: |
+| `Dockerfile` | `Dockerfile` | `.dockerfile` |
+| `.gitignore` | `gitignore` | `.gitignore` |
+| `.gitlab-ci.yml` | `gitlab-ci` | `.yml` |
+| `LICENSE` | `LICENSE` | `.txt` |
+
+Each template must go in its respective subdirectory, have the correct
+extension and not be empty. So, the hierarchy should look like this:
+
+```text
+|-- README.md
+|-- Dockerfile
+ |-- custom_dockerfile.dockerfile
+ |-- another_dockerfile.dockerfile
+|-- gitignore
+ |-- custom_gitignore.gitignore
+ |-- another_gitignore.gitignore
+|-- gitlab-ci
+ |-- custom_gitlab-ci.yml
+ |-- another_gitlab-ci.yml
+|-- LICENSE
+ |-- custom_license.txt
+ |-- another_license.txt
+```
+
+Once this is established, the list of custom templates will be included when
+creating a new file and the template type is selected. These will appear at the
+top of the list.
+
+![Custom template dropdown menu](img/file_template_user_dropdown.png)
+
+If this feature is disabled or no templates are present, there will be
+no "Custom" section in the selection dropdown.
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 6a1e8004f87..4d1b7d0f252 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -49,5 +49,15 @@ block access to the server itself. The ports used for the protocol, be it SSH or
HTTP, will still be accessible. What GitLab does is restrict access on the
application level.
+## Allow mirrors to be set up for projects
+
+> [Introduced][ee-3586] in GitLab 10.3.
+
+This option is enabled by default. By disabling it, both pull and push mirroring will no longer
+work in every repository and can only be re-enabled on a per-project basis by an admin.
+
+![Mirror settings](img/mirror_settings.png)
+
[ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696
[ce-18021]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18021
+[ee-3586]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3586
diff --git a/doc/user/application_security/container_scanning/img/container_scanning.png b/doc/user/application_security/container_scanning/img/container_scanning.png
new file mode 100644
index 00000000000..e47f62acd9d
--- /dev/null
+++ b/doc/user/application_security/container_scanning/img/container_scanning.png
Binary files differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
new file mode 100644
index 00000000000..ce86ade3c1a
--- /dev/null
+++ b/doc/user/application_security/container_scanning/index.md
@@ -0,0 +1,202 @@
+# Container Scanning **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3672)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can check your Docker
+images (or more precisely the containers) for known vulnerabilities by using
+[Clair](https://github.com/coreos/clair) and [clair-scanner](https://github.com/arminc/clair-scanner),
+two open source tools for Vulnerability Static Analysis for containers.
+
+You can take advantage of Container Scanning by either [including the CI job](#including-the-provided-template) in
+your existing `.gitlab-ci.yml` file or by implicitly using
+[Auto Container Scanning](../../../topics/autodevops/index.md#auto-container-scanning)
+that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+
+GitLab checks the Container Scanning report, compares the found vulnerabilities
+between the source and target branches, and shows the information right on the
+merge request.
+
+![Container Scanning Widget](img/container_scanning.png)
+
+## Use cases
+
+If you distribute your application with Docker, then there's a great chance
+that your image is based on other Docker images that may in turn contain some
+known vulnerabilities that could be exploited.
+
+Having an extra job in your pipeline that checks for those vulnerabilities,
+and the fact that they are displayed inside a merge request, makes it very easy
+to perform audits for your Docker-based apps.
+
+## Requirements
+
+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.
+- To [build and push](../../../ci/docker/using_docker_build.md#container-registry-examples)
+ your Docker image to your project's [Container Registry](../../project/container_registry.md).
+ The name of the Docker image should match the following scheme:
+
+ ```
+ $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
+ ```
+
+ The variables above can be found in the
+ [predefined environment variables](../../../ci/variables/predefined_variables.md)
+ document.
+
+## Configuring Container Scanning
+
+To enable Container Scanning in your project, define a job in your
+`.gitlab-ci.yml` file that generates the
+[Container Scanning report artifact](../../../ci/yaml/README.md#artifactsreportscontainer_scanning-ultimate).
+
+This can be done in two ways:
+
+- For GitLab 11.9 and later, including the provided
+ `Container-Scanning.gitlab-ci.yml` template (recommended).
+- Manually specifying the job definition. Not recommended unless using GitLab
+ 11.8 and earlier.
+
+### Including the provided template
+
+NOTE: **Note:**
+The CI/CD Container Scanning template is supported on GitLab 11.9 and later versions.
+For earlier versions, use the [manual job definition](#manual-job-definition-for-gitlab-115-and-later).
+
+A CI/CD [Container Scanning template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml)
+with the default Container Scanning job definition is provided as a part of your GitLab
+installation that you can [include](../../../ci/yaml/README.md#includetemplate)
+in your `.gitlab-ci.yml` file.
+
+To enable Container Scanning using the provided template, add the following to
+your `.gitlab-ci.yml` file:
+
+```yaml
+include:
+ template: Container-Scanning.gitlab-ci.yml
+```
+
+The included template will:
+
+- Create a `container_scanning` job in your CI/CD pipeline.
+- Pull the already built Docker image from your project's
+ [Container Registry](../../project/container_registry.md) (see [requirements](#requirements))
+ and scan it for possible vulnerabilities.
+
+The report will be saved as a
+[Container Scanning report artifact](../../../ci/yaml/README.md#artifactsreportscontainer_scanning-ultimate)
+that you can later download and analyze.
+Due to implementation limitations, we always take the latest Container Scanning
+artifact available. Behind the scenes, the
+[GitLab Container Scanning analyzer](https://gitlab.com/gitlab-org/security-products/container-scanning)
+is used and runs the scans.
+
+If you want to whitelist some specific vulnerabilities, you can do so by defining
+them in a YAML file named `clair-whitelist.yml`. Read more in the
+[Clair documentation](https://github.com/arminc/clair-scanner/blob/master/README.md#example-whitelist-yaml-file).
+
+### Manual job definition for GitLab 11.5 and later
+
+CAUTION: **Caution:**
+The job definition shown below is supported on GitLab 11.5 and later versions.
+However, if you're using GitLab 11.9+, it's recommended to use
+[the provided Container Scanning template](#including-the-provided-template).
+
+For GitLab 11.5 and GitLab Runner 11.5 and later, the following `container_scanning`
+job can be added:
+
+```yaml
+container_scanning:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ ## Define two new variables based on GitLab's CI/CD predefined variables
+ ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
+ CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
+ CI_APPLICATION_TAG: $CI_COMMIT_SHA
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - docker run -d --name db arminc/clair-db:latest
+ - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
+ - apk add -U wget ca-certificates
+ - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
+ - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
+ - mv clair-scanner_linux_amd64 clair-scanner
+ - chmod +x clair-scanner
+ - touch clair-whitelist.yml
+ - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
+ - retries=0
+ - echo "Waiting for clair daemon to start"
+ - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
+ - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ artifacts:
+ reports:
+ container_scanning: gl-container-scanning-report.json
+```
+
+### Manual job definition for GitLab 11.4 and earlier (deprecated)
+
+CAUTION: **Deprecated:**
+Before GitLab 11.5, the Container Scanning job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained, they have been deprecated
+and may be removed in the next major release, GitLab 12.0. You are strongly
+advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the Container Scanning job should look like:
+
+```yaml
+container_scanning:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ ## Define two new variables based on GitLab's CI/CD predefined variables
+ ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
+ CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
+ CI_APPLICATION_TAG: $CI_COMMIT_SHA
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - docker run -d --name db arminc/clair-db:latest
+ - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
+ - apk add -U wget ca-certificates
+ - docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
+ - wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
+ - mv clair-scanner_linux_amd64 clair-scanner
+ - chmod +x clair-scanner
+ - touch clair-whitelist.yml
+ - while( ! wget -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; done
+ - retries=0
+ - echo "Waiting for clair daemon to start"
+ - while( ! wget -T 10 -q -O /dev/null http://docker:6060/v1/namespaces ) ; do sleep 1 ; echo -n "." ; if [ $retries -eq 10 ] ; then echo " Timeout, aborting." ; exit 1 ; fi ; retries=$(($retries+1)) ; done
+ - ./clair-scanner -c http://docker:6060 --ip $(hostname -i) -r gl-container-scanning-report.json -l clair.log -w clair-whitelist.yml ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG} || true
+ artifacts:
+ paths: [gl-container-scanning-report.json]
+```
+
+Alternatively, the job name could be `sast:container`
+and the artifact name could be `gl-sast-container-report.json`.
+These names have been deprecated with GitLab 11.0
+and may be removed in the next major release, GitLab 12.0.
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups and projects. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
diff --git a/doc/user/application_security/dast/img/dast_all.png b/doc/user/application_security/dast/img/dast_all.png
new file mode 100644
index 00000000000..b6edc928dc3
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_all.png
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_single.png b/doc/user/application_security/dast/img/dast_single.png
new file mode 100644
index 00000000000..26ca4bde786
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_single.png
Binary files differ
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
new file mode 100644
index 00000000000..904c9e8fefe
--- /dev/null
+++ b/doc/user/application_security/dast/index.md
@@ -0,0 +1,262 @@
+# Dynamic Application Security Testing (DAST) **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4348)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4.
+
+Running [static checks](../sast/index.md) on your code is the first step to detect
+vulnerabilities that can put the security of your code at risk. Yet, once
+deployed, your application is exposed to a new category of possible attacks,
+such as cross-site scripting or broken authentication flaws. This is where
+Dynamic Application Security Testing (DAST) comes into place.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your running web application(s)
+for known vulnerabilities using Dynamic Application Security Testing (DAST).
+
+You can take advantage of DAST by either [including the CI job](#configuring-dast) in
+your existing `.gitlab-ci.yml` file or by implicitly using
+[Auto DAST](../../../topics/autodevops/index.md#auto-dast-ultimate)
+that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+
+GitLab checks the DAST report, compares the found vulnerabilities between the source and target
+branches, and shows the information right on the merge request.
+
+![DAST Widget](img/dast_all.png)
+
+By clicking on one of the detected linked vulnerabilities, you will be able to
+see the details and the URL(s) affected.
+
+![DAST Widget Clicked](img/dast_single.png)
+
+[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing)
+is using the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
+to perform an analysis on your running web application.
+
+By default, DAST executes [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan) and will perform passive scanning only. It will not actively attack your application.
+
+However, DAST can be [configured](#full-scan)
+to also perform a so-called "active scan". That is, attack your application and produce a more extensive security report.
+It can be very useful combined with [Review Apps](../../../ci/review_apps/index.md).
+
+## Use cases
+
+It helps you automatically find security vulnerabilities in your running web
+applications while you are developing and testing your applications.
+
+## Requirements
+
+To run a DAST job, you need GitLab Runner with the
+[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
+[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
+executor running in privileged mode. If you're using the shared Runners on GitLab.com,
+this is enabled by default.
+
+## Configuring DAST
+
+To enable DAST in your project, define a job in your `.gitlab-ci.yml` file that generates the
+[DAST report artifact](../../../ci/yaml/README.md#artifactsreportsdast-ultimate).
+
+This can be done in two ways:
+
+- For GitLab 11.9 and later, including the provided DAST `.gitlab-ci.yml` template (recommended).
+- Manually specifying the job definition. Not recommended unless using GitLab
+ 11.8 and earlier.
+
+### Including the provided template
+
+NOTE: **Note:**
+The CI/CD DAST template is supported on GitLab 11.9 and later versions.
+For earlier versions, use the [manual job definition](#manual-job-definition-for-gitlab-115-and-later).
+
+A CI/CD [DAST template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml)
+with the default DAST job definition is provided as a part of your GitLab
+installation which you can [include](../../../ci/yaml/README.md#includetemplate)
+in your `.gitlab-ci.yml` file.
+
+To enable DAST using the provided template, add the following to your `.gitlab-ci.yml`
+file:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+variables:
+ DAST_WEBSITE: https://example.com
+```
+
+The included template will create a `dast` job in your CI/CD pipeline and scan
+your project's source code for possible vulnerabilities.
+
+The report will be saved as a
+[DAST report artifact](../../../ci/yaml/README.md#artifactsreportsdast-ultimate)
+that you can later download and analyze. Due to implementation limitations we
+always take the latest DAST artifact available. Behind the scenes, the
+[GitLab DAST Docker image](https://gitlab.com/gitlab-org/security-products/dast)
+is used to run the tests on the specified URL and scan it for possible vulnerabilities.
+
+There are two ways to define the URL to be scanned by DAST:
+
+- Set the `DAST_WEBSITE` [variable](../../../ci/yaml/README.md#variables).
+- Add it in an `environment_url.txt` file at the root of your project.
+
+#### Authenticated scan
+
+It's also possible to authenticate the user before performing the DAST checks:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+variables:
+ DAST_WEBSITE: https://example.com
+ DAST_AUTH_URL: https://example.com/sign-in
+ DAST_USERNAME: john.doe@example.com
+ DAST_PASSWORD: john-doe-password
+ DAST_USERNAME_FIELD: session[user] # the name of username field at the sign-in HTML form
+ DAST_PASSWORD_FIELD: session[password] # the name of password field at the sign-in HTML form
+ DAST_AUTH_EXCLUDE_URLS: http://example.com/sign-out,http://example.com/sign-out-2 # optional, URLs to skip during the authenticated scan; comma-separated, no spaces in between
+```
+
+The report will be saved as a
+[DAST report artifact](../../../ci/yaml/README.md#artifactsreportsdast-ultimate)
+that you can later download and analyze.
+Due to implementation limitations, we always take the latest DAST artifact available.
+
+#### Full scan
+
+DAST can be configured to perform [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan), which
+includes both passive and active scanning against the same target website:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+variables:
+ DAST_FULL_SCAN_ENABLED: "true"
+```
+
+#### Customizing the DAST settings
+
+The SAST 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 [DAST README](https://gitlab.com/gitlab-org/security-products/dast#settings).
+
+For example:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+variables:
+ DAST_WEBSITE: https://example.com
+ DAST_TARGET_AVAILABILITY_TIMEOUT: 120
+```
+
+Because the template is [evaluated before](../../../ci/yaml/README.md#include) the pipeline
+configuration, the last mention of the variable will take precedence.
+
+#### Overriding the DAST template
+
+If you want to override the job definition (for example, change properties like
+`variables` or `dependencies`), you need to declare a `dast` job after the
+template inclusion and specify any additional keys under it. For example:
+
+```yaml
+include:
+ template: DAST.gitlab-ci.yml
+
+dast:
+ stage: dast # IMPORTANT: don't forget to add this
+ variables:
+ DAST_WEBSITE: https://example.com
+ CI_DEBUG_TRACE: "true"
+```
+
+As the DAST job belongs to a separate `dast` stage that runs after all
+[default stages](../../../ci/yaml/README.md#stages),
+don't forget to add `stage: dast` when you override the template job definition.
+
+### Manual job definition for GitLab 11.5 and later
+
+For GitLab 11.5 and GitLab Runner 11.5 and later, the following `dast`
+job can be added:
+
+```yaml
+dast:
+ image: registry.gitlab.com/gitlab-org/security-products/zaproxy
+ variables:
+ website: "https://example.com"
+ allow_failure: true
+ script:
+ - mkdir /zap/wrk/
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
+ - cp /zap/wrk/gl-dast-report.json .
+ artifacts:
+ reports:
+ dast: gl-dast-report.json
+```
+
+Where the `website` variable holds the URL to run the tests against.
+
+For an authenticated scan, use the following definition:
+
+```yaml
+dast:
+ image: registry.gitlab.com/gitlab-org/security-products/zaproxy
+ variables:
+ website: "https://example.com"
+ login_url: "https://example.com/sign-in"
+ username: "john.doe@example.com"
+ password: "john-doe-password"
+ allow_failure: true
+ script:
+ - mkdir /zap/wrk/
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website
+ --auth-url $login_url
+ --auth-username $username
+ --auth-password $password || true
+ - cp /zap/wrk/gl-dast-report.json .
+ artifacts:
+ reports:
+ dast: gl-dast-report.json
+```
+
+See the [zaproxy documentation](https://gitlab.com/gitlab-org/security-products/zaproxy)
+to learn more about the authentication settings.
+
+### Manual job definition for GitLab 11.4 and earlier (deprecated)
+
+CAUTION: **Caution:**
+Before GitLab 11.5, DAST job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained they have been deprecated
+and may be removed in next major release, GitLab 12.0. You are strongly advised
+to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+dast:
+ image: registry.gitlab.com/gitlab-org/security-products/zaproxy
+ variables:
+ website: "https://example.com"
+ allow_failure: true
+ script:
+ - mkdir /zap/wrk/
+ - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
+ - cp /zap/wrk/gl-dast-report.json .
+ artifacts:
+ paths: [gl-dast-report.json]
+```
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups and projects. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
new file mode 100644
index 00000000000..18df356f846
--- /dev/null
+++ b/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
new file mode 100644
index 00000000000..2d0c2be4233
--- /dev/null
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -0,0 +1,225 @@
+# Dependency Scanning **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5105)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your dependencies for known
+vulnerabilities using Dependency Scanning.
+
+You can take advantage of Dependency Scanning by either [including the CI job](#including-the-provided-template)
+in your existing `.gitlab-ci.yml` file or by implicitly using
+[Auto Dependency Scanning](../../../topics/autodevops/index.md#auto-dependency-scanning-ultimate)
+that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+
+GitLab checks the Dependency Scanning report, compares the found vulnerabilities
+between the source and target branches, and shows the information right on the
+merge request.
+
+![Dependency Scanning Widget](img/dependency_scanning.png)
+
+The results are sorted by the priority of the vulnerability:
+
+1. High
+1. Medium
+1. Low
+1. Unknown
+1. Everything else
+
+## Use cases
+
+It helps to automatically find security vulnerabilities in your dependencies
+while you are developing and testing your applications. For example when your
+application is using an external (open source) library which is known to be vulnerable.
+
+## Requirements
+
+To run a Dependency Scanning job, you need GitLab Runner with the
+[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
+[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
+executor running in privileged mode. If you're using the shared Runners on GitLab.com,
+this is enabled by default.
+
+## Supported languages and package managers
+
+The following languages and dependency managers are supported.
+
+| Language (package managers) | Scan tool |
+|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
+| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [Retire.js](https://retirejs.github.io/retire.js) |
+| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
+| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [bundler-audit](https://github.com/rubysec/bundler-audit) |
+| Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
+| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
+
+Some scanners require to send a list of project dependencies to GitLab's central
+servers to check for vulnerabilities. To learn more about this or to disable it,
+refer to the [GitLab Dependency Scanning tool documentation](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).
+
+## Configuring Dependency Scanning
+
+To enable Dependency Scanning in your project, define a job in your `.gitlab-ci.yml`
+file that generates the
+[Dependency Scanning report artifact](../../../ci/yaml/README.md#artifactsreportsdependency_scanning-ultimate).
+
+This can be done in two ways:
+
+- For GitLab 11.9 and later, including the provided Dependency Scanning
+ `.gitlab-ci.yml` template (recommended).
+- Manually specifying the job definition. Not recommended unless using GitLab
+ 11.8 and earlier.
+
+### Including the provided template
+
+NOTE: **Note:**
+The CI/CD Dependency Scanning template is supported on GitLab 11.9 and later versions.
+For earlier versions, use the [manual job definition](#manual-job-definition-for-gitlab-115-and-later).
+
+A CI/CD [Dependency Scanning template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml)
+with the default Dependency Scanning job definition is provided as a part of your GitLab
+installation which you can [include](../../../ci/yaml/README.md#includetemplate)
+in your `.gitlab-ci.yml` file.
+
+To enable Dependency Scanning using the provided template, add the following to
+your `.gitlab-ci.yml` file:
+
+```yaml
+include:
+ template: Dependency-Scanning.gitlab-ci.yml
+```
+
+The included template will create a `dependency_scanning` job in your CI/CD
+pipeline and scan your project's source code for possible vulnerabilities.
+
+The report will be saved as a
+[Dependency Scanning report artifact](../../../ci/yaml/README.md#artifactsreportsdependency_scanning-ultimate)
+that you can later download and analyze. Due to implementation limitations, we
+always take the latest Dependency Scanning artifact available.
+
+Some security scanners require to send a list of project dependencies to GitLab
+central servers to check for vulnerabilities. To learn more about this or to
+disable it, check the
+[GitLab Dependency Scanning tool documentation](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).
+
+#### Customizing the Dependency Scanning settings
+
+The Dependency Scanning 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
+[Dependency Scanning tool documentation](https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings).
+
+For example:
+
+```yaml
+include:
+ template: Dependency-Scanning.gitlab-ci.yml
+
+variables:
+ DEP_SCAN_DISABLE_REMOTE_CHECKS: true
+```
+
+Because template is [evaluated before](../../../ci/yaml/README.md#include) the pipeline
+configuration, the last mention of the variable will take precedence.
+
+#### Overriding the Dependency Scanning template
+
+If you want to override the job definition (for example, change properties like
+`variables` or `dependencies`), you need to declare a `dependency_scanning` job
+after the template inclusion and specify any additional keys under it. For example:
+
+```yaml
+include:
+ template: Dependency-Scanning.gitlab-ci.yml
+
+dependency_scanning:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+### Manual job definition for GitLab 11.5 and later
+
+For GitLab 11.5 and GitLab Runner 11.5 and later, the following `dependency_scanning`
+job can be added:
+
+```yaml
+dependency_scanning:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export DS_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
+ - |
+ docker run \
+ --env DS_ANALYZER_IMAGES \
+ --env DS_ANALYZER_IMAGE_PREFIX \
+ --env DS_ANALYZER_IMAGE_TAG \
+ --env DS_DEFAULT_ANALYZERS \
+ --env DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ --env DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ --env DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ --env DS_RUN_ANALYZER_TIMEOUT \
+ --volume "$PWD:/code" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_VERSION" /code
+ dependencies: []
+ artifacts:
+ reports:
+ dependency_scanning: gl-dependency-scanning-report.json
+```
+
+You can supply many other [settings variables](https://gitlab.com/gitlab-org/security-products/dependency-scanning#settings)
+via `docker run --env` to customize your job execution.
+
+### Manual job definition for GitLab 11.4 and earlier (deprecated)
+
+CAUTION: **Caution:**
+Before GitLab 11.5, the Dependency Scanning job and artifact had to be named specifically
+to automatically extract the report data and show it in the merge request widget.
+While these old job definitions are still maintained, they have been deprecated
+and may be removed in the next major release, GitLab 12.0. You are strongly advised
+to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+dependency_scanning:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export DS_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
+ - |
+ docker run \
+ --env DS_ANALYZER_IMAGES \
+ --env DS_ANALYZER_IMAGE_PREFIX \
+ --env DS_ANALYZER_IMAGE_TAG \
+ --env DS_DEFAULT_ANALYZERS \
+ --env DS_EXCLUDED_PATHS \
+ --env DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ --env DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ --env DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ --env DS_RUN_ANALYZER_TIMEOUT \
+ --volume "$PWD:/code" \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_VERSION" /code
+ artifacts:
+ paths: [gl-dependency-scanning-report.json]
+```
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups and projects. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
diff --git a/doc/user/application_security/img/create_issue_with_list_hover.png b/doc/user/application_security/img/create_issue_with_list_hover.png
new file mode 100644
index 00000000000..7d70e8299f5
--- /dev/null
+++ b/doc/user/application_security/img/create_issue_with_list_hover.png
Binary files differ
diff --git a/doc/user/application_security/img/interactive_reports.png b/doc/user/application_security/img/interactive_reports.png
new file mode 100644
index 00000000000..9f9812dc69d
--- /dev/null
+++ b/doc/user/application_security/img/interactive_reports.png
Binary files differ
diff --git a/doc/user/application_security/img/issue.png b/doc/user/application_security/img/issue.png
new file mode 100644
index 00000000000..6467201df3f
--- /dev/null
+++ b/doc/user/application_security/img/issue.png
Binary files differ
diff --git a/doc/user/application_security/img/vulnerability_solution.png b/doc/user/application_security/img/vulnerability_solution.png
new file mode 100644
index 00000000000..7443b9b6eea
--- /dev/null
+++ b/doc/user/application_security/img/vulnerability_solution.png
Binary files differ
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
new file mode 100644
index 00000000000..64e72fab198
--- /dev/null
+++ b/doc/user/application_security/index.md
@@ -0,0 +1,104 @@
+# GitLab Secure **[ULTIMATE]**
+
+Check your application for security vulnerabilities that may lead to unauthorized access,
+data leaks, and denial of services. GitLab will perform static and dynamic tests on the
+code of your application, looking for known flaws and report them in the merge request
+so you can fix them before merging. Security teams can use dashboards to get a
+high-level view on projects and groups, and start remediation processes when needed.
+
+## Security scanning tools
+
+GitLab can scan and report any vulnerabilities found in your project.
+
+| Secure scanning tools | Description |
+|:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------|
+| [Container Scanning](container_scanning/index.md) **[ULTIMATE]** | Scan Docker containers for known vulnerabilities. |
+| [Dependency Scanning](dependency_scanning/index.md) **[ULTIMATE]** | Analyze your dependencies for known vulnerabilities. |
+| [Dynamic Application Security Testing (DAST)](dast/index.md) **[ULTIMATE]** | Analyze running web applications for known vulnerabilities. |
+| [License Management](license_management/index.md) **[ULTIMATE]** | Search your project's dependencies for their licenses. |
+| [Security Dashboard](security_dashboard/index.md) **[ULTIMATE]** | View vulnerabilities in all your projects and groups. |
+| [Static Application Security Testing (SAST)](sast/index.md) **[ULTIMATE]** | Analyze source code for known vulnerabilities. |
+
+## Interacting with the vulnerabilities
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 10.8.
+
+CAUTION: **Warning:**
+This feature is currently [Alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga) and while you can start using it, it may receive important changes in the future.
+
+Each security vulnerability in the merge request report or the
+[Security Dashboard](security_dashboard/index.md) is actionable. Clicking on an
+entry, a detailed information will pop up with different possible options:
+
+- [Dismiss vulnerability](#dismissing-a-vulnerability): Dismissing a vulnerability
+ will place a <s>strikethrough</s> styling on it.
+- [Create issue](#creating-an-issue-for-a-vulnerability): The new issue will
+ have the title and description pre-populated with the information from the
+ vulnerability report and will be created as [confidential](../project/issues/confidential_issues.md) by default.
+- [Solution](#solutions-for-vulnerabilities): For some vulnerabilities
+ ([Dependency Scanning](dependency_scanning/index.md) and [Container Scanning](container_scanning/index.md))
+ a solution is provided for how to fix the vulnerability.
+
+![Interacting with security reports](img/interactive_reports.png)
+
+### Dismissing a vulnerability
+
+You can dismiss vulnerabilities by clicking the **Dismiss vulnerability** button.
+This will dismiss the vulnerability and re-render it to reflect its dismissed state.
+If you wish to undo this dismissal, you can click the **Undo dismiss** button.
+
+### Creating an issue for a vulnerability
+
+You can create an issue for a vulnerability by selecting the **Create issue**
+button from within the vulnerability modal or using the action buttons to the right of
+a vulnerability row when in the group security dashboard.
+
+This will create a [confidential issue](../project/issues/confidential_issues.md)
+on the project this vulnerability came from and pre-fill it with some useful
+information taken from the vulnerability report. Once the issue is created, you
+will be redirected to it so you can edit, assign, or comment on it.
+
+Upon returning to the group security dashboard, you'll see that
+the vulnerability will now have an associated issue next to the name.
+
+![Linked issue in the group security dashboard](img/issue.png)
+
+### Solutions for vulnerabilities
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.7.
+
+CAUTION: **Warning:**
+Automatic Patch creation is only available for a subset of
+[Dependency Scanning](dependency_scanning/index.md). At the moment only Node.JS
+projects managed with yarn are supported.
+
+Some vulnerabilities can be fixed by applying the solution that GitLab
+automatically generates.
+
+#### Manually applying the suggested patch
+
+Some vulnerabilities can be fixed by applying a patch that is automatically
+generated by GitLab. To apply the fix:
+
+1. Click on the vulnerability.
+1. Download and review the patch file `remediation.patch`.
+2. Ensure your local project has the same commit checked out that was used to generate the patch.
+3. Run `git apply remediation.patch`.
+4. Verify and commit the changes to your branch.
+
+![Apply patch for dependency scanning](img/vulnerability_solution.png)
+
+#### Creating a merge request from a vulnerability
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9224) in
+ [GitLab Ultimate](https://about.gitlab.com/pricing) 11.9.
+
+In certain cases, GitLab will allow you to create a merge request that will
+automatically remediate the vulnerability. Any vulnerability that has a
+[solution](#solutions-for-vulnerabilities) can have a merge request created to
+automatically solve the issue.
+
+If this action is available there will be a **Create merge request** button in the vulnerability modal.
+Clicking on this button will create a merge request to apply the solution onto the source branch.
+
+![Create merge request from vulnerability](img/create_issue_with_list_hover.png)
diff --git a/doc/user/application_security/license_management/img/license_management.png b/doc/user/application_security/license_management/img/license_management.png
new file mode 100644
index 00000000000..cdce6b5fe38
--- /dev/null
+++ b/doc/user/application_security/license_management/img/license_management.png
Binary files differ
diff --git a/doc/user/application_security/license_management/img/license_management_decision.png b/doc/user/application_security/license_management/img/license_management_decision.png
new file mode 100644
index 00000000000..0763130c375
--- /dev/null
+++ b/doc/user/application_security/license_management/img/license_management_decision.png
Binary files differ
diff --git a/doc/user/application_security/license_management/img/license_management_pipeline_tab.png b/doc/user/application_security/license_management/img/license_management_pipeline_tab.png
new file mode 100644
index 00000000000..80ffca815b9
--- /dev/null
+++ b/doc/user/application_security/license_management/img/license_management_pipeline_tab.png
Binary files differ
diff --git a/doc/user/application_security/license_management/img/license_management_settings.png b/doc/user/application_security/license_management/img/license_management_settings.png
new file mode 100644
index 00000000000..b5490e59074
--- /dev/null
+++ b/doc/user/application_security/license_management/img/license_management_settings.png
Binary files differ
diff --git a/doc/user/application_security/license_management/index.md b/doc/user/application_security/license_management/index.md
new file mode 100644
index 00000000000..8b75995c377
--- /dev/null
+++ b/doc/user/application_security/license_management/index.md
@@ -0,0 +1,264 @@
+# License Management **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5483)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can search your project dependencies for their licenses
+using License Management.
+
+You can take advantage of License Management by either [including the job](#configuring-license-management)
+in your existing `.gitlab-ci.yml` file or by implicitly using
+[Auto License Management](../../../topics/autodevops/index.md#auto-license-management-ultimate)
+that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+
+GitLab checks the License Management report, compares the licenses between the
+source and target branches, and shows the information right on the merge request.
+Blacklisted licenses will be clearly visible with an `x` red icon next to them
+as well as new licenses which need a decision from you. In addition, you can
+[manually approve or blacklist](#project-policies-for-license-management)
+licenses in your project's settings.
+
+NOTE: **Note:**
+If the license management report doesn't have anything to compare to, no information
+will be displayed in the merge request area. That is the case when you add the
+`license_management` job in your `.gitlab-ci.yml` for the first time.
+Consecutive merge requests will have something to compare to and the license
+management report will be shown properly.
+
+![License Management Widget](img/license_management.png)
+
+If you are a project or group Maintainer, you can click on a license to be given
+the choice to approve it or blacklist it.
+
+![License approval decision](img/license_management_decision.png)
+
+## Use cases
+
+It helps you find what licenses your project uses in its dependencies, and decide for each of then
+whether to allow it or forbid it. For example, your application is using an external (open source)
+library whose license is incompatible with yours.
+
+## Supported languages and package managers
+
+The following languages and package managers are supported.
+
+| Language | Package managers |
+|------------|-------------------------------------------------------------------|
+| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/) |
+| Go | [Godep](https://github.com/tools/godep), go get |
+| Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) |
+| .NET | [Nuget](https://www.nuget.org/) |
+| Python | [pip](https://pip.pypa.io/en/stable/) |
+| Ruby | [gem](https://rubygems.org/) |
+
+## Requirements
+
+To run a License Management scanning job, you need GitLab Runner with the
+[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html).
+
+## Configuring License Management
+
+To enable License Management in your project, define a job in your `.gitlab-ci.yml`
+file that generates the [License Management report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate).
+
+This can be done in two ways:
+
+- For GitLab 11.9 and later, including the provided License Management `.gitlab-ci.yml` template (recommended).
+- Manually specifying the job definition. Not recommended unless using GitLab
+ 11.8 and earlier.
+
+### Including the provided template
+
+NOTE: **Note:**
+The CI/CD License Management template is supported on GitLab 11.9 and later versions.
+For earlier versions, use the [manual job definition](#manual-job-definition-for-gitlab-115-and-later).
+
+A CI/CD [License Management template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml)
+with the default License Management job definition is provided as a part of your GitLab
+installation which you can [include](../../../ci/yaml/README.md#includetemplate)
+in your `.gitlab-ci.yml` file.
+
+To enable License Management using the provided template, add the following to
+your `.gitlab-ci.yml` file:
+
+```yaml
+include:
+ template: License-Management.gitlab-ci.yml
+```
+
+The included template will create a `license_management` job in your CI/CD pipeline
+and scan your dependencies to find their licenses.
+
+The report will be saved as a
+[License Management report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate)
+that you can later download and analyze. Due to implementation limitations, we
+always take the latest License Management artifact available. Behind the scenes, the
+[GitLab License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
+is used to detect the languages/frameworks and in turn analyzes the licenses.
+
+#### Installing custom dependencies
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.4.
+
+The `license_management` image already embeds many auto-detection scripts, languages,
+and packages. Nevertheless, it's almost impossible to cover all cases for all projects.
+That's why sometimes it's necessary to install extra packages, or to have extra steps
+in the project automated setup, like the download and installation of a certificate.
+For that, a `LICENSE_MANAGEMENT_SETUP_CMD` environment variable can be passed to the container,
+with the required commands to run before the license detection.
+
+If present, this variable will override the setup step necessary to install all the packages
+of your application (e.g.: for a project with a `Gemfile`, the setup step could be
+`bundle install`).
+
+For example:
+
+```yaml
+include:
+ template: License-Management.gitlab-ci.yml
+
+variables:
+ LICENSE_MANAGEMENT_SETUP_CMD: sh my-custom-install-script.sh
+```
+
+In this example, `my-custom-install-script.sh` is a shell script at the root
+directory of your project.
+
+#### Overriding the template
+
+If you want to override the job definition (for example, change properties like
+`variables` or `dependencies`), you need to declare a `license_management` job
+after the template inclusion and specify any additional keys under it. For example:
+
+```yaml
+include:
+ template: License-Management.gitlab-ci.yml
+
+license_management:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+#### Configuring Maven projects
+
+The License Management tool provides a `MAVEN_CLI_OPTS` environment variable which can hold
+the command line arguments to pass to the `mvn install` command which is executed under the hood.
+Feel free to use it for the customization of Maven execution. For example:
+
+```yaml
+include:
+ template: License-Management.gitlab-ci.yml
+
+license_management:
+ variables:
+ MAVEN_CLI_OPTS: --debug
+```
+
+`mvn install` runs through all of the [build life cycle](http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html)
+stages prior to `install`, including `test`. Running unit tests is not directly
+necessary for the license scanning purposes and consumes time, so it's skipped
+by having the default value of `MAVEN_CLI_OPTS` as `-DskipTests`. If you want
+to supply custom `MAVEN_CLI_OPTS` and skip tests at the same time, don't forget
+to explicitly add `-DskipTests` to your options.
+If you still need to run tests during `mvn install`, add `-DskipTests=false` to
+`MAVEN_CLI_OPTS`.
+
+### Manual job definition for GitLab 11.5 and later
+
+For GitLab 11.5 and GitLab Runner 11.5 and later, the following `license_management`
+job can be added:
+
+```yaml
+license_management:
+ image:
+ name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ entrypoint: [""]
+ stage: test
+ allow_failure: true
+ script:
+ - /run.sh analyze .
+ artifacts:
+ reports:
+ license_management: gl-license-management-report.json
+```
+
+If you want to install custom project dependencies via the `SETUP_CMD` variable:
+
+```yaml
+license_management:
+ image:
+ name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ entrypoint: [""]
+ stage: test
+ variables:
+ SETUP_CMD: ./my-custom-install-script.sh
+ allow_failure: true
+ script:
+ - /run.sh analyze .
+ artifacts:
+ reports:
+ license_management: gl-license-management-report.json
+```
+
+### Manual job definition for GitLab 11.4 and earlier (deprecated)
+
+CAUTION: **Caution:**
+Before GitLab 11.5, the License Management job and artifact had to be named specifically
+to automatically extract the report data and show it in the merge request widget.
+While these old job definitions are still maintained, they have been deprecated
+and may be removed in the next major release, GitLab 12.0. You are strongly advised
+to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the job should look like:
+
+```yaml
+license_management:
+ image:
+ name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ entrypoint: [""]
+ stage: test
+ allow_failure: true
+ script:
+ - /run.sh analyze .
+ artifacts:
+ paths: [gl-license-management-report.json]
+```
+
+## Project policies for License Management
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5940)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.4.
+
+From the project's settings:
+
+- The list of licenses and their status can be managed.
+- Licenses can be manually approved or blacklisted.
+
+To approve or blacklist a license:
+
+1. Either use the **Manage licenses** button in the merge request widget, or
+ navigate to the project's **Settings > CI/CD** and expand the
+ **License Management** section.
+1. Click the **Add a license** button.
+1. In the **License name** dropdown, either:
+ - Select one of the available licenses. You can search for licenses in the field
+ at the top of the list.
+ - Enter arbitrary text in the field at the top of the list. This will cause the text to be
+ added as a license name to the list.
+1. Select the **Approve** or **Blacklist** radio button to approve or blacklist respectively
+ the selected license.
+
+ ![License Management Settings](img/license_management_settings.png)
+
+## License Management report under pipelines
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2.
+
+From your project's left sidebar, navigate to **CI/CD > Pipelines** and click on the
+pipeline ID that has a `license_management` job to see the Licenses tab with the listed
+licenses (if any).
+
+![License Management Pipeline Tab](img/license_management_pipeline_tab.png)
diff --git a/doc/user/application_security/sast/img/sast.png b/doc/user/application_security/sast/img/sast.png
new file mode 100644
index 00000000000..2c75592c32a
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast.png
Binary files differ
diff --git a/doc/user/application_security/sast/img/security_report.png b/doc/user/application_security/sast/img/security_report.png
new file mode 100644
index 00000000000..ba41b707238
--- /dev/null
+++ b/doc/user/application_security/sast/img/security_report.png
Binary files differ
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
new file mode 100644
index 00000000000..02c115b7f22
--- /dev/null
+++ b/doc/user/application_security/sast/index.md
@@ -0,0 +1,254 @@
+# Static Application Security Testing (SAST) **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3775)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3.
+
+NOTE: **4 of the top 6 attacks were application based.**
+Download our whitepaper,
+["A Seismic Shift in Application Security"](https://about.gitlab.com/resources/whitepaper-seismic-shift-application-security/)
+to learn how to protect your organization.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your source code for known
+vulnerabilities using Static Application Security Testing (SAST).
+
+You can take advantage of SAST by either [including the CI job](#configuring-sast) in
+your existing `.gitlab-ci.yml` file or by implicitly using
+[Auto SAST](../../../topics/autodevops/index.md#auto-sast-ultimate)
+that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+
+GitLab checks the SAST report, compares the found vulnerabilities between the
+source and target branches, and shows the information right on the merge request.
+
+![SAST Widget](img/sast.png)
+
+The results are sorted by the priority of the vulnerability:
+
+1. Critical
+1. High
+1. Medium
+1. Low
+1. Unknown
+1. Everything else
+
+## Use cases
+
+- Your code has a potentially dangerous attribute in a class, or unsafe code
+ that can lead to unintended code execution.
+- Your application is vulnerable to cross-site scripting (XSS) attacks that can
+ be leveraged to unauthorized access to session data.
+
+## Requirements
+
+To run a SAST job, you need GitLab Runner with the
+[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
+[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
+executor running in privileged mode. If you're using the shared Runners on GitLab.com,
+this is enabled by default.
+
+## Supported languages and frameworks
+
+The following table shows which languages, package managers and frameworks are supported and which tools are used.
+
+| Language (package managers) / framework | Scan tool | Introduced in GitLab Version |
+|-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------|
+| .NET | [Security Code Scan](https://security-code-scan.github.io) | 11.0 |
+| Any | [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) | 11.9 |
+| C/C++ | [Flawfinder](https://www.dwheeler.com/flawfinder/) | 10.7 |
+| Elixir (Phoenix) | [Sobelow](https://github.com/nccgroup/sobelow) | 11.10 |
+| Go | [Gosec](https://github.com/securego/gosec) | 10.7 |
+| Groovy ([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.3 (Gradle) & 11.9 (Ant, Maven, SBT) |
+| Java ([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 | 10.6 (Maven), 10.8 (Gradle) & 11.9 (Ant, SBT) |
+| Javascript | [ESLint security plugin](https://github.com/nodesecurity/eslint-plugin-security) | 11.8 |
+| Node.js | [NodeJsScan](https://github.com/ajinabraham/NodeJsScan) | 11.1 |
+| PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | 10.8 |
+| Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/openstack/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 |
+
+NOTE: **Note:**
+The Java analyzers can also be used for variants like the
+[Gradle wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html),
+[Grails](https://grails.org/) and the [Maven wrapper](https://github.com/takari/maven-wrapper).
+
+## Configuring SAST
+
+To enable SAST in your project, define a job in your `.gitlab-ci.yml` file that generates the
+[SAST report artifact](../../../ci/yaml/README.md#artifactsreportssast-ultimate).
+
+This can be done in two ways:
+
+- For GitLab 11.9 and later, including the provided SAST `.gitlab-ci.yml` template (recommended).
+- Manually specifying the job definition. Not recommended unless using GitLab
+ 11.8 and earlier.
+
+### Including the provided template
+
+NOTE: **Note:**
+The CI/CD SAST template is supported on GitLab 11.9 and later versions.
+For earlier versions, use the [manual job definition](#manual-job-definition-for-gitlab-115-and-later).
+
+A CI/CD [SAST template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml)
+with the default SAST job definition is provided as a part of your GitLab
+installation which you can [include](../../../ci/yaml/README.md#includetemplate)
+in your `.gitlab-ci.yml` file.
+
+To enable SAST using the provided template, add the following to your `.gitlab-ci.yml`
+file:
+
+```yaml
+include:
+ template: SAST.gitlab-ci.yml
+```
+
+The included template will create a `sast` job in your CI/CD pipeline and scan
+your project's source code for possible vulnerabilities.
+
+The report will be saved as a
+[SAST report artifact](../../../ci/yaml/README.md#artifactsreportssast-ultimate)
+that you can later download and analyze. Due to implementation limitations, we
+always take the latest SAST artifact available. Behind the scenes, the
+[GitLab SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast)
+is used to detect the languages/frameworks and in turn runs the matching scan tools.
+
+#### Customizing the SAST settings
+
+The SAST 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
+[SAST tool documentation](https://gitlab.com/gitlab-org/security-products/sast#settings).
+
+In the following example, we include the SAST template and at the same time we
+set the `SAST_GOSEC_LEVEL` variable to `2`:
+
+```yaml
+include:
+ template: SAST.gitlab-ci.yml
+
+variables:
+ SAST_GOSEC_LEVEL: 2
+```
+
+Because the template is [evaluated before](../../../ci/yaml/README.md#include)
+the pipeline configuration, the last mention of the variable will take precedence.
+
+#### Overriding the SAST template
+
+If you want to override the job definition (for example, change properties like
+`variables` or `dependencies`), you need to declare a `sast` job after the
+template inclusion and specify any additional keys under it. For example:
+
+```yaml
+include:
+ template: SAST.gitlab-ci.yml
+
+sast:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+### Manual job definition for GitLab 11.5 and later
+
+For GitLab 11.5 and GitLab Runner 11.5 and later, the following `sast`
+job can be added:
+
+```yaml
+sast:
+ stage: test
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
+ - |
+ docker run \
+ --env SAST_ANALYZER_IMAGES \
+ --env SAST_ANALYZER_IMAGE_PREFIX \
+ --env SAST_ANALYZER_IMAGE_TAG \
+ --env SAST_DEFAULT_ANALYZERS \
+ --env SAST_EXCLUDED_PATHS \
+ --env SAST_BANDIT_EXCLUDED_PATHS \
+ --env SAST_BRAKEMAN_LEVEL \
+ --env SAST_GOSEC_LEVEL \
+ --env SAST_FLAWFINDER_LEVEL \
+ --env SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ --env SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
+ --env SAST_RUN_ANALYZER_TIMEOUT \
+ --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
+ dependencies: []
+ artifacts:
+ reports:
+ sast: gl-sast-report.json
+```
+
+You can supply many other [settings variables](https://gitlab.com/gitlab-org/security-products/sast#settings)
+via `docker run --env` to customize your job execution.
+
+## Manual job definition for GitLab 11.4 and earlier (deprecated)
+
+CAUTION: **Deprecated:**
+Before GitLab 11.5, the SAST job and artifact had to be named specifically
+to automatically extract report data and show it in the merge request widget.
+While these old job definitions are still maintained, they have been deprecated
+and may be removed in the next major release, GitLab 12.0. You are strongly
+advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+
+For GitLab 11.4 and earlier, the SAST job should look like:
+
+```yaml
+sast:
+ image: docker:stable
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:stable-dind
+ script:
+ - export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
+ - docker run
+ --env SAST_CONFIDENCE_LEVEL="${SAST_CONFIDENCE_LEVEL:-3}"
+ --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
+ artifacts:
+ paths: [gl-sast-report.json]
+```
+
+## Secret detection
+
+GitLab is also able to detect secrets and credentials that have been unintentionally pushed to the repository.
+For example, an API key that allows write access to third-party deployment environments.
+
+This check is performed by a specific analyzer during the `sast` job. It runs regardless of the programming
+language of your app, and you don't need to change anything to your
+CI/CD configuration file to turn it on. Results are available in the SAST report.
+
+GitLab currently includes [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) checks.
+
+## Security report under pipelines
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3776)
+in [GitLab Ultimate](https://about.gitlab.com/pricing) 10.6.
+
+Visit any pipeline page which has a `sast` job and you will be able to see
+the security report tab with the listed vulnerabilities (if any).
+
+![Security Report](img/security_report.png)
+
+## Security Dashboard
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups and projects. Read more about the
+[Security Dashboard](../security_dashboard/index.md).
+
+## Interacting with the vulnerabilities
+
+Once a vulnerability is found, you can interact with it. Read more on how to
+[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
diff --git a/doc/user/application_security/security_dashboard/img/dashboard.png b/doc/user/application_security/security_dashboard/img/dashboard.png
new file mode 100644
index 00000000000..d52a6dacdbf
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/dashboard.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
new file mode 100644
index 00000000000..3294e59e943
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
new file mode 100644
index 00000000000..ca31e15c65f
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -0,0 +1,103 @@
+# GitLab Security Dashboard **[ULTIMATE]**
+
+The Security Dashboard is a good place to get an overview of all the security
+vulnerabilities in your groups and projects.
+
+You can also drill down into a vulnerability and get extra information, see which
+project it comes from, the file it's in, and various metadata to help you analyze
+the risk. You can also action these vulnerabilities by creating an issue for them,
+or by dismissing them.
+
+To benefit from the Security Dashboard you must first configure one of the
+[security reports](../index.md).
+
+## Supported reports
+
+The Security Dashboard supports the following reports:
+
+- [Container Scanning](../container_scanning/index.md)
+- [DAST](../dast/index.md)
+- [Dependency Scanning](../dependency_scanning/index.md)
+- [SAST](../sast/index.md)
+
+## Requirements
+
+To use the project or group security dashboard:
+
+1. At least one project inside a group must be configured with at least one of
+ the [supported reports](#supported-reports).
+2. The configured jobs must use the [new `reports` syntax](../../../ci/yaml/README.md#artifactsreports).
+3. [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or newer must be used.
+ If you're using the shared Runners on GitLab.com, this is already the case.
+
+## Project Security Dashboard
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6165) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.1.
+
+At the project level, the Security Dashboard displays the latest security reports
+for your project. Use it to find and fix vulnerabilities affecting the
+[default branch](../../project/repository/branches/index.md#default-branch).
+
+![Project Security Dashboard](img/project_security_dashboard.png)
+
+## Group Security Dashboard
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6709) in
+ [GitLab Ultimate](https://about.gitlab.com/pricing) 11.5.
+
+The group Security Dashboard gives an overview of the vulnerabilities of all the
+projects in a group and its subgroups.
+
+First, navigate to the Security Dashboard found under your group's
+**Overview > Security Dashboard**.
+
+Once you're on the dashboard, at the top you should see a series of filters for:
+
+- Severity
+- Report type
+- Project
+
+![dashboard with action buttons and metrics](img/dashboard.png)
+
+Selecting one or more filters will filter the results in this page.
+The first section is an overview of all the vulnerabilities, grouped by severity.
+Underneath this overview is a timeline chart that shows how many open
+vulnerabilities your projects had at various points in time. You can filter among 30, 60, and
+90 days, with the default being 90. Hover over the chart to get more details about
+the open vulnerabilities at a specific time.
+
+Finally, there is a list of all the vulnerabilities in the group, sorted by severity.
+In that list, you can see the severity of the vulnerability, its name, its
+confidence (likelihood of the vulnerability to be a positive one), and the project
+it's from.
+
+If you hover over a row, there will appear some actions you can take:
+
+- "More info"
+- "Create issue"
+- "Dismiss vulnerability"
+
+Read more on how to [interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
+
+## Keeping the dashboards up to date
+
+The Security Dashboard displays information from the results of the most recent
+security scan on the [default branch](../../project/repository/branches/index.md#default-branch),
+which means that security scans are performed every time the branch is updated.
+
+If the default branch is updated infrequently, scans are run infrequently and the
+information on the Security Dashboard can become outdated as new vulnerabilities
+are discovered.
+
+To ensure the information on the Security Dashboard is regularly updated,
+[configure a scheduled pipeline](../../project/pipelines/schedules.md) to run a
+daily security scan. This will update the information displayed on the Security
+Dashboard regardless of how often the default branch is updated.
+
+That way, reports are created even if no code change happens.
+
+## Security scans using Auto DevOps
+
+When using [Auto DevOps](../../../topics/autodevops/index.md), use
+[special environment variables](../../../topics/autodevops/index.md#environment-variables)
+to configure daily security scans.
diff --git a/doc/user/discussions/img/mr_review_resolve.png b/doc/user/discussions/img/mr_review_resolve.png
new file mode 100644
index 00000000000..34176f3fa8e
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_resolve.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_resolve2.png b/doc/user/discussions/img/mr_review_resolve2.png
new file mode 100644
index 00000000000..e4adb5f2c2d
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_resolve2.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_second_comment.png b/doc/user/discussions/img/mr_review_second_comment.png
new file mode 100644
index 00000000000..920ea05ad66
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_second_comment.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_second_comment_added.png b/doc/user/discussions/img/mr_review_second_comment_added.png
new file mode 100644
index 00000000000..1fb54348547
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_second_comment_added.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_start.png b/doc/user/discussions/img/mr_review_start.png
new file mode 100644
index 00000000000..38b44bda0d2
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_start.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_unresolve.png b/doc/user/discussions/img/mr_review_unresolve.png
new file mode 100644
index 00000000000..da895ceb89f
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_unresolve.png
Binary files differ
diff --git a/doc/user/discussions/img/mr_review_unresolve2.png b/doc/user/discussions/img/mr_review_unresolve2.png
new file mode 100644
index 00000000000..a824b806e4a
--- /dev/null
+++ b/doc/user/discussions/img/mr_review_unresolve2.png
Binary files differ
diff --git a/doc/user/discussions/img/pending_review_comment.png b/doc/user/discussions/img/pending_review_comment.png
new file mode 100644
index 00000000000..916ef5b7452
--- /dev/null
+++ b/doc/user/discussions/img/pending_review_comment.png
Binary files differ
diff --git a/doc/user/discussions/img/review_comment_quickactions.png b/doc/user/discussions/img/review_comment_quickactions.png
new file mode 100644
index 00000000000..bd9880c329a
--- /dev/null
+++ b/doc/user/discussions/img/review_comment_quickactions.png
Binary files differ
diff --git a/doc/user/discussions/img/review_preview.png b/doc/user/discussions/img/review_preview.png
new file mode 100644
index 00000000000..4bf53a81b9c
--- /dev/null
+++ b/doc/user/discussions/img/review_preview.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index bf41fdecd10..5d69efc3600 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -5,6 +5,7 @@ The ability to contribute conversationally is offered throughout GitLab.
You can leave a comment in the following places:
- issues
+- epics **[ULTIMATE]**
- merge requests
- snippets
- commits
@@ -279,6 +280,75 @@ edit existing comments. Non-team members are restricted from adding or editing c
Additionally, locked issues and merge requests can not be reopened.
+## Merge Request Reviews **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4213) in GitLab 11.4.
+
+When looking at a Merge Request diff, you are able to start a review.
+This allows you to create comments inside a Merge Request that are **only visible to you** until published,
+in order to allow you to submit them all as a single action.
+
+### Starting a review
+
+In order to start a review, simply write a comment on a diff as normal under the **Changes** tab
+in an MR and click on the **Start a review** button.
+
+![Starting a review](img/mr_review_start.png)
+
+Once a review is started, you will see any comments that are part of this review marked `Pending`.
+All comments that are part of a review show two buttons:
+
+- **Submit review**: Submits all comments that are part of the review, making them visible to other users.
+- **Add comment now**: Submits the specific comment as a regular comment instead of as part of the review.
+
+![A comment that is part of a review](img/pending_review_comment.png)
+
+You can use [quick actions] inside review comments. The comment will show the actions that will be performed once published.
+
+![A review comment with quick actions](img/review_comment_quickactions.png)
+
+To add more comments to a review, start writing a comment as normal and click the **Add to review** button.
+
+![Adding a second comment to a review](img/mr_review_second_comment.png)
+
+This will add the comment to the review.
+
+![Second review comment](img/mr_review_second_comment_added.png)
+
+### Resolving/Unresolving discussions
+
+Review comments can also resolve/unresolve [resolvable discussions](#resolvable-comments-and-discussions).
+When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve
+the discussion once published.
+
+![Resolve checkbox](img/mr_review_resolve.png)
+![Unresolve checkbox](img/mr_review_unresolve.png)
+
+If a particular pending comment will resolve or unresolve the discussion, this will be shown on the pending
+comment itself.
+
+![Resolve status](img/mr_review_resolve2.png)
+![Unresolve status](img/mr_review_unresolve2.png)
+
+### Submitting a review
+
+If you have any comments that have not been submitted, you will see a bar at the
+bottom of the screen with two buttons:
+
+- **Discard**: Discards all comments that have not been submitted.
+- **Finish review**: Opens a list of comments ready to be submitted for review.
+ Clicking **Submit review** will publish all comments. Any quick actions
+ submitted are performed at this time.
+
+Alternatively, every pending comment has a button to finish the entire review.
+
+![Review submission](img/review_preview.png)
+
+Submitting the review will send a single email to every notifiable user of the
+merge request with all the comments associated to it.
+
+Replying to this email will, consequentially, create a new comment on the associated merge request.
+
## Filtering notes
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26723) in GitLab 11.5.
@@ -315,11 +385,6 @@ the Merge Request authored by the user that applied them.
![Add a new comment](img/insert_suggestion.png)
- > **Note:**
- The suggestion will only affect the commented line. Multi-line
- suggestions are currently not supported. Will be introduced by
- [#53310](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310).
-
1. In the comment, add your suggestion to the pre-populated code block:
![Add a suggestion into a code block tagged properly](img/make_suggestion.png)
@@ -331,13 +396,10 @@ the Merge Request authored by the user that applied them.
![Apply suggestions](img/suggestion.png)
- > **Note:**
- Discussions are _not_ automatically resolved. Will be introduced by
- [#54405](https://gitlab.com/gitlab-org/gitlab-ce/issues/54405).
-
Once the author applies a suggestion, it will be marked with the **Applied** label,
-and GitLab will create a new commit with the message `Apply suggestion to <file-name>`
-and push the suggested change directly into the codebase in the merge request's branch.
+the discussion will be automatically resolved, and GitLab will create a new commit
+with the message `Apply suggestion to <file-name>` and push the suggested change
+directly into the codebase in the merge request's branch.
[Developer permission](../permissions.md) is required to do so.
> **Note:**
@@ -348,19 +410,23 @@ Custom commit messages will be introduced by
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310) in GitLab 11.10.
-Reviewers can also suggest changes to
-multiple lines with a single suggestion within Merge Request diff discussions.
+Reviewers can also suggest changes to multiple lines with a single suggestion
+within Merge Request diff discussions by adjusting the range offsets. The
+offsets are relative to the position of the diff discussion, and specify the
+range to be replaced by the suggestion when it is applied.
![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png)
-In the example above, the suggestion covers three lines above and four lines below the commented diff line.
-It'd change from 3 lines _above_ to 4 lines _below_ the commented Diff line.
+In the example above, the suggestion covers three lines above and four lines
+below the commented line. When applied, it would replace from 3 lines _above_
+to 4 lines _below_ the commented line, with the suggested change.
![Multi-line suggestion preview](img/multi-line-suggestion-preview.png)
NOTE: **Note:**
-Suggestions covering multiple lines are limited to 100 lines _above_ and 100 lines _below_
-the commented diff line, allowing up to 200 changed lines per suggestion.
+Suggestions covering multiple lines are limited to 100 lines _above_ and 100
+lines _below_ the commented diff line, allowing up to 200 changed lines per
+suggestion.
## Start a discussion by replying to a standard comment
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 984881ef26c..53c82169e15 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -28,6 +28,7 @@ deployments.
| [Helm Tiller](https://docs.helm.sh) | 11.6+ | 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. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 11.6+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a 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. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
| [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
+| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 11.11+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 11.10+ | GitLab Runner is the open source 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. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
NOTE: **Note:**
@@ -38,8 +39,6 @@ applications in a group-level cluster is planned for future releases. For update
- Support installing [JupyterHub in group-level
clusters](https://gitlab.com/gitlab-org/gitlab-ce/issues/51989)
-- Support installing [Prometheus in group-level
- clusters](https://gitlab.com/gitlab-org/gitlab-ce/issues/51963)
## RBAC compatibility
@@ -72,6 +71,29 @@ Add another cluster similar to the first one and make sure to
[set an environment scope](#environment-scopes-premium) that will
differentiate the new cluster from the rest.
+## Gitlab-managed clusters
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5.
+> Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11.
+
+NOTE: **Note:**
+Only available when creating clusters. Existing clusters not managed by GitLab
+cannot become GitLab-managed later.
+
+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
+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.
+
+NOTE: **Note:**
+If you [install applications](#installing-applications) on your cluster, GitLab will create
+the resources required to run these even if you have chosen to manage your own cluster.
+
## Base domain
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8.
@@ -88,7 +110,7 @@ The domain should have a wildcard DNS configured to the Ingress IP address.
When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with
[environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/README.html#limiting-environment-scopes-of-variables-premium)
+[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium)
work.
While evaluating which environment matches the environment scope of a
diff --git a/doc/user/group/contribution_analytics/img/group_stats_cal.png b/doc/user/group/contribution_analytics/img/group_stats_cal.png
new file mode 100644
index 00000000000..0238c7bf088
--- /dev/null
+++ b/doc/user/group/contribution_analytics/img/group_stats_cal.png
Binary files differ
diff --git a/doc/user/group/contribution_analytics/img/group_stats_graph.png b/doc/user/group/contribution_analytics/img/group_stats_graph.png
new file mode 100644
index 00000000000..ccfd3782c6f
--- /dev/null
+++ b/doc/user/group/contribution_analytics/img/group_stats_graph.png
Binary files differ
diff --git a/doc/user/group/contribution_analytics/img/group_stats_table.png b/doc/user/group/contribution_analytics/img/group_stats_table.png
new file mode 100644
index 00000000000..f1d1031fa18
--- /dev/null
+++ b/doc/user/group/contribution_analytics/img/group_stats_table.png
Binary files differ
diff --git a/doc/user/group/contribution_analytics/index.md b/doc/user/group/contribution_analytics/index.md
new file mode 100644
index 00000000000..bc88eff9ed2
--- /dev/null
+++ b/doc/user/group/contribution_analytics/index.md
@@ -0,0 +1,55 @@
+# Contribution Analytics **[STARTER]**
+
+>**Note:**
+Introduced in [GitLab Starter][ee] 8.3.
+
+Track your team members' activity across your organization.
+
+## Overview
+
+With Contribution Analytics you can get an overview of the activity of
+issues, merge requests, and push events of your organization and its members.
+
+The analytics page is located at **Group > Contribution Analytics**
+under the URL `/groups/<groupname>/analytics`.
+
+## Use cases
+
+- Analyze your team's contributions over a period of time and offer a bonus for the top contributors
+- Identify opportunities for improvement with group members who may benefit from additional support
+
+## Using Contribution Analytics
+
+There are three main bar graphs that are deducted from the number of
+contributions per group member. These contributions include push events, merge
+requests and closed issues. Hovering on each bar you can see the number of
+events for a specific member.
+
+![Contribution analytics bar graphs](img/group_stats_graph.png)
+
+## Changing the period time
+
+There are three periods you can choose from, 'Last week', 'Last month' and
+'Last three months'. The default is 'Last week'.
+
+You can choose which period to display by using the dropdown calendar menu in
+the upper right corner.
+
+![Contribution analytics choose period](img/group_stats_cal.png)
+
+## Sorting by different factors
+
+Apart from the bar graphs you can also see the contributions per group member
+which are depicted in a table that can be sorted by:
+
+* Member name
+* Number of pushed events
+* Number of opened issues
+* Number of closed issues
+* Number of opened MRs
+* Number of accepted MRs
+* Number of total contributions
+
+![Contribution analytics contributions table](img/group_stats_table.png)
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/group/epics/img/button_close_epic.png b/doc/user/group/epics/img/button_close_epic.png
new file mode 100644
index 00000000000..aa1a889ea23
--- /dev/null
+++ b/doc/user/group/epics/img/button_close_epic.png
Binary files differ
diff --git a/doc/user/group/epics/img/button_reopen_epic.png b/doc/user/group/epics/img/button_reopen_epic.png
new file mode 100644
index 00000000000..7a953189270
--- /dev/null
+++ b/doc/user/group/epics/img/button_reopen_epic.png
Binary files differ
diff --git a/doc/user/group/epics/img/child_epics_roadmap.png b/doc/user/group/epics/img/child_epics_roadmap.png
new file mode 100644
index 00000000000..819fed58989
--- /dev/null
+++ b/doc/user/group/epics/img/child_epics_roadmap.png
Binary files differ
diff --git a/doc/user/group/epics/img/containing_epic.png b/doc/user/group/epics/img/containing_epic.png
new file mode 100644
index 00000000000..5ed693e6d6a
--- /dev/null
+++ b/doc/user/group/epics/img/containing_epic.png
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view.png b/doc/user/group/epics/img/epic_view.png
new file mode 100644
index 00000000000..dbda98e4351
--- /dev/null
+++ b/doc/user/group/epics/img/epic_view.png
Binary files differ
diff --git a/doc/user/group/epics/img/epics_list_view.png b/doc/user/group/epics/img/epics_list_view.png
new file mode 100644
index 00000000000..b30608d9d31
--- /dev/null
+++ b/doc/user/group/epics/img/epics_list_view.png
Binary files differ
diff --git a/doc/user/group/epics/img/epics_search.png b/doc/user/group/epics/img/epics_search.png
new file mode 100644
index 00000000000..96335527468
--- /dev/null
+++ b/doc/user/group/epics/img/epics_search.png
Binary files differ
diff --git a/doc/user/group/epics/img/epics_sort.png b/doc/user/group/epics/img/epics_sort.png
new file mode 100644
index 00000000000..b23c65fd0ef
--- /dev/null
+++ b/doc/user/group/epics/img/epics_sort.png
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
new file mode 100644
index 00000000000..6035f0c7326
--- /dev/null
+++ b/doc/user/group/epics/index.md
@@ -0,0 +1,217 @@
+# Epics **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate][ee] 10.2.
+
+Epics let you manage your portfolio of projects more efficiently and with less
+effort by tracking groups of issues that share a theme, across projects and
+milestones.
+
+![epics list view](img/epics_list_view.png)
+
+## Use cases
+
+- Suppose your team is working on a large feature that involves multiple discussions throughout different issues created in distinct projects within a [Group](../index.md). With Epics, you can track all the related activities that together contribute to that single feature.
+- Track when the work for the group of issues is targeted to begin, and when it is targeted to end.
+- Discuss and collaborate on feature ideas and scope at a high-level.
+
+## Creating an epic
+
+A paginated list of epics is available in each group from where you can create
+a new epic. The list of epics includes also epics from all subgroups of the
+selected group. From your group page:
+
+1. Go to **Epics**
+1. Click the **New epic** button at the top right
+1. Enter a descriptive title and hit **Create epic**
+
+Once created, you will be taken to the view for that newly-created epic where
+you can change its title, description, start date, and due date.
+
+![epic view](img/epic_view.png)
+
+## Adding an issue to an epic
+
+An epic contains a list of issues and an issue can be associated with at most
+one epic. When on an epic, you can add its associated issues:
+
+1. Click the plus icon (<kbd>+</kbd>) under the epic description.
+1. Paste the link of the issue (you can hit <kbd>Spacebar</kbd> to add more than
+ one issues at a time).
+1. Click **Add**.
+
+Any issue belonging to a project in the epic's group or any of the epic's
+subgroups are eligible to be added. To remove an issue from an epic, click
+on the <kbd>x</kbd> button in the epic's issue list.
+
+NOTE: **Note:**
+When you add an issue or an epic to an epic that's already associated with another epic,
+the issue or the epic is automatically removed from the previous epic.
+
+## Multi-level child epics
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8333) in GitLab Ultimate 11.7.
+
+Much like adding issues to an epic, an epic can have multiple child epics with
+the maximum depth being 5. To add a child epic:
+
+1. Click the plus icon (<kbd>+</kbd>) under the epic description.
+1. Paste the link of the epic.
+1. Click **Add**.
+
+Any epic that belongs to a group or subgroup of the parent epic's group is
+eligible to be added. To remove a child epic from a parent epic,
+click on the <kbd>x</kbd> button in the parent epic's epic list.
+
+## Start date and due date
+
+For each of the dates in the sidebar of an epic, you can choose to either:
+
+- Enter a fixed value.
+- Inherit a dynamic value called "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 attached 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 in that if milestones are re-assigned to the issues, if the
+milestone dates change, or if issues are added or removed from the epic, then
+the re-calculation will happen immediately to set a new dynamic date.
+
+## Roadmap in epics
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.10.
+
+If your epic contains one or more [child epics](#multi-level-child-epics) which
+have a [start or due date](#start-date-and-due-date), then you can see a
+[roadmap](../roadmap/index.md) view of the child epics under the parent epic itself.
+
+![Child epics roadmap](img/child_epics_roadmap.png)
+
+## Reordering issues and child epics
+
+Drag and drop to reorder issues and child epics. New issues and child epics added to an epic appear at the top of the list.
+
+## Deleting an epic
+
+NOTE: **Note:**
+To delete an epic, you need to be an [Owner][permissions] of a group/subgroup.
+
+When inside a single epic view, click the **Delete** button to delete the epic.
+A modal will pop-up to confirm your action.
+
+Deleting an epic releases all existing issues from their associated epic in the
+system.
+
+## Closing and reopening epics
+
+### Using buttons
+
+Whenever you decide that there is no longer need for that epic,
+close the epic using the close button:
+
+![close epic - button](img/button_close_epic.png)
+
+You can always reopen it using the reopen button.
+
+![reopen epic - button](img/button_reopen_epic.png)
+
+### Using quick actions
+
+You can close or reopen an epic using [Quick actions](../../project/quick_actions.md)
+
+## Navigating to an epic from an issue
+
+If an issue belongs to an epic, you can navigate to the containing epic with the
+link in the issue sidebar.
+
+![containing epic](img/containing_epic.png)
+
+## Promoting an issue to an epic
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
+
+If you have [permissions](../../permissions.md) to close an issue and create an
+epic in the parent group, you can promote an issue to an epic with the `/promote`
+[quick action](../../project/quick_actions.md#quick-actions-for-epics-ultimate).
+Only issues from projects that are in groups can be promoted.
+
+When the quick action is executed:
+
+- An epic is created in the same group as the project of the issue.
+- Subscribers of the issue are notified that the epic was created.
+
+The following issue metadata will be copied to the epic:
+
+- Title, description, activity/comment thread.
+- Upvotes/downvotes.
+- Participants.
+- Group labels that the issue already has.
+
+## Searching for an epic from epics list page
+
+> Introduced in [GitLab Ultimate][ee] 10.5.
+
+You can search for an epic from the list of epics using filtered search bar (similar to
+that of Issues and Merge requests) based on following parameters:
+
+- Title or description
+- Author name / username
+- Labels
+
+![epics search](img/epics_search.png)
+
+To search, go to the list of epics and click on the field **Search or filter results...**.
+It will display a dropdown menu, from which you can add an author. You can also enter plain
+text to search by epic title or description. When done, press <kbd>Enter</kbd> on your
+keyboard to filter the list.
+
+You can also sort epics list by:
+
+- **Created date**
+- **Last updated**
+- **Start date**
+- **Due date**
+
+Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [roadmap](../roadmap/index.md).
+
+![epics sort](img/epics_sort.png)
+
+## Permissions
+
+If you have access to view an epic and have access to view an issue already
+added to that epic, then you can view the issue in the epic issue list.
+
+If you have access to edit an epic and have access to edit an issue, then you
+can add the issue to or remove it from the epic.
+
+Note that for a given group, the visibility of all projects must be the same as
+the group, or less restrictive. That means if you have access to a group's epic,
+then you already have access to its projects' issues.
+
+You may also consult the [group permissions table][permissions].
+
+[ee]: https://about.gitlab.com/pricing/
+[permissions]: ../../permissions.md#group-members-permissions
+
+## Thread
+
+- Comments: collaborate on that epic by posting comments in its thread.
+These text fields also fully support
+[GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm).
+
+## Comment, or start a discussion
+
+Once you wrote your comment, you can either:
+
+- Click "Comment" and your comment will be published.
+- Click "Start discussion": start a thread within that epic's thread to discuss specific points.
+
+## Award emoji
+
+- You can [award an emoji](../../award_emojis.md) to that epic or its comments.
+
+## Notifications
+
+- [Receive notifications](../../../workflow/notifications.md) for epic events.
diff --git a/doc/user/group/img/group_file_template_dropdown.png b/doc/user/group/img/group_file_template_dropdown.png
new file mode 100644
index 00000000000..d9caae1a223
--- /dev/null
+++ b/doc/user/group/img/group_file_template_dropdown.png
Binary files differ
diff --git a/doc/user/group/img/group_file_template_settings.png b/doc/user/group/img/group_file_template_settings.png
new file mode 100644
index 00000000000..ca42f7726db
--- /dev/null
+++ b/doc/user/group/img/group_file_template_settings.png
Binary files differ
diff --git a/doc/user/group/img/group_issue_board.png b/doc/user/group/img/group_issue_board.png
new file mode 100644
index 00000000000..a0da74a320f
--- /dev/null
+++ b/doc/user/group/img/group_issue_board.png
Binary files differ
diff --git a/doc/user/group/img/member_lock.png b/doc/user/group/img/member_lock.png
new file mode 100644
index 00000000000..3f1382e76c6
--- /dev/null
+++ b/doc/user/group/img/member_lock.png
Binary files differ
diff --git a/doc/user/group/img/membership_lock.png b/doc/user/group/img/membership_lock.png
deleted file mode 100644
index c9ad82c90f2..00000000000
--- a/doc/user/group/img/membership_lock.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 74735886350..06564fd6cd1 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -5,10 +5,12 @@ and grant members access to several projects at once.
Groups can also be nested in [subgroups](subgroups/index.md).
-Find your groups by expanding the left menu and clicking **Groups**:
+Find your groups by clicking **Groups** in the top navigation.
![GitLab Groups](img/groups.png)
+> The groups dropdown in the top navigation was [introduced][ce-36234] in [GitLab 11.1](https://about.gitlab.com/2018/07/22/gitlab-11-1-released/#groups-dropdown-in-navigation).
+
The Groups page displays all groups you are a member of, how many projects it holds,
how many members it has, the group visibility, and, if you have enough permissions,
a link to the group settings. By clicking the last button you can leave that group.
@@ -44,7 +46,7 @@ For example, consider a user named Alex:
1. Alex creates an account on GitLab.com with the username `alex`;
their profile will be accessed under `https://gitlab.example.com/alex`
-1. Alex creates a group for their team with the groupname `alex-team`;
+1. Alex creates a group for their team with the group name `alex-team`;
the group and its projects will be accessed under `https://gitlab.example.com/alex-team`
1. Alex creates a subgroup of `alex-team` with the subgroup name `marketing`;
this subgroup and its projects will be accessed under `https://gitlab.example.com/alex-team/marketing`
@@ -68,7 +70,7 @@ together in a single list view.
You can create a group in GitLab from:
-1. The Groups page: expand the left menu, click **Groups**, and click the green button **New group**:
+1. The Groups page: from the top menu, click **Groups**, and click the green button **New group**:
![new group from groups page](img/new_group_from_groups.png)
@@ -80,14 +82,21 @@ Add the following information:
![new group info](img/create_new_group_info.png)
-1. Set the **Group path** which will be the **namespace** under which your projects
- will be hosted (path can contain only letters, digits, underscores, dashes
- and dots; it cannot start with dashes or end in dot).
-1. The **Group name** will populate with the path. Optionally, you can change
- it. This is the name that will display in the group views.
-1. Optionally, you can add a description so that others can briefly understand
+1. The **Group name** will populate the URL automatically. Optionally, you can change it.
+ This is the name that is displayed in the group views.
+ The name can contain only:
+ - Alphanumeric characters.
+ - Underscores.
+ - Dashes and dots.
+ - Spaces.
+1. The **Group URL**, which will be the namespace under which your projects will be hosted.
+ The URL can contain only:
+ - Alphanumeric characters.
+ - Underscores.
+ - Dashes and dots. It cannot start with dashes or end in dot.
+1. Optionally, you can add a brief description to tell others
what this group is about.
-1. Optionally, choose an avatar for your project.
+1. Optionally, choose an avatar for your group.
1. Choose the [visibility level](../../public_access/public_access.md).
## Add users to a group
@@ -153,10 +162,14 @@ There are two different ways to add a new project to a group:
### Default project creation level
+> [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
+> Brought to [GitLab Starter][ee] in 10.7.
+> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
+
Group owners or administrators can allow users with the
Developer role to create projects under groups.
-By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under agroup, but this can be changed either within the group settings for a group, or
+By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under a group, but this can be changed either within the group settings for a group, or
be set globally by a GitLab administrator in the Admin area
at **Settings > General > Visibility and access controls**.
@@ -178,6 +191,32 @@ Alternatively, you can [lock the sharing with group feature](#share-with-group-l
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for more information.
+## Epics **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate][ee] 10.2.
+
+Epics let you manage your portfolio of projects more efficiently and with less
+effort by tracking groups of issues that share a theme, across projects and
+milestones.
+
+[Learn more about Epics.](epics/index.md)
+
+## Group Security Dashboard **[ULTIMATE]**
+
+Get an overview of the vulnerabilities of all the projects in a group and its subgroups.
+
+[Learn more about the Group Security Dashboard.](security_dashboard/index.md)
+
+## Insights **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate][ee] 11.9 behind the `insights` feature flag.
+
+Configure the Insights that matter for your groups or projects to explore data
+such as triage hygiene, issues created/closed per a given period, average time
+for merge requests to be merged and much more.
+
+[Learn more about Insights](insights/index.md).
+
## Transferring groups
From GitLab 10.5, groups can be transferred in the following ways:
@@ -250,7 +289,7 @@ working together in a project.
To inherit the group membership, you share the project between the
two groups A and B. **Share with group lock** prevents any project within
the group from being shared with another group. By doing so, you
-guarantee only the right group members have access to that projects.
+guarantee only the right group members have access to those projects.
To enable this feature, navigate to the group settings page. Select
**Share with group lock** and **Save the group**.
@@ -259,17 +298,50 @@ To enable this feature, navigate to the group settings page. Select
#### Member Lock **[STARTER]**
-With **Member Lock** it is possible to lock membership in project to the
-level of members in group.
+With Member lock, it is possible to lock membership in a project to the
+level of members in the group.
+
+Member lock lets a group owner lock down any new project membership to all the
+projects within the group, allowing tighter control over project membership.
-Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#member-lock).
+For instance, if you want to lock the group for an [Audit Event](https://docs.gitlab.com/ee/administration/audit_events.html),
+you enable Member lock to guarantee that membership of a project cannot be modified during that audit.
-#### Group-level file templates **[PREMIUM]**
+To enable this feature:
-Group-level file templates allow you to share a set of templates for common file
-types with every project in a group.
+1. Navigate to the group's **Settings > General** page.
+1. Expand the **Permissions, LFS, 2FA** section and select **Member lock**.
+1. Click the **Save changes** button.
-Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium).
+![Checkbox for membership lock](img/member_lock.png)
+
+This will disable the option for all users who previously had permissions to
+operate project memberships so no new users can be added. Furthermore, any
+request to add a new user to a project through API will not be possible.
+
+#### Group file templates **[PREMIUM]**
+
+Group file templates allow you to share a set of templates for common file
+types with every project in a group. It is analogous to the
+[instance template repository](https://docs.gitlab.com/ee/user/admin_area/settings/instance_template_repository.html)
+feature, and the selected project should follow the same naming conventions as
+are documented on that page.
+
+Only projects that are in the group may be chosen as the source of templates.
+This includes projects shared with the group, but **excludes** projects in
+subgroups or parent groups of the group being configured.
+
+This feature may be configured for subgroups as well as parent groups. A project
+in a subgroup will have access to the templates for that subgroup, as well as
+any parent groups.
+
+![Group file template dropdown](img/group_file_template_dropdown.png)
+
+To enable this feature, navigate to the group settings page, expand the
+**Templates** section, choose a project to act as the template repository, and
+**Save group**.
+
+![Group file template settings](img/group_file_template_settings.png)
#### Group-level project templates **[PREMIUM]**
@@ -282,6 +354,19 @@ Define project templates at a group-level by setting a group as a template sourc
access each project's settings, and remove any project from the same screen.
- **Webhooks**: configure [webhooks](../project/integrations/webhooks.md) to your group.
- **Kubernetes cluster integration**: connect your GitLab group with [Kubernetes clusters](clusters/index.md).
-- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html#audit-events)
+- **Audit Events**: view [Audit Events](https://docs.gitlab.com/ee/administration/audit_events.html)
for the group. **[STARTER ONLY]**
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group.
+
+## User contribution analysis **[STARTER]**
+
+With [GitLab Contribution Analytics](contribution_analytics/index.md)
+you have an overview of the contributions (pushes, merge requests,
+and issues) performed by your group members.
+
+## Issues analytics **[PREMIUM]**
+
+With [GitLab Issues Analytics](issues_analytics/index.md), in groups, you can see a bar chart of the number of issues created each month.
+
+[ee]: https://about.gitlab.com/pricing/
+[ee-2534]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2534
diff --git a/doc/user/group/insights/img/insights_example_stacked_bar_chart.png b/doc/user/group/insights/img/insights_example_stacked_bar_chart.png
new file mode 100644
index 00000000000..791d0e4bcdf
--- /dev/null
+++ b/doc/user/group/insights/img/insights_example_stacked_bar_chart.png
Binary files differ
diff --git a/doc/user/group/insights/img/insights_group_configuration.png b/doc/user/group/insights/img/insights_group_configuration.png
new file mode 100644
index 00000000000..0af0073e448
--- /dev/null
+++ b/doc/user/group/insights/img/insights_group_configuration.png
Binary files differ
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
new file mode 100644
index 00000000000..5283d8da77d
--- /dev/null
+++ b/doc/user/group/insights/index.md
@@ -0,0 +1,39 @@
+# Insights **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.9 behind the `insights` feature flag.
+
+CAUTION: **Beta:**
+Insights is considered beta, and is not ready for production use.
+Follow [gitlab-org&725](https://gitlab.com/groups/gitlab-org/-/epics/725) for
+updates.
+
+Configure the Insights that matter for your groups to explore data such as
+triage hygiene, issues created/closed per a given period, average time for merge
+requests to be merged and much more.
+
+![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png)
+
+## Configure your Insights
+
+Navigate to your group's **Settings > General**, expand **Insights**, and choose
+the project that holds your `.gitlab/insights.yml` configuration file:
+
+![group insights configuration](img/insights_group_configuration.png)
+
+If no configuration was set, a [default configuration file](
+https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/fixtures/insights/ee/fixtures/insights/default.yml)
+will be used.
+
+See the [Project's Insights documentation](https://docs.gitlab.com/ee/user/project/insights/index.html) for
+more details about the `.gitlab/insights.yml` configuration file.
+
+## Permissions
+
+If you have access to view a group, then you have access to view their Insights.
+
+NOTE: **Note:**
+Issues or merge requests that you don't have access to (because you don't have
+access to the project they belong to, or because they are confidential) are
+filtered out of the Insights charts.
+
+You may also consult the [group permissions table](../../permissions.md#group-members-permissions).
diff --git a/doc/user/group/issues_analytics/img/issues_created_per_month.png b/doc/user/group/issues_analytics/img/issues_created_per_month.png
new file mode 100644
index 00000000000..96f0d36c917
--- /dev/null
+++ b/doc/user/group/issues_analytics/img/issues_created_per_month.png
Binary files differ
diff --git a/doc/user/group/issues_analytics/index.md b/doc/user/group/issues_analytics/index.md
new file mode 100644
index 00000000000..cf53d7423a6
--- /dev/null
+++ b/doc/user/group/issues_analytics/index.md
@@ -0,0 +1,16 @@
+# Issues Analytics **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7478) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
+
+GitLab by default displays a bar chart of the number of issues created each month, for the
+current month, and 12 months prior, for a total of 13 months.
+
+You can change the total number of months displayed by setting a URL parameter.
+For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
+would show a total of 15 months for the chart in the GitLab.org group.
+
+The **Search or filter results...** field can be used for filtering the issues by any attribute. For example, labels, assignee, milestone, and author.
+
+To access the chart, navigate to a group's sidebar and select **Issues > Analytics**.
+
+![Issues created per month](img/issues_created_per_month.png)
diff --git a/doc/user/group/roadmap/img/epics_state_dropdown.png b/doc/user/group/roadmap/img/epics_state_dropdown.png
new file mode 100644
index 00000000000..cbcc3658a54
--- /dev/null
+++ b/doc/user/group/roadmap/img/epics_state_dropdown.png
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_timeline_months.png b/doc/user/group/roadmap/img/roadmap_timeline_months.png
new file mode 100644
index 00000000000..5a046b21507
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_timeline_months.png
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_timeline_quarters.png b/doc/user/group/roadmap/img/roadmap_timeline_quarters.png
new file mode 100644
index 00000000000..56f428cb471
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_timeline_quarters.png
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_timeline_weeks.png b/doc/user/group/roadmap/img/roadmap_timeline_weeks.png
new file mode 100644
index 00000000000..bf4c1dc0284
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_timeline_weeks.png
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_view.png b/doc/user/group/roadmap/img/roadmap_view.png
new file mode 100644
index 00000000000..ff41a2e0441
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_view.png
Binary files differ
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
new file mode 100644
index 00000000000..310c4bb88d0
--- /dev/null
+++ b/doc/user/group/roadmap/index.md
@@ -0,0 +1,64 @@
+# Roadmap **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing) 10.5.
+
+An Epic within a group containing **Start date** and/or **Due date**
+can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page
+shows such a visualization for all the epics which are under a group and/or its subgroups.
+
+![roadmap view](img/roadmap_view.png)
+
+A dropdown allows you to show only open or closed epics. By default, all epics are shown.
+
+![epics state dropdown](img/epics_state_dropdown.png)
+
+Epics in the view can be sorted by:
+
+- **Created date**
+- **Last updated**
+- **Start date**
+- **Due date**
+
+Each option contains a button that can toggle the order between **ascending** and **descending**. The sort option and order will be persisted to be used wherever epics are browsed including the [epics list view](../epics/index.md).
+
+Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
+
+## Timeline duration
+
+Starting with [GitLab Ultimate][ee] 11.0, Roadmap supports three different date ranges; Quarters, Months (Default) and Weeks.
+
+### Quarters
+
+![roadmap date range in quarters](img/roadmap_timeline_quarters.png)
+
+In _Quarters_ preset, roadmap shows epics which have start or due dates _falling within_ or
+_going through_ **past quarter**, **current quarter** and **next 4 quarters**, where _today_
+is shown by the vertical red line in the timeline. The sub-headers underneath the quarter name on
+the timeline header represent the month of the quarter.
+
+### Months
+
+![roadmap date range in months](img/roadmap_timeline_months.png)
+
+In _Months_ preset, roadmap shows epics which have start or due dates _falling within_ or
+_going through_ **past month**, **current month** and **next 5 months**, where _today_
+is shown by the vertical red line in the timeline. The sub-headers underneath the month name on
+the timeline header represent the date on starting day (Sunday) of the week. This preset is
+selected by default.
+
+### Weeks
+
+![roadmap date range in weeks](img/roadmap_timeline_weeks.png)
+
+In _Weeks_ preset, roadmap shows epics which have start or due dates _falling within_ or
+_going through_ **past week**, **current week** and **next 4 weeks**, where _today_
+is shown by the vertical red line in the timeline. The sub-headers underneath the week name on
+the timeline header represent the days of the week.
+
+## Timeline bar for an epic
+
+The timeline bar indicates the approximate position of an epic based on its start
+and due date. If an epic doesn't have a due date, the timeline bar fades
+away towards the future. Similarly, if an epic doesn't have a start date, the
+timeline bar becomes more visible as it approaches the epic's due date on the
+timeline.
diff --git a/doc/user/group/saml_sso/img/group_saml_configuration_information.png b/doc/user/group/saml_sso/img/group_saml_configuration_information.png
new file mode 100644
index 00000000000..98b83d0cb0f
--- /dev/null
+++ b/doc/user/group/saml_sso/img/group_saml_configuration_information.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/group_saml_settings.png b/doc/user/group/saml_sso/img/group_saml_settings.png
new file mode 100644
index 00000000000..d95acb5075f
--- /dev/null
+++ b/doc/user/group/saml_sso/img/group_saml_settings.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/scim_advanced.png b/doc/user/group/saml_sso/img/scim_advanced.png
new file mode 100644
index 00000000000..3b70e3fbe83
--- /dev/null
+++ b/doc/user/group/saml_sso/img/scim_advanced.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/scim_attribute_mapping.png b/doc/user/group/saml_sso/img/scim_attribute_mapping.png
new file mode 100644
index 00000000000..c9f6b71f5b0
--- /dev/null
+++ b/doc/user/group/saml_sso/img/scim_attribute_mapping.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/scim_token.png b/doc/user/group/saml_sso/img/scim_token.png
new file mode 100644
index 00000000000..7eb52bf6ea2
--- /dev/null
+++ b/doc/user/group/saml_sso/img/scim_token.png
Binary files differ
diff --git a/doc/user/group/saml_sso/img/unlink_group_saml.png b/doc/user/group/saml_sso/img/unlink_group_saml.png
new file mode 100644
index 00000000000..0561443b5f4
--- /dev/null
+++ b/doc/user/group/saml_sso/img/unlink_group_saml.png
Binary files differ
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
new file mode 100644
index 00000000000..ee3137d032e
--- /dev/null
+++ b/doc/user/group/saml_sso/index.md
@@ -0,0 +1,91 @@
+# SAML SSO for GitLab.com Groups **[SILVER ONLY]**
+
+> Introduced in [GitLab.com Silver](https://about.gitlab.com/pricing/) 11.0.
+
+NOTE: **Note:**
+This topic is for SAML on GitLab.com Silver tier and above. For SAML on self-managed GitLab instances, see [SAML OmniAuth Provider](../../../integration/saml.md).
+
+Currently SAML on GitLab.com can be used to automatically add users to a group, and does not yet sign users into GitLab.com. Users should already have an account on the GitLab instance, or can create one when logging in for the first time.
+
+User synchronization for GitLab.com is partially supported using [SCIM](scim_setup.md).
+
+NOTE: **Note:**
+SAML SSO for groups is used only as a convenient way to add users and does not sync users between providers without using SCIM. If a group is not using SCIM, group Owners will still need to manage user accounts, such as removing users when necessary.
+
+## Configuring your Identity Provider
+
+1. Navigate to the group and click **Settings > SAML SSO**.
+1. Configure your SAML server using the **Assertion consumer service URL** and **Issuer**. See [your identity provider's documentation](#providers) for more details.
+1. Configure the SAML response to include a NameID that uniquely identifies each user.
+1. Configure required assertions using the [table below](#assertions).
+1. Once the identity provider is set up, move on to [configuring GitLab](#configuring-gitlab).
+
+![Issuer and callback for configuring SAML identity provider with GitLab.com](img/group_saml_configuration_information.png)
+
+NOTE: **Note:**
+Partial SSO enforcement was introduced in [11.8](https://gitlab.com/gitlab-org/gitlab-ee/issues/5291). With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users can no longer be added manually. After a user has been added to the group, GitLab does not continue to enforce the use of SSO, but we'll [add a persistent check](https://gitlab.com/gitlab-org/gitlab-ee/issues/9255) in a later version.
+
+### 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 unique ID or username. Email could also be used as the NameID, but only if it can be guaranteed to never change.
+
+### Assertions
+
+| Field | Supported keys | Notes |
+|-|----------------|-------------|
+| Email | `email`, `mail` | (required) |
+| Full Name | `name` | |
+| First Name | `first_name`, `firstname`, `firstName` | |
+| Last Name | `last_name`, `lastname`, `lastName` | |
+
+## Configuring GitLab
+
+Once you've set up your identity provider to work with GitLab, you'll need to configure GitLab to use it for authentication:
+
+1. Navigate to the group's **Settings > SAML SSO**.
+1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign on URL** field.
+1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
+1. Check the **Enable SAML authentication for this group** checkbox.
+1. Click the **Save changes** button.
+
+![Group SAML Settings for GitLab.com](img/group_saml_settings.png)
+
+## Providers
+
+| Provider | Documentation |
+|----------|---------------|
+| ADFS (Active Directory Federation Services) | [Create a Relying Party Trust](https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-relying-party-trust) |
+| Azure | [Configuring single sign-on to applications](https://docs.microsoft.com/en-us/azure/active-directory/active-directory-saas-custom-apps) |
+| 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) |
+| Okta | [Setting up a SAML application in Okta](https://developer.okta.com/standards/SAML/setting_up_a_saml_application_in_okta) |
+| 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://docs.pingidentity.com/bundle/p1_enterpriseConfigSsoSaml_cas/page/enableAppWithoutURL.html) |
+
+## Unlinking accounts
+
+Users can unlink SAML for a group from their profile page. This can be helpful if:
+
+- You no longer want a group to be able to sign you in to GitLab.com.
+- Your SAML NameID has changed and so GitLab can no longer find your user.
+
+For example, to unlink the `MyOrg` account, the following **Disconnect** button will be available under **Profile > Accounts**:
+
+![Unlink Group SAML](img/unlink_group_saml.png)
+
+## Glossary
+
+| Term | Description |
+|------|-------------|
+| Identity Provider | The service which manages your user identities such as ADFS, Okta, Onelogin or Ping Identity. |
+| Service Provider | SAML considers GitLab to be a service provider. |
+| Assertion | A piece of information about a user's identity, such as their name or role. Also know as claims or attributes. |
+| SSO | Single Sign On. |
+| Assertion consumer service URL | The callback on GitLab where users will be redirected after successfully authenticating with the identity provider. |
+| Issuer | How GitLab identifies itself to the identity provider. Also known as a "Relying party trust identifier". |
+| Certificate fingerprint | Used to confirm that communications over SAML are secure by checking that the server is signing communications with the correct certificate. Also known as a certificate thumbprint. |
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
new file mode 100644
index 00000000000..ec27ec3e336
--- /dev/null
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -0,0 +1,102 @@
+# SCIM provisioning using SAML SSO for Groups **[SILVER ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab.com Silver](https://about.gitlab.com/pricing/) 11.10.
+
+GitLab's [SCIM API](https://docs.gitlab.com/ee/api/scim.html) implements part of [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
+
+Currently, the following actions are available:
+
+- CREATE
+- UPDATE
+- DELETE (deprovisioning)
+
+The following identity providers are supported:
+
+- Azure
+
+## 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
+
+Once [Single sign-on](index.md) has been configured, we can:
+
+1. Navigate to the group and click **Settings > SAML SSO**.
+1. Click on the **Generate a SCIM token** button.
+1. Save the token and URL so they can be used in the next step.
+
+![SCIM token configuration](img/scim_token.png)
+
+## SCIM IdP configuration
+
+### Configuration on Azure
+
+In the [Single sign-on](index.md) configuration for the group, make sure
+that the **Name identifier value** (NameID) points to a unique identifier, such
+as the `user.objectid`. This will match the `extern_uid` used on GitLab.
+
+The GitLab app in Azure needs to be configured following
+[Azure's SCIM setup](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/use-scim-to-provision-users-and-groups#getting-started).
+
+Note the following:
+
+- The `Tenant URL` and `secret token` are the ones retrieved in the
+[previous step](#gitlab-configuration).
+- Should there be any problems with the availability of GitLab or similar
+errors, the notification email set will get those.
+- For mappings, we will only leave `Synchronize Azure Active Directory Users to AppName` enabled.
+
+You can then test the connection clicking on `Test Connection`.
+
+### Synchronize Azure Active Directory users
+
+1. Click on `Synchronize Azure Active Directory Users to AppName`, to configure
+the attribute mapping.
+1. Select the unique identifier (in the example `objectId`) as the `id` and `externalId`,
+and enable the `Create`, `Update`, and `Delete` actions.
+1. Map the `userPricipalName` to `emails[type eq "work"].value` and `mailNickname` to
+`userName`.
+
+ Example configuration:
+
+ ![Azure's attribute mapping configuration](img/scim_attribute_mapping.png)
+
+1. Click on **Show advanced options > Edit attribute list for AppName**.
+1. Leave the `id` as the primary and only required field.
+
+ NOTE: **Note:**
+ `username` should neither be primary nor required as we don't support
+ that field on GitLab SCIM yet.
+
+ ![Azure's attribute advanced configuration](img/scim_advanced.png)
+
+1. Save all the screens and, in the **Provisioning** step, set
+the `Provisioning Status` to `ON`.
+
+ NOTE: **Note:**
+ You can control what is actually synced by selecting the `Scope`. For example,
+ `Sync only assigned users and groups` will only sync the users assigned to
+ the application (`Users and groups`), otherwise it will sync the whole Active Directory.
+
+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.
diff --git a/doc/user/group/security_dashboard/index.md b/doc/user/group/security_dashboard/index.md
new file mode 100644
index 00000000000..43e910b29fe
--- /dev/null
+++ b/doc/user/group/security_dashboard/index.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html).
diff --git a/doc/user/index.md b/doc/user/index.md
index 8164b31c37e..67d5cbf06ec 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -51,19 +51,20 @@ With GitLab Enterprise Edition, you can also:
- Provide support with [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html).
- Improve collaboration with
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals),
+ [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals-starter),
[Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
and [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards-starter).
- Create formal relationships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html).
- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software.
-- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html) and [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html) for faster, more advanced code search across your entire GitLab instance.
+- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) 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](https://docs.gitlab.com/ee/integration/kerberos.html).
-- [Mirror a repository](https://docs.gitlab.com/ee/workflow/repository_mirroring.html) from elsewhere on your local server.
+- [Mirror a repository](../workflow/repository_mirroring.md) from elsewhere on your local server.
- [Export issues as CSV](https://docs.gitlab.com/ee/user/project/issues/csv_export.html).
- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html).
- [Lock files](https://docs.gitlab.com/ee/user/project/file_lock.html) to prevent conflicts.
- View the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
- Leverage continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html).
+- Scan your code for vulnerabilities and [display them in merge requests](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more.
@@ -106,8 +107,8 @@ to enjoy the best of GitLab.
- [Permissions](permissions.md): Learn the different set of permissions levels for each
user type (guest, reporter, developer, maintainer, owner).
- [Feature highlight](feature_highlight.md): Learn more about the little blue dots
- around the app that explain certain features
-- [Abuse reports](abuse_reports.md): Report abuse from users to GitLab administrators
+ around the app that explain certain features.
+- [Abuse reports](abuse_reports.md): Report abuse from users to GitLab administrators.
## Groups
@@ -171,3 +172,8 @@ Learn what is [Git](../topics/git/index.md) and its best practices.
## Instance statistics
See [various statistics](instance_statistics/index.md) of your GitLab instance.
+
+## Operations Dashboard **[PREMIUM]**
+
+See [Operations Dashboard](operations_dashboard/index.md) for a summary of each
+project's operational health.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 9891a43aa61..c1c2c036bae 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -20,6 +20,7 @@ You can use GFM in the following areas:
- Snippets (the snippet must be named with a `.md` extension)
- Wiki pages
- Markdown documents inside repositories
+- Epics **[ULTIMATE]**
You can also use other rich text files in GitLab. You might have to install a
dependency to do so. Please see the [`github-markup` gem readme](https://github.com/gitlabhq/markup#markups) for more information.
@@ -320,6 +321,7 @@ GFM will recognize the following:
| `#12345` | issue |
| `!123` | merge request |
| `$123` | snippet |
+| `&123` | epic **[ULTIMATE]** |
| `~123` | label by ID |
| `~bug` | one-word label by name |
| `~"feature request"` | multi-word label by name |
@@ -340,6 +342,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project%123` | project milestone |
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
+| `group1/subgroup&123` | epic **[ULTIMATE]** |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
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
new file mode 100644
index 00000000000..9d6a509ea72
--- /dev/null
+++ b/doc/user/operations_dashboard/img/index_operations_dashboard_top_bar_icon.png
Binary files differ
diff --git a/doc/user/operations_dashboard/img/index_operations_dashboard_with_projects.png b/doc/user/operations_dashboard/img/index_operations_dashboard_with_projects.png
new file mode 100644
index 00000000000..ee33eee8134
--- /dev/null
+++ b/doc/user/operations_dashboard/img/index_operations_dashboard_with_projects.png
Binary files differ
diff --git a/doc/user/operations_dashboard/index.md b/doc/user/operations_dashboard/index.md
new file mode 100644
index 00000000000..66362f27299
--- /dev/null
+++ b/doc/user/operations_dashboard/index.md
@@ -0,0 +1,37 @@
+# Operations Dashboard **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5781)
+in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.5.
+[Moved](https://gitlab.com/gitlab-org/gitlab-ee/issues/9218) to
+[GitLab Premium](https://about.gitlab.com/pricing/) in 11.10.
+
+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)
+
+## Adding a project to the dashboard
+
+NOTE: **Note:**
+For GitLab.com, the Operations Dashboard is available for free for public projects.
+If your project is private, the group it belongs to must have a
+[Gold](https://about.gitlab.com/pricing/) plan.
+
+To add a project to the dashboard:
+
+1. Click the **Add projects** button in the homescreen of the dashboard.
+1. Search and add one or more projects using the **Search your projects** field.
+1. Click the **Add projects** button.
+
+Once added, the dashboard will display the project's number of active alerts,
+last commit, pipeline status, and when it was last deployed.
+
+![Operations Dashboard with projects](img/index_operations_dashboard_with_projects.png)
+
+## Making it the default dashboard when you sign in
+
+The Operations Dashboard can also be made the default GitLab dashboard shown when
+you sign in. To make it the default, visit your [profile preferences](../profile/preferences.md).
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a340dd063e4..9b298e4eb30 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -42,8 +42,7 @@ The following table depicts the various user permission levels in a project.
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Lock issue discussions | | ✓ | ✓ | ✓ | ✓ |
-| Lock merge request discussions | | | ✓ | ✓ | ✓ |
+| See related issues | ✓ | ✓ | ✓ | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
@@ -87,11 +86,12 @@ The following table depicts the various user permission levels in a project.
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
-| View approved/blacklisted licenses **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
+| View approved/blacklisted licenses **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
| Use security dashboard **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
| Dismiss vulnerability **[ULTIMATE]** | | | ✓ | ✓ | ✓ |
| Apply code change suggestions | | | ✓ | ✓ | ✓ |
| Use environment terminals | | | | ✓ | ✓ |
+| Run Web IDE's Interactive Web Terminals **[ULTIMATE ONLY]** | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ |
@@ -120,6 +120,7 @@ The following table depicts the various user permission levels in a project.
| Remove protected branches [^4] | | | | | |
| View project Audit Events | | | | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ |
+| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
## Project features permissions
@@ -172,8 +173,12 @@ read through the documentation on [permissions and access to confidential issues
### Releases permissions
-[Project Releases](project/releases/index.md) can be read by all project
-members (Reporters, Developers, Maintainers, Owners) **except Guests**.
+[Project Releases](project/releases/index.md) can be read by project
+members with Reporter, Developer, Maintainer, and Owner permissions.
+Guest users can access Release pages for downloading assets but
+are not allowed to download the source code nor see repository
+information such as tags and commits.
+
Releases can be created, updated, or deleted via [Releases APIs](../api/releases/index.md)
by project Developers, Maintainers, and Owners.
@@ -200,6 +205,7 @@ group.
| Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ |
| Delete group epic **[ULTIMATE]** | | | | | ✓ |
| View group Audit Events | | | | | ✓ |
+| View Insights charts **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ |
### Subgroup permissions
@@ -344,7 +350,7 @@ for details about the pipelines security model.
## LDAP users permissions
Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
-Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/index.html#updating-user-permissions-new-feature) to learn more.
+Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) to learn more.
[^1]: On public and internal projects, all users are able to perform this action
[^2]: Guest users can only view the confidential issues they created themselves
diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index b497cc414af..0d7bbb0af79 100644
--- a/doc/user/profile/account/delete_account.md
+++ b/doc/user/profile/account/delete_account.md
@@ -14,7 +14,7 @@ Deleting a user will delete all projects in that user namespace.
[GitLab 9.1][ce-10273], and from the API in [GitLab 9.3][ce-11853].
When a user account is deleted, not all associated records are deleted with it.
-Here's a list of things that will not be deleted:
+Here's a list of things that will **not** be deleted:
- Issues that the user created
- Merge requests that the user created
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index f399dc40164..b61216b7b67 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -79,6 +79,7 @@ You have 8 options here that you can use for your default dashboard view:
- Your [Todos](../../workflow/todos.md)
- Assigned Issues
- Assigned Merge Requests
+- Operations Dashboard **[PREMIUM]**
### Project overview content
diff --git a/doc/user/project/canary_deployments.md b/doc/user/project/canary_deployments.md
new file mode 100644
index 00000000000..cce862b0911
--- /dev/null
+++ b/doc/user/project/canary_deployments.md
@@ -0,0 +1,71 @@
+# Canary Deployments **[PREMIUM]**
+
+> [Introduced][ee-1659] in [GitLab Premium][eep] 9.1.
+
+A popular [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration)
+strategy, where a small portion of the fleet is updated to the new version of
+your application.
+
+## Overview
+
+When embracing [Continuous Delivery][cd-blog], a company needs to decide what
+type of deployment strategy to use. One of the most popular strategies is canary
+deployments, where a small portion of the fleet is updated to the new version
+first. This subset, the canaries, then serve as the proverbial
+[canary in the coal mine](https://en.wiktionary.org/wiki/canary_in_a_coal_mine).
+
+If there is a problem with the new version of the application, only a small
+percentage of users are affected and the change can either be fixed or quickly
+reverted.
+
+Leveraging [Kubernetes' Canary deployments][kube-canary], visualize your canary
+deployments right inside the [Deploy Board], without the need to leave GitLab.
+
+## Use cases
+
+Canary deployments can be used when you want to ship features to only a portion of
+your pods fleet and watch their behavior as a percentage of your user base
+visits the temporarily deployed feature. If all works well, you can deploy the
+feature to production knowing that it won't cause any problems.
+
+Canary deployments are also especially useful for backend refactors, performance
+improvements, or other changes where the user interface doesn't change, but you
+want to make sure the performance stays the same, or improves. Developers need
+to be careful when using canaries with user-facing changes, because by default,
+requests from the same user will be randomly distributed between canary and
+non-canary pods, which could result in confusion or even errors. If needed, you
+may want to consider [setting `service.spec.sessionAffinity` to `ClientIP` in
+your Kubernetes service definitions][kube-net], but that is beyond the scope of
+this document.
+
+## Enabling Canary Deployments
+
+Canary deployments require that you properly configure Deploy Boards:
+
+1. Follow the steps to [enable Deploy Boards](deploy_boards.md#enabling-deploy-boards).
+1. To track canary deployments you need to label your Kubernetes deployments and
+ pods with `track: canary`. To get started quickly, you can use the [Auto Deploy]
+ template for canary deployments that GitLab provides.
+
+Depending on the deploy, the label should be either `stable` or `canary`.
+Usually, `stable` and blank or missing label means the same thing, and `canary`
+or any other track means canary/temporary.
+This allows GitLab to discover whether deployment is stable or canary (temporary).
+
+Once all of the above are set up and the pipeline has run at least once,
+navigate to the environments page under **Pipelines > Environments**.
+As the pipeline executes Deploy Boards will clearly mark canary pods, enabling
+quick and easy insight into the status of each environment and deployment.
+
+Canary deployments are marked with a yellow dot in the Deploy Board so that you
+can easily notice them.
+
+![Canary deployments on Deploy Board](img/deploy_boards_canary_deployments.png)
+
+[autodeploy]: ../../ci/autodeploy/index.md "GitLab Autodeploy"
+[eep]: https://about.gitlab.com/pricing/
+[ee-1659]: https://gitlab.com/gitlab-org/gitlab-ee/issues/1659
+[kube-canary]: https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments
+[deploy board]: deploy_boards.md
+[cd-blog]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/
+[kube-net]: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
diff --git a/doc/user/project/ci_cd_for_external_repo.md b/doc/user/project/ci_cd_for_external_repo.md
new file mode 100644
index 00000000000..51b86a68c7b
--- /dev/null
+++ b/doc/user/project/ci_cd_for_external_repo.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html).
diff --git a/doc/user/project/clusters/img/k8s_cluster_monitoring.png b/doc/user/project/clusters/img/k8s_cluster_monitoring.png
new file mode 100644
index 00000000000..e449893a606
--- /dev/null
+++ b/doc/user/project/clusters/img/k8s_cluster_monitoring.png
Binary files differ
diff --git a/doc/user/project/clusters/img/kubernetes_pod_logs.png b/doc/user/project/clusters/img/kubernetes_pod_logs.png
new file mode 100644
index 00000000000..e664a47386a
--- /dev/null
+++ b/doc/user/project/clusters/img/kubernetes_pod_logs.png
Binary files differ
diff --git a/doc/user/project/clusters/img/pod_logs_deploy_board.png b/doc/user/project/clusters/img/pod_logs_deploy_board.png
new file mode 100644
index 00000000000..7f83382968b
--- /dev/null
+++ b/doc/user/project/clusters/img/pod_logs_deploy_board.png
Binary files differ
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 1983513174c..157afb3a78c 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -70,6 +70,7 @@ new Kubernetes cluster to your project:
- **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.
- **RBAC-enabled cluster** - Leave this checked if using default GKE creation options, see the [RBAC section](#role-based-access-control-rbac) 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
@@ -124,26 +125,26 @@ To add an existing Kubernetes cluster to your project:
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
- ```
+ ```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:
@@ -188,6 +189,9 @@ To add an existing Kubernetes cluster to your project:
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.
@@ -214,6 +218,29 @@ 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.
+## Gitlab-managed clusters
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22011) in GitLab 11.5.
+> Became [optional](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/26565) in GitLab 11.11.
+
+NOTE: **Note:**
+Only available when creating clusters. Existing clusters not managed by GitLab
+cannot become GitLab-managed later.
+
+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
+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](#deployment-variables)
+that will be used by your deployment jobs, otherwise a namespace will be created for you.
+
+NOTE: **Note:**
+If you [install applications](#installing-applications) on your cluster, GitLab will create
+the resources required to run these even if you have chosen to manage your own cluster.
+
## Base domain
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24580) in GitLab 11.8.
@@ -257,11 +284,11 @@ GitLab will create the necessary service accounts and privileges in order to ins
NOTE: **Note:**
Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
-- When you install Helm Tiller into your cluster, the `tiller` service account
+- When you install Helm into your cluster, the `tiller` service account
will be 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 Tiller will also create additional service accounts and other resources for each
+ 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.
@@ -278,8 +305,8 @@ The following sections summarize which resources will be created on ABAC/RBAC cl
| `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 |
-| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
-| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster |
### Role-based access control (RBAC)
@@ -290,9 +317,12 @@ The following sections summarize which resources will be created on ABAC/RBAC cl
| `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 |
-| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
-| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
-| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating/Adding a new GKE Cluster |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Deploying to a cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Deploying to a cluster |
+| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Deploying to a cluster |
+
+NOTE: **Note:**
+Project-specific resources are only created if your cluster is [managed by GitLab](#gitlab-managed-clusters).
### Security of GitLab Runners
@@ -315,25 +345,29 @@ install it manually.
## Installing applications
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
-be added directly to your configured cluster. Those applications are
+be added directly to your configured cluster. These applications are
needed for [Review Apps](../../../ci/review_apps/index.md) and
-[deployments](../../../ci/environments.md). You can install them after you
+[deployments](../../../ci/environments.md) when using [Auto DevOps](../../../topics/autodevops/index.md).
+You can install them after you
[create a cluster](#adding-and-creating-a-new-gke-cluster-via-gitlab).
+Applications managed by GitLab will be installed onto the `gitlab-managed-apps` namespace. This differrent
+from the namespace used for project deployments. It is only created once and its name is not configurable.
+
To see a list of available applications to install:
1. Navigate to your project's **Operations > Kubernetes**.
1. Select your cluster.
-Install Helm Tiller first because it's used to install other applications.
+Install Helm first as it's used to install other applications.
NOTE: **Note:**
-As of GitLab 11.6, Helm Tiller will be upgraded to the latest version supported
+As of GitLab 11.6, Helm will be upgraded to the latest version supported
by GitLab before installing any of the applications.
| Application | GitLab version | Description | Helm Chart |
| ----------- | :------------: | ----------- | --------------- |
-| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | 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. | n/a |
+| [Helm](https://docs.helm.sh/) | 10.2+ | 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. | n/a |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) |
| [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
@@ -345,9 +379,9 @@ With the exception of Knative, the applications will be installed in a dedicated
namespace called `gitlab-managed-apps`.
CAUTION: **Caution:**
-If you have an existing Kubernetes cluster with Tiller already installed,
+If you have an existing Kubernetes cluster with Helm already installed,
you should be careful as GitLab cannot detect it. In this case, installing
-Tiller via the applications will result in the cluster having it twice, which
+Helm via the applications will result in the cluster having it twice, which
can lead to confusion during deployments.
### Upgrading applications
@@ -372,6 +406,27 @@ Upgrades will reset values back to the values built into the `runner`
chart plus the values set by
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/vendor/runner/values.yaml)
+### Uninstalling applications
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/60665) in
+> GitLab 11.11.
+
+The applications below can be uninstalled.
+
+| Application | GitLab version | Notes |
+| ----------- | -------------- | ----- |
+| Prometheus | 11.11+ | All data will be deleted and cannot be restored. |
+
+To uninstall an application:
+
+1. Navigate to your project's **Operations > Kubernetes**.
+1. Select your cluster.
+1. Click the **Uninstall** button for the application.
+
+Support for uninstalling all applications will be progressively
+introduced (see [related
+epic](https://gitlab.com/groups/gitlab-org/-/epics/1201)).
+
### Troubleshooting applications
Applications can fail with the following error:
@@ -384,7 +439,7 @@ To avoid installation errors:
- Before starting the installation of applications, make sure that time is synchronized
between your GitLab server and your Kubernetes cluster.
-- Ensure certificates are not out of sync. When installing applications, GitLab expects a new cluster with no previous installation of Tiller.
+- Ensure certificates are not out of sync. When installing applications, GitLab expects a new cluster with no previous installation of Helm.
You can confirm that the certificates match via `kubectl`:
@@ -493,7 +548,7 @@ differentiate the new cluster with the rest.
When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/README.html#limiting-environment-scopes-of-variables-premium) work.
+[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/index.html#limiting-environment-scopes-of-environment-variables-premium) work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
@@ -565,7 +620,9 @@ service account of the cluster integration.
### Troubleshooting failed deployment jobs
GitLab will create a namespace and service account specifically for your
-deployment jobs, immediately before the jobs starts.
+deployment jobs. On project level clusters, this happens when the cluster
+is created. On group level clusters, resources are created immediately
+before the deployment job starts.
However, sometimes GitLab can not create them. In such instances, your job will fail with the message:
@@ -589,7 +646,7 @@ Common reasons for failure include:
When [Prometheus is deployed](#installing-applications), GitLab will automatically monitor the cluster's health. At the top of the cluster settings page, CPU and Memory utilization is displayed, along with the total amount available. Keeping an eye on cluster resources can be important, if the cluster runs out of memory pods may be shutdown or fail to start.
-![Cluster Monitoring](https://docs.gitlab.com/ee/user/project/clusters/img/k8s_cluster_monitoring.png)
+![Cluster Monitoring](img/k8s_cluster_monitoring.png)
## Enabling or disabling the Kubernetes cluster integration
@@ -620,7 +677,7 @@ and add a Kubernetes cluster again.
## View Kubernetes pod logs from GitLab **[ULTIMATE]**
Learn how to easily
-[view the logs of running pods in connected Kubernetes clusters](https://docs.gitlab.com/ee/user/project/clusters/kubernetes_pod_logs.html).
+[view the logs of running pods in connected Kubernetes clusters](kubernetes_pod_logs.md).
## What you can get with the Kubernetes integration
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
new file mode 100644
index 00000000000..d5b60250860
--- /dev/null
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -0,0 +1,21 @@
+# Kubernetes Pod Logs **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4752) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
+
+GitLab makes it easy to view the logs of running pods in [connected Kubernetes clusters](index.md).
+By displaying the logs directly in GitLab, developers can avoid having to manage console tools or jump to a different interface.
+
+## 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](https://docs.gitlab.com/ee/user/project/deploy_boards.html):
+
+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](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
+1. When mousing over the list of pods, a tooltip will appear with the exact pod name and status.
+![Deploy Boards pod list](img/pod_logs_deploy_board.png)
+1. Click on the desired pod to bring up the logs view, which will contain the last 500 lines for that pod. Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab-ee/issues/6502).
+![Deploy Boards pod list](img/kubernetes_pod_logs.png)
+
+## Requirements
+
+[Enabling Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html#enabling-deploy-boards) is required in order to be able to use Pod Logs.
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 5b7e9ef906f..dfbed0ec95f 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -158,21 +158,6 @@ Follow these steps to deploy a function using the Node.js runtime to your Knativ
description: "node.js runtime function"
environment:
MY_FUNCTION: echo-js
-
- echo-rb:
- handler: MyEcho.my_function
- source: ./echo-rb
- runtime: https://gitlab.com/gitlab-org/serverless/runtimes/ruby
- description: "Ruby runtime function"
- environment:
- MY_FUNCTION: echo-rb
-
- echo-docker:
- handler: echo-docker
- source: ./echo-docker
- description: "Dockerfile runtime function"
- environment:
- MY_FUNCTION: echo-docker
```
Explanation of the fields used above:
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
new file mode 100644
index 00000000000..a4937e6cf6b
--- /dev/null
+++ b/doc/user/project/code_owners.md
@@ -0,0 +1,85 @@
+# Code Owners **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6916)
+in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
+
+You can use a `CODEOWNERS` file to specify users that are responsible
+for certain files in a repository.
+
+You can choose and add the `CODEOWNERS` file in three places:
+
+- to the root directory of the repository
+- inside the `.gitlab/` directory
+- inside the `docs/` directory
+
+The `CODEOWNERS` file is scoped to a branch, which means that with the
+introduction of new files, the person adding the new content can
+specify themselves as a code owner, all before the new changes
+get merged to the default branch.
+
+When a file matches multiple entries in the `CODEOWNERS` file,
+the users from all entries are displayed on the blob page of
+the given file.
+
+## The syntax of Code Owners files
+
+Files can be specified using the same kind of patterns you would use
+in the `.gitignore` file followed by the `@username` or email of one
+or more users that should be owners of the file.
+
+The order in which the paths are defined is significant: the last
+pattern that matches a given path will be used to find the code
+owners.
+
+Starting a line with a `#` indicates a comment. This needs to be
+escaped using `\#` to address files for which the name starts with a
+`#`.
+
+Example `CODEOWNERS` file:
+
+```
+# This is an example code owners file, lines starting with a `#` will
+# be ignored.
+
+# app/ @commented-rule
+
+# We can specifiy a default match using wildcards:
+* @default-codeowner
+
+# Rules defined later in the file take precedence over the rules
+# defined before.
+# This will match all files for which the file name ends in `.rb`
+*.rb @ruby-owner
+
+# Files with a `#` can still be accesssed by escaping the pound sign
+\#file_with_pound.rb @owner-file-with-pound
+
+# Multiple codeowners can be specified, separated by whitespace
+CODEOWNERS @multiple @owners @tab-separated
+
+# Both usernames or email addresses can be used to match
+# users. Everything else will be ignored. For example this will
+# specify `@legal` and a user with email `janedoe@gitlab.com` as the
+# owner for the LICENSE file
+LICENSE @legal this_does_not_match janedoe@gitlab.com
+
+# Ending a path in a `/` will specify the code owners for every file
+# nested in that directory, on any level
+/docs/ @all-docs
+
+# Ending a path in `/*` will specify code owners for every file in
+# that directory, but not nested deeper. This will match
+# `docs/index.md` but not `docs/projects/index.md`
+/docs/* @root-docs
+
+# This will make a `lib` directory nested anywhere in the repository
+# match
+lib/ @lib-owner
+
+# This will only match a `config` directory in the root of the
+# repository
+/config/ @config-owner
+
+# If the path contains spaces, these need to be escaped like this:
+path\ with\ spaces/ @space-owner
+```
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
new file mode 100644
index 00000000000..2f2a9c5eec3
--- /dev/null
+++ b/doc/user/project/deploy_boards.md
@@ -0,0 +1,132 @@
+# Deploy Boards **[PREMIUM]**
+
+> [Introduced][ee-1589] in [GitLab Premium][ee] 9.0.
+
+GitLab's Deploy Boards offer a consolidated view of the current health and
+status of each CI [environment] running on [Kubernetes], displaying the status
+of the pods in the deployment. Developers and other teammates can view the
+progress and status of a rollout, pod by pod, in the workflow they already use
+without any need to access Kubernetes.
+
+## Overview
+
+With Deploy Boards you can gain more insight into deploys with benefits such as:
+
+- Following a deploy from the start, not just when it's done
+- Watching the rollout of a build across multiple servers
+- Finer state detail (Waiting, Deploying, Finished, Unknown)
+- See [Canary Deployments](canary_deployments.md)
+
+Here's an example of a Deploy Board of the production environment.
+
+![Deploy Boards landing page](img/deploy_boards_landing_page.png)
+
+The squares represent pods in your Kubernetes cluster that are associated with
+the given environment. Hovering above each square you can see the state of a
+deploy rolling out. The percentage is the percent of the pods that are updated
+to the latest release.
+
+Since Deploy Boards are tightly coupled with Kubernetes, there is some required
+knowledge. In particular you should be familiar with:
+
+- [Kubernetes pods](https://kubernetes.io/docs/user-guide/pods)
+- [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
+- [Kubernetes namespaces](https://kubernetes.io/docs/user-guide/namespaces/)
+- [Kubernetes canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
+
+## Use cases
+
+Since the Deploy Board is a visual representation of the Kubernetes pods for a
+specific environment, there are lot of uses cases. To name a few:
+
+- You want to promote what's running in staging, to production. You go to the
+ environments list, verify that what's running in staging is what you think is
+ running, then click on the [manual action](../../ci/yaml/README.md#whenmanual) to deploy to production.
+- You trigger a deploy, and you've got lots of containers to upgrade so you know
+ it'll take a while (you've also throttled your deploy to only take down X
+ containers at a time). But you need to tell someone when it's deployed, so you
+ go to the environments list, look at the production environment to see what
+ the progress is in real-time as each pod is rolled.
+- You get a report that something is weird in production, so you look at the
+ production environment to see what is running, and if a deploy is ongoing or
+ stuck or failed.
+- You've got an MR that looks good, but you want to run it on staging because
+ staging is set up in some way closer to production. You go to the environment
+ list, find the [Review App][review apps] you're interested in, and click the
+ manual action to deploy it to staging.
+
+## Enabling Deploy Boards
+
+To display the Deploy Boards for a specific [environment] you should:
+
+1. Have a Kubernetes cluster up and running.
+
+ NOTE: **Running on OpenShift:**
+ If you are using OpenShift, ensure that you're using the `Deployment` resource
+ instead of `DeploymentConfiguration`, otherwise the Deploy Boards won't render
+ correctly. For more information, read the
+ [OpenShift docs](https://docs.openshift.com/container-platform/3.7/dev_guide/deployments/kubernetes_deployments.html#kubernetes-deployments-vs-deployment-configurations)
+ and [GitLab issue #4584](https://gitlab.com/gitlab-org/gitlab-ee/issues/4584).
+
+1. [Configure GitLab Runner][runners] with the [Docker][docker-exec] or
+ [Kubernetes][kube-exec] executor.
+1. Configure the [Kubernetes service][kube-service] in your project for the
+ cluster. The Kubernetes namespace is of particular note as you will need it
+ for your deployment scripts (exposed by the `KUBE_NAMESPACE` env variable).
+1. Ensure Kubernetes annotations of `app.gitlab.com/env: $CI_ENVIRONMENT_SLUG`
+ and `app.gitlab.com/app: $CI_PROJECT_PATH_SLUG` are applied to the
+ deployments, replica sets, and pods, where `$CI_ENVIRONMENT_SLUG` and
+ `$CI_PROJECT_PATH_SLUG` are the values of the CI variables. This is so we can
+ lookup the proper environment in a cluster/namespace which may have more
+ than one. These resources should be contained in the namespace defined in
+ the Kubernetes service setting. You can use an [Autodeploy] `.gitlab-ci.yml`
+ template which has predefined stages and commands to use, and automatically
+ applies the annotations. Each project will need to have a unique namespace in
+ Kubernetes as well. The image below demonstrates how this is shown inside
+ Kubernetes.
+
+ NOTE: **Note:**
+ The Kubernetes label of `app` is deprecated and may be removed in next major
+ release, GitLab 12.0.
+
+ ![Deploy Boards Kubernetes Label](img/deploy_boards_kubernetes_label.png)
+
+Once all of the above are set up and the pipeline has run at least once,
+navigate to the environments page under **Operations > Environments**.
+
+Deploy Boards are visible by default. You can explicitly click
+the triangle next to their respective environment name in order to hide them.
+GitLab will then query Kubernetes for the state of each pod (e.g., waiting,
+deploying, finished, unknown), and the Deploy Board status will finally appear.
+
+GitLab will only display a Deploy Board for top-level environments. Foldered
+environments like `review/*` (usually used for [Review Apps]) won't have a
+Deploy Board attached to them.
+
+## Canary Deployments
+
+A popular CI strategy, where a small portion of the fleet is updated to the new
+version of your application.
+
+[Read more about Canary Deployments.](canary_deployments.md)
+
+## Further reading
+
+- [GitLab Autodeploy][autodeploy]
+- [GitLab CI environment variables][variables]
+- [Environments and deployments][environment]
+- [Kubernetes deploy example][kube-deploy]
+
+[ee-1589]: https://gitlab.com/gitlab-org/gitlab-ee/issues/1589 "Deploy Boards initial issue"
+[ee]: https://about.gitlab.com/pricing/ "GitLab Enterprise Edition landing page"
+[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
+[kubernetes]: https://kubernetes.io "Kubernetes website"
+[environment]: ../../ci/environments.md "Environments and deployments documentation"
+[docker-exec]: https://docs.gitlab.com/runner/executors/docker.html "GitLab Runner Docker executor"
+[kube-exec]: https://docs.gitlab.com/runner/executors/kubernetes.html "GitLab Runner Kubernetes executor"
+[kube-service]: integrations/kubernetes.md "Kubernetes project service"
+[review apps]: ../../ci/review_apps/index.md "Review Apps documentation"
+[variables]: ../../ci/variables/README.md "GitLab CI variables"
+[autodeploy]: ../../ci/autodeploy/index.md "GitLab Autodeploy"
+[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
+[runners]: ../../ci/runners/README.md
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index 08647caaf07..e230444fa67 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -2,8 +2,12 @@
>[Introduced][ce-4981] in GitLab 8.11.
+We all know that a properly submitted issue is more likely to be addressed in
+a timely manner by the developers of a project.
+
Description templates allow you to define context-specific templates for issue
-and merge request description fields for your project.
+and merge request description fields for your project, as well as help filter
+out a lot of unnecessary noise from issues.
## Overview
@@ -18,6 +22,18 @@ Description templates must be written in [Markdown](../markdown.md) and stored
in your project's repository under a directory named `.gitlab`. Only the
templates of the default branch will be taken into account.
+## Use-cases
+
+- Add a template to be used in every issue for a specific project,
+giving instructions and guidelines, requiring for information specific to that subject.
+For example, if you have a project for tracking new blog posts, you can require the
+title, outlines, author name, author social media information, etc.
+- Following the previous example, you can make a template for every MR submitted
+with a new blog post, requiring information about the post date, frontmatter data,
+images guidelines, link to the related issue, reviewer name, etc.
+- You can also create issues and merge request templates for different
+stages of your workflow, e.g., feature proposal, feature improvement, bug report, etc.
+
## Creating issue templates
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
@@ -39,6 +55,32 @@ changes you made after picking the template and return it to its initial status.
![Description templates](img/description_templates.png)
+## Setting a default template for issues and merge requests **[STARTER]**
+
+> **Notes:**
+> - This feature was introduced before [description templates](#overview) and is
+> available in [GitLab Starter][products]. It can be enabled
+> in the project's settings.
+> - Templates for issues were [introduced][ee-28] in GitLab EE 8.1.
+> - Templates for merge requests were [introduced][ee-7478ece] in GitLab EE 6.9.
+
+The visibility of issues and/or merge requests should be set to either "Everyone
+with access" or "Only team members" in your project's **Settings** otherwise the
+template text areas won't show. This is the default behavior so in most cases
+you should be fine.
+
+Go to your project's **Settings** and fill in the "Default description template
+for issues" and "Default description template for merge requests" text areas
+for issues and merge requests respectively. Since GitLab issues and merge
+request support [Markdown](../markdown.md), you can use special markup like
+headings, lists, etc.
+
+![Default description templates](img/description_templates_default_settings.png)
+
+After you add the description, hit **Save changes** for the settings to take
+effect. Now, every time a new issue or merge request is created, it will be
+pre-filled with the text you entered in the template(s).
+
## Description template example
We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples.
@@ -93,3 +135,6 @@ Possible fixes
[ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981
[gitlab-ce-templates]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/.gitlab
+[ee-28]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/28 "Merge Request for adding issues template"
+[ee-7478ece]: https://gitlab.com/gitlab-org/gitlab-ee/commit/7478ece8b48e80782b5465b96c79f85cc91d391b "Commit that introduced merge requests templates"
+[products]: https://about.gitlab.com/pricing/
diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md
new file mode 100644
index 00000000000..541023692ea
--- /dev/null
+++ b/doc/user/project/file_lock.md
@@ -0,0 +1,107 @@
+# File Locking **[PREMIUM]**
+
+> **Notes:**
+> - [Introduced][ee-440] in [GitLab Premium][ee] 8.9.
+> - This feature needs to have a license with the "File Lock" option enabled. If
+> you are using Premium but you don't see the "Lock" button,
+> ask your GitLab administrator.
+
+File Locking helps you avoid merge conflicts and better manage your binary files.
+Lock any file or directory, make your changes, and then unlock it so another
+member of the team can edit it.
+
+## Overview
+
+Working with multiple people on the same file can be a risk. Conflicts
+when merging a non-text file are hard to overcome and will require a lot
+of manual work to resolve. With GitLab Premium, File
+Locking helps you avoid merge conflicts and better manage your binary
+files by preventing everyone, except you, from modifying a specific file
+or entire directory.
+
+## Use-cases
+
+The file locking feature is useful in situations when:
+
+- Multiple people are working on the same file and you want to avoid merge
+ conflicts.
+- Your repository contains binary files in which situation there is no easy
+ way to tell the diff between yours and your colleagues' changes.
+- Prevent design assets from being overwritten.
+
+Locked directories are locked recursively, which means that everything that
+lies under them is also locked.
+
+## Permissions on file locking
+
+The user that locks a file or directory **is the only one** that can edit and
+push their changes back to the repository where the locked objects are located.
+
+Locks can be created by any person who has [push access] to the repository; i.e.,
+Developer and higher level, and can be removed solely by their author and any
+user with Maintainer permissions and above.
+
+If a file is locked and you are not the author of its locked state, a
+pre-receive hook will reject your changes when you try to push. In the
+following example, a user who has no permissions on the locked `.gitignore`
+file will see the message below:
+
+```bash
+Counting objects: 3, done.
+Delta compression using up to 4 threads.
+Compressing objects: 100% (3/3), done.
+Writing objects: 100% (3/3), 320 bytes | 0 bytes/s, done.
+Total 3 (delta 1), reused 0 (delta 0)
+remote: GitLab: The path '.gitignore' is locked by Administrator
+To https://example.com/gitlab-org/gitlab-ce.git
+ ! [remote rejected] master -> master (pre-receive hook declined)
+ error: failed to push some refs to 'https://example.com/gitlab-org/gitlab-ce.git'
+```
+
+Similarly, when a user that is not the author of the locked state of a file
+accepts a merge request, an error message will appear stating that the file
+is locked.
+
+![Merge request error message](img/file_lock_merge_request_error_message.png)
+
+## Locking a file or a directory
+
+>**Note:**
+Locking only works for the default branch you have set in the project's settings
+(usually `master`).
+
+To lock a file, navigate to the repository tree under the **Repository > Files** tab,
+pick the file you want to lock and hit the "Lock" button.
+
+![Locking file](img/file_lock.png)
+
+---
+
+To lock an entire directory, look for the "Lock" link next to "History".
+
+![Locking directory](img/file_lock_folders.png)
+
+---
+
+After you lock a file or directory, it will appear as locked in the repository
+view.
+
+![Repository view](img/file_lock_repository_view.png)
+
+## Unlocking a file or a directory
+
+To unlock a file or a directory, follow the same procedure as when you locked
+them. For a detailed view of every existing lock, see the next section on
+"Viewing and managing existing locks".
+
+## Viewing and managing existing locks
+
+To view or manage every existing lock, navigate to the
+**Project > Repository > Locked Files** area. There, you can view all existing
+locks and [remove the ones you have permission for](#permissions-on-file-locking).
+
+![Locked Files](img/file_lock_list.png)
+
+[ee-440]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/440 "File Lock"
+[ee]: https://about.gitlab.com/pricing/
+[push access]: ../../user/permissions.md
diff --git a/doc/user/project/img/assignee_lists.png b/doc/user/project/img/assignee_lists.png
new file mode 100644
index 00000000000..f2660cd8f80
--- /dev/null
+++ b/doc/user/project/img/assignee_lists.png
Binary files differ
diff --git a/doc/user/project/img/deploy_boards_canary_deployments.png b/doc/user/project/img/deploy_boards_canary_deployments.png
new file mode 100644
index 00000000000..037b4a908ab
--- /dev/null
+++ b/doc/user/project/img/deploy_boards_canary_deployments.png
Binary files differ
diff --git a/doc/user/project/img/deploy_boards_kubernetes_label.png b/doc/user/project/img/deploy_boards_kubernetes_label.png
new file mode 100644
index 00000000000..19785c74d07
--- /dev/null
+++ b/doc/user/project/img/deploy_boards_kubernetes_label.png
Binary files differ
diff --git a/doc/user/project/img/deploy_boards_landing_page.png b/doc/user/project/img/deploy_boards_landing_page.png
new file mode 100644
index 00000000000..c9621a06860
--- /dev/null
+++ b/doc/user/project/img/deploy_boards_landing_page.png
Binary files differ
diff --git a/doc/user/project/img/description_templates_default_settings.png b/doc/user/project/img/description_templates_default_settings.png
new file mode 100644
index 00000000000..ab314e83d06
--- /dev/null
+++ b/doc/user/project/img/description_templates_default_settings.png
Binary files differ
diff --git a/doc/user/project/img/file_lock.png b/doc/user/project/img/file_lock.png
new file mode 100644
index 00000000000..33f96f20e91
--- /dev/null
+++ b/doc/user/project/img/file_lock.png
Binary files differ
diff --git a/doc/user/project/img/file_lock_folders.png b/doc/user/project/img/file_lock_folders.png
new file mode 100644
index 00000000000..5ff3d4d9dbd
--- /dev/null
+++ b/doc/user/project/img/file_lock_folders.png
Binary files differ
diff --git a/doc/user/project/img/file_lock_list.png b/doc/user/project/img/file_lock_list.png
new file mode 100644
index 00000000000..2c276335c83
--- /dev/null
+++ b/doc/user/project/img/file_lock_list.png
Binary files differ
diff --git a/doc/user/project/img/file_lock_merge_request_error_message.png b/doc/user/project/img/file_lock_merge_request_error_message.png
new file mode 100644
index 00000000000..65bc3692da0
--- /dev/null
+++ b/doc/user/project/img/file_lock_merge_request_error_message.png
Binary files differ
diff --git a/doc/user/project/img/file_lock_repository_view.png b/doc/user/project/img/file_lock_repository_view.png
new file mode 100644
index 00000000000..8f4ef52aacd
--- /dev/null
+++ b/doc/user/project/img/file_lock_repository_view.png
Binary files differ
diff --git a/doc/user/project/img/key_value_labels.png b/doc/user/project/img/key_value_labels.png
new file mode 100644
index 00000000000..bec901f127f
--- /dev/null
+++ b/doc/user/project/img/key_value_labels.png
Binary files differ
diff --git a/doc/user/project/img/labels_epic_sidebar.png b/doc/user/project/img/labels_epic_sidebar.png
new file mode 100644
index 00000000000..f8162d89e9d
--- /dev/null
+++ b/doc/user/project/img/labels_epic_sidebar.png
Binary files differ
diff --git a/doc/user/project/img/project_security_dashboard.png b/doc/user/project/img/project_security_dashboard.png
new file mode 100644
index 00000000000..3294e59e943
--- /dev/null
+++ b/doc/user/project/img/project_security_dashboard.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_select_roles_and_users.png b/doc/user/project/img/protected_branches_select_roles_and_users.png
new file mode 100644
index 00000000000..4f62b057cc7
--- /dev/null
+++ b/doc/user/project/img/protected_branches_select_roles_and_users.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_select_roles_and_users_list.png b/doc/user/project/img/protected_branches_select_roles_and_users_list.png
new file mode 100644
index 00000000000..519f2267496
--- /dev/null
+++ b/doc/user/project/img/protected_branches_select_roles_and_users_list.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_confirmation_email.png b/doc/user/project/img/service_desk_confirmation_email.png
new file mode 100644
index 00000000000..e08dced1e38
--- /dev/null
+++ b/doc/user/project/img/service_desk_confirmation_email.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_disabled.png b/doc/user/project/img/service_desk_disabled.png
new file mode 100644
index 00000000000..3ae64dcbe96
--- /dev/null
+++ b/doc/user/project/img/service_desk_disabled.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_enabled.png b/doc/user/project/img/service_desk_enabled.png
new file mode 100644
index 00000000000..329348e4b52
--- /dev/null
+++ b/doc/user/project/img/service_desk_enabled.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_issue_tracker.png b/doc/user/project/img/service_desk_issue_tracker.png
new file mode 100644
index 00000000000..485751fab0a
--- /dev/null
+++ b/doc/user/project/img/service_desk_issue_tracker.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_nav_item.png b/doc/user/project/img/service_desk_nav_item.png
new file mode 100644
index 00000000000..3420e355f67
--- /dev/null
+++ b/doc/user/project/img/service_desk_nav_item.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_reply.png b/doc/user/project/img/service_desk_reply.png
new file mode 100644
index 00000000000..ae6ce31713b
--- /dev/null
+++ b/doc/user/project/img/service_desk_reply.png
Binary files differ
diff --git a/doc/user/project/img/service_desk_thread.png b/doc/user/project/img/service_desk_thread.png
new file mode 100644
index 00000000000..8d8ac39cc5b
--- /dev/null
+++ b/doc/user/project/img/service_desk_thread.png
Binary files differ
diff --git a/doc/user/project/import/gemnasium.md b/doc/user/project/import/gemnasium.md
new file mode 100644
index 00000000000..dc5b3fcd0bb
--- /dev/null
+++ b/doc/user/project/import/gemnasium.md
@@ -0,0 +1,102 @@
+# Gemnasium **[ULTIMATE]**
+
+This guide describes how to migrate from Gemnasium.com to your own GitLab
+instance or GitLab.com.
+
+## Why is Gemnasium.com closed?
+
+Gemnasium has been [acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html)
+in January 2018. Since May 15, 2018, the services provided by Gemnasium are no longer available.
+The team behind Gemnasium has joined GitLab as the new Security Products team
+and is working on a wider range of tools than just Dependency Scanning:
+[SAST](https://docs.gitlab.com/ee/user/application_security/sast/index.html),
+[DAST](https://docs.gitlab.com/ee/user/application_security/dast/index.html),
+[Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html) and more.
+If you want to continue monitoring your dependencies, see the
+[Migrating to GitLab](#migrating-to-gitlab) section below.
+
+## What happened to my account?
+
+Your account has been automatically closed on May 15th, 2018. If you had a paid
+subscription at that time, your card will be refunded on a pro rata temporis basis.
+You may contact `gemnasium@gitlab.com` regarding your closed account.
+
+## Will my account/data be transferred to GitLab Inc.?
+
+All accounts and data have been deleted on May 15th, 2018. GitLab Inc.
+doesn't know anything about your private data, nor your projects, and therefore
+if they were vulnerable or not. GitLab Inc. takes personal information very seriously.
+
+## What happened to my badge?
+
+To avoid broken 404 images, all badges pointing to Gemnasium.com will be a
+placeholder, inviting you to migrate to GitLab (and pointing to this page).
+
+## Migrating to GitLab
+
+Gemnasium has been ported and integrated directly into GitLab CI/CD.
+You can still benefit from our dependency monitoring features, and it requires
+some steps to migrate your projects. There is no automatic import since GitLab
+doesn't know anything about any projects which existed on Gemnasium.com.
+Security features are free for public (open-source) projects hosted on GitLab.com.
+
+### If your project is hosted on GitLab (https://gitlab.com / self-hosted)
+
+You're almost set! If you're already using
+[Auto DevOps](../../../topics/autodevops/), you are already covered.
+Otherwise, you must configure your `.gitlab-ci.yml` according to the
+[dependency scanning page](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+
+### If your project is hosted on GitHub (https://github.com / GitHub Enterprise)
+
+Since [GitLab 10.6 comes with GitHub integration](https://about.gitlab.com/features/github/),
+GitLab users can now create a CI/CD project in GitLab connected to an external
+GitHub.com or GitHub Enterprise repository. This will automatically prompt
+GitLab CI/CD to run whenever code is pushed to GitHub and post CI/CD results
+back to both GitLab and GitHub when completed.
+
+1. Create a new project, and select the "CI/CD for external repo" tab:
+
+ ![Create new Project](img/gemnasium/create_project.png)
+
+1. Use the "GitHub" button to connect your repositories.
+
+ ![Connect from GitHub](img/gemnasium/connect_github.png)
+
+1. Select the project(s) to be set up with GitLab CI/CD and chose "Connect".
+
+ ![Select projects](img/gemnasium/select_project.png)
+
+ Once the configuration is done, you may click on your new
+ project on GitLab.
+
+ ![click on connected project](img/gemnasium/project_connected.png)
+
+ Your project is now mirrored on GitLab, where the Runners will be able to access
+ your source code and run your tests.
+
+ Optional step: If you set this up on GitLab.com, make sure the project is
+ public (in the project settings) if your GitHub project is public, since
+ the security feature is available only for [GitLab Ultimate](https://about.gitlab.com/pricing).
+
+1. To set up the dependency scanning job, corresponding to what Gemnasium was
+ doing, you must create a `.gitlab-ci.yml` file, or update it according to
+ the [dependency scanning docs](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+ The mirroring is pull-only by default, so you may create or update the file on
+ GitHub:
+
+ ![Edit gitlab-ci.yml file](img/gemnasium/edit_gitlab-ci.png)
+
+1. Once your file has been committed, a new pipeline will be automatically
+ triggered if your file is valid:
+
+ ![pipeline](img/gemnasium/pipeline.png)
+
+1. The result of the job will be visible directly from the pipeline view:
+
+ ![security report](img/gemnasium/report.png)
+
+NOTE: **Note:**
+If you don't commit very often to your project, you may want to use
+[scheduled pipelines](../pipelines/schedules.md) to run the job on a regular
+basis.
diff --git a/doc/user/project/import/img/gemnasium/connect_github.png b/doc/user/project/import/img/gemnasium/connect_github.png
new file mode 100644
index 00000000000..5933f4d5d0a
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/connect_github.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/create_project.png b/doc/user/project/import/img/gemnasium/create_project.png
new file mode 100644
index 00000000000..6e00bf63405
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/create_project.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/edit_gitlab-ci.png b/doc/user/project/import/img/gemnasium/edit_gitlab-ci.png
new file mode 100644
index 00000000000..8336c803b83
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/edit_gitlab-ci.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/pipeline.png b/doc/user/project/import/img/gemnasium/pipeline.png
new file mode 100644
index 00000000000..ae4d5295ffa
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/pipeline.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/project_connected.png b/doc/user/project/import/img/gemnasium/project_connected.png
new file mode 100644
index 00000000000..8e7216c015b
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/project_connected.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/report.png b/doc/user/project/import/img/gemnasium/report.png
new file mode 100644
index 00000000000..5c4d58662c0
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/report.png
Binary files differ
diff --git a/doc/user/project/import/img/gemnasium/select_project.png b/doc/user/project/import/img/gemnasium/select_project.png
new file mode 100644
index 00000000000..588c5ca7ce1
--- /dev/null
+++ b/doc/user/project/import/img/gemnasium/select_project.png
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index ca02e4e9e96..ebbc5ca133b 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -13,18 +13,38 @@
1. [From TFS](tfs.md)
1. [From repo by URL](repo_by_url.md)
1. [By uploading a manifest file (AOSP)](manifest.md)
+1. [From Gemnasium](gemnasium.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
repository is too large the import can timeout.
+There is also the option of [connecting your external repository to get CI/CD benefits](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html). **[PREMIUM]**
+
## Migrating from self-hosted GitLab to GitLab.com
-You can copy your repos by changing the remote and pushing to the new server,
-but issues and merge requests can't be imported.
+If you only need to migrate git repos, you can [import each project by URL](repo_by_url.md), but issues and merge requests can't be imported.
If you want to retain all metadata like issues and merge requests, you can use
-the [import/export feature](../settings/import_export.md).
+the [import/export feature](../settings/import_export.md) to export projects from self-hosted GitLab and import those projects into GitLab.com.
+
+NOTE: **Note:**
+This approach assumes all users from the self-hosted instance have already been migrated.
+If the users haven't been migrated yet, the user conducting the import
+will take the place of all references to the missing user(s).
+
+If you need to migrate all data over, you can leverage our [api](../../../api/README.md) to migrate from self-hosted to GitLab.com.
+The order of assets to migrate from a self-hosted instance to GitLab is the following:
+
+1. [Users](../../../api/users.md)
+1. [Groups](../../../api/groups.md)
+1. [Projects](../../../api/projects.md)
+1. [Project variables](../../../api/project_level_variables.md)
+
+Keep in mind the limitations of the [import/export feature](../settings/import_export.md#exported-contents).
+
+You will still need to migrate your Container Registry over a series of
+Docker pulls and pushes and re-run any CI pipelines to retrieve any build artifacts.
## Migrating between two self-hosted GitLab instances
@@ -32,3 +52,6 @@ The best method for migrating a project from one GitLab instance to another,
perhaps from an old server to a new server for example, is to
[back up the project](../../../raketasks/backup_restore.md),
then restore it on the new server.
+
+In the event of merging two GitLab instances together (for example, both instances have existing data on them and one can't be wiped),
+refer to the instructions in [Migrating from self-hosted GitLab to GitLab.com](#migrating-from-self-hosted-gitlab-to-gitlabcom).
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 64139f9dbe9..6b3b40bf9f8 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -13,7 +13,7 @@ the number of private projects you create.
When you create a project in GitLab, you'll have access to a large number of
[features](https://about.gitlab.com/features/):
-**Issues and merge requests:**
+**Repositories:**
- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
- [Issue Boards](issue_board.md): Organize and prioritize your workflow
@@ -28,6 +28,13 @@ When you create a project in GitLab, you'll have access to a large number of
permission to create tags, and prevent accidental update or deletion
- [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)
+
+**Issues and merge requests:**
+
+- [Issue tracker](issues/index.md): Discuss implementations with your team within issues
+ - [Issue Boards](issue_board.md): Organize and prioritize your workflow
+ - [Multiple Issue Boards](issue_board.md#multiple-issue-boards-starter): Allow your teams to create their own workflows (Issue Boards) for the same project **[STARTER]**
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html): Ask for approval before
@@ -67,6 +74,8 @@ When you create a project in GitLab, you'll have access to a large number of
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [Kubernetes cluster integration](clusters/index.md): Connecting your GitLab project
with a Kubernetes cluster
+ - [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html): Feature flags allow you to ship a project in
+ different flavors by dynamically toggling certain functionality **[PREMIUM]**
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
@@ -75,12 +84,17 @@ When you create a project in GitLab, you'll have access to a large number of
- [Wiki](wiki/index.md): document your GitLab project in an integrated Wiki.
- [Snippets](../snippets.md): store, share and collaborate on code snippets.
- [Cycle Analytics](cycle_analytics.md): review your development lifecycle.
+- [Security Dashboard](security_dashboard.md): Security Dashboard. **[ULTIMATE]**
- [Syntax highlighting](highlighting.md): an alternative to customize
your code blocks, overriding GitLab's default choice of language.
- [Badges](badges.md): badges for the project overview.
- [Releases](releases/index.md): a way to track deliverables in your project as snapshot in time of
the source, build output, and other metadata or artifacts
associated with a released version of your code.
+- [Maven packages](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html): your private Maven repository in GitLab. **[PREMIUM]**
+- [NPM packages](https://docs.gitlab.com/ee/user/project/packages/npm_registry.html): your private NPM package registry in GitLab. **[PREMIUM]**
+- [Code owners](code_owners.md): specify code owners for certain files **[STARTER]**
+- [License Management](https://docs.gitlab.com/ee/user/application_security/license_management/index.html): approve and blacklist licenses for projects. **[ULTIMATE]**
### Project integrations
@@ -116,6 +130,13 @@ Read through the documentation on [project settings](settings/index.md).
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
+## CI/CD for external repositories **[PREMIUM]**
+
+Instead of importing a repository directly to GitLab, you can connect your repository
+as a CI/CD project.
+
+Read through the documentation on [CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html).
+
## Project members
Learn how to [add members to your projects](members/index.md).
diff --git a/doc/user/project/insights/img/insights_example_bar_chart.png b/doc/user/project/insights/img/insights_example_bar_chart.png
new file mode 100644
index 00000000000..eb96eb4a90f
--- /dev/null
+++ b/doc/user/project/insights/img/insights_example_bar_chart.png
Binary files differ
diff --git a/doc/user/project/insights/img/insights_example_bar_time_series_chart.png b/doc/user/project/insights/img/insights_example_bar_time_series_chart.png
new file mode 100644
index 00000000000..28aa81939d8
--- /dev/null
+++ b/doc/user/project/insights/img/insights_example_bar_time_series_chart.png
Binary files differ
diff --git a/doc/user/project/insights/img/insights_example_line_chart.png b/doc/user/project/insights/img/insights_example_line_chart.png
new file mode 100644
index 00000000000..131dbedd35e
--- /dev/null
+++ b/doc/user/project/insights/img/insights_example_line_chart.png
Binary files differ
diff --git a/doc/user/project/insights/img/insights_example_pie_chart.png b/doc/user/project/insights/img/insights_example_pie_chart.png
new file mode 100644
index 00000000000..518ed7338f9
--- /dev/null
+++ b/doc/user/project/insights/img/insights_example_pie_chart.png
Binary files differ
diff --git a/doc/user/project/insights/img/insights_example_stacked_bar_chart.png b/doc/user/project/insights/img/insights_example_stacked_bar_chart.png
new file mode 100644
index 00000000000..aafec4b394e
--- /dev/null
+++ b/doc/user/project/insights/img/insights_example_stacked_bar_chart.png
Binary files differ
diff --git a/doc/user/project/insights/img/project_insights.png b/doc/user/project/insights/img/project_insights.png
new file mode 100644
index 00000000000..2d0292dda54
--- /dev/null
+++ b/doc/user/project/insights/img/project_insights.png
Binary files differ
diff --git a/doc/user/project/insights/index.md b/doc/user/project/insights/index.md
new file mode 100644
index 00000000000..720f4c6604e
--- /dev/null
+++ b/doc/user/project/insights/index.md
@@ -0,0 +1,300 @@
+# Insights **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.9 behind the `insights` feature flag.
+
+CAUTION: **Beta:**
+Insights is considered beta, and is not ready for production use.
+Follow [gitlab-org&725](https://gitlab.com/groups/gitlab-org/-/epics/725) for
+updates.
+
+Configure the Insights that matter for your projects to explore data such as
+triage hygiene, issues created/closed per a given period, average time for merge
+requests to be merged and much more.
+
+![Insights example stacked bar chart](img/project_insights.png)
+
+NOTE: **Note:**
+This feature is [also available at the group level](https://docs.gitlab.com/ee/user/group/insights/index.html).
+
+## Configure your Insights
+
+Insights are configured using a YAML file called `.gitlab/insights.yml` within
+a project. That file will then be used in the project's Insights page.
+
+See [Writing your `.gitlab/insights.yml`](#writing-your-gitlabinsightsyml) below
+for details about the content of this file.
+
+NOTE: **Note:**
+Once the configuration file is created, you can also
+[use it for your project's group](https://docs.gitlab.com/ee/user/group/insights/index.html#configure-your-insights).
+
+NOTE: **Note:**
+If the project doesn't have any configuration file, it'll try to use
+the group configuration if possible. If the group doesn't have any
+configuration, the default configuration will be used.
+
+## Permissions
+
+If you have access to view a project, then you have access to view their
+Insights.
+
+NOTE: **Note:**
+Issues or merge requests that you don't have access to (because you don't have
+access to the project they belong to, or because they are confidential) are
+filtered out of the Insights charts.
+
+You may also consult the [group permissions table](../../permissions.md#group-members-permissions).
+
+## Writing your `.gitlab/insights.yml`
+
+The `.gitlab/insights.yml` file defines the structure and order of the Insights
+charts that will be displayed in each Insights page of your project or group.
+
+Each page has a unique key and a collection of charts to fetch and display.
+
+For example, here's a single definition for Insights that will display one page with one chart:
+
+```yaml
+bugsCharts:
+ title: 'Charts for Bugs'
+ charts:
+ - title: Monthly Bugs Created (bar)
+ type: bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ group_by: month
+ period_limit: 24
+```
+
+Each chart definition is made up of a hash composed of key-value pairs.
+
+For example, here's single chart definition:
+
+```yaml
+monthlyBugsCreated:
+ title: Monthly Bugs Created (bar)
+ type: bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ group_by: month
+ period_limit: 24
+```
+
+## Configuration parameters
+
+A chart is defined as a list of parameters that define the chart's behavior.
+
+The following table lists available parameters for charts:
+
+| Keyword | Description |
+|:---------------------------------------------------|:------------|
+| [`title`](#title) | The title of the chart. This will displayed on the Insights page. |
+| [`type`](#type) | The type of chart: `bar`, `line`, `stacked-bar`, `pie` etc. |
+| [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. |
+
+## Parameter details
+
+The following are detailed explanations for parameters used to configure
+Insights charts.
+
+### `title`
+
+`title` is the title of the chart as it will be displayed on the Insights page.
+For example:
+
+```yaml
+monthlyBugsCreated:
+ title: Monthly Bugs Created (bar)
+```
+
+### `type`
+
+`type` is the chart type.
+
+For example:
+
+```yaml
+monthlyBugsCreated:
+ title: Monthly Bugs Created (bar)
+ type: bar
+```
+
+Supported values are:
+
+| Name | Example |
+| ----- | ------- |
+| `bar` | ![Insights example bar chart](img/insights_example_bar_chart.png) |
+| `bar` (time series, i.e. when `group_by` is used) | ![Insights example bar time series chart](img/insights_example_bar_time_series_chart.png) |
+| `pie` | ![Insights example pie chart](img/insights_example_pie_chart.png) |
+| `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) |
+| `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) |
+
+### `query`
+
+`query` allows to define the conditions for issues / merge requests to be part
+of the chart.
+
+Example:
+
+```yaml
+monthlyBugsCreated:
+ title: Monthly Bugs Created (bar)
+ type: bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ collection_labels:
+ - S1
+ - S2
+ - S3
+ - S4
+ group_by: week
+ period_limit: 104
+```
+
+#### `query.issuable_type`
+
+Defines the type of "issuable" you want to create a chart for.
+
+Supported values are:
+
+- `issue`: The chart will display issues' data.
+- `merge_request`: The chart will display merge requests' data.
+
+#### `query.issuable_state`
+
+Filter by the state of the queried "issuable".
+
+If you omit it, no state filter will be applied.
+
+Supported values are:
+
+- `opened`: Open issues / merge requests.
+- `closed`: Closed Open issues / merge requests.
+- `locked`: Issues / merge requests that have their discussion locked.
+- `merged`: Merged merge requests.
+
+#### `query.filter_labels`
+
+Filter by labels applied to the queried "issuable".
+
+If you omit it, no labels filter will be applied. All the defined labels must be
+applied to the "issuable" in order for it to be selected.
+
+Example:
+
+```yaml
+monthlyBugsCreated:
+ title: Monthly regressions Created (bar)
+ type: bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ - regression
+```
+
+#### `query.collection_labels`
+
+Group "issuable" by the configured labels.
+
+If you omit it, no grouping will be done. When using this keyword, you need to
+set `type` to either `line` or `stacked-bar`.
+
+Example:
+
+```yaml
+weeklyBugsBySeverity:
+ title: Weekly Bugs By Severity (stacked bar)
+ type: stacked-bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ collection_labels:
+ - S1
+ - S2
+ - S3
+ - S4
+```
+
+#### `query.group_by`
+
+Define the X-axis of your chart.
+
+Supported values are:
+
+- `day`: Group data per day.
+- `week`: Group data per week.
+- `month`: Group data per month.
+
+#### `query.period_limit`
+
+Define how far "issuables" are queried in the past.
+
+The unit is related to the `query.group_by` you defined. For instance if you
+defined `query.group_by: 'day'` then `query.period_limit: 365` would mean
+"Gather and display data for the last 365 days".
+
+If you omit it, default values will be applied depending on the `query.group_by`
+you defined.
+
+| `query.group_by` | Default value |
+| ---------------- | ------------- |
+| `day` | 30 |
+| `week` | 4 |
+| `month` | 12 |
+
+## Complete example
+
+```yaml
+bugsCharts:
+ title: 'Charts for Bugs'
+ charts:
+ - title: Monthly Bugs Created (bar)
+ type: bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ group_by: month
+ period_limit: 24
+ - title: Weekly Bugs By Severity (stacked bar)
+ type: stacked-bar
+ query:
+ issuable_type: issue
+ issuable_state: opened
+ filter_labels:
+ - bug
+ collection_labels:
+ - S1
+ - S2
+ - S3
+ - S4
+ group_by: week
+ period_limit: 104
+ - title: Monthly Bugs By Team (line)
+ type: line
+ query:
+ issuable_type: merge_request
+ issuable_state: opened
+ filter_labels:
+ - bug
+ collection_labels:
+ - Manage
+ - Plan
+ - Create
+ group_by: month
+ period_limit: 24
+```
diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md
index 6fc083170b6..23f1ce7a15a 100644
--- a/doc/user/project/integrations/custom_issue_tracker.md
+++ b/doc/user/project/integrations/custom_issue_tracker.md
@@ -7,9 +7,9 @@ in the table below.
| Field | Description |
| ----- | ----------- |
-| `title` | A title for the issue tracker (to differentiate between instances, for example) |
+| `title` | A title for the issue tracker (to differentiate between instances, for example). |
| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | Currently unused. Will be changed in a future release. |
+| `project_url` | The URL to the project in the custom issue tracker. |
| `issues_url` | The URL to the issue in the issue tracker project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. For example, `https://customissuetracker.com/project-name/:id`. |
| `new_issue_url` | Currently unused. Will be changed in a future release. |
diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md
new file mode 100644
index 00000000000..cdb0e34fdf6
--- /dev/null
+++ b/doc/user/project/integrations/github.md
@@ -0,0 +1,48 @@
+# GitHub project integration **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3836) in GitLab Premium 10.6.
+
+GitLab provides an integration for updating the pipeline statuses on GitHub.
+This is especially useful if using GitLab for CI/CD only.
+
+This project integration is separate from the [instance wide GitHub integration](../import/github.md#mirroring-and-pipeline-status-sharing)
+and is automatically configured on [GitHub import](../../../integration/github.md).
+
+![Pipeline status update on GitHub](img/github_status_check_pipeline_update.png)
+
+## Configuration
+
+### Complete these steps on GitHub
+
+This integration requires a [GitHub API token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)
+with `repo:status` access granted:
+
+1. Go to your "Personal access tokens" page at https://github.com/settings/tokens
+1. Click "Generate New Token"
+1. Ensure that `repo:status` is checked and click "Generate token"
+1. Copy the generated token to use on GitLab
+
+### Complete these steps on GitLab
+
+1. Navigate to the project you want to configure.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "GitHub".
+1. Select the "Active" checkbox.
+1. Paste the token you've generated on GitHub
+1. Enter the path to your project on GitHub, such as `https://github.com/username/repository`
+1. Optionally check "Static status check names" checkbox to enable static status check names.
+1. Save or optionally click "Test Settings".
+
+#### Static / dynamic status check names
+
+Since GitLab 11.5 it is possible to opt-in to using static status check names.
+
+This makes it possible to mark these status checks as _Required_ on GitHub.
+If you check "Static status check names" checkbox on the integration page, your
+GitLab instance host name is going to be appended to a status check name,
+whereas in case of dynamic status check names, a branch name is going to be
+appended.
+
+Dynamic status check name is a default behavior.
+
+![Configure GitHub Project Integration](img/github_configuration.png)
diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
new file mode 100644
index 00000000000..8e062ca627b
--- /dev/null
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -0,0 +1,65 @@
+# GitLab Slack application **[FREE ONLY]**
+
+NOTE: **Note:**
+The GitLab Slack application is only configurable for GitLab.com. It will **not**
+work for on-premises installations where you can configure the
+[Slack slash commands](slack_slash_commands.md) service instead. We're working
+with Slack on making this configurable for all GitLab installations, but there's
+no ETA.
+It was first introduced in GitLab 9.4 and distributed to Slack App Directory in
+GitLab 10.2.
+
+Slack provides a native application which you can enable via your project's
+integrations on GitLab.com.
+
+## Slack App Directory
+
+The simplest way to enable the GitLab Slack application for your workspace is to
+install the [GitLab application](https://slack-platform.slack.com/apps/A676ADMV5-gitlab) from
+the [Slack App Directory](https://slack.com/apps).
+
+Clicking install will take you to the
+[GitLab Slack application landing page](https://gitlab.com/profile/slack/edit)
+where you can select a project to enable the GitLab Slack application for.
+
+![GitLab Slack application landing page](img/gitlab_slack_app_landing_page.png)
+
+## Configuration
+
+Alternatively, you can configure the Slack application with a project's
+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].
+
+To enable GitLab's service for your Slack team:
+
+1. Go to your project's **Settings > Integration > Slack application** (only
+ visible on GitLab.com)
+1. Click the "Add to Slack" button
+
+That's all! You can now start using the Slack slash commands.
+
+## Usage
+
+After confirming the installation, you, and everyone else in your Slack team,
+can use all the [slash commands].
+
+When you perform your first slash command you will be asked to authorize your
+Slack user on GitLab.com.
+
+The only difference with the [manually configurable Slack slash commands][slack-manual]
+is that all the commands should be prefixed with the `/gitlab` keyword.
+We are working on making this configurable in the future.
+
+For example, to show the issue number `1001` under the `gitlab-org/gitlab-ce`
+project, you would do:
+
+```
+/gitlab gitlab-org/gitlab-ce 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/github_configuration.png b/doc/user/project/integrations/img/github_configuration.png
new file mode 100644
index 00000000000..9f2d47921c7
--- /dev/null
+++ b/doc/user/project/integrations/img/github_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/github_status_check_pipeline_update.png b/doc/user/project/integrations/img/github_status_check_pipeline_update.png
new file mode 100644
index 00000000000..9d02ca18497
--- /dev/null
+++ b/doc/user/project/integrations/img/github_status_check_pipeline_update.png
Binary files differ
diff --git a/doc/user/project/integrations/img/gitlab_slack_app_landing_page.png b/doc/user/project/integrations/img/gitlab_slack_app_landing_page.png
new file mode 100644
index 00000000000..57cd35c9f5d
--- /dev/null
+++ b/doc/user/project/integrations/img/gitlab_slack_app_landing_page.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_add_metric.png b/doc/user/project/integrations/img/prometheus_add_metric.png
new file mode 100644
index 00000000000..e85670e1a13
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_add_metric.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_alert.png b/doc/user/project/integrations/img/prometheus_alert.png
new file mode 100644
index 00000000000..a37f0477fd9
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_alert.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_service_alerts.png b/doc/user/project/integrations/img/prometheus_service_alerts.png
new file mode 100644
index 00000000000..a81dfe7da14
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_service_alerts.png
Binary files differ
diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md
index 8b1908c46fe..1c64b275d6e 100644
--- a/doc/user/project/integrations/mock_ci.md
+++ b/doc/user/project/integrations/mock_ci.md
@@ -5,7 +5,7 @@
To set up the mock CI service server, respond to the following endpoints
- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json`
- - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }`
+ - Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success-with-warnings'|'skipped'|'not_found'] }`
- If the service returns a 404, it is interpreted as `pending`
- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}`
- Just where the build is linked to, doesn't matter if implemented
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 42c7824a125..f560de427c5 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -14,8 +14,7 @@ want to configure.
![Project services list](img/project_services.png)
-Below, you will find a list of the currently supported ones accompanied with
-comprehensive documentation.
+Below, you will find a list of the currently supported ones accompanied with comprehensive documentation.
## Services
@@ -35,10 +34,12 @@ Click on the service links to see further configuration instructions and details
| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
+| [GitHub](github.md) **[PREMIUM]** | Sends pipeline notifications to GitHub |
| [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
+| [Jenkins](https://docs.gitlab.com/ee/integration/jenkins.html) **[STARTER]** | An extendable open source continuous integration server |
| JetBrains TeamCity CI | A continuous integration and build server |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
@@ -46,7 +47,8 @@ Click on the service links to see further configuration instructions and details
| Packagist | Update your project on Packagist, the main Composer repository |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications |
-| [Slack slash commands](slack_slash_commands.md) | Use slash commands in Slack to control GitLab |
+| [Slack slash commands](slack_slash_commands.md) **[CORE ONLY]** | Use slash commands in Slack to control GitLab |
+| [GitLab Slack application](gitlab_slack_application.md) **[FREE ONLY]** | Use Slack's official application |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 43a9e24526d..40113624430 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -13,7 +13,7 @@ There are two ways to set up Prometheus integration, depending on where your app
- For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes).
- For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus).
-Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-cicd-environments).
+Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-cicd-environments). You are also able to [add your own metrics](#adding-additional-metrics-premium) as well.
## Enabling Prometheus Integration
@@ -93,6 +93,85 @@ GitLab will automatically scan the Prometheus server for metrics from known serv
You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
+### Adding additional metrics **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3799) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.6.
+
+Additional metrics can be monitored by adding them on the Prometheus integration page. Once saved, they will be displayed on the environment performance dashboard.
+
+![Add New Metric](img/prometheus_add_metric.png)
+
+A few fields are required:
+
+- **Name**: Chart title
+- **Type**: Type of metric. Metrics of the same type will be shown together.
+- **Query**: Valid [PromQL query](https://prometheus.io/docs/prometheus/latest/querying/basics/).
+- **Y-axis label**: Y axis title to display on the dashboard.
+- **Unit label**: Query units, for example `req / sec`. Shown next to the value.
+
+Multiple metrics can be displayed on the same chart if the fields **Name**, **Type**, and **Y-axis label** match between metrics. For example, a metric with **Name** `Requests Rate`, **Type** `Business`, and **Y-axis label** `rec / sec` would display on the same chart as a second metric with the same values. A **Legend label** is suggested if this feature used.
+
+#### Query Variables
+
+GitLab supports a limited set of [CI variables](../../../ci/variables/README.html) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `CI_ENVIRONMENT_SLUG`. The supported variables are:
+
+- CI_ENVIRONMENT_SLUG
+- KUBE_NAMESPACE
+
+To specify a variable in a query, enclose it in curly braces with a leading percent. For example: `%{ci_environment_slug}`.
+
+### Setting up alerts for Prometheus metrics **[ULTIMATE]**
+
+#### Managed Prometheus instances
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6590) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2 for [custom metrics](#adding-additional-metrics-premium), and 11.3 for [library metrics](prometheus_library/metrics.md).
+
+For managed Prometheus instances using auto configuration, alerts for metrics [can be configured](#adding-additional-metrics-premium) directly in the performance dashboard.
+
+To set an alert, click on the alarm icon in the top right corner of the metric you want to create the alert for. A dropdown
+will appear, with options to set the threshold and operator. Click **Add** to save and activate the alert.
+
+![Adding an alert](img/prometheus_alert.png)
+
+To remove the alert, click back on the alert icon for the desired metric, and click **Delete**.
+
+#### External Prometheus instances
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9258) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.8.
+
+For manually configured Prometheus servers, a notify endpoint is provided to use with Prometheus webhooks. If you have manual configuration enabled, an **Alerts** section is added to **Settings > Integrations > Prometheus**. This contains the *URL* and *Authorization Key*. The **Reset Key** button will invalidate the key and generate a new one.
+
+![Prometheus service configuration of Alerts](img/prometheus_service_alerts.png)
+
+To send GitLab alert notifications, copy the *URL* and *Authorization Key* into the [`webhook_configs`](https://prometheus.io/docs/alerting/configuration/#webhook_config) section of your Prometheus Alertmanager configuration:
+
+```yaml
+receivers:
+ name: gitlab
+ webhook_configs:
+ - http_config:
+ bearer_token: 9e1cbfcd546896a9ea8be557caf13a76
+ send_resolved: true
+ url: http://192.168.178.31:3001/root/manual_prometheus/prometheus/alerts/notify.json
+ ...
+```
+
+### Taking action on incidents **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
+
+Alerts can be used to trigger actions, like open an issue automatically. To configure the actions:
+
+1. Navigate to your project's **Settings > Operations > Incidents**.
+1. Enable the option to create issues.
+1. Choose the [issue template](../description_templates.md) to create the issue from.
+1. Optionally, select whether to send an email notification to the developers of the project.
+1. Click **Save changes**.
+
+Once enabled, an issue will be opened automatically when an alert is triggered. To further customize the issue, you can add labels, mentions, or any other supported [quick action](../quick_actions.md) in the selected issue template.
+
+If the metric exceeds the threshold of the alert for over 5 minutes, an email will be sent to all [Maintainers and Owners](../../permissions.md#project-members-permissions) of the project.
+
## Determining the performance impact of a merge
> [Introduced][ce-10408] in GitLab 9.2.
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 7a45c87ada0..436129f1dbc 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -1,6 +1,6 @@
# Monitoring Kubernetes
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0.
GitLab has support for automatically detecting and monitoring Kubernetes metrics.
@@ -35,3 +35,25 @@ Prometheus needs to be deployed into the cluster and configured properly in orde
In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available.
Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment.
+
+## Displaying Canary metrics **[PREMIUM]**
+
+> Introduced in [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15201).
+
+GitLab also gathers Kubernetes metrics for [canary deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html), allowing easy comparison between the current deployed version and the canary.
+
+These metrics expect the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name to begin with `$CI_ENVIRONMENT_SLUG-canary`, to isolate the canary metrics.
+
+### Canary metrics supported
+
+- Average Memory Usage (MB)
+
+ ```
+ avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024
+ ```
+
+- Average CPU Utilization (%)
+
+ ```
+ avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))
+ ```
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
index c267da69bb3..371e78ca3a4 100644
--- a/doc/user/project/integrations/slack_slash_commands.md
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -1,10 +1,15 @@
-# Slack slash commands
+# Slack slash commands **[CORE ONLY]**
-> Introduced in GitLab 8.15
+> Introduced in GitLab 8.15.
-Slack slash commands allow you to control GitLab and view content right inside Slack, without having to leave it. This requires configurations in both Slack and GitLab.
+Slack slash commands allow you to control GitLab and view content right inside
+Slack, without having to leave it. This requires configurations in both Slack and GitLab.
-> Note: GitLab can also send events (e.g. issue created) to Slack as notifications. This is the separately configured [Slack Notifications Service](slack.md).
+GitLab can also send events (e.g., `issue created`) to Slack as notifications.
+This is the separately configured [Slack Notifications Service](slack.md).
+
+NOTE: **Note:**
+For GitLab.com, use the [Slack app](gitlab_slack_application.md) instead.
## Configuration
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index ad47b848bea..31020de5208 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -151,7 +151,7 @@ Create lists for each of your team members and quickly drag-and-drop issues onto
## Permissions
-[Developers and up](../permissions.md) can use all the functionality of the
+[Reporters and up](../permissions.md) can use all the functionality of the
Issue Board, that is, create or delete lists and drag issues from one list to another.
## GitLab Enterprise features for Issue Boards
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index 9a147deecd4..5e05846b77f 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -65,6 +65,26 @@ 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._
+## New issue via Service Desk **[PREMIUM]**
+
+Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) to your project and offer email support.
+By doing so, when your customer sends a new email, a new issue can be created in
+the appropriate project and followed up from there.
+
+## New issue from the group-level Issue Tracker
+
+Head to the Group dashboard and click "Issues" in the sidebar to visit the Issue Tracker
+for all projects in your Group. Select the project you'd like to add an issue for
+using the dropdown button at the top-right of the page.
+
+![Select project to create issue](img/select_project_from_group_level_issue_tracker.png)
+
+We'll keep track of the project you selected most recently, and use it as the default
+for your next visit. This should save you a lot of time and clicks, if you mostly
+create issues for the same project.
+
+![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png)
+
## New issue via URL with prefilled fields
You can link directly to the new issue page for a given project, with prefilled
diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md
new file mode 100644
index 00000000000..56b94585672
--- /dev/null
+++ b/doc/user/project/issues/csv_export.md
@@ -0,0 +1,77 @@
+# Export Issues to CSV **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1126) in [GitLab Starter 9.0](https://about.gitlab.com/2017/03/22/gitlab-9-0-released/#export-issues-ees-eep).
+
+Issues can be exported as CSV from GitLab and are sent to your default notification email as an attachment.
+
+## Overview
+
+**Export Issues to CSV** enables you and your team to export all the data collected from issues into
+a **[comma-separated values](https://en.wikipedia.org/wiki/Comma-separated_values)** (CSV) file,
+which stores tabular data in plain text.
+
+> _CSVs are a handy way of getting data from one program to another where one program cannot read the other ones normal output._ [Ref](https://www.quora.com/What-is-a-CSV-file-and-its-uses)
+
+CSV files can be used with any plotter or spreadsheet-based program, such as Microsoft Excel,
+Open Office Calc, or Google Spreadsheets.
+
+## Use cases
+
+Among numerous use cases for exporting issues for CSV, we can name a few:
+
+- Make a snapshot of issues for offline analysis or to communicate with other teams who may not be in GitLab
+- Create diagrams, graphs, and charts from the CSV data
+- Present the data in any other format for auditing or sharing reasons
+- Import the issues elsewhere to a system outside of GitLab
+- Long-term issues' data analysis with multiple snapshots created along the time
+- Use the long-term data to gather relevant feedback given in the issues, and improve your product based on real metrics
+
+## Choosing which issues to include
+
+From the issues page you can narrow down which issues to export using the search bar, along with the All/Open/Closed tabs. All issues returned will be exported, including those not shown on the first page.
+
+![CSV export button](img/csv_export_button.png)
+
+You will be asked to confirm the number of issues and email address for the export, after which the email will begin being prepared.
+
+![CSV export modal dialog](img/csv_export_modal.png)
+
+## Sorting
+
+Exported issues are always sorted by `Issue ID`.
+
+## Format
+
+> **Time Estimate** and **Time Spent** columns were [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2627) in GitLab Starter 10.0.
+>
+> **Weight** and **Locked** columns were [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5300) in GitLab Starter 10.8.
+
+Data will be encoded with a comma as the column delimiter, with `"` used to quote fields if needed, and newlines to separate rows. The first row will be the headers, which are listed in the following table along with a description of the values:
+
+
+| Column | Description |
+|---------|-------------|
+| Issue ID | Issue `iid` |
+| URL | A link to the issue on GitLab |
+| Title | Issue `title` |
+| State | `Open` or `Closed` |
+| Description | Issue `description` |
+| Author | Full name of the issue author |
+| Author Username | Username of the author, with the `@` symbol omitted |
+| Assignee | Full name of the issue assignee |
+| Assignee Username | Username of the author, with the `@` symbol omitted |
+| Confidential | `Yes` or `No` |
+| Locked | `Yes` or `No` |
+| Due Date | Formated as `YYYY-MM-DD` |
+| Created At (UTC) | Formated as `YYYY-MM-DD HH:MM:SS` |
+| Updated At (UTC) | Formated as `YYYY-MM-DD HH:MM:SS` |
+| 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 |
+
+
+## Limitations
+
+As the issues will be sent as an email attachment, there is a limit on how much data can be exported. Currently this limit is 20MB to ensure successful delivery across a range of email providers. If this limit is reached we suggest narrowing the search before export, perhaps by exporting open and closed issues separately.
diff --git a/doc/user/project/issues/img/create_issue_from_group_level_issue_tracker.png b/doc/user/project/issues/img/create_issue_from_group_level_issue_tracker.png
new file mode 100644
index 00000000000..8996490ae63
--- /dev/null
+++ b/doc/user/project/issues/img/create_issue_from_group_level_issue_tracker.png
Binary files differ
diff --git a/doc/user/project/issues/img/csv_export_button.png b/doc/user/project/issues/img/csv_export_button.png
new file mode 100644
index 00000000000..f9fcfd71c2f
--- /dev/null
+++ b/doc/user/project/issues/img/csv_export_button.png
Binary files differ
diff --git a/doc/user/project/issues/img/csv_export_modal.png b/doc/user/project/issues/img/csv_export_modal.png
new file mode 100644
index 00000000000..f988deec966
--- /dev/null
+++ b/doc/user/project/issues/img/csv_export_modal.png
Binary files differ
diff --git a/doc/user/project/issues/img/multiple_assignees.gif b/doc/user/project/issues/img/multiple_assignees.gif
new file mode 100644
index 00000000000..055a0efd9ae
--- /dev/null
+++ b/doc/user/project/issues/img/multiple_assignees.gif
Binary files differ
diff --git a/doc/user/project/issues/img/multiple_assignees_for_issues.png b/doc/user/project/issues/img/multiple_assignees_for_issues.png
new file mode 100644
index 00000000000..e8ae37d427d
--- /dev/null
+++ b/doc/user/project/issues/img/multiple_assignees_for_issues.png
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_add.png b/doc/user/project/issues/img/related_issues_add.png
new file mode 100644
index 00000000000..f59d2335386
--- /dev/null
+++ b/doc/user/project/issues/img/related_issues_add.png
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_remove.png b/doc/user/project/issues/img/related_issues_remove.png
new file mode 100644
index 00000000000..be2ec59e61b
--- /dev/null
+++ b/doc/user/project/issues/img/related_issues_remove.png
Binary files differ
diff --git a/doc/user/project/issues/img/select_project_from_group_level_issue_tracker.png b/doc/user/project/issues/img/select_project_from_group_level_issue_tracker.png
new file mode 100644
index 00000000000..97d93620b2a
--- /dev/null
+++ b/doc/user/project/issues/img/select_project_from_group_level_issue_tracker.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 653bd94e513..ef9fcaec3e6 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -50,7 +50,7 @@ where there is shared ownership of an issue.
In [GitLab Starter](https://about.gitlab.com/pricing/), you can
assign multiple people to an issue.
-Learn more in the [Multiple Assignees documentation](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html).
+Learn more in the [Multiple Assignees documentation](multiple_assignees_for_issues.md).
#### 4. Milestone
@@ -103,7 +103,7 @@ Learn more in the [Issue Weight documentation](https://docs.gitlab.com/ee/workfl
- Unsubscribe: if you are receiving notifications on that issue but no
longer want to receive them, unsubscribe from it.
-Read more in the [notifications documentation](../../../workflow/notifications.md#issue--merge-request-events).
+Read more in the [notifications documentation](https://docs.gitlab.com/ee/workflow/notifications.html#issue--epics--merge-request-events).
#### 11. Reference
diff --git a/doc/user/project/issues/multiple_assignees_for_issues.md b/doc/user/project/issues/multiple_assignees_for_issues.md
new file mode 100644
index 00000000000..8781ebdd5b0
--- /dev/null
+++ b/doc/user/project/issues/multiple_assignees_for_issues.md
@@ -0,0 +1,41 @@
+# Multiple Assignees for Issues **[STARTER]**
+
+> **Note:**
+[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1904)
+in [GitLab Starter 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues).
+
+## Overview
+
+In large teams, where there is shared ownership of an issue, it can be difficult
+to track who is working on it, who already completed their contributions, who
+didn't even start yet.
+
+In [GitLab Enterprise Edition](https://about.gitlab.com/pricing/),
+you can also select multiple assignees to an issue, making it easier to
+track, and making clearer who is accountable for it.
+
+![multiple assignees for issues](img/multiple_assignees_for_issues.png)
+
+## Use cases
+
+Consider a team formed by frontend developers, backend developers,
+UX designers, QA testers, and a product manager working together to bring an idea to
+market.
+
+Multiple Assignees for Issues makes collaboration smother,
+and allows shared responsibilities to be clearly displayed.
+All assignees are shown across your team's workflows and receive notifications (as they
+would as single assignees), simplifying communication and ownership.
+
+Once an assignee had their work completed, they would remove themselves as assignees, making
+it clear that their role is complete.
+
+## How it works
+
+From an opened issue, expand the right sidebar, locate the assignees entry,
+and click on **Edit**. From the dropdown menu, select as many users as you want
+to assign the issue to.
+
+![adding multiple assignees](img/multiple_assignees.gif)
+
+An assignee can be easily removed by deselecting them from the same dropdown menu.
diff --git a/doc/user/project/issues/related_issues.md b/doc/user/project/issues/related_issues.md
new file mode 100644
index 00000000000..db0ab65b442
--- /dev/null
+++ b/doc/user/project/issues/related_issues.md
@@ -0,0 +1,45 @@
+# Related issues **[STARTER]**
+
+> [Introduced][ee-1797] in [GitLab Starter][ee] 9.4.
+
+Related issues are a bi-directional relationship between any two issues
+and appear in a block below the issue description. Issues can be across groups
+and projects.
+
+The relationship only shows up in the UI if the user can see both issues.
+
+## Adding a related issue
+
+You can relate one issue to another by clicking the related issues "+" button
+in the header of the related issue block. Then, input the issue reference number
+or paste in the full URL of the issue.
+
+Issues of the same project can be specified just by the reference number.
+Issues from a different project require additional information like the
+group and the project name. For example:
+
+- same project: `#44`
+- same group: `project#44 `
+- different group: `group/project#44`
+
+Valid references will be added to a temporary list that you can review.
+When ready, click the green "Add related issues" button to submit.
+
+![Adding a related issue](img/related_issues_add.png)
+
+## Removing a related issue
+
+In the related issues block, click the "x" icon on the right-side of each issue
+token that you wish to remove. Due to the bi-directional relationship, it
+will no longer appear in either issue.
+
+![Removing a related issue](img/related_issues_remove.png)
+
+Please access our [permissions] page for more information.
+
+Additionally, you are also able to manage related issues through [our API].
+
+[ee]: https://about.gitlab.com/pricing/
+[ee-1797]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1797
+[permissions]: ../../permissions.md
+[Our API]: https://docs.gitlab.com/ee/api/issue_links.html
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 9c045dfcce4..ac91cd4ea98 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -8,13 +8,52 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels:
-- **Project labels** can be assigned to issues or merge requests in that project only.
+- **Project labels** can be assigned to issues or merge requests in that project only.
- **Group labels** can be assigned to any issue or merge request of any project in that group or any subgroups of the group.
+## Scoped labels **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9175) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+
+Scoped labels allow teams to use the simple and familiar feature of labels to
+annotate their issues, merge requests, and epics to achieve custom fields and
+custom workflow states by leveraging a special label title syntax.
+
+A scoped label is a kind of label defined only by a special double-colon syntax
+in the label’s title, using the format `key::value`. For example:
+
+![A sample scoped label](img/key_value_labels.png)
+
+Two scoped labels with the same key but a different value cannot simultaneously
+apply to an issue, epic, or merge request. For example, if an issue already has `priority::3`
+and you apply `priority::2` to it, `priority::3` is automatically removed from the issue.
+
+An issue, epic, or merge request cannot have two scoped labels with the same key.
+For example, if an issue is already labeled `priority::3` and you apply the label `priority::2` to it,
+`priority::3` is automatically removed.
+
+### Workflows with scoped labels **[PREMIUM]**
+
+Suppose you wanted a custom field in issues to track the platform operating system
+that your features target, where each issue should only target one platform. You
+would then create labels `platform::iOS`, `platform::Android`, `platform::Linux`,
+etc., as necessary. Applying any one of these labels on a given issue would
+automatically remove any other existing label that starts with `platform::`.
+
+The same pattern could be applied to represent the workflow states of your teams.
+Suppose you have the labels `workflow::development`, `workflow::review`, and
+`workflow::deployed`. If an issue already has the label `workflow::development`
+applied, and a developer wanted to advance the issue to `workflow::review`, they
+would simply apply that label, and the `workflow::development` label would
+automatically be removed. This behavior already exists when you move issues
+across label lists in an [issue board](issue_board.md#creating-workflows), but
+now, team members who may not be working in an issue board directly would still
+be able to advance workflow states consistently in issues themselves.
+
## Creating labels
>**Note:**
-A permission level of `Developer` or higher is required to create labels.
+A permission level of Reporter or higher is required to create labels.
### New project label
@@ -35,6 +74,9 @@ GitLab will add the following default labels to the project:
To create a **group label**, follow similar steps from above to project labels. Navigate to **Issues > Labels** in the group and create it from there.
This page only shows group labels in this group.
+Alternatively, you can create group labels also from Epic sidebar. Please note that the created label will belong to the immediate group to which epic belongs.
+
+![Create Labels from Epic](img/labels_epic_sidebar.png)
Group labels appear in every label list page of the group's child projects.
@@ -42,14 +84,14 @@ Group labels appear in every label list page of the group's child projects.
### New project label from sidebar
-From the sidebar of an issue or a merge request, you can create a create a new **project label** inline immediately, instead of navigating to the project label list page.
+From the sidebar of an issue or a merge request, you can create a new **project label** inline immediately, instead of navigating to the project label list page.
![Labels inline](img/new_label_from_sidebar.gif)
## Editing labels
NOTE: **Note:**
-A permission level of `Developer` or higher is required to edit labels.
+A permission level of Reporter or higher is required to edit labels.
You can update a label by navigating to **Issues > Labels** in the project or group and clicking the pencil icon.
@@ -81,7 +123,7 @@ top-right:
GitLab will consider the label title and description for the search.
-## Filtering issues and merge requests by label
+## Filtering issues, merge requests and epics by label
### Filtering in list pages
@@ -89,11 +131,16 @@ From the project issue list page and the project merge request list page, you ca
From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels (including subgroup ancestors and subgroup descendants) and project labels.
+From the group epic list page, you can [filter](../search/index.md#issues-and-merge-requests) by both current group labels as well as decendent group labels.
+
![Labels group issues](img/labels_group_issues.png)
### Filtering in issue boards
- From [project boards](issue_board.md), you can filter by both group labels and project labels in the [search and filter bar](../search/index.md#issue-boards).
+- From [group issue boards](issue_board.md#group-issue-boards-premium), you can filter by only group labels in the [search and filter bar](../search/index.md#issue-boards). **[PREMIUM]**
+- From [project boards](issue_board.md), you can filter by both group labels and project labels in the [issue board configuration](issue_board.md#configurable-issue-boards-starter). **[STARTER]**
+- From [group issue boards](issue_board.md#group-issue-boards-premium), you can filter by only group labels in the [issue board configuration](issue_board.md#configurable-issue-boards-starter). **[STARTER]**
## Subscribing to labels
diff --git a/doc/user/project/maven_packages.md b/doc/user/project/maven_packages.md
new file mode 100644
index 00000000000..d32d6084b38
--- /dev/null
+++ b/doc/user/project/maven_packages.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/project/packages/maven_repository.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/project/packages/maven_repository.html).
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
new file mode 100644
index 00000000000..65ee2e128ae
--- /dev/null
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -0,0 +1,54 @@
+# Browser Performance Testing **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3507)
+in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
+
+## Overview
+
+If your application offers a web interface and you are using
+[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance
+impact of pending code changes.
+
+GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source
+tool for measuring the performance of web sites, and has built a simple
+[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance)
+which outputs the results in a file called `performance.json`. This plugin
+outputs the performance score for each page that is analyzed.
+
+The [Sitespeed.io performance score](http://examples.sitespeed.io/6.0/2017-11-23-23-43-35/help.html)
+is a composite value based on best practices, and we will be expanding support
+for [additional metrics](https://gitlab.com/gitlab-org/gitlab-ee/issues/4370)
+in a future release.
+
+Going a step further, GitLab can show the Performance report right
+in the merge request widget area:
+
+## Use cases
+
+For instance, consider the following workflow:
+
+1. A member of the marketing team is attempting to track engagement by adding a new tool
+1. With browser performance metrics, they see how their changes are impacting the usability of the page for end users
+1. The metrics show that after their changes the performance score of the page has gone down
+1. When looking at the detailed report, they see that the new Javascript library was included in `<head>` which affects loading page speed
+1. They ask a front end developer to help them, who sets the library to load asynchronously
+1. The frontend developer approves the merge request and authorizes its deployment to production
+
+## How it works
+
+First of all, you need to define a job in your `.gitlab-ci.yml` file that generates the
+[Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium).
+For more information on how the Performance job should look like, check the
+example on [Testing Browser Performance](../../../ci/examples/browser_performance.md).
+
+GitLab then checks this report, compares key performance metrics for each page
+between the source and target branches, and shows the information right on the merge request.
+
+>**Note:**
+If the Performance report doesn't have anything to compare to, no information
+will be displayed in the merge request area. That is the case when you add the
+Performance job in your `.gitlab-ci.yml` for the very first time.
+Consecutive merge requests will have something to compare to and the Performance
+report will be shown properly.
+
+![Performance Widget](img/browser_performance_testing.png)
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
new file mode 100644
index 00000000000..e6811b5df5e
--- /dev/null
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -0,0 +1,82 @@
+# Code Quality **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1984)
+in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
+
+## Overview
+
+If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your
+source code quality using GitLab Code Quality.
+Code Quality uses [Code Climate Engines](https://codeclimate.com), which are
+free and open source. Code Quality doesn’t require a Code Climate subscription.
+
+Going a step further, GitLab can show the Code Quality report right
+in the merge request widget area:
+
+![Code Quality Widget](img/code_quality.gif)
+
+## Use cases
+
+For instance, consider the following workflow:
+
+1. Your backend team member starts a new implementation for making certain feature in your app faster
+1. With Code Quality reports, they analyze how their implementation is impacting the code quality
+1. The metrics show that their code degrade the quality in 10 points
+1. You ask a co-worker to help them with this modification
+1. They both work on the changes until Code Quality report displays no degradations, only improvements
+1. You approve the merge request and authorize its deployment to staging
+1. Once verified, their changes are deployed to production
+
+## How it works
+
+First of all, you need to define a job in your `.gitlab-ci.yml` file that generates the
+[Code Quality report artifact](../../../ci/yaml/README.md#artifactsreportscodequality-starter).
+
+The Code Quality report artifact is a subset of the
+[Code Climate spec](https://github.com/codeclimate/spec/blob/master/SPEC.md#data-types).
+It must be a JSON file containing an array of objects with the following properties:
+
+| Name | Description |
+| ---------------------- | -------------------------------------------------------------------------------------- |
+| `description` | A description of the code quality violation. |
+| `fingerprint` | A unique fingerprint to identify the code quality violation. For example, an MD5 hash. |
+| `location.path` | The relative path to the file containing the code quality violation. |
+| `location.lines.begin` | The line on which the code quality violation occurred. |
+
+Example:
+
+```json
+[
+ {
+ "description": "'unused' is assigned a value but never used.",
+ "fingerprint": "7815696ecbf1c96e6894b779456d330e",
+ "location": {
+ "path": "lib/index.js",
+ "lines": {
+ "begin": 42
+ }
+ }
+ }
+]
+```
+
+NOTE: **Note:**
+Although the Code Climate spec supports more properties, those are ignored by GitLab.
+
+For more information on how the Code Quality job should look like, check the
+example on [analyzing a project's code quality](../../../ci/examples/code_quality.md).
+
+GitLab then checks this report, compares the metrics between the source and target
+branches, and shows the information right on the merge request.
+
+CAUTION: **Caution:**
+If multiple jobs in a pipeline generate a code quality artifact, only the artifact from
+the last created job (the job with the largest job ID) is used. To avoid confusion,
+configure only one job to generate a code quality artifact.
+
+NOTE: **Note:**
+If the Code Quality report doesn't have anything to compare to, no information
+will be displayed in the merge request area. That is the case when you add the
+Code Quality job in your `.gitlab-ci.yml` for the very first time.
+Consecutive merge requests will have something to compare to and the Code Quality
+report will be shown properly.
diff --git a/doc/user/project/merge_requests/code_quality_diff.md b/doc/user/project/merge_requests/code_quality_diff.md
new file mode 100644
index 00000000000..890058eec6f
--- /dev/null
+++ b/doc/user/project/merge_requests/code_quality_diff.md
@@ -0,0 +1,6 @@
+---
+redirect_from: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html'
+redirect_to: 'code_quality.md'
+---
+
+This document was moved to [another location](code_quality.md).
diff --git a/doc/user/project/merge_requests/container_scanning.md b/doc/user/project/merge_requests/container_scanning.md
new file mode 100644
index 00000000000..4d41e424f4a
--- /dev/null
+++ b/doc/user/project/merge_requests/container_scanning.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
diff --git a/doc/user/project/merge_requests/dast.md b/doc/user/project/merge_requests/dast.md
new file mode 100644
index 00000000000..b676c661267
--- /dev/null
+++ b/doc/user/project/merge_requests/dast.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dast/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
diff --git a/doc/user/project/merge_requests/dependency_scanning.md b/doc/user/project/merge_requests/dependency_scanning.md
new file mode 100644
index 00000000000..3a8b53b425c
--- /dev/null
+++ b/doc/user/project/merge_requests/dependency_scanning.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
diff --git a/doc/user/project/merge_requests/img/approvals_can_override.png b/doc/user/project/merge_requests/img/approvals_can_override.png
new file mode 100644
index 00000000000..8d207d018e0
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_can_override.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_premium_mr_widget.png b/doc/user/project/merge_requests/img/approvals_premium_mr_widget.png
new file mode 100644
index 00000000000..b6dc86f312e
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_premium_mr_widget.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit.png
new file mode 100644
index 00000000000..b6f6188b9cd
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_premium_project_edit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_remove_on_push.png b/doc/user/project/merge_requests/img/approvals_remove_on_push.png
new file mode 100644
index 00000000000..73964827587
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_remove_on_push.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_starter_project_edit.png b/doc/user/project/merge_requests/img/approvals_starter_project_edit.png
new file mode 100644
index 00000000000..868b9d58740
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_starter_project_edit.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approvals_starter_project_empty.png b/doc/user/project/merge_requests/img/approvals_starter_project_empty.png
new file mode 100644
index 00000000000..7375820224c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approvals_starter_project_empty.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approve.png b/doc/user/project/merge_requests/img/approve.png
new file mode 100644
index 00000000000..e68259ac5c2
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approve.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/approve_additionally.png b/doc/user/project/merge_requests/img/approve_additionally.png
new file mode 100644
index 00000000000..3db5a9159e5
--- /dev/null
+++ b/doc/user/project/merge_requests/img/approve_additionally.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/browser_performance_testing.png b/doc/user/project/merge_requests/img/browser_performance_testing.png
new file mode 100644
index 00000000000..eea77fb8b93
--- /dev/null
+++ b/doc/user/project/merge_requests/img/browser_performance_testing.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/code_quality.gif b/doc/user/project/merge_requests/img/code_quality.gif
new file mode 100644
index 00000000000..bab921cf38b
--- /dev/null
+++ b/doc/user/project/merge_requests/img/code_quality.gif
Binary files differ
diff --git a/doc/user/project/merge_requests/img/container_scanning.png b/doc/user/project/merge_requests/img/container_scanning.png
new file mode 100644
index 00000000000..e47f62acd9d
--- /dev/null
+++ b/doc/user/project/merge_requests/img/container_scanning.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/create-issue-with-list-hover.png b/doc/user/project/merge_requests/img/create-issue-with-list-hover.png
new file mode 100644
index 00000000000..7d70e8299f5
--- /dev/null
+++ b/doc/user/project/merge_requests/img/create-issue-with-list-hover.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dast_all.png b/doc/user/project/merge_requests/img/dast_all.png
new file mode 100644
index 00000000000..b6edc928dc3
--- /dev/null
+++ b/doc/user/project/merge_requests/img/dast_all.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dast_single.png b/doc/user/project/merge_requests/img/dast_single.png
new file mode 100644
index 00000000000..26ca4bde786
--- /dev/null
+++ b/doc/user/project/merge_requests/img/dast_single.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dependency_scanning.png b/doc/user/project/merge_requests/img/dependency_scanning.png
new file mode 100644
index 00000000000..18df356f846
--- /dev/null
+++ b/doc/user/project/merge_requests/img/dependency_scanning.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/filter_approver_merge_requests.png b/doc/user/project/merge_requests/img/filter_approver_merge_requests.png
new file mode 100644
index 00000000000..9c386391a4f
--- /dev/null
+++ b/doc/user/project/merge_requests/img/filter_approver_merge_requests.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/interactive_reports.png b/doc/user/project/merge_requests/img/interactive_reports.png
new file mode 100644
index 00000000000..9f9812dc69d
--- /dev/null
+++ b/doc/user/project/merge_requests/img/interactive_reports.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management.png b/doc/user/project/merge_requests/img/license_management.png
new file mode 100644
index 00000000000..cdce6b5fe38
--- /dev/null
+++ b/doc/user/project/merge_requests/img/license_management.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_decision.png b/doc/user/project/merge_requests/img/license_management_decision.png
new file mode 100644
index 00000000000..0763130c375
--- /dev/null
+++ b/doc/user/project/merge_requests/img/license_management_decision.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_pipeline_tab.png b/doc/user/project/merge_requests/img/license_management_pipeline_tab.png
new file mode 100644
index 00000000000..80ffca815b9
--- /dev/null
+++ b/doc/user/project/merge_requests/img/license_management_pipeline_tab.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_settings.png b/doc/user/project/merge_requests/img/license_management_settings.png
new file mode 100644
index 00000000000..b5490e59074
--- /dev/null
+++ b/doc/user/project/merge_requests/img/license_management_settings.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
new file mode 100644
index 00000000000..9ae6e350798
--- /dev/null
+++ b/doc/user/project/merge_requests/img/multiple_assignees_for_merge_requests_sidebar.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/remove_approval.png b/doc/user/project/merge_requests/img/remove_approval.png
new file mode 100644
index 00000000000..6083e1745ef
--- /dev/null
+++ b/doc/user/project/merge_requests/img/remove_approval.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/sast.png b/doc/user/project/merge_requests/img/sast.png
new file mode 100644
index 00000000000..2c75592c32a
--- /dev/null
+++ b/doc/user/project/merge_requests/img/sast.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/security_report.png b/doc/user/project/merge_requests/img/security_report.png
new file mode 100644
index 00000000000..ba41b707238
--- /dev/null
+++ b/doc/user/project/merge_requests/img/security_report.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/vulnerability_solution.png b/doc/user/project/merge_requests/img/vulnerability_solution.png
new file mode 100644
index 00000000000..7443b9b6eea
--- /dev/null
+++ b/doc/user/project/merge_requests/img/vulnerability_solution.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ba7d05a7ad7..2bb2d906453 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -23,7 +23,7 @@ With GitLab merge requests, you can:
- 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.html#time-tracking)
+- 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
@@ -33,9 +33,16 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
-- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) **[PREMIUM]**
-- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers **[STARTER]**
-- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]**
+- Prepare a full review and submit it once it's ready with [Merge Request Reviews](https://docs.gitlab.com/ee/user/discussions/index.md#merge-request-reviews-premium) **[PREMIUM]**
+- View the deployment process across projects with [Multi-Project Pipelines](https://docs.gitlab.com/ee/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 Management](https://docs.gitlab.com/ee/user/application_security/license_management/index.md) **[ULTIMATE]**
+- Analyze your source code for vulnerabilities with [Static Application Security Testing](https://docs.gitlab.com/ee/user/application_security/sast/index.md) **[ULTIMATE]**
+- Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](https://docs.gitlab.com/ee/user/application_security/dast/index.md) **[ULTIMATE]**
+- Analyze your dependencies for vulnerabilities with [Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.md) **[ULTIMATE]**
+- Analyze your Docker images for vulnerabilities with [Container Scanning](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.md) **[ULTIMATE]**
+- Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing-premium) **[PREMIUM]**
## Use cases
@@ -43,19 +50,21 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your 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 request the approval from your manager
-1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
+1. You avoid using dependencies whose license is not compatible with your project with [License Management reports](license_management.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. 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
-B. Consider you're a web developer writing a webpage for your company's:
+B. Consider you're a web developer writing a webpage for your company's website:
1. You checkout a new branch, and submit a new page through a merge request
1. You gather feedback from your reviewers
1. Your changes are previewed with [Review Apps](../../../ci/review_apps/index.md)
1. You request your web designers for their implementation
-1. You request the [approval](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your manager **[STARTER]**
+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/2016/08/26/ci-deployment-and-environments/)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
@@ -159,6 +168,21 @@ in a Merge Request. To do so, click the **...** button in the gutter of the Merg
![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](https://docs.gitlab.com/ee/user/discussions/index.html#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-ce/issues/18008) in GitLab 11.6.
@@ -169,6 +193,28 @@ 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-ee/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-and-merge-requests).
+
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
@@ -314,6 +360,52 @@ 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 request approvals **[STARTER]**
+
+> Included in [GitLab Starter][products].
+
+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][products] 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)
+
+## Browser Performance Testing **[PREMIUM]**
+
+> Introduced in [GitLab Premium][products] 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)
+
+## Security reports **[ULTIMATE]**
+
+GitLab can scan and report any vulnerabilities found in your project.
+
+[Read more about security reports.](https://docs.gitlab.com/ee/user/application_security/index.html)
+
+## Live preview with Review Apps
+
+If you configured [Review Apps](https://about.gitlab.com/features/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.
+
+[Read more about Review Apps.](../../../ci/review_apps/index.md)
+
## Merge request diff file navigation
When reviewing changes in the **Changes** tab the diff can be navigated using
@@ -506,5 +598,11 @@ And to check out a particular merge request:
git checkout origin/merge-requests/1
```
+[products]: https://about.gitlab.com/products/ "GitLab products page"
[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/license_management.md b/doc/user/project/merge_requests/license_management.md
new file mode 100644
index 00000000000..08704425a75
--- /dev/null
+++ b/doc/user/project/merge_requests/license_management.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/license_management/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
new file mode 100644
index 00000000000..265871a7b4b
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -0,0 +1,319 @@
+# Merge request approvals **[STARTER]**
+
+> Introduced in [GitLab Enterprise Edition 7.12](https://about.gitlab.com/2015/06/22/gitlab-7-12-released/#merge-request-approvers-ee-only).
+
+NOTE: **Note:**
+If you are running a self-managed instance, the new interface shown on
+this page will not be available unless the feature flag
+`approval_rules` is enabled, which 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 `Feature.enable(:approval_rules)` to enable the feature flag.
+
+The documentation for the older interface can be accessed
+[here](/11.7/ee/user/project/merge_requests/merge_request_approvals.html).
+
+## Overview
+
+Merge request approvals enable enforced code review by requiring specified people to approve a merge request before it can be unblocked for merging.
+
+## Use cases
+
+1. Enforcing review of all code that gets merged into a repository.
+2. Specifying code maintainers for an entire repository.
+3. Specifying reviewers for a given proposed code change.
+4. Specifying categories of reviewers, such as BE, FE, QA, DB, etc., for all proposed code changes.
+
+## Editing approvals
+
+To edit the merge request approvals:
+
+1. Navigate to your project's **Settings > General** and expand
+ **Merge request approvals**.
+
+ ![Approvals starter project empty](img/approvals_starter_project_empty.png)
+
+1. Click **Edit**.
+1. Search for users or groups that will be [eligible to approve](#eligible-approvers)
+ merge requests and click the **Add** button to add them as approvers. Note: selecting
+ approvers is optional.
+1. Set the minimum number of required approvals under the **No. approvals required**
+ box. Note: the minimum can be 0.
+1. Click **Update approvers**.
+
+ ![Approvals starter project edit](img/approvals_starter_project_edit.png)
+
+The steps above are the minimum required to get approvals working in your
+merge requests, but there are a couple more options available that might be
+suitable to your workflow:
+
+- Choose whether the default settings can be
+ [overridden per merge request](#overriding-the-merge-request-approvals-default-settings)
+- Choose whether [approvals will be reset with new pushed commits](#resetting-approvals-on-push)
+
+## Editing approvals **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1979) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
+
+CAUTION: **Caution:**
+There was a [regression affecting this feature in 11.8](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9648). We recommend upgrading _at least_ to version 11.8.2. to avoid any issues.
+
+NOTE: **Note:**
+In 11.8 this feature does not work in [private groups](https://gitlab.com/gitlab-org/gitlab-ee/issues/10356).
+
+For GitLab Premium, [multiple approver rules](#multiple-approval-rules-premium) can be configured. To configure the merge
+request approval rules:
+
+1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
+1. Click **Add approvers** to create a new approval rule.
+1. Just like in [GitLab Starter](#editing-approvals), select the approval members and aprovals required.
+1. Give the approval rule a name that describes the set of approvers selected.
+1. Click **Add approvers** to submit the new rule.
+
+ ![Approvals premium project edit](img/approvals_premium_project_edit.png)
+
+## Multiple approval rules **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1979) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
+
+For GitLab Premium, a merge request's overall approval status is determined by a set of rules. Each rule contains:
+
+- A set of [eligible approvers](#eligible-approvers).
+- A minimum number of approvals required.
+
+When an [eligible approver](#eligible-approvers) approves a merge request, it will reduce the number of approvals left for
+all rules that the approver belongs to.
+
+![Approvals premium merge request widget](img/approvals_premium_mr_widget.png)
+
+If no approval rules are set, then the overall minimum number of approvals required can be configured. With no approval rules,
+any [eligible approver](#eligible-approvers) may approve.
+
+## Eligible approvers
+
+The following can approve merge requests:
+
+- Users being added as approvers at project or merge request level.
+- [Code owners](https://docs.gitlab.com/ee/user/project/code_owners.html) related to the merge request ([introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5).
+
+An individual user can be added as an approver for a project if they are a member of:
+
+- The project.
+- The project's immediate parent group.
+- A group that has access to the project via a [share](../members/share_project_with_groups.md).
+
+A group can also be added as an approver. [In the future](https://gitlab.com/gitlab-org/gitlab-ee/issues/2048),
+group approvers will be restricted.
+
+If a user is added as an individual approver and is also part of a group approver,
+then that user is just counted once. The merge request author, as well as users who have committed
+to the merge request, do not count as eligible approvers,
+if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-their-own-merge-requests) (enabled by default)
+and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default)
+are enabled on the project settings.
+
+### Implicit approvers
+
+If the number of required approvals is greater than the number of approvers,
+other users will become implicit approvers to fill the gap.
+Those implicit approvers include members of the given project with Developer role or higher.
+
+## Adding or removing an approval
+
+If approvals are activated for the given project, when a user visits an open
+merge request, depending on their [eligibility](#eligible-approvers), one of
+the following is possible:
+
+- **They are not an eligible approver**: They cannot do anything with respect
+ to approving this merge request.
+
+- **They have not approved this merge request**:
+
+ - If the required number of approvals has _not_ been yet met, they can approve
+ it by clicking the displayed **Approve** button.
+ ![Approve](img/approve.png)
+ - If the required number of approvals has already been met, they can still
+ approve it by clicking the displayed **Approve additionally** button.
+ ![Add approval](img/approve_additionally.png)
+
+- **They have already approved this merge request**: They can remove their approval.
+
+ ![Remove approval](img/remove_approval.png)
+
+NOTE: **Note:**
+The merge request author is only allowed to approve their own merge request
+if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-their-own-merge-requests) is disabled on the project settings.
+
+For a given merge request, if the approval restrictions have been satisfied,
+the merge request is unblocked and can be merged.
+Note, that meeting the required number of approvals is a necessary, but not
+sufficient condition for unblocking a merge request from being merged. There
+are other conditions that may block it, such as merge conflicts,
+[pending discussions](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-discussions-are-resolved)
+or a [failed CI/CD pipeline](merge_when_pipeline_succeeds.md).
+
+## Code Owners approvals **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
+
+It is possible to require at least one approval for each entry in the
+[`CODEOWNERS` file](https://docs.gitlab.com/ee/user/project/code_owners.html) that matches a file changed in
+the merge request. To enable this feature:
+
+1. Navigate to your project's **Settings > General** and expand
+ **Merge request approvals**.
+1. Tick the **Require approval from code owners** checkbox
+ checkbox.
+1. Click **Save changes**.
+
+When this feature is enabled, all merge requests will need approval
+from one code owner per matched rule before it can be merged.
+
+## Overriding the merge request approvals default settings
+
+> Introduced in GitLab Enterprise Edition 9.4.
+
+NOTE: **Note:**
+If you are using GitLab Premium, things are a little different with [multiple approval rules](#multiple-approval-rules-premium).
+Read the differences [in GitLab Premium when overriding merge request approvals](#overriding-merge-request-approvals-default-settings-premium).
+
+If approvals are [set at the project level](#editing-approvals), the
+default configuration (number of required approvals and approvers) can be
+overridden for each merge request in that project.
+
+One possible scenario would be to to assign a group of approvers at the project
+level and change them later when creating or editing the merge request.
+
+First, you have to enable this option in the project's settings:
+
+1. Navigate to your project's **Settings > General** and expand
+ **Merge request approvals**
+1. Tick the "Can override approvers and approvals required per merge request"
+ checkbox
+
+ ![Approvals can override](img/approvals_can_override.png)
+
+1. Click **Save changes**
+
+NOTE: **Note:**
+If approver overriding is enabled
+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:
+
+1. Click **Edit** under the **Approvers** section.
+1. Search for users or groups that will be [eligible to approve](#eligible-approvers)
+ merge requests and click the **Add** button to add them as approvers or
+ remove existing approvers that were set in the project's settings.
+1. If you want to change the number of required approvals, set a new number
+ in the **No. approvals required** box.
+1. Click **Update approvers**.
+
+There are however some restrictions:
+
+- The amount of required approvals, if changed, must be greater than the default
+ set at the project level. This ensures that you're not forced to adjust settings
+ when someone is unavailable for approval, yet the process is still enforced.
+
+NOTE: **Note:**
+If you are contributing to a forked project, things are a little different.
+Read what happens when the
+[source and target branches are not the same](#merge-requests-with-different-source-branch-and-target-branch-projects).
+
+## Overriding merge request approvals default settings **[PREMIUM]**
+
+In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), and
+**Can override approvers and approvals required per merge request** is checked, there are a few more
+restrictions (compared to [GitLab Starter](#overriding-the-merge-request-approvals-default-settings)):
+
+- Approval rules can be added to an MR with no restriction.
+- For project sourced approval rules, editing and removing approvers is not allowed.
+- The approvals required of all approval rules is configurable, but if a rule is backed by a project rule, then it is restricted
+to the minimum approvals required set in the project's corresponding rule.
+
+## Resetting approvals on push
+
+If approvals are [set at the project level](#editing-approvals),
+you can choose whether all approvals on a merge request are removed when
+new commits are pushed to the source branch of the merge request:
+
+1. Navigate to your project's **Settings > General** and expand
+ **Merge request approvals**
+1. Tick the "Remove all approvals in a merge request when new commits are pushed to its source branch"
+ checkbox
+
+ ![Approvals remove on push](img/approvals_remove_on_push.png)
+
+1. Click **Save changes**
+
+NOTE: **Note:**
+Approvals do not get reset when [rebasing a merge request](fast_forward_merge.md)
+from the UI.
+However, approvals will be reset if the target branch is changed.
+
+If you want approvals to persist, independent of changes to the merge request,
+turn this setting to off by unchecking the box and saving the changes.
+
+## Allowing merge request authors to approve their own merge requests
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3349) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
+
+You can allow merge request authors to self-approve merge requests by
+enabling it [at the project level](#editing-approvals). Authors
+also need to be included in the approvers list in order to be able to
+approve their merge request.
+
+1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
+1. Uncheck the **Prevent approval of merge requests by merge request author** checkbox, which is enabled by default.
+1. Click **Save changes**.
+
+## Prevent approval of merge requests by their committers
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10441) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.10.
+
+You can prevent users that have committed to a merge request from approving it by
+enabling [**Prevent approval of merge requests by their committers**](#prevent-approval-of-merge-requests-by-their-committers).
+
+1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
+1. Tick the checkbox **Prevent approval of merge requests by their committers**.
+1. Click **Save changes**.
+
+## Merge requests with different source branch and target branch projects
+
+If the merge request source branch and target branch belong to different
+projects (which happens in merge requests in forked projects), everything is
+with respect to the target branch's project (typically the original project).
+In particular, since the merge request in this case is part of the target
+branch's project, the relevant settings are the target project's. The source
+branch's project settings are not applicable. Even if you start the merge
+request from the source branch's project UI, pay attention to the created merge
+request itself. It belongs to the target branch's project.
+
+## Approver suggestions
+
+Approvers are suggested for merge requests based on the previous authors of the files affected by the merge request.
+
+## Filtering merge requests by approvers
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9468) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.9.
+
+To filter merge requests by an individual approver, you can type (or select from
+the dropdown) `approver` and select the user.
+
+![Filter MRs by an approver](img/filter_approver_merge_requests.png)
diff --git a/doc/user/project/merge_requests/sast.md b/doc/user/project/merge_requests/sast.md
new file mode 100644
index 00000000000..688cc79d0f6
--- /dev/null
+++ b/doc/user/project/merge_requests/sast.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/sast/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
diff --git a/doc/user/project/merge_requests/sast_docker.md b/doc/user/project/merge_requests/sast_docker.md
new file mode 100644
index 00000000000..4d41e424f4a
--- /dev/null
+++ b/doc/user/project/merge_requests/sast_docker.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
diff --git a/doc/user/project/milestones/burndown_charts.md b/doc/user/project/milestones/burndown_charts.md
new file mode 100644
index 00000000000..0ad08da8ff5
--- /dev/null
+++ b/doc/user/project/milestones/burndown_charts.md
@@ -0,0 +1,70 @@
+# Burndown Charts **[STARTER]**
+
+> **Notes:**
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1540) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1 for project milestones.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5354) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.8 for group milestones.
+> - [Added](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6495) to [GitLab Starter](https://about.gitlab.com/pricing/) 11.2 for group milestones.
+> - Closed or reopened issues prior to GitLab 9.1 won't have a `closed_at`
+> value, so the burndown chart considers them as closed on the milestone
+> `start_date`. In that case, a warning will be displayed.
+
+## Overview
+
+Burndown Charts are visual representations of the progress of completing a milestone.
+
+![burndown chart](img/burndown_chart.png)
+
+At a glance, you see the current state for the completion a given milestone.
+Without them, you would have to organize the data from the milestone and plot it
+yourself to have the same sense of progress.
+
+GitLab Starter plots it for you and presents it in a clear and beautiful chart.
+
+For an overview, check the video demonstration on [Mapping Work Versus Time, With Burndown Charts](https://about.gitlab.com/2017/04/25/mapping-work-to-do-versus-time-with-burndown-charts/).
+
+## Use cases
+
+Burndown Charts, in general, are used for tracking and analyzing the completion of
+a milestone. Therefore, their use cases are tied to the
+[use you are assigning your milestone to](index.md).
+
+To exemplify, suppose you lead a team of developers in a large company,
+and you follow this workflow:
+
+- Your company set the goal for the quarter to deliver 10 new features for your app
+ in the upcoming major release.
+- You create a milestone, and remind your team to assign that milestone to every new issue
+ and merge request that's part of the launch of your app.
+- Every week, you open the milestone, visualize the progress, identify the gaps,
+ and help your team to get their work done.
+- Every month, you check in with your supervisor, and show the progress of that milestone
+ from the Burndown Chart.
+- By the end of the quarter, your team successfully delivered 100% of that milestone, as
+ it was taken care of closely throughout the whole quarter.
+
+## How it works
+
+A Burndown Chart is available for every project or group milestone that has been attributed a **start
+date** and a **due date**.
+
+Find your project's **Burndown Chart** under **Project > Issues > Milestones**,
+and select a milestone from your current ones, while for group's, access the **Groups** dashboard,
+select a group, and go through **Issues > Milestones** on the sidebar.
+
+NOTE: **Note:**
+You're able to [promote project](https://docs.gitlab.com/ee/user/project/milestones/#promoting-project-milestones-to-group-milestones) to group milestones and still see the **Burndown Chart** for them, respecting license limitations.
+
+The chart indicates the project's progress throughout that milestone (for issues assigned to it).
+
+In particular, it shows how many issues were or are still open for a given day in the
+milestone's corresponding period.
+
+The Burndown Chart tracks when issues were created and when they were last closed—not their full history. For each day, it takes the number of issues still open and issues created that day and subtracts the number of issues closed that day.
+**Issues that were created and assigned a milestone before its start date—and remain open as of the start date—are considered as having been opened on the start date**. Therefore, when the milestone start date is changed the number of opened issues on each day may change.
+Reopened issues are
+considered as having been opened on the day after they were last closed.
+
+The Burndown Chart can also be toggled to display the cumulative open issue
+weight for a given day. When using this feature, make sure issue weights have
+been properly assigned, since an open issue with no weight adds zero to the
+cumulative value.
diff --git a/doc/user/project/milestones/img/burndown_chart.png b/doc/user/project/milestones/img/burndown_chart.png
new file mode 100644
index 00000000000..e06b24f9907
--- /dev/null
+++ b/doc/user/project/milestones/img/burndown_chart.png
Binary files differ
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index a7d6144e3ec..0d8ee0a6cd2 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -83,7 +83,10 @@ From the project issue/merge request list pages and the group issue/merge reques
### Filtering in issue boards
-From [project issue boards](../issue_board.md), you can filter by both group milestones and project milestones in the [search and filter bar](../../search/index.md#issue-boards).
+- From [project issue boards](../issue_board.md), you can filter by both group milestones and project milestones in the [search and filter bar](../../search/index.md#issue-boards).
+- From [group issue boards](../issue_board.md#group-issue-boards-premium), you can filter by only group milestones in the [search and filter bar](../../search/index.md#issue-boards). **[PREMIUM]**
+- From [project issue boards](../issue_board.md), you can filter by both group milestones and project milestones in the [issue board configuration](../issue_board.md#configurable-issue-boards-starter). **[STARTER]**
+- From [group issue boards](../issue_board.md#group-issue-boards-premium) you can filter by only group milestones in the [issue board configuration](../issue_board.md#configurable-issue-boards-starter). **[STARTER]**
### Special milestone filters
@@ -98,16 +101,17 @@ When filtering by milestone, in addition to choosing a specific project mileston
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 | ✓ | |
+| 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.
@@ -118,6 +122,17 @@ These features are only available for project milestones and not group milestone
- 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).
+
+### Project Burndown Charts **[STARTER]**
+
+For project milestones in [GitLab Starter](https://about.gitlab.com/pricing), a [burndown chart](burndown_charts.md) is in the milestone view, showing the progress of completing a milestone.
+
+![burndown chart](img/burndown_chart.png)
+
+### Group Burndown Charts **[PREMIUM]**
+
+For group milestones in [GitLab Premium](https://about.gitlab.com/pricing), a [burndown chart](burndown_charts.md) is in the milestone view, showing the progress of completing a milestone.
### Milestone sidebar
diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md
new file mode 100644
index 00000000000..a5db3d70bb9
--- /dev/null
+++ b/doc/user/project/operations/feature_flags.md
@@ -0,0 +1,177 @@
+# Feature Flags **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) in GitLab 11.4.
+
+CAUTION: **Warning:**
+This an _alpha_ feature and is subject to change at any time without
+prior notice.
+
+Feature flags allow you to ship a project in different flavors by
+dynamically toggling certain functionality.
+
+## Overview
+
+Feature Flags offer a feature toggle system for your application. They enable teams
+to achieve Continuous Delivery by deploying new features to production at smaller
+batches for controlled testing, separating feature delivery from customer launch.
+This helps reducing risk and allows you to easily manage which features to enable.
+
+GitLab offers a Feature Flags interface that allows you to create, toggle and
+remove feature flags.
+
+## How it works
+
+Underneath, GitLab uses [unleash](https://github.com/Unleash/unleash), a feature
+toggle service. GitLab provides an API where your application can talk to and get the
+list of feature flags you set in GitLab.
+
+The application must be configured to talk to GitLab, so that's up to the
+developers to use a compatible [client library](#client-libraries) and
+integrate it in their app.
+
+By setting a flag active or inactive via GitLab, your application will automatically
+know which features to enable or disable respectively.
+
+## Adding a new feature flag
+
+To add a new feature flag:
+
+1. Navigate to your project's **Operations > Feature Flags**.
+1. Click on the **New Feature Flag** button.
+1. Give it a name.
+
+ NOTE: **Note:**
+ A name can contain only lowercase letters, digits, underscores (`_`)
+ and dashes (`-`), must start with a letter, and cannot end with a dash (`-`)
+ or an underscore (`_`).
+
+1. Give it a description (optional, 255 characters max).
+1. Define environment [specs](#define-environment-specs). If you want the flag on by default, enable the catch-all [wildcard spec (`*`)](#define-environment-specs)
+1. Click **Create feature flag**.
+
+Once a feature flag is created, the list of existing feature flags will be presented
+with ability to edit or remove them.
+
+To make a feature flag active or inactive, click the pencil icon to edit it,
+and toggle the status for each [spec](#define-environment-specs).
+
+![Feature flags list](img/feature_flags_list.png)
+
+## Define environment specs
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8621) in GitLab 11.8.
+
+In general, an application is deployed to multiple environments, such as
+production, staging and [review apps](../../../ci/review_apps/index.md).
+For example, you may not want to enable a feature flag on production until your QA team has
+first confirmed that the feature is working correctly on testing environments.
+
+To handle these situations, you can enable a feature flag on a particular environment
+with [Environment specs](../../../ci/environments.md#scoping-environments-with-specs-premium).
+You can define multiple specs per flag so that you can control your feature flag more granularly.
+
+To define specs for each environment:
+
+1. Navigate to your project's **Operations > Feature Flags**.
+1. Click on the **New Feature Flag** button or edit an existing flag.
+1. Set the status of the default [spec](../../../ci/environments.md#scoping-environments-with-specs-premium) (`*`). This status will be used for _all_ environments.
+1. If you want to enable/disable the feature on a specific environment, create a new [spec](../../../ci/environments.md#scoping-environments-with-specs-premium) and type the environment name.
+1. Set the status of the additional spec. This status takes precedence over the default spec's status since we always use the most specific match available.
+1. Click **Create feature flag** or **Update feature flag**.
+
+![Feature flag specs list](img/specs_list.png)
+
+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.
+
+## Integrating with your application
+
+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
+
+To get the access credentials that your application will need to talk to GitLab:
+
+1. Navigate to your project's **Operations > Feature Flags**.
+1. Click on the **Configure** button to see the values:
+ - **API URL**: URL where the client (application) connects to get a list of feature flags.
+ - **Instance ID**: Unique token that authorizes the retrieval of the feature flags.
+ - **Application name**: The name of the running environment. For instance,
+ if the application runs for production server, application name would be
+ `production` or similar. This value is used for
+ [the environment spec evaluation](#define-environment-specs).
+
+NOTE: **Note:**
+The meaning of these fields might change over time. For example, we are not sure
+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
+
+GitLab currently implements a single backend that is compatible with
+[Unleash](https://github.com/Unleash/unleash#client-implementations) clients.
+
+Unleash clients allow the developers to define in the app's code the default
+values for flags. Each feature flag evaluation can express the desired
+outcome in case the flag isn't present on the provided configuration file.
+
+Unleash currently offers a number of official SDKs for various frameworks and
+a number of community contributed libraries.
+
+Official clients:
+
+- [unleash/unleash-client-java](https://github.com/unleash/unleash-client-java)
+- [unleash/unleash-client-node](https://github.com/unleash/unleash-client-node)
+- [unleash/unleash-client-go](https://github.com/unleash/unleash-client-go)
+- [unleash/unleash-client-ruby](https://github.com/unleash/unleash-client-ruby)
+
+Community contributed clients:
+
+- [stiano/unleash-client-dotnet](https://github.com/stiano/unleash-client-dotnet) (.Net Core)
+- [onybo/unleash-client-core](https://github.com/onybo/unleash-client-core) (.Net Core)
+- [aes/unleash-client-python](https://github.com/aes/unleash-client-python) (Python 3)
+
+### Golang application example
+
+Here's an example of how to integrate the feature flags in a Golang application:
+
+```golang
+package main
+
+import (
+ "io"
+ "log"
+ "net/http"
+
+ "github.com/Unleash/unleash-client-go"
+)
+
+type metricsInterface struct {
+}
+
+func init() {
+ unleash.Initialize(
+ unleash.WithUrl("https://gitlab.com/api/v4/feature_flags/unleash/42"),
+ unleash.WithInstanceId("29QmjsW6KngPR5JNPMWx"),
+ unleash.WithAppName("production"),
+ unleash.WithListener(&metricsInterface{}),
+ )
+}
+
+func helloServer(w http.ResponseWriter, req *http.Request) {
+ if unleash.IsEnabled("my_feature_name") {
+ io.WriteString(w, "Feature enabled\n")
+ } else {
+ io.WriteString(w, "hello, world!\n")
+ }
+}
+
+func main() {
+ http.HandleFunc("/", helloServer)
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
+```
diff --git a/doc/user/project/operations/img/feature_flags_list.png b/doc/user/project/operations/img/feature_flags_list.png
new file mode 100644
index 00000000000..5313a163fec
--- /dev/null
+++ b/doc/user/project/operations/img/feature_flags_list.png
Binary files differ
diff --git a/doc/user/project/operations/img/specs_list.png b/doc/user/project/operations/img/specs_list.png
new file mode 100644
index 00000000000..9630c907cfc
--- /dev/null
+++ b/doc/user/project/operations/img/specs_list.png
Binary files differ
diff --git a/doc/user/project/operations/index.md b/doc/user/project/operations/index.md
index b0f9936be5c..0086c15c98a 100644
--- a/doc/user/project/operations/index.md
+++ b/doc/user/project/operations/index.md
@@ -7,5 +7,5 @@ your applications:
- Deploy to different [environments](../../../ci/environments.md).
- Connect your project to a [Kubernetes cluster](../clusters/index.md).
- Discover and view errors generated by your applications with [Error Tracking](error_tracking.md).
-- Create, toggle, and remove [Feature Flags](https://docs.gitlab.com/ee/user/project/operations/feature_flags.html). **[PREMIUM]**
-- [Trace](https://docs.gitlab.com/ee/user/project/operations/tracing.html) the performance and health of a deployed application. **[ULTIMATE]**
+- Create, toggle, and remove [Feature Flags](feature_flags.md). **[PREMIUM]**
+- [Trace](tracing.md) the performance and health of a deployed application. **[ULTIMATE]**
diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md
new file mode 100644
index 00000000000..0416e096cf2
--- /dev/null
+++ b/doc/user/project/operations/tracing.md
@@ -0,0 +1,34 @@
+# Tracing **[ULTIMATE]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/7903) in GitLab Ultimate 11.5.
+
+Tracing provides insight into the performance and health of a deployed application,
+tracking each function or microservice which handles a given request.
+
+This makes it easy to
+understand the end-to-end flow of a request, regardless of whether you are using a monolithic or distributed system.
+
+## Jaeger tracing
+
+[Jaeger](https://www.jaegertracing.io/) is an open source, end-to-end distributed
+tracing system used for monitoring and troubleshooting microservices-based distributed
+systems.
+
+### Deploying Jaeger
+
+To learn more about deploying Jaeger, read the official
+[Getting Started documentation](https://www.jaegertracing.io/docs/latest/getting-started/).
+There is an easy to use [all-in-one Docker image](https://www.jaegertracing.io/docs/latest/getting-started/#AllinoneDockerimage),
+as well as deployment options for [Kubernetes](https://github.com/jaegertracing/jaeger-kubernetes)
+and [OpenShift](https://github.com/jaegertracing/jaeger-openshift).
+
+### Enabling Jaeger
+
+GitLab provides an easy way to open the Jaeger UI from within your project:
+
+1. [Set up Jaeger](#deploying-jaeger) and configure your application using one of the
+ [client libraries](https://www.jaegertracing.io/docs/latest/client-libraries/).
+1. Navigate to your project's **Settings > Operations** and provide the Jaeger URL.
+1. Click **Save changes** for the changes to take effect.
+1. You can now visit **Operations > Tracing** in your project's sidebar and
+ GitLab will redirect you to the configured Jaeger URL. \ No newline at end of file
diff --git a/doc/user/project/packages/img/maven_package_view.png b/doc/user/project/packages/img/maven_package_view.png
new file mode 100644
index 00000000000..2eb4b6f76b4
--- /dev/null
+++ b/doc/user/project/packages/img/maven_package_view.png
Binary files differ
diff --git a/doc/user/project/packages/img/npm_package_view.png b/doc/user/project/packages/img/npm_package_view.png
new file mode 100644
index 00000000000..8baf7d0ef9f
--- /dev/null
+++ b/doc/user/project/packages/img/npm_package_view.png
Binary files differ
diff --git a/doc/user/project/packages/maven.md b/doc/user/project/packages/maven.md
new file mode 100644
index 00000000000..266225fdb8d
--- /dev/null
+++ b/doc/user/project/packages/maven.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'maven_repository.md'
+---
+
+This document was moved to [another location](maven_repository.md).
diff --git a/doc/user/project/packages/maven_packages.md b/doc/user/project/packages/maven_packages.md
new file mode 100644
index 00000000000..266225fdb8d
--- /dev/null
+++ b/doc/user/project/packages/maven_packages.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'maven_repository.md'
+---
+
+This document was moved to [another location](maven_repository.md).
diff --git a/doc/user/project/packages/maven_repository.md b/doc/user/project/packages/maven_repository.md
new file mode 100644
index 00000000000..94785eb6aec
--- /dev/null
+++ b/doc/user/project/packages/maven_repository.md
@@ -0,0 +1,341 @@
+# GitLab Maven Repository **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5811) in
+ [GitLab Premium](https://about.gitlab.com/pricing/) 11.3.
+
+With the GitLab [Maven](https://maven.apache.org) Repository, every
+project can have its own space to store its Maven artifacts.
+
+![GitLab Maven Repository](img/maven_package_view.png)
+
+## Enabling the Maven Repository
+
+NOTE: **Note:**
+This option is available only if your GitLab administrator has
+[enabled support for the Maven repository](https://docs.gitlab.com/ee/administration/packages.html).**[PREMIUM ONLY]**
+
+After the Packages feature is enabled, the Maven Repository will be available for
+all new projects by default. To enable it for existing projects, or if you want
+to disable it:
+
+1. Navigate to your project's **Settings > General > Permissions**.
+1. Find the Packages feature and enable or disable it.
+1. Click on **Save changes** for the changes to take effect.
+
+You should then be able to see the **Packages** section on the left sidebar.
+Next, you must configure your project to authorize with the GitLab Maven
+repository.
+
+## Authenticating to the GitLab Maven Repository
+
+If a project is private or you want to upload Maven artifacts to GitLab,
+credentials will need to be provided for authorization. Support is available for
+[personal access tokens](#authenticating-with-a-personal-access-token) and
+[CI job tokens](#authenticating-with-a-ci-job-token) only.
+[Deploy tokens](../deploy_tokens/index.md) and regular username/password
+credentials do not work.
+
+### Authenticating with a personal access token
+
+To authenticate with a [personal access token](../../profile/personal_access_tokens.md),
+add a corresponding section to your
+[`settings.xml`](https://maven.apache.org/settings.html) file:
+
+```xml
+<settings>
+ <servers>
+ <server>
+ <id>gitlab-maven</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Private-Token</name>
+ <value>REPLACE_WITH_YOUR_PERSONAL_ACCESS_TOKEN</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+</settings>
+```
+
+You should now be able to upload Maven artifacts to your project.
+
+### Authenticating with a CI job token
+
+If you're using Maven with GitLab CI/CD, a CI job token can be used instead
+of a personal access token.
+
+To authenticate with a CI job token, add a corresponding section to your
+[`settings.xml`](https://maven.apache.org/settings.html) file:
+
+```xml
+<settings>
+ <servers>
+ <server>
+ <id>gitlab-maven</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Job-Token</name>
+ <value>${env.CI_JOB_TOKEN}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+</settings>
+```
+
+You can read more on
+[how to create Maven packages using GitLab CI/CD](#creating-maven-packages-with-gitlab-cicd).
+
+## Configuring your project to use the GitLab Maven repository URL
+
+To download and upload packages from GitLab, you need a `repository` and
+`distributionManagement` section in your `pom.xml` file.
+
+Depending on your workflow and the amount of Maven packages you have, there are
+3 ways you can configure your project to use the GitLab endpoint for Maven packages:
+
+- **Project level**: Useful when you have few Maven packages which are not under
+ the same GitLab group.
+- **Group level**: Useful when you have many Maven packages under the same GitLab
+ group.
+- **Instance level**: Useful when you have many Maven packages under different
+ GitLab groups or on their own namespace.
+
+NOTE: **Note:**
+In all cases, you need a project specific URL for uploading a package in
+the `distributionManagement` section.
+
+### Project level Maven endpoint
+
+The example below shows how the relevant `repository` section of your `pom.xml`
+would look like:
+
+```xml
+<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </repository>
+</repositories>
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </snapshotRepository>
+</distributionManagement>
+```
+
+The `id` must be the same with what you
+[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
+
+Replace `PROJECT_ID` with your project ID which can be found on the home page
+of your project.
+
+If you have a self-hosted GitLab installation, replace `gitlab.com` with your
+domain name.
+
+NOTE: **Note:**
+For retrieving artifacts, you can use either the
+[URL encoded](../../../api/README.md#namespaced-path-encoding) path of the project
+(e.g., `group%2Fproject`) or the project's ID (e.g., `42`). However, only the
+project's ID can be used for uploading.
+
+### Group level Maven endpoint
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8798) in GitLab Premium 11.7.
+
+If you rely on many packages, it might be inefficient to include the `repository` section
+with a unique URL for each package. Instead, you can use the group level endpoint for
+all your Maven packages stored within one GitLab group. Only packages you have access to
+will be available for download.
+
+The group level endpoint works with any package names, which means the you
+have the flexibility of naming compared to [instance level endpoint](#instance-level-maven-endpoint).
+However, GitLab will not guarantee the uniqueness of the package names within
+the group. You can have two projects with the same package name and package
+version. As a result, GitLab will serve whichever one is more recent.
+
+The example below shows how the relevant `repository` section of your `pom.xml`
+would look like. You still need a project specific URL for uploading a package in
+the `distributionManagement` section:
+
+```xml
+<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/groups/my-group/-/packages/maven</url>
+ </repository>
+</repositories>
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </snapshotRepository>
+</distributionManagement>
+```
+
+The `id` must be the same with what you
+[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
+
+Replace `my-group` with your group name and `PROJECT_ID` with your project ID
+which can be found on the home page of your project.
+
+If you have a self-hosted GitLab installation, replace `gitlab.com` with your
+domain name.
+
+NOTE: **Note:**
+For retrieving artifacts, you can use either the
+[URL encoded](../../../api/README.md#namespaced-path-encoding) path of the group
+(e.g., `group%2Fsubgroup`) or the group's ID (e.g., `12`).
+
+### Instance level Maven endpoint
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8274) in GitLab Premium 11.7.
+
+If you rely on many packages, it might be inefficient to include the `repository` section
+with a unique URL for each package. Instead, you can use the instance level endpoint for
+all maven packages stored in GitLab and the packages you have access to will be available
+for download.
+
+Note that **only packages that have the same path as the project** are exposed via
+the instance level endpoint.
+
+| Project | Package | Instance level endpoint available |
+| ------- | ------- | --------------------------------- |
+| `foo/bar` | `foo/bar/1.0-SNAPSHOT` | Yes |
+| `gitlab-org/gitlab-ce` | `foo/bar/1.0-SNAPSHOT` | No |
+| `gitlab-org/gitlab-ce` | `gitlab-org/gitlab-ce/1.0-SNAPSHOT` | Yes |
+
+The example below shows how the relevant `repository` section of your `pom.xml`
+would look like. You still need a project specific URL for uploading a package in
+the `distributionManagement` section:
+
+```xml
+<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/packages/maven</url>
+ </repository>
+</repositories>
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven</url>
+ </snapshotRepository>
+</distributionManagement>
+```
+
+The `id` must be the same with what you
+[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
+
+Replace `PROJECT_ID` with your project ID which can be found on the home page
+of your project.
+
+If you have a self-hosted GitLab installation, replace `gitlab.com` with your
+domain name.
+
+NOTE: **Note:**
+For retrieving artifacts, you can use either the
+[URL encoded](../../../api/README.md#namespaced-path-encoding) path of the project
+(e.g., `group%2Fproject`) or the project's ID (e.g., `42`). However, only the
+project's ID can be used for uploading.
+
+## Uploading packages
+
+Once you have set up the [authentication](#authenticating-to-the-gitlab-maven-repository)
+and [configuration](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
+test to upload a Maven artifact from a project of yours:
+
+```sh
+mvn deploy
+```
+
+You can then navigate to your project's **Packages** page and see the uploaded
+artifacts or even delete them.
+
+## Creating Maven packages with GitLab CI/CD
+
+Once you have your repository configured to use the GitLab Maven Repository,
+you can configure GitLab CI/CD to build new packages automatically. The example below
+shows how to create a new package each time the `master` branch is updated:
+
+1. Create a `ci_settings.xml` file that will serve as Maven's `settings.xml` file.
+ Add the server section with the same id you defined in your `pom.xml` file.
+ For example, in our case it's `gitlab-maven`:
+
+ ```xml
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
+ <servers>
+ <server>
+ <id>gitlab-maven</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Job-Token</name>
+ <value>${env.CI_JOB_TOKEN}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+ </settings>
+ ```
+
+1. Make sure your `pom.xml` file includes the following:
+
+ ```xml
+ <repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
+ </repository>
+ </repositories>
+ <distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/maven</url>
+ </snapshotRepository>
+ </distributionManagement>
+ ```
+
+ TIP: **Tip:**
+ You can either let Maven utilize the CI environment variables or hardcode your project's ID.
+
+1. Add a `deploy` job to your `.gitlab-ci.yml` file:
+
+ ```yaml
+ deploy:
+ image: maven:3.3.9-jdk-8
+ script:
+ - 'mvn deploy -s ci_settings.xml'
+ only:
+ - master
+ ```
+
+1. Push those files to your repository.
+
+The next time the `deploy` job runs, it will copy `ci_settings.xml` to the
+user's home location (in this case the user is `root` since it runs in a
+Docker container), and Maven will utilize the configured CI
+[environment variables](../../../ci/variables/README.md#predefined-environment-variables).
diff --git a/doc/user/project/packages/npm_registry.md b/doc/user/project/packages/npm_registry.md
new file mode 100644
index 00000000000..9f4c01c9a0a
--- /dev/null
+++ b/doc/user/project/packages/npm_registry.md
@@ -0,0 +1,120 @@
+# GitLab NPM Registry **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5934)
+ in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
+
+With the GitLab NPM Registry, every
+project can have its own space to store NPM packages.
+
+![GitLab NPM Registry](img/npm_package_view.png)
+
+NOTE: **Note:**
+Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported.
+
+
+NOTE: **Note:**
+As `@group/subgroup/project` is not a valid NPM package name, publishing a package
+within a subgroup is not supported yet.
+
+## Enabling the NPM Registry
+
+NOTE: **Note:**
+This option is available only if your GitLab administrator has
+[enabled support for the NPM registry](https://docs.gitlab.com/ee/administration/packages.html).**[PREMIUM ONLY]**
+
+After the NPM registry is enabled, it will be available for all new projects
+by default. To enable it for existing projects, or if you want to disable it:
+
+1. Navigate to your project's **Settings > General > Permissions**.
+1. Find the Packages feature and enable or disable it.
+1. Click on **Save changes** for the changes to take effect.
+
+You should then be able to see the **Packages** section on the left sidebar.
+
+Before proceeding to authenticating with the GitLab NPM Registry, you should
+get familiar with the package naming convention.
+
+## Package naming convention
+
+**Only packages that have the same path as the project** are supported. For
+ example:
+
+| Project | Package | Supported |
+| ---------------------- | ----------------------- | --------- |
+| `foo/bar` | `@foo/bar` | Yes |
+| `gitlab-org/gitlab-ce` | `@gitlab-org/gitlab-ce` | Yes |
+| `gitlab-org/gitlab-ce` | `@foo/bar` | No |
+
+Now, you can configure your project to authenticate with the GitLab NPM
+Registry.
+
+## Authenticating to the GitLab NPM Registry
+
+If a project is private or you want to upload an NPM package to GitLab,
+credentials will need to be provided for authentication. Support is available
+only for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
+
+CAUTION: **2FA not supported:**
+Authentication for personal access tokens is not yet supported
+([#9140](https://gitlab.com/gitlab-org/gitlab-ee/issues/9140)). If you have 2FA
+enabled, you won't be able to authenticate to the GitLab NPM Registry.
+
+### Authenticating with an OAuth token
+
+To authenticate with an [OAuth token](../../../api/oauth2.md#resource-owner-password-credentials-flow),
+add a corresponding section to your `.npmrc` file:
+
+```ini
+; Set URL for your scoped packages.
+; For example package with name `@foo/bar` will use this URL for download
+@foo:registry=https://gitlab.com/api/v4/packages/npm/
+
+; Add the OAuth token for the scoped packages URL. This will allow you to download
+; `@foo/` packages from private projects.
+//gitlab.com/api/v4/packages/npm/:_authToken=<your_oauth_token>
+
+; Add OAuth token for uploading to the registry. Replace <your_project_id>
+; with the project you want your package to be uploaded to.
+//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken=<your_oauth_token>
+```
+
+Replace `<your_project_id>` with your project ID which can be found on the home page
+of your project and `<your_oauth_token>` with your OAuth token.
+
+If you have a self-hosted GitLab installation, replace `gitlab.com` with your
+domain name.
+
+You should now be able to download and upload NPM packages to your project.
+
+## Uploading packages
+
+Before you will be able to upload a package, you need to specify the registry
+for NPM. To do this, add the following section to the bottom of `package.json`:
+
+```json
+ "publishConfig": {
+ "@foo:registry":"https://gitlab.com/api/v4/projects/<your_project_id>/packages/npm/"
+ }
+```
+
+Replace `<your_project_id>` with your project ID, which can be found on the home
+page of your project, and replace `@foo` with your own scope.
+
+If you have a self-hosted GitLab installation, replace `gitlab.com` with your
+domain name.
+
+Once you have enabled it and set up [authentication](#authenticating-to-the-gitlab-npm-registry),
+you can upload an NPM package to your project:
+
+```sh
+npm publish
+```
+
+You can then navigate to your project's **Packages** page and see the uploaded
+packages or even delete them.
+
+## Uploading a package with the same version twice
+
+If you upload a package with a same name and version twice, GitLab will show
+both packages in the UI, but the GitLab NPM Registry will expose the most recent
+one as it supports only one package per version for `npm install`.
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index f552f60a07e..87cd4941ae6 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -54,7 +54,7 @@ Of course, before building it, you had to install Jekyll in your computer.
For that, you had to open your terminal and run `gem install jekyll`.
Right? GitLab CI + GitLab Runner do the same thing. But you need to
write in the `.gitlab-ci.yml` the script you want to run so
-GitLab Runner will do it for you. It looks more complicated then it
+GitLab Runner will do it for you. It looks more complicated than it
is. What you need to tell the Runner:
```
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index 1034ba1733d..faf51154e3b 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -94,8 +94,9 @@ You can also take some **optional** further steps:
- _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`: navigate to project's **Settings** >
- expand **Advanced settings** > and scroll down to **Rename repository**.
+ - 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
diff --git a/doc/user/project/pipelines/img/pipeline_schedule_variables.png b/doc/user/project/pipelines/img/pipeline_schedule_variables.png
index 74692add93a..29846206491 100644
--- a/doc/user/project/pipelines/img/pipeline_schedule_variables.png
+++ b/doc/user/project/pipelines/img/pipeline_schedule_variables.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png
index 95203ec861b..e135dd51070 100644
--- a/doc/user/project/pipelines/img/pipeline_schedules_new_form.png
+++ b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png
Binary files differ
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index a3f40c20192..629b5e1fde4 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -55,7 +55,8 @@ For more examples on artifacts, follow the [artifacts reference in
> **Note:**
> With [GitLab 10.1][ce-14399], HTML files in a public project can be previewed
> directly in a new tab without the need to download them when
-> [GitLab Pages](../../../administration/pages/index.md) is enabled
+> [GitLab Pages](../../../administration/pages/index.md) is enabled.
+> The same holds for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 89f1beb6d1f..dd5427ab35a 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -79,7 +79,7 @@ For example, only two pipelines will be created per day if:
To change the Sidekiq worker's frequency:
-1. Edit the `pipeline_schedule_worker_cron` value in your instance's `gitlab.rb` file.
+1. Edit the `gitlab_rails['pipeline_schedule_worker_cron']` value in your instance's `gitlab.rb` file.
1. [Restart GitLab](../../../administration/restart_gitlab.md).
For GitLab.com, refer to the [dedicated settings page](../../gitlab_com/index.md#cron-jobs).
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 2060b5dd4a2..56e8f1731ae 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -15,12 +15,10 @@ By default, a protected branch does four simple things:
- it prevents **anyone** from force pushing to the branch
- it prevents **anyone** from deleting the branch
-See the [Changelog](#changelog) section for changes over time.
+>**Note**:
+A GitLab admin is allowed to push to the protected branches.
->
->Additional functionality for GitLab Enterprise Edition:
->
->- Restrict push and merge access to [certain users][ee-restrict]
+See the [Changelog](#changelog) section for changes over time.
## Configuring protected branches
@@ -68,6 +66,21 @@ dropdown list in the "Already protected" area.
If you don't choose any of those options while creating a protected branch,
they are set to "Maintainers" by default.
+## Restricting push and merge access to certain users **[STARTER]**
+
+> This feature was [introduced][ce-5081] in [GitLab Starter][ee] 8.11.
+
+With GitLab Enterprise Edition you can restrict access to protected branches
+by choosing a role (Maintainers, Developers) as well as certain users. From the
+dropdown menu select the role and/or the users you want to have merge or push
+access.
+
+![Select roles and users](img/protected_branches_select_roles_and_users.png)
+
+Click **Protect** and the branch will appear in the "Protected branch" list.
+
+![Roles and users list](img/protected_branches_select_roles_and_users_list.png)
+
## Wildcard protected branches
> [Introduced][ce-4665] in GitLab 8.10.
@@ -169,3 +182,4 @@ for details about the pipelines security model.
[ce-21393]: https://gitlab.com/gitlab-org/gitlab-ce/issues/21393
[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users
[perm]: ../permissions.md
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 88f4de891a1..15eb862b431 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -26,9 +26,10 @@ discussions, and descriptions:
| `/award :emoji:` | Toggle emoji award | ✓ | ✓ |
| `/assign me` | Assign yourself | ✓ | ✓ |
| `/assign @user` | Assign one user | ✓ | ✓ |
-| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | |
-| `/unassign` | Remove assignee(s) | ✓ | ✓ |
-| `/reassign @user1 @user2` | Change assignee | ✓ | ✓ |
+| `/assign @user1 @user2` | Assign multiple users **[STARTER]** | ✓ | ✓ |
+| `/unassign @user1 @user2` | Remove assignee(s) **[STARTER]** | ✓ | ✓ |
+| `/reassign @user1 @user2` | Change assignee **[STARTER]** | ✓ | ✓ |
+| `/unassign` | Remove current assignee | ✓ | ✓ |
| `/milestone %milestone` | Set milestone | ✓ | ✓ |
| `/remove_milestone` | Remove milestone | ✓ | ✓ |
| `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ |
@@ -45,13 +46,15 @@ discussions, and descriptions:
| `/remove_due_date` | Remove due date | ✓ | |
| `/weight 0,1,2, ...` | Set weight **[STARTER]** | ✓ | |
| `/clear_weight` | Clears weight **[STARTER]** | ✓ | |
-| `/epic <group&epic &#124; Epic URL>` | Add to epic **[ULTIMATE]** | ✓ | |
+| `/epic <&epic &#124; group&epic &#124; Epic URL>` | Add to epic **[ULTIMATE]** | ✓ | |
| `/remove_epic` | Removes from epic **[ULTIMATE]** | ✓ | |
+| `/promote` | Promote issue to epic **[ULTIMATE]** | ✓ | |
| `/confidential` | Make confidential | ✓ | |
| `/duplicate #issue` | Mark this issue as a duplicate of another issue | ✓ |
| `/move path/to/project` | Move this issue to another project | ✓ | |
| `/target_branch <Local branch Name>` | Set target branch | | ✓ |
| `/wip` | Toggle the Work In Progress status | | ✓ |
+| `/approve` | Approve the merge request | | ✓ |
| `/merge` | Merge (when pipeline succeeds) | | ✓ |
| `/create_merge_request <branch name>` | Create a new merge request starting from the current issue | ✓ | |
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
index 6495ede42e0..3260a355fdc 100644
--- a/doc/user/project/repository/gpg_signed_commits/index.md
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -264,7 +264,7 @@ To remove a GPG key from your account:
## Rejecting commits that are not signed **[PREMIUM]**
You can configure your project to reject commits that aren't GPG-signed
-via [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html).
+via [push rules](../../../../push_rules/push_rules.md).
## GPG signing API
diff --git a/doc/user/project/repository/img/download_source_code.png b/doc/user/project/repository/img/download_source_code.png
new file mode 100644
index 00000000000..17f2cb4b3e8
--- /dev/null
+++ b/doc/user/project/repository/img/download_source_code.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 22d912cd9d1..97ecc4c0d65 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -97,7 +97,7 @@ Some things to note about precedence:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2508) in GitLab 9.1
-[Jupyter][jupyter] Notebook (previously IPython Notebook) files are used for
+[Jupyter](https://jupyter.org) Notebook (previously IPython Notebook) files are used for
interactive computing in many fields and contain a complete record of the
user's sessions and include code, narrative text, equations and rich output.
@@ -220,14 +220,10 @@ Select branches to compare using the [branch filter search box](branches/index.m
Find it under your project's **Repository > Compare**.
-## Locked files
+## Locked files **[PREMIUM]**
-> Available in [GitLab Premium](https://about.gitlab.com/pricing/).
-
-Lock your files to prevent any conflicting changes.
-
-[File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html) is available only in
-[GitLab Premium](https://about.gitlab.com/pricing/).
+Use [File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html) to
+lock your files to prevent any conflicting changes.
## Repository's API
@@ -241,4 +237,21 @@ Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be clon
in Xcode using the new **Open in Xcode** button, located next to the Git URL
used for cloning your project. The button is only shown on macOS.
-[jupyter]: https://jupyter.org
+## Download Source Code
+
+> Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24704) in GitLab 11.11.
+
+The source code stored in a repository can be downloaded from the UI.
+By clicking the download icon, a dropdown will open with links to download the following:
+
+![Download source code](img/download_source_code.png)
+
+- **Source code:**
+ allows users to download the source code on branch they're currently
+ viewing. Available extensions: `zip`, `tar`, `tar.gz`, and `tar.bz2`.
+- **Directory:**
+ only shows up when viewing a sub-directory. This allows users to download
+ the specific directory they're currently viewing. Also available in `zip`,
+ `tar`, `tar.gz`, and `tar.bz2`.
+- **Artifacts:**
+ allows users to download the artifacts of the latest CI build.
diff --git a/doc/user/project/repository/reducing_the_repo_size_using_git.md b/doc/user/project/repository/reducing_the_repo_size_using_git.md
index 672567a8d7d..2339759ecc8 100644
--- a/doc/user/project/repository/reducing_the_repo_size_using_git.md
+++ b/doc/user/project/repository/reducing_the_repo_size_using_git.md
@@ -98,6 +98,12 @@ up its own internal state, maximizing the space saved.
`git gc` against the repository. You will receive an email once it has
completed.
+This process will remove some copies of the rewritten commits from GitLab's
+cache and database, but there are still numerous gaps in coverage - at present,
+some of the copies may persist indefinitely. [Clearing the instance cache]
+(../../../administration/raketasks/maintenance.md#clear-redis-cache) may help to
+remove some of them, but it should not be depended on for security purposes!
+
## Using `git filter-branch`
1. Navigate to your repository:
diff --git a/doc/user/project/security_dashboard.md b/doc/user/project/security_dashboard.md
new file mode 100644
index 00000000000..43e910b29fe
--- /dev/null
+++ b/doc/user/project/security_dashboard.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html'
+---
+
+This document was moved to [another location](https://docs.gitlab.com/ee/user/application_security/security_dashboard/index.html).
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
new file mode 100644
index 00000000000..1a582164d03
--- /dev/null
+++ b/doc/user/project/service_desk.md
@@ -0,0 +1,124 @@
+# Service Desk **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/149) in [GitLab Premium 9.1](https://about.gitlab.com/2017/04/22/gitlab-9-1-released/#service-desk-eep).
+
+## Overview
+
+Service Desk is a module that allows your team to connect directly
+with any external party through email right inside of GitLab; no external tools required.
+An ongoing conversation right where your software is built ensures that user feedback ends
+up directly where it's needed, helping you build the right features to solve your users'
+real problems.
+
+With Service Desk, you can provide efficient email support to your customers, who can now
+email you bug reports, feature requests, or general feedback that will all end up in your
+GitLab project as new issues. In turn, your team can respond straight from the project.
+
+As Service Desk is built right into GitLab itself, the complexity and inefficiencies
+of multiple tools and external integrations are eliminated, significantly shortening
+the cycle time from feedback to software update.
+
+For an overview, check the video demonstration on [GitLab Service Desk](https://about.gitlab.com/2017/05/09/demo-service-desk/).
+
+## Use cases
+
+For instance, let's assume you develop a game for iOS or Android.
+The codebase is hosted in your GitLab instance, built and deployed
+with GitLab CI.
+
+Here's how Service Desk will work for you:
+
+1. You'll provide a project-specific email address to your paying customers, who can email you directly from within the app
+1. Each email they send creates an issue in the appropriate project
+1. Your team members navigate to the Service Desk issue tracker, where they can see new support requests and respond inside associated issues
+1. Your team communicates back and forth with the customer to understand the request
+1. Your team starts working on implementing code to solve your customer's problem
+1. When your team finishes the implementation, whereupon the merge request is merged and the issue is closed automatically
+1. The customer will have been attended successfully via email, without having real access to your GitLab instance
+1. Your team saved time by not having to leave GitLab (or setup any integrations) to follow up with your customer
+
+## How it works
+
+GitLab Service Desk is a simple way to allow people to create issues in your
+GitLab instance without needing their own user account.
+
+It provides a unique email address for end users to create issues in a project,
+and replies can be sent either through the GitLab interface or by email. End
+users will only see the thread through email.
+
+## Configuring Service Desk
+
+> **Note:**
+Service Desk is enabled on GitLab.com. If you're a
+[Silver subscriber](https://about.gitlab.com/gitlab-com/),
+you can skip the step 1 below; you only need to enable it per project.
+
+1. [Set up incoming email](../../administration/incoming_email.md#set-it-up) for the GitLab instance. This must
+ support [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing).
+2. Navigate to your project's **Settings** and scroll down to the **Service Desk**
+ section.
+3. If you have the correct access and an Premium license,
+ you will see an option to set up Service Desk:
+
+ ![Activate Service Desk option](img/service_desk_disabled.png)
+
+4. Checking that box will enable Service Desk for the project, and show a
+ unique email address to email issues to the project. These issues will be
+ [confidential](issues/confidential_issues.md), so they will only be visible to project members.
+
+ **Warning**: this email address can be used by anyone to create an issue on
+ this project, whether or not they have access to your GitLab instance.
+ We recommend **putting this behind an alias** so that it can be changed if
+ needed, and **[enabling Akismet](../../integration/akismet.md)** on your GitLab instance to add spam
+ checking to this service. Unblocked email spam would result in many spam
+ issues being created, and may disrupt your GitLab service.
+
+ ![Service Desk enabled](img/service_desk_enabled.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._
+
+
+5. Service Desk is now enabled for this project! You should be able to access it from your project's navigation **Issue submenu**:
+
+ ![Service Desk Navigation Item](img/service_desk_nav_item.png)
+
+## Using Service Desk
+
+### As an end user (issue creator)
+
+To create a Service Desk issue, an end user doesn't need to know anything about
+the GitLab instance. They just send an email to the address they are given, and
+receive an email back confirming receipt:
+
+![Service Desk enabled](img/service_desk_confirmation_email.png)
+
+This also gives the end user an option to unsubscribe.
+
+If they don't choose to unsubscribe, then any new comments added to the issue
+will be sent as emails:
+
+![Service Desk reply email](img/service_desk_reply.png)
+
+And any responses they send will be displayed in the issue itself.
+
+### As a responder to the issue
+
+For responders to the issue, everything works as usual. They'll see a familiar looking
+issue tracker, where they can see issues created via customer support requests and
+filter and interact with them just like other GitLab issues.
+
+![Service Desk Issue tracker](img/service_desk_issue_tracker.png)
+
+Messages from the end user will show as coming from the special Support Bot user, but apart from that,
+you can read and write comments as you normally do:
+
+![Service Desk issue thread](img/service_desk_thread.png)
+
+> Note that the project's visibility (private, internal, public) does not affect Service Desk.
+
+### Support Bot user
+
+Behind the scenes, Service Desk works by the special Support Bot user creating issues. This user
+does not count toward the license limit count.
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index d5f4a7fd4ac..99dd018a3ba 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -42,9 +42,9 @@ Set up your project's merge request settings:
![project's merge request settings](img/merge_requests_settings.png)
-### Service Desk
+### Service Desk **[PREMIUM]**
-Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support. Service Desk is available in [GitLab Premium](https://about.gitlab.com/pricing/).
+Enable [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) for your project to offer customer support.
### Export project
@@ -128,3 +128,7 @@ namespace if needed.
### Error Tracking
Configure Error Tracking to discover and view [Sentry errors within GitLab](../operations/error_tracking.md).
+
+### Jaeger tracing **[ULTIMATE]**
+
+Add the URL of a Jaeger server to allow your users to [easily access the Jaeger UI from within GitLab](../operations/tracing.md).
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 46a1b2bc3aa..2a2507d98a3 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -128,5 +128,123 @@ IDE. An example `package.json` is below.
}
```
+## Interactive Web Terminals for the Web IDE **[ULTIMATE ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5426) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
+
+CAUTION: **Warning:**
+Interactive Web Terminals for the Web IDE is currently in **Beta**.
+
+[Interactive web terminals](../../../ci/interactive_web_terminal/index.md)
+give the user access to a terminal to interact with the Runner directly from
+GitLab, including through the Web IDE.
+
+Only project [**maintainers**](../../permissions.md#project-members-permissions)
+can run Interactive Web Terminals through the Web IDE.
+
+CAUTION: **Warning:**
+GitLab.com [does not support Interactive Web Terminals yet](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611).
+Shared Runners in private instances are not supported either.
+
+### Runner configuration
+
+Some things need to be configured in the runner for the interactive web terminal
+to work:
+
+- The Runner needs to have
+ [`[session_server]` configured properly](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section).
+- If you are using a reverse proxy with your GitLab instance, web terminals need to be
+ [enabled](../../../administration/integration/terminal.md#enabling-and-disabling-terminal-support). **[ULTIMATE ONLY]**
+
+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 close the terminal window.
+
+NOTE: **Note:** Not all executors are
+[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart)
+
+### Web IDE configuration file
+
+In order to enable the Web IDE terminals you need to create the file
+`.gitlab/.gitlab-webide.yml` inside the repository's root. This
+file is fairly similar to the [CI configuration file](../../../ci/yaml/README.md)
+syntax but with some restrictions:
+
+- No global blocks can be defined (ie: `before_script` or `after_script`)
+- Only one job named `terminal` can be added to this file.
+- Only the keywords `image`, `services`, `tags`, `before_script`, `script`, and
+`variables` are allowed to be used to configure the job.
+- To connect to the interactive terminal, the `terminal` job must be still alive
+and running, otherwise the terminal won't be able to connect to the job's session.
+By default the `script` keyword has the value `sleep 60` to prevent
+the job from ending and giving the Web IDE enough time to connect. This means
+that, if you override the default `script` value, you'll have to add a command
+which would keep the job running, like `sleep`.
+
+In the code below there is an example of this configuration file:
+
+```yaml
+terminal:
+ before_script:
+ - apt-get update
+ script: sleep 60
+ variables:
+ RAILS_ENV: "test"
+ NODE_ENV: "test"
+```
+
+Once the terminal has started, the console will be displayed and we could access
+the project repository files.
+
+**Important**. The terminal job is branch dependant. This means that the
+configuration file used to trigger and configure the terminal will be the one in
+the selected branch of the Web IDE.
+
+If there is no configuration file in a branch, an error message will be shown.
+
+### Running Interactive Terminals in the Web IDE
+
+If Interactive Terminals are available for the current user, the **Terminal** button
+will be visible in the right sidebar of the Web IDE. Click this button to open
+or close the terminal tab.
+
+Once open, the tab will show the **Start Web Terminal** button. This button may
+be disabled if the environment is not configured correctly. If so, a status
+message will describe the issue. Here are some reasons why **Start Web Terminal**
+may be disabled:
+
+- `.gitlab/.gitlab-webide.yml` does not exist or is set up incorrectly.
+- No active private runners are available for the project.
+
+If active, clicking the **Start Web Terminal** button will load the terminal view
+and start connecting to the runner's terminal. At any time, the **Terminal** tab
+can be closed and reopened and the state of the terminal will not be affected.
+
+When the terminal is started and is successfully connected to the runner, then the
+runner's shell prompt will appear in the terminal. From here, you can enter
+commands that will be executed within the runner's environment. This is similar
+to running commands in a local terminal or through SSH.
+
+While the terminal is running, it can be stopped by clicking **Stop Terminal**.
+This will disconnect the terminal and stop the runner's terminal job. From here,
+click **Restart Terminal** to start a new terminal session.
+
+### Limitations
+
+Interactive Terminals is in a beta phase and will continue to be improved upon in upcoming
+releases. In the meantime, please note that the user is limited to having only one
+active terminal at a time.
+
+### Troubleshooting
+
+- If the terminal's text is gray and unresponsive, then the terminal has stopped
+ and it can no longer be used. A stopped terminal can be restarted by clicking
+ **Restart Terminal**.
+- If the terminal displays **Connection Failure**, then the terminal could not
+ connect to the runner. Please try to stop and restart the terminal. If the
+ problem persists, double check your runner configuration.
+
+
[ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/search/advanced_global_search.md b/doc/user/search/advanced_global_search.md
new file mode 100644
index 00000000000..38b26f31417
--- /dev/null
+++ b/doc/user/search/advanced_global_search.md
@@ -0,0 +1,75 @@
+# Advanced Global Search **[STARTER ONLY]**
+
+> - [Introduced][ee-109] in GitLab [Starter][ee] 8.4.
+> - This is the user documentation. To install and configure Elasticsearch,
+> visit the [administrator documentation](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+
+NOTE: **Note**
+Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
+
+Leverage Elasticsearch for faster, more advanced code search across your entire
+GitLab instance.
+
+## Overview
+
+The Advanced Global Search in GitLab is a powerful search service that saves
+you time. Instead of creating duplicate code and wasting time, you can
+now search for code within other teams that can help your own project.
+
+GitLab leverages the search capabilities of [Elasticsearch] and enables it when
+searching in:
+
+- GitLab application
+- Projects
+- Repositories
+- Commits
+- Issues
+- Merge requests
+- Milestones
+- Notes (comments)
+- Snippets
+- Wiki
+
+## Use cases
+
+The Advanced Global Search can be useful in various scenarios.
+
+### Faster searches
+
+If you are dealing with huge amount of data and want to keep GitLab's search
+fast, the Advanced Global Search will help you achieve that.
+
+### Promote innersourcing
+
+Your company may consist of many different developer teams each of which has
+their own group where the various projects are hosted. Some of your applications
+may be connected to each other, so your developers need to instantly search
+throughout the GitLab instance and find the code they search for.
+
+## Searching globally
+
+Just use the search as before and GitLab will show you matching code from each
+project you have access to.
+
+![Advanced Global Search](img/advanced_global_search.png)
+
+You can also use the [Advanced Syntax Search](advanced_search_syntax.md) which
+provides some useful queries.
+
+>**Note:**
+Elasticsearch has only data for the default branch. That means that if you go
+to the repository tree and switch the branch from the default to something else,
+then the "Code" tab in the search result page will be served by the regular
+search even if Elasticsearch is enabled.
+
+[ee-1305]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1305
+[aws-elastic]: http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg.html
+[aws-iam]: http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
+[aws-instance-profile]: http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli
+[ee-109]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/109 "Elasticsearch Merge Request"
+[elasticsearch]: https://www.elastic.co/products/elasticsearch "Elasticsearch website"
+[install]: https://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html "Elasticsearch installation documentation"
+[pkg]: https://about.gitlab.com/downloads/ "Download Omnibus GitLab"
+[elastic-settings]: https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration.html#settings "Elasticsearch configuration settings"
+[ee]: https://about.gitlab.com/pricing/
+[es]: https://www.elastic.co/products/elasticsearch
diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md
new file mode 100644
index 00000000000..8d4aac5f502
--- /dev/null
+++ b/doc/user/search/advanced_search_syntax.md
@@ -0,0 +1,69 @@
+# Advanced Syntax Search **[STARTER ONLY]**
+
+> **Notes:**
+> - Introduced in [GitLab Enterprise Starter][ee] 9.2
+> - This is the user documentation. To install and configure Elasticsearch,
+> visit the [administrator documentation](https://docs.gitlab.com/ee/integration/elasticsearch.html).
+
+NOTE: **Note**
+Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
+
+Use advanced queries for more targeted search results.
+
+## Overview
+
+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.
+
+## Use cases
+
+Let's say for example that the product you develop relies on the code of another
+product that's hosted under some other group.
+
+Since under your GitLab instance there are hosted hundreds of different projects,
+you need the search results to be as efficient as possible. You have a feeling
+of what you want to find (e.g., a function name), but at the same you're also
+not so sure.
+
+In that case, using the advanced search syntax in your query will yield much
+better results.
+
+## Using the Advanced Syntax Search
+
+The Advanced Syntax Search supports fuzzy or exact search queries with prefixes,
+boolean operators, and much more.
+
+Full details can be found in the [Elasticsearch documentation][elastic], but
+here's a quick guide:
+
+- Searches look for all the words in a query, in any order - e.g.: searching
+ issues for `display bug` will return all issues matching both those words, in any order.
+- To find the exact phrase (stemming still applies), use double quotes: `"display bug"`
+- To find bugs not mentioning display, use `-`: `bug -display`
+- To find a bug in display or sound, use `|`: `bug display | sound`
+- To group terms together, use parentheses: `bug | (display +sound)`
+- To match a partial word, use `*`: `bug find_by_*`
+- To find a term containing one of these symbols, use `\`: `argument \-last`
+
+### Syntax search filters
+
+The Advanced Syntax Search also supports the use of filters. The available filters are:
+
+ - filename: Filters by filename. You can use the glob (`*`) operator for fuzzy matching.
+ - path: Filters by path. You can use the glob (`*`) operator for fuzzy matching.
+ - extension: Filters by extension in the filename. Please write the extension without a leading dot. Exact match only.
+
+To use them, simply add them to your query in the format `<filter_name>:<value>` without
+ any spaces between the colon (`:`) and the value.
+
+Examples:
+
+- Finding a file with any content named `hello_world.rb`: `* filename:hello_world.rb`
+- Finding a file named `hello_world` with the text `whatever` inside of it: `whatever filename:hello_world`
+- Finding the text 'def create' inside files with the `.rb` extension: `def create extension:rb`
+- Finding the text `sha` inside files in a folder called `encryption`: `sha path:encryption`
+- Finding any file starting with `hello` containing `world` and with the `.js` extension: `world filename:hello* extension:js`
+
+[ee]: https://about.gitlab.com/pricing/
+[elastic]: https://www.elastic.co/guide/en/elasticsearch/reference/5.3/query-dsl-simple-query-string-query.html#_simple_query_string_syntax
diff --git a/doc/user/search/img/advanced_global_search.png b/doc/user/search/img/advanced_global_search.png
new file mode 100644
index 00000000000..4903bbb07e1
--- /dev/null
+++ b/doc/user/search/img/advanced_global_search.png
Binary files differ
diff --git a/doc/user/search/img/multiple_assignees.png b/doc/user/search/img/multiple_assignees.png
new file mode 100644
index 00000000000..5c46f3dda46
--- /dev/null
+++ b/doc/user/search/img/multiple_assignees.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 705983cce2a..bb6c48471c7 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -84,6 +84,12 @@ You can view recent searches by clicking on the little arrow-clock icon, which i
Individual filters can be removed by clicking on the filter's (x) button or backspacing. The entire search filter can be cleared by clicking on the search box's (x) button.
+## Filtering with multiple filters of the same type
+
+Some filters can be added multiple times. These include but are not limited to assignees and labels. When you filter with these multiple filters of the same type, the AND logic is applied. For example, if you were filtering `assignee:@sam assignee:@sarah`, your results will only include entries whereby the assignees are assigned to both Sam and Sarah are returned.
+
+![multiple assignees filtering](img/multiple_assignees.png)
+
### Shortcut
You'll also find a shortcut on the search field on the top-right of the project's dashboard to
@@ -136,3 +142,18 @@ you'll be able to, besides filtering them by **Name**, **Author**, **Assignee**,
and **Labels**, select multiple issues to add to a list of your choice:
![search and select issues to add to board](img/search_issues_board.png)
+
+## Advanced Global Search **[STARTER]**
+
+Leverage Elasticsearch for faster, more advanced code search across your entire
+GitLab instance.
+
+[Learn how to use the Advanced Global Search.](advanced_global_search.md)
+
+## Advanced Syntax Search **[STARTER]**
+
+Use advanced queries for more targeted search results.
+
+[Learn how to use the Advanced Syntax Search.](advanced_search_syntax.md)
+
+[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 272f7807ac0..84ab7840140 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,12 +13,15 @@ comments: false
- [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](https://docs.gitlab.com/ee/user/project/issues/csv_export.html) **[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](https://docs.gitlab.com/ee/user/project/file_lock.html) **[PREMIUM]**
- [Labels](../user/project/labels.md)
+- [Issue weight](https://docs.gitlab.com/ee/workflow/issue_weight.html) **[STARTER]**
- [Notification emails](notifications.md)
- [Projects](../user/project/index.md)
- [Project forking workflow](forking_workflow.md)
@@ -41,6 +44,9 @@ comments: false
- [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](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) **[STARTER]**
+- [Repository mirroring](repository_mirroring.md) **[STARTER]**
+- [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html) **[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)
diff --git a/doc/workflow/ff_merge.md b/doc/workflow/ff_merge.md
new file mode 100644
index 00000000000..11e9e1bbd6b
--- /dev/null
+++ b/doc/workflow/ff_merge.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../user/project/merge_requests/fast_forward_merge.md'
+---
+
+This document was moved to [user/project/merge_requests/fast_forward_merge](../user/project/merge_requests/fast_forward_merge.md).
diff --git a/doc/workflow/git_annex.md b/doc/workflow/git_annex.md
new file mode 100644
index 00000000000..84d25951908
--- /dev/null
+++ b/doc/workflow/git_annex.md
@@ -0,0 +1,238 @@
+# 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-ee/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
diff --git a/doc/workflow/git_lfs.md b/doc/workflow/git_lfs.md
new file mode 100644
index 00000000000..da217b0a5da
--- /dev/null
+++ b/doc/workflow/git_lfs.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'lfs/manage_large_binaries_with_git_lfs.md'
+---
+
+This document was moved to [another location](lfs/manage_large_binaries_with_git_lfs.md).
diff --git a/doc/workflow/img/copy_ssh_public_key_button.png b/doc/workflow/img/copy_ssh_public_key_button.png
new file mode 100644
index 00000000000..e20dae09a4d
--- /dev/null
+++ b/doc/workflow/img/copy_ssh_public_key_button.png
Binary files differ
diff --git a/doc/workflow/issue_weight.md b/doc/workflow/issue_weight.md
new file mode 100644
index 00000000000..267160dae2a
--- /dev/null
+++ b/doc/workflow/issue_weight.md
@@ -0,0 +1,22 @@
+# Issue Weight **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/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)
diff --git a/doc/workflow/issue_weight/issue.png b/doc/workflow/issue_weight/issue.png
new file mode 100644
index 00000000000..3800b5940b8
--- /dev/null
+++ b/doc/workflow/issue_weight/issue.png
Binary files differ
diff --git a/doc/workflow/lfs/images/git-annex-branches.png b/doc/workflow/lfs/images/git-annex-branches.png
new file mode 100644
index 00000000000..3d614f68177
--- /dev/null
+++ b/doc/workflow/lfs/images/git-annex-branches.png
Binary files differ
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
new file mode 100644
index 00000000000..1aeab5980a3
--- /dev/null
+++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -0,0 +1,261 @@
+# 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][annex-gitlab-use] 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:**
+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][annex-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][2fa], make sure to use a
+ [personal access token][token] 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 commandline 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][doc-1]
+- (GitLab Docs) [Git LFS][doc-2]
+
+[2fa]: ../../user/profile/account/two_factor_authentication.md
+[token]: ../../user/profile/account/two_factor_authentication.html#personal-access-tokens
+[annex-tips]: ../git_annex.html#troubleshooting-tips
+[annex-direct]: https://git-annex.branchable.com/direct_mode/
+[annex-gitlab-use]: ../git_annex.md#using-gitlab-git-annex
+[annex-ee]: https://docs.gitlab.com/ee/workflow/git_annex.html
+[bkp-ext-drive]: https://www.thomas-krenn.com/en/wiki/Git-annex_Repository_on_an_External_Hard_Drive
+[doc-1]: https://docs.gitlab.com/ee/workflow/git_annex.html
+[doc-2]: https://docs.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html
+[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-ee/issues/1648
+[lfs-track]: https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/#tracking-files-with-lfs
+[post-1]: https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/
+[post-2]: https://about.gitlab.com/2015/11/23/announcing-git-lfs-support-in-gitlab/
+[post-3]: https://about.gitlab.com/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/workflow/merge_request_approvals.md b/doc/workflow/merge_request_approvals.md
new file mode 100644
index 00000000000..f8a99ec3d57
--- /dev/null
+++ b/doc/workflow/merge_request_approvals.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html'
+---
+
+This document was moved to [user/project/merge_requests/merge_request_approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 6ce789998a4..0eb4f85e0ec 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -73,40 +73,43 @@ Below is the table of events users can be notified of:
| Group access level changed | User | Sent when user group access level is changed |
| Project moved | Project members [1] | [1] not disabled |
-### Issue / Merge request events
+### 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 issue/merge request title or description
- - anyone mentioned by `@username` in any of the comments on the issue/merge request
- ...with notification level "Participating" or higher
+ - 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
+- 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 | |
+| New issue | |
+| Close issue | |
| Reassign issue | The above, plus the old assignee |
-| Reopen issue | |
+| 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 | |
+| 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 | |
+| 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
diff --git a/doc/workflow/rebase_before_merge.md b/doc/workflow/rebase_before_merge.md
new file mode 100644
index 00000000000..10e768d3371
--- /dev/null
+++ b/doc/workflow/rebase_before_merge.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../user/project/merge_requests/fast_forward_merge.md'
+---
+
+This document was moved to [another location](../user/project/merge_requests/fast_forward_merge.md).
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 9fcadbf3bee..2f8f1545b84 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -222,8 +222,10 @@ 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. GitLab will generate a 4096-bit RSA
-key and display the public component of that key to you.
+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:
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index 6ed6b0bda66..6714d08f2f7 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -85,6 +85,14 @@ You can see GitLab's keyboard shortcuts by using 'shift + ?'
| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file |
| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file |
+## Epics **[ULTIMATE]**
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>r</kbd> | Reply (quoting selected text) |
+| <kbd>e</kbd> | Edit description |
+| <kbd>l</kbd> | Change label |
+
## Wiki pages
| Keyboard Shortcut | Description |
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 830f17aa7f2..32907db4f46 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -25,9 +25,8 @@ will still be shown in the body of the _To do_ tab.
A Todo appears in your Todos dashboard when:
-- an issue or merge request is assigned to you,
-- you are `@mentioned` in an issue or merge request, be it the description of
- the issue/merge request or in a comment,
+- an issue or merge request is assigned to you
+- you are `@mentioned` in the description or in a comment of an issue, merge request, or 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.
@@ -63,14 +62,14 @@ for filtering; otherwise, they appear as normal.
### Manually creating a Todo
-You can also add an issue or merge request to your Todos dashboard by clicking
-the "Add todo" button in the issue or merge request sidebar.
+You can also add an issue, merge request or epic to your Todos dashboard by clicking
+the "Add todo" button in the sidebar of the issue, merge request, or epic **[ULTIMATE]**.
![Adding a Todo from the issuable sidebar](img/todos_add_todo_sidebar.png)
## Marking a Todo as done
-Any action to the corresponding issue or merge request will mark your Todo as
+Any action to the corresponding issue, merge request or epic **[ULTIMATE]** will mark your Todo as
**Done**. Actions that dismiss Todos include:
- changing the assignee
@@ -84,10 +83,10 @@ Todos are personal, and they're only marked as done if the action is coming from
you. If you close the issue or merge request, your Todo will automatically
be marked as done.
-If someone else closes, merges, or takes action on the issue or merge
+If someone else closes, merges, or takes action on the issue, epic or merge
request, your Todo will remain pending. This prevents other users from closing issues without you being notified.
-There is just one Todo per issue or merge request, so mentioning a user a
+There is just one Todo per issue, epic or merge request, so mentioning a user a
hundred times in an issue will only trigger one Todo.
---
@@ -97,7 +96,7 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png)
-A Todo can also be marked as done from the issue or merge request sidebar using
+A Todo can also be marked as done from the issue, merge request or epic sidebar using
the "Mark todo as done" button.
![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
@@ -114,7 +113,7 @@ There are four kinds of filters you can use on your Todos dashboard.
| Project | Filter by project |
| Group | Filter by group |
| Author | Filter by the author that triggered the Todo |
-| Type | Filter by issue or merge request |
+| Type | Filter by issue, merge request, or epic **[ULTIMATE]** |
| Action | Filter by the action that triggered the Todo |
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-todo).
diff --git a/jest.config.js b/jest.config.js
index fdbbe977f0b..84481642250 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -2,6 +2,10 @@ const IS_EE = require('./config/helpers/is_ee_env');
const reporters = ['default'];
+// To have consistent date time parsing both in local and CI environments we set
+// the timezone of the Node process. https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27738
+process.env.TZ = 'GMT';
+
if (process.env.CI) {
reporters.push([
'jest-junit',
@@ -24,6 +28,7 @@ module.exports = {
'^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',
},
collectCoverageFrom: ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/coverage-frontend/',
diff --git a/lib/api/api.rb b/lib/api/api.rb
index bf8ddba6f0d..f4a96b9711b 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -6,7 +6,7 @@ module API
LOG_FILENAME = Rails.root.join("log", "api_json.log")
- NO_SLASH_URL_PART_REGEX = %r{[^/]+}
+ NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze
NAMESPACE_OR_PROJECT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
COMMIT_ENDPOINT_REQUIREMENTS = NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
USER_REQUIREMENTS = { user_id: NO_SLASH_URL_PART_REGEX }.freeze
@@ -134,6 +134,7 @@ module API
mount ::API::Pipelines
mount ::API::PipelineSchedules
mount ::API::ProjectClusters
+ mount ::API::ProjectEvents
mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectHooks
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 5c98b0ad56c..65d7f68bbf9 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -162,8 +162,8 @@ module API
result = DeleteBranchService.new(user_project, current_user)
.execute(params[:branch])
- if result[:status] != :success
- render_api_error!(result[:message], result[:return_code])
+ if result.error?
+ render_api_error!(result.message, result.http_status)
end
end
end
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 8afe6dda414..5928ee1657b 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -134,9 +134,13 @@ module API
post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do
noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
notes = readable_discussion_notes(noteable, params[:discussion_id])
+ first_note = notes.first
break not_found!("Discussion") if notes.empty?
- break bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
+
+ unless first_note.part_of_discussion? || first_note.to_discussion.can_convert_to_discussion?
+ break bad_request!("Discussion can not be replied to.")
+ end
opts = {
note: params[:body],
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4bdac278add..90ed24a2ded 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -922,7 +922,15 @@ module API
end
class NamespaceBasic < Grape::Entity
- expose :id, :name, :path, :kind, :full_path, :parent_id
+ expose :id, :name, :path, :kind, :full_path, :parent_id, :avatar_url
+
+ expose :web_url do |namespace|
+ if namespace.user?
+ Gitlab::Routing.url_helpers.user_url(namespace.owner)
+ else
+ namespace.web_url
+ end
+ end
end
class Namespace < NamespaceBasic
@@ -1148,22 +1156,33 @@ module API
end
end
- class Release < TagRelease
+ class Release < Grape::Entity
expose :name
+ expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? }
+ expose :description
expose :description_html do |entity|
MarkupHelper.markdown_field(entity, :description)
end
expose :created_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
- expose :commit, using: Entities::Commit
+ expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? }
expose :assets do
- expose :assets_count, as: :count
- expose :sources, using: Entities::Releases::Source
+ expose :assets_count, as: :count do |release, _|
+ assets_to_exclude = can_download_code? ? [] : [:sources]
+ release.assets_count(except: assets_to_exclude)
+ end
+ expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? }
expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted
end
end
+
+ private
+
+ def can_download_code?
+ Ability.allowed?(options[:current_user], :download_code, object.project)
+ end
end
class Tag < Grape::Entity
@@ -1269,7 +1288,7 @@ module API
end
class Variable < Grape::Entity
- expose :key, :value
+ expose :variable_type, :key, :value
expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
end
diff --git a/lib/api/events.rb b/lib/api/events.rb
index b98aa9f31e1..e4c017fab42 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -4,34 +4,11 @@ module API
class Events < Grape::API
include PaginationParams
include APIGuard
+ helpers ::API::Helpers::EventsHelpers
- helpers do
- params :event_filter_params do
- optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
- optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
- optional :before, type: Date, desc: 'Include only events created before this date'
- optional :after, type: Date, desc: 'Include only events created after this date'
- end
-
- params :sort_params do
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return events sorted in ascending and descending order'
- end
-
- def present_events(events)
- events = paginate(events)
-
- present events, with: Entities::Event
- end
-
- def find_events(source)
- EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute
- end
- end
+ allow_access_with_scope :read_user, if: -> (request) { request.get? }
resource :events do
- allow_access_with_scope :read_user, if: -> (request) { request.get? }
-
desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.'
success Entities::Event
@@ -55,8 +32,6 @@ module API
requires :id, type: String, desc: 'The ID or Username of the user'
end
resource :users do
- allow_access_with_scope :read_user, if: -> (request) { request.get? }
-
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
@@ -76,25 +51,5 @@ module API
present_events(events)
end
end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc "List a Project's visible events" do
- success Entities::Event
- end
- params do
- use :pagination
- use :event_filter_params
- use :sort_params
- end
-
- get ":id/events" do
- events = find_events(user_project)
-
- present_events(events)
- end
- end
end
end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index 3f048e0dc56..47fcbabb4d4 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -47,6 +47,7 @@ module API
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
+ optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
end
post ':id/variables' do
variable_params = declared_params(include_missing: false)
@@ -67,6 +68,7 @@ module API
optional :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
+ optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
end
# rubocop: disable CodeReuse/ActiveRecord
put ':id/variables/:key' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8a21d44b4bf..7e4539d0419 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -67,10 +67,6 @@ module API
initial_current_user != current_user
end
- def user_namespace
- @user_namespace ||= find_namespace!(params[:id])
- end
-
def user_group
@group ||= find_group!(params[:id])
end
diff --git a/lib/api/helpers/events_helpers.rb b/lib/api/helpers/events_helpers.rb
new file mode 100644
index 00000000000..bf3b76bb92d
--- /dev/null
+++ b/lib/api/helpers/events_helpers.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module EventsHelpers
+ extend Grape::API::Helpers
+
+ params :event_filter_params do
+ optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
+ optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
+ optional :before, type: Date, desc: 'Include only events created before this date'
+ optional :after, type: Date, desc: 'Include only events created after this date'
+ end
+
+ params :sort_params do
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return events sorted in ascending and descending order'
+ end
+
+ def present_events(events)
+ events = paginate(events)
+
+ present events, with: Entities::Event
+ end
+
+ def find_events(source)
+ EventsFinder.new(params.merge(source: source, current_user: current_user, with_associations: true)).execute
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
index 793ae11b41d..9cdde25fe4e 100644
--- a/lib/api/helpers/related_resources_helpers.rb
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -13,6 +13,10 @@ module API
available?(:merge_requests, project, options[:current_user])
end
+ def expose_path(path)
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, path)
+ end
+
def expose_url(path)
url_options = Gitlab::Application.routes.default_url_options
protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 00f0bbab231..c82fd230d7a 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -265,6 +265,8 @@ module API
params[:changes], push_options.as_json)
if Feature.enabled?(:mr_push_options, default_enabled: true)
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/61359')
+
mr_options = push_options.get(:merge_request)
output.merge!(process_mr_push_options(mr_options, project, user, params[:changes])) if mr_options.present?
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 000c00ea9f9..d0a93b77951 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -192,6 +192,7 @@ module API
params.delete(:iid) unless current_user.can?(:set_issue_iid, user_project)
issue_params = declared_params(include_missing: false)
+ issue_params[:system_note_timestamp] = params[:created_at]
issue_params = convert_parameters_from_legacy_format(issue_params)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index ce85772e4ed..daa98c22e5e 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -367,6 +367,10 @@ module API
merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
+ if merge_when_pipeline_succeeds || merge_request.merge_when_pipeline_succeeds
+ render_api_error!('Not allowed: pipeline does not exist', 405) unless merge_request.head_pipeline
+ end
+
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 3cc09f6ac3f..77ecb3e7cde 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -44,6 +44,8 @@ module API
requires :id, type: String, desc: "Namespace's ID or path"
end
get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ user_namespace = find_namespace!(params[:id])
+
present user_namespace, with: Entities::Namespace, current_user: current_user
end
end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
index c86b50d3736..1d1ef1afc6b 100644
--- a/lib/api/pipeline_schedules.rb
+++ b/lib/api/pipeline_schedules.rb
@@ -118,6 +118,7 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
+ optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
end
post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do
authorize! :update_pipeline_schedule, pipeline_schedule
@@ -138,6 +139,7 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
requires :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
+ optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
end
put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
authorize! :update_pipeline_schedule, pipeline_schedule
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index b62ec887183..dcc8d94fb79 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -54,6 +54,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb
new file mode 100644
index 00000000000..734311e1142
--- /dev/null
+++ b/lib/api/project_events.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ class ProjectEvents < Grape::API
+ include PaginationParams
+ include APIGuard
+ helpers ::API::Helpers::EventsHelpers
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "List a Project's visible events" do
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+
+ get ":id/events" do
+ events = find_events(user_project)
+
+ present_events(events)
+ end
+ end
+ end
+end
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index cb85028f22c..6b17f4317db 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -23,7 +23,7 @@ module API
get ':id/releases' do
releases = ::ReleasesFinder.new(user_project, current_user).execute
- present paginate(releases), with: Entities::Release
+ present paginate(releases), with: Entities::Release, current_user: current_user
end
desc 'Get a single project release' do
@@ -34,9 +34,9 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
end
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do
- authorize_read_release!
+ authorize_download_code!
- present release, with: Entities::Release
+ present release, with: Entities::Release, current_user: current_user
end
desc 'Create a new release' do
@@ -63,7 +63,7 @@ module API
.execute
if result[:status] == :success
- present result[:release], with: Entities::Release
+ present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@@ -86,7 +86,7 @@ module API
.execute
if result[:status] == :success
- present result[:release], with: Entities::Release
+ present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@@ -107,7 +107,7 @@ module API
.execute
if result[:status] == :success
- present result[:release], with: Entities::Release
+ present result[:release], with: Entities::Release, current_user: current_user
else
render_api_error!(result[:message], result[:http_status])
end
@@ -135,6 +135,10 @@ module API
authorize! :destroy_release, release
end
+ def authorize_download_code!
+ authorize! :download_code, release
+ end
+
def release
@release ||= user_project.releases.find_by_tag(params[:tag])
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index c60d25b88cb..ea36c24eca2 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -15,12 +15,14 @@ module API
optional :info, type: Hash, desc: %q(Runner's metadata)
optional :active, type: Boolean, desc: 'Should Runner be active'
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+ optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
+ desc: 'The access_level of the runner'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
end
post '/' do
- attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :maximum_timeout])
+ attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout])
.merge(get_runner_details_from_request)
attributes =
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index b064747e5fc..8046acfa397 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -150,6 +150,12 @@ module API
given elasticsearch_indexing: ->(val) { val } do
optional :elasticsearch_search, type: Boolean, desc: 'Enable Elasticsearch search'
requires :elasticsearch_url, type: String, desc: 'The url to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://localhost:9200, http://localhost:9201")'
+ optional :elasticsearch_limit_indexing, type: Boolean, desc: 'Limit Elasticsearch to index certain namespaces and projects'
+ end
+
+ given elasticsearch_limit_indexing: ->(val) { val } do
+ optional :elasticsearch_namespace_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The namespace ids to index with Elasticsearch.'
+ optional :elasticsearch_project_ids, type: Array[Integer], coerce_with: Validations::Types::LabelsList.coerce, desc: 'The project ids to index with Elasticsearch.'
end
optional :email_additional_text, type: String, desc: 'Additional text added to the bottom of every email for legal/auditing/compliance reasons'
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 3489ba827e4..a1bb21b3a06 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -55,6 +55,7 @@ module API
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
+ optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
if Gitlab.ee?
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
@@ -80,6 +81,7 @@ module API
optional :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
+ optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
if Gitlab.ee?
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 994074ddc67..5724adb2c40 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -33,7 +33,8 @@ module API
authorize! :read_wiki, user_project
entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
- present user_project.wiki.pages, with: entity
+
+ present user_project.wiki.list_pages(load_content: params[:with_content]), with: entity
end
desc 'Get a wiki page' do
diff --git a/lib/banzai/color_parser.rb b/lib/banzai/color_parser.rb
index 6d01d51955c..cce79e73d2d 100644
--- a/lib/banzai/color_parser.rb
+++ b/lib/banzai/color_parser.rb
@@ -2,13 +2,13 @@
module Banzai
module ColorParser
- ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/ # 0.0..1.0
- PERCENTS = /(?:\d{1,2}|100)%/ # 00%..100%
- ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/
- BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/ # 00..255
- DEGS = /-?\d+(?:deg)?/i # [-]digits[deg]
- RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i # [-](digits[.digits] OR .digits)rad
- HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/
+ ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/.freeze # 0.0..1.0
+ PERCENTS = /(?:\d{1,2}|100)%/.freeze # 00%..100%
+ ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/.freeze
+ BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/.freeze # 00..255
+ DEGS = /-?\d+(?:deg)?/i.freeze # [-]digits[deg]
+ RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i.freeze # [-](digits[.digits] OR .digits)rad
+ HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/.freeze
RGB_FORMAT = %r{
(?:rgba?
\(
@@ -20,7 +20,7 @@ module Banzai
#{ALPHA_CHANNEL}
\)
)
- }xi
+ }xi.freeze
HSL_FORMAT = %r{
(?:hsla?
\(
@@ -28,11 +28,11 @@ module Banzai
#{ALPHA_CHANNEL}
\)
)
- }xi
+ }xi.freeze
FORMATS = [HEX_FORMAT, RGB_FORMAT, HSL_FORMAT].freeze
- COLOR_FORMAT = /\A(#{Regexp.union(FORMATS)})\z/ix
+ COLOR_FORMAT = /\A(#{Regexp.union(FORMATS)})\z/ix.freeze
# Public: Analyzes whether the String is a color code.
#
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 086adf59d2b..56214043d87 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -33,7 +33,7 @@ module Banzai
# https://github.com/vmg/rinku/blob/v2.0.1/ext/rinku/autolink.c#L65
#
# Rubular: http://rubular.com/r/nrL3r9yUiq
- LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!\?|!|\.|,|:)}
+ LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!\?|!|\.|,|:)}.freeze
# Text matching LINK_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
diff --git a/lib/banzai/filter/front_matter_filter.rb b/lib/banzai/filter/front_matter_filter.rb
index a27d18facd1..544231adea4 100644
--- a/lib/banzai/filter/front_matter_filter.rb
+++ b/lib/banzai/filter/front_matter_filter.rb
@@ -20,7 +20,7 @@ module Banzai
\s*
^\k<delim> # closing front matter marker
\s*
- }mx
+ }mx.freeze
def call
html.sub(PATTERN) do |_match|
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 77e4c438bd0..4d67140b0a1 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -82,16 +82,18 @@ module Banzai
def object_link_text(object, matches)
label_suffix = ''
+ parent = project || group
if project || full_path_ref?(matches)
project_path = full_project_path(matches[:namespace], matches[:project])
parent_from_ref = from_ref_cached(project_path)
- reference = parent_from_ref.to_human_reference(project || group)
+ reference = parent_from_ref.to_human_reference(parent)
label_suffix = " <i>in #{reference}</i>" if reference.present?
end
- LabelsHelper.render_colored_label(object, label_suffix: label_suffix, title: tooltip_title(object))
+ presenter = object.present(issuable_subject: parent)
+ LabelsHelper.render_colored_label(presenter, label_suffix: label_suffix, title: tooltip_title(presenter))
end
def tooltip_title(label)
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index 50bf823929c..ee7f10ebdf6 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -33,7 +33,7 @@ module Banzai
(?<new_link>.+?)
(?<title>\ ".+?")?
\)
- }x
+ }x.freeze
# Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index f2ae17b44fa..ade4d260be1 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -17,7 +17,7 @@ module Banzai
# :toc - String containing Table of Contents data as a `ul` element with
# `li` child elements.
class TableOfContentsFilter < HTML::Pipeline::Filter
- PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u
+ PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze
def call
return doc if context[:no_header_anchors]
@@ -31,6 +31,7 @@ module Banzai
if header_content = node.children.first
id = node
.text
+ .strip
.downcase
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb
index 239780d8626..9b7d1548056 100644
--- a/lib/declarative_policy/preferred_scope.rb
+++ b/lib/declarative_policy/preferred_scope.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module DeclarativePolicy
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index 1204e53ee2e..3f107fbbf3b 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -36,8 +36,8 @@ module Gitlab
end
COM_URL = 'https://gitlab.com'.freeze
- APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
- SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
+ APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}.freeze
+ SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
VERSION = File.read(root.join("VERSION")).strip.freeze
INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
@@ -59,7 +59,11 @@ module Gitlab
end
def self.ee?
- Object.const_defined?(:License)
+ if ENV['IS_GITLAB_EE'].present?
+ Gitlab::Utils.to_boolean(ENV['IS_GITLAB_EE'])
+ else
+ Object.const_defined?(:License)
+ end
end
def self.process_name
diff --git a/lib/gitlab/action_view_output/context.rb b/lib/gitlab/action_view_output/context.rb
new file mode 100644
index 00000000000..9fbc9811636
--- /dev/null
+++ b/lib/gitlab/action_view_output/context.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# This file was simplified from https://raw.githubusercontent.com/rails/rails/195f39804a7a4a0034f25e8704220e03d95a752a/actionview/lib/action_view/context.rb.
+#
+# It is only needed by modules that need to call ActionView helper
+# methods (e.g. those in
+# https://github.com/rails/rails/tree/c4d3e202e10ae627b3b9c34498afb45450652421/actionview/lib/action_view/helpers)
+# to generate tags outside of a Rails controller (e.g. API, Sidekiq,
+# etc.).
+#
+# In Rails 5, ActionView::Context automatically includes CompiledTemplates.
+# This means that any module that includes ActionView::Context is now a descendant
+# of CompiledTemplates.
+#
+# When a partial is rendered for the first time, it runs
+# Module#module_eval, which will evaluate a string source that defines a
+# new method. For example:
+#
+# def _app_views_profiles_show_html_haml___1285955918103175884_70307801785400(local_assigns, output_buffer)
+# "hello world"
+# end
+#
+# When a new method is defined, the Ruby interpreter clears the method
+# cache for all descendants, and all methods for those modules will have
+# to be redefined. This can lead to a significant performance penalty.
+#
+# Rails 6 fixes this behavior by moving out the `include
+# CompiledTemplates` into ActionView::Base so that including `ActionView::Context`
+# doesn't quietly affect other modules in this way.
+
+if Rails::VERSION::STRING.start_with?('6')
+ raise 'This module is no longer needed in Rails 6. Use ActionView::Context instead.'
+end
+
+module Gitlab
+ module ActionViewOutput
+ module Context
+ attr_accessor :output_buffer, :view_flow
+ end
+ end
+end
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
index 78fa25c5516..4ebf2afb9cb 100644
--- a/lib/gitlab/auth/result.rb
+++ b/lib/gitlab/auth/result.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
index a84f794bfae..1924f2ffee2 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -8,8 +8,8 @@ module Gitlab
self.table_name = 'untracked_files_for_uploads'
# Ends with /:random_hex/:filename
- FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
- FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/
+ FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}.freeze
+ FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/.freeze
# These regex patterns are tested against a relative path, relative to
# the upload directory.
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 81ca2b0a9b7..1ee44a3a5a9 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -16,7 +16,7 @@ module Gitlab
RELATIVE_UPLOAD_DIR
)
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
- START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}
+ START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}.freeze
EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze
EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index 1d3ddeeb0f1..ff2694abd5e 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -201,6 +201,7 @@ module Gitlab
target_branch: Gitlab::Git.ref_name(pull_request.target_branch_name),
target_branch_sha: pull_request.target_branch_sha,
state: pull_request.state,
+ state_id: MergeRequest.available_states[pull_request.state],
author_id: author_id,
assignee_id: nil,
created_at: pull_request.created_at,
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index 1dbd564fb6f..4ddc1c718c7 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -48,7 +48,7 @@ module Gitlab
if project.empty_repo?
protected_branch_push_checks
- elsif creation? && protected_branch_creation_enabled?
+ elsif creation?
protected_branch_creation_checks
elsif deletion?
protected_branch_deletion_checks
@@ -124,10 +124,6 @@ module Gitlab
Gitlab::Routing.url_helpers.project_project_members_url(project)
end
- def protected_branch_creation_enabled?
- Feature.enabled?(:protected_branch_creation, project, default_enabled: true)
- end
-
def matching_merge_request?
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
diff --git a/lib/gitlab/checks/lfs_check.rb b/lib/gitlab/checks/lfs_check.rb
index cc6a14d2d9a..67a65d61441 100644
--- a/lib/gitlab/checks/lfs_check.rb
+++ b/lib/gitlab/checks/lfs_check.rb
@@ -7,6 +7,7 @@ module Gitlab
ERROR_MESSAGE = 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'.freeze
def validate!
+ return unless Feature.enabled?(:lfs_check, default_enabled: true)
return unless project.lfs_enabled?
return if skip_lfs_integrity_check
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 7011dd1aaf2..1c3ce08be76 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -11,8 +11,8 @@ module Gitlab
ParserError = Class.new(StandardError)
InvalidStreamError = Class.new(StandardError)
- VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
- INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
+ VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/.freeze
+ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}.freeze
attr_reader :stream, :path, :full_version
diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
index 41135ae62bb..dbdc59505ac 100644
--- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
+++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
@@ -6,7 +6,10 @@ module Gitlab
module Prerequisite
class KubernetesNamespace < Base
def unmet?
- deployment_cluster.present? && kubernetes_namespace.new_record?
+ deployment_cluster.present? &&
+ deployment_cluster.managed? &&
+ !deployment_cluster.project_type? &&
+ kubernetes_namespace.new_record?
end
def complete!
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index a3f6cc31321..eeaac52eefd 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -11,7 +11,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
+ ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management metrics].freeze
attributes ALLOWED_KEYS
@@ -28,6 +28,7 @@ module Gitlab
validates :dast, array_of_strings_or_string: true
validates :performance, array_of_strings_or_string: true
validates :license_management, array_of_strings_or_string: true
+ validates :metrics, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/ci/config/external/processor.rb b/lib/gitlab/ci/config/external/processor.rb
index 1dd2d42016a..4a049ecae49 100644
--- a/lib/gitlab/ci/config/external/processor.rb
+++ b/lib/gitlab/ci/config/external/processor.rb
@@ -11,7 +11,8 @@ module Gitlab
@values = values
@external_files = External::Mapper.new(values, project: project, sha: sha, user: user, expandset: expandset).process
@content = {}
- rescue External::Mapper::Error => e
+ rescue External::Mapper::Error,
+ OpenSSL::SSL::SSLError => e
raise IncludeError, e.message
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index 03af99ba9a5..c911bfa7ff6 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab
diff --git a/lib/gitlab/ci/pipeline/chain/skip.rb b/lib/gitlab/ci/pipeline/chain/skip.rb
index 7d6e0704d4a..df92e229f12 100644
--- a/lib/gitlab/ci/pipeline/chain/skip.rb
+++ b/lib/gitlab/ci/pipeline/chain/skip.rb
@@ -7,7 +7,7 @@ module Gitlab
class Skip < Chain::Base
include ::Gitlab::Utils::StrongMemoize
- SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
+ SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i.freeze
def perform!
if skipped?
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
new file mode 100644
index 00000000000..5fcc9406cc8
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class NotEquals < Lexeme::Operator
+ PATTERN = /!=/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ @left.evaluate(variables) != @right.evaluate(variables)
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
new file mode 100644
index 00000000000..14544d33e25
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class NotMatches < Lexeme::Operator
+ PATTERN = /\!~/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ text = @left.evaluate(variables)
+ regexp = @right.evaluate(variables)
+
+ regexp.scan(text.to_s).none?
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index f26542361a2..e14edfae51d 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -15,7 +15,9 @@ module Gitlab
Expression::Lexeme::Pattern,
Expression::Lexeme::Null,
Expression::Lexeme::Equals,
- Expression::Lexeme::Matches
+ Expression::Lexeme::Matches,
+ Expression::Lexeme::NotEquals,
+ Expression::Lexeme::NotMatches
].freeze
MAX_TOKENS = 100
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index b03611f756e..ab5ae9caeea 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -8,13 +8,24 @@ module Gitlab
StatementError = Class.new(Expression::ExpressionError)
GRAMMAR = [
+ # presence matchers
%w[variable],
+
+ # positive matchers
%w[variable equals string],
%w[variable equals variable],
%w[variable equals null],
%w[string equals variable],
%w[null equals variable],
- %w[variable matches pattern]
+ %w[variable matches pattern],
+
+ # negative matchers
+ %w[variable notequals string],
+ %w[variable notequals variable],
+ %w[variable notequals null],
+ %w[string notequals variable],
+ %w[null notequals variable],
+ %w[variable notmatches pattern]
].freeze
def initialize(statement, variables = {})
diff --git a/lib/gitlab/ci/status/build/failed_allowed.rb b/lib/gitlab/ci/status/build/failed_allowed.rb
index d7570fdd3e2..8972498a069 100644
--- a/lib/gitlab/ci/status/build/failed_allowed.rb
+++ b/lib/gitlab/ci/status/build/failed_allowed.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def group
- 'failed_with_warnings'
+ 'failed-with-warnings'
end
def status_tooltip
diff --git a/lib/gitlab/ci/status/stage/factory.rb b/lib/gitlab/ci/status/stage/factory.rb
index 58f4642510b..e50b0853725 100644
--- a/lib/gitlab/ci/status/stage/factory.rb
+++ b/lib/gitlab/ci/status/stage/factory.rb
@@ -6,7 +6,8 @@ module Gitlab
module Stage
class Factory < Status::Factory
def self.extended_statuses
- [Status::SuccessWarning]
+ [[Status::SuccessWarning],
+ [Status::Stage::PlayManual]]
end
def self.common_helpers
diff --git a/lib/gitlab/ci/status/stage/play_manual.rb b/lib/gitlab/ci/status/stage/play_manual.rb
new file mode 100644
index 00000000000..ac3fc0912fa
--- /dev/null
+++ b/lib/gitlab/ci/status/stage/play_manual.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Status
+ module Stage
+ class PlayManual < Status::Extended
+ include Gitlab::Routing
+
+ def action_icon
+ 'play'
+ end
+
+ def action_title
+ 'Play all manual'
+ end
+
+ def action_path
+ pipeline = subject.pipeline
+
+ project_stage_play_manual_path(pipeline.project, pipeline, subject.name)
+ end
+
+ def action_method
+ :post
+ end
+
+ def action_button_title
+ _('Play all manual')
+ end
+
+ def self.matches?(stage, user)
+ stage.manual_playable?
+ end
+
+ def has_action?
+ true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/status/success_warning.rb b/lib/gitlab/ci/status/success_warning.rb
index 6632cd9b143..47623ad945f 100644
--- a/lib/gitlab/ci/status/success_warning.rb
+++ b/lib/gitlab/ci/status/success_warning.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def group
- 'success_with_warnings'
+ 'success-with-warnings'
end
def self.matches?(subject, user)
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 1e9591e113b..9d99d04d263 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -394,6 +394,7 @@ rollout 100%:
--set postgresql.postgresDatabase="$POSTGRES_DB" \
--set postgresql.imageTag="$POSTGRES_VERSION" \
--set application.initializeCommand="$DB_INITIALIZE" \
+ $HELM_UPGRADE_EXTRA_ARGS \
--namespace="$KUBE_NAMESPACE" \
"$name" \
chart/
@@ -403,6 +404,7 @@ rollout 100%:
--wait \
--set application.initializeCommand="" \
--set application.migrateCommand="$DB_MIGRATE" \
+ $HELM_UPGRADE_EXTRA_ARGS \
--namespace="$KUBE_NAMESPACE" \
"$name" \
chart/
@@ -432,6 +434,7 @@ rollout 100%:
--set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
--set postgresql.postgresDatabase="$POSTGRES_DB" \
--set application.migrateCommand="$DB_MIGRATE" \
+ $HELM_UPGRADE_EXTRA_ARGS \
--namespace="$KUBE_NAMESPACE" \
"$name" \
chart/
@@ -487,7 +490,7 @@ rollout 100%:
fi
helm init --client-only
- helm repo add gitlab ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io}
+ helm repo add ${AUTO_DEVOPS_CHART_REPOSITORY_NAME:-gitlab} ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} ${AUTO_DEVOPS_CHART_REPOSITORY_USERNAME:+"--username" "$AUTO_DEVOPS_CHART_REPOSITORY_USERNAME"} ${AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD:+"--password" "$AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD"}
if [[ ! -d "$auto_chart" ]]; then
helm fetch ${auto_chart} --untar
fi
diff --git a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
index 02d02250bbf..a9e195370f7 100644
--- a/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Pages/Hexo.gitlab-ci.yml
@@ -1,10 +1,11 @@
# Full project: https://gitlab.com/pages/hexo
-image: node:6.10.0
+image: node:10.15.3
pages:
script:
- - npm install
- - ./node_modules/hexo/bin/hexo generate
+ - npm install hexo-cli -g
+ - test -e package.json && npm install
+ - hexo generate
artifacts:
paths:
- public
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 eef361c19e9..324e39c7747 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -22,7 +22,7 @@ container_scanning:
DOCKER_SERVICE: docker
DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/
# https://hub.docker.com/r/arminc/clair-local-scan/tags
- CLAIR_LOCAL_SCAN_VERSION: v2.0.6
+ CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
allow_failure: true
services:
- docker:stable-dind
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 2a90cc9a06c..fd7fac5dcab 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -30,6 +30,7 @@ dast:
- |
function dast_run() {
docker run \
+ --env DAST_FULL_SCAN_ENABLED \
--env DAST_TARGET_AVAILABILITY_TIMEOUT \
--volume "$PWD:/output" \
--volume /var/run/docker.sock:/var/run/docker.sock \
@@ -46,7 +47,8 @@ dast:
--auth-username $DAST_USERNAME \
--auth-password $DAST_PASSWORD \
--auth-username-field $DAST_USERNAME_FIELD \
- --auth-password-field $DAST_PASSWORD_FIELD
+ --auth-password-field $DAST_PASSWORD_FIELD \
+ --auth-exclude-urls $DAST_AUTH_EXCLUDE_URLS
else
dast_run
fi
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 7f80a6e9285..8dd9775c583 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -20,16 +20,27 @@ dependency_scanning:
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
+ }
- |
docker run \
- --env DS_ANALYZER_IMAGES \
- --env DS_ANALYZER_IMAGE_PREFIX \
- --env DS_ANALYZER_IMAGE_TAG \
- --env DS_DEFAULT_ANALYZERS \
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS \
- --env DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env DS_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env DS_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ DS_ANALYZER_IMAGES \
+ DS_ANALYZER_IMAGE_PREFIX \
+ DS_ANALYZER_IMAGE_TAG \
+ DS_DEFAULT_ANALYZERS \
+ DS_EXCLUDED_PATHS \
+ DEP_SCAN_DISABLE_REMOTE_CHECKS \
+ DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ DS_PULL_ANALYZER_IMAGE_TIMEOUT \
+ DS_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_VERSION" /code
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index b941e89991e..706692e063b 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -20,18 +20,30 @@ 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
+ }
- |
docker run \
- --env SAST_ANALYZER_IMAGES \
- --env SAST_ANALYZER_IMAGE_PREFIX \
- --env SAST_ANALYZER_IMAGE_TAG \
- --env SAST_DEFAULT_ANALYZERS \
- --env SAST_BRAKEMAN_LEVEL \
- --env SAST_GOSEC_LEVEL \
- --env SAST_FLAWFINDER_LEVEL \
- --env SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- --env SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- --env SAST_RUN_ANALYZER_TIMEOUT \
+ $(propagate_env_vars \
+ SAST_ANALYZER_IMAGES \
+ SAST_ANALYZER_IMAGE_PREFIX \
+ SAST_ANALYZER_IMAGE_TAG \
+ SAST_DEFAULT_ANALYZERS \
+ SAST_EXCLUDED_PATHS \
+ SAST_BANDIT_EXCLUDED_PATHS \
+ SAST_BRAKEMAN_LEVEL \
+ SAST_GOSEC_LEVEL \
+ SAST_FLAWFINDER_LEVEL \
+ SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
+ SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
+ SAST_RUN_ANALYZER_TIMEOUT \
+ ) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SAST_VERSION" /app/bin/run /code
diff --git a/lib/gitlab/ci/templates/dotNET-Core.yml b/lib/gitlab/ci/templates/dotNET-Core.yml
new file mode 100644
index 00000000000..558ca3d22e1
--- /dev/null
+++ b/lib/gitlab/ci/templates/dotNET-Core.yml
@@ -0,0 +1,107 @@
+# This is a simple example illustrating how to build and test .NET Core project
+# with GitLab Continuous Integration / Continuous Delivery.
+
+# ### Specify the Docker image
+#
+# Instead of installing .NET Core SDK manually, a docker image is used
+# with already pre-installed .NET Core SDK.
+# The 'latest' tag targets the latest available version of .NET Core SDK image.
+# If preferred, you can explicitly specify version of .NET Core e.g. using '2.2-sdk' tag.
+#
+# See other available tags for .NET Core: https://hub.docker.com/r/microsoft/dotnet
+# Learn more about Docker tags: https://docs.docker.com/glossary/?term=tag
+# and the Docker itself: https://opensource.com/resources/what-docker
+image: microsoft/dotnet:latest
+
+# ### Define variables
+#
+variables:
+ # 1) Name of directory where restore and build objects are stored.
+ OBJECTS_DIRECTORY: 'obj'
+ # 2) Name of directory used for keeping restored dependencies.
+ NUGET_PACKAGES_DIRECTORY: '.nuget'
+ # 3) A relative path to the source code from project repository root.
+ # NOTE: Please edit this path so it matches the structure of your project!
+ SOURCE_CODE_PATH: '*/*/'
+
+# ### Define stage list
+#
+# In this example there are only two stages.
+# Initially, the project will be built and then tested.
+stages:
+ - build
+ - test
+
+# ### Define global cache rule
+#
+# Before building the project, all dependencies (e.g. third-party NuGet packages)
+# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines.
+# Each machine is used only once (for security reasons) and after that it is removed.
+# What that means is that before every job a dependency restore must be performed
+# because restored dependencies are removed along with machines. Fortunately,
+# GitLab provides cache mechanism with the aim of keeping restored dependencies
+# for other jobs. This example shows how to configure cache to pass over restored
+# dependencies for re-use.
+#
+# With global cache rule, cached dependencies will be downloaded before every job
+# and then unpacked to the paths as specified below.
+cache:
+ # Per-stage and per-branch caching.
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+ paths:
+ # Specify three paths that should be cached:
+ #
+ # 1) Main JSON file holding information about package dependency tree, packages versions,
+ # frameworks etc. It also holds information where to the dependencies were restored.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
+ # 2) Other NuGet and MSBuild related files. Also needed.
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
+ # 3) Path to the directory where restored dependencies are kept.
+ - '$NUGET_PACKAGES_DIRECTORY'
+ # 'pull-push' policy means that latest cache will be downloaded (if exists)
+ # before executing the job, and a newer version will be uploaded afterwards.
+ # Such setting saves time when there are no changes in referenced third-party
+ # packages. For example if you run a pipeline with changes in your code,
+ # but with no changes within third-party packages which your project is using,
+ # then project restore will happen in next to no time as all required dependencies
+ # will already be there — unzipped from cache. 'pull-push' policy is a default
+ # cache policy, you do not have to specify it explicitly.
+ policy: pull-push
+
+# ### Restore project dependencies
+#
+# NuGet packages by default are restored to '.nuget/packages' directory
+# in the user's home directory. That directory is out of scope of GitLab caching.
+# To get around this a custom path can be specified using '--packages <PATH>' option
+# for 'dotnet restore' command. In this example a temporary directory is created
+# in the root of project repository, so it's content can be cached.
+#
+# Learn more about GitLab cache: https://docs.gitlab.com/ee/ci/caching/index.html
+before_script:
+ - 'dotnet restore --packages $NUGET_PACKAGES_DIRECTORY'
+
+build:
+ stage: build
+ # ### Build all projects discovered from solution file.
+ #
+ # Note: this will fail if you have any projects in your solution that are not
+ # .NET Core based projects e.g. WCF service, which is based on .NET Framework,
+ # not .NET Core. In such scenario you will need to build every .NET Core based
+ # project by explicitly specifying a relative path to the directory
+ # where it is located e.g. 'dotnet build ./src/ConsoleApp'.
+ # Only one project path can be passed as a parameter to 'dotnet build' command.
+ script:
+ - 'dotnet build --no-restore'
+
+tests:
+ stage: test
+ # ### Run the tests
+ #
+ # You can either run tests for all test projects that are defined in your solution
+ # with 'dotnet test' or run tests only for specific project by specifying
+ # a relative path to the directory where it is located e.g. 'dotnet test ./test/UnitTests'.
+ #
+ # You may want to define separate testing jobs for different types of testing
+ # e.g. integration tests, unit tests etc.
+ script:
+ - 'dotnet test --no-restore'
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index bf5f2a31f0e..dfae260239e 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -209,10 +209,7 @@ module Gitlab
end
def paths
- [
- default_path,
- deprecated_path
- ].compact
+ [default_path]
end
def default_directory
@@ -227,15 +224,6 @@ module Gitlab
File.join(default_directory, "#{job.id}.log")
end
- def deprecated_path
- File.join(
- Settings.gitlab_ci.builds_path,
- job.created_at.utc.strftime("%Y_%m"),
- job.project.ci_id.to_s,
- "#{job.id}.log"
- ) if job.project&.ci_id
- end
-
def trace_artifact
job.job_artifacts_trace
end
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 833aa75adb5..aab10aef398 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -27,13 +27,9 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner
# expects).
#
- # If the `variable_masking` feature is enabled we expose the `masked`
- # attribute, otherwise it's not exposed.
- #
def to_runner_variable
@variable.reject do |hash_key, hash_value|
- (hash_key == :file && hash_value == false) ||
- (hash_key == :masked && !Feature.enabled?(:variable_masking))
+ hash_key == :file && hash_value == false
end
end
diff --git a/lib/gitlab/content_disposition.rb b/lib/gitlab/content_disposition.rb
index 32207514ce5..ff6154a5b26 100644
--- a/lib/gitlab/content_disposition.rb
+++ b/lib/gitlab/content_disposition.rb
@@ -22,13 +22,13 @@ module Gitlab
end
# rubocop:disable Style/VariableInterpolation
- TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/.freeze
def ascii_filename
'filename="' + percent_escape(::I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
end
- RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/.freeze
# rubocop:enable Style/VariableInterpolation
def utf8_filename
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index d347f3c13a4..3ef19d801b7 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -99,15 +99,13 @@ module Gitlab
end
CATEGORY_LABELS = {
- docs: "~Documentation",
+ docs: "~Documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
none: "",
qa: "~QA"
}.freeze
-
- # rubocop:disable Style/RegexpLiteral
CATEGORIES = {
- %r{\Adoc/} => :docs,
- %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
+ %r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
+ %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\A(ee/)?app/(assets|views)/} => :frontend,
%r{\A(ee/)?public/} => :frontend,
@@ -148,10 +146,9 @@ module Gitlab
# Fallbacks in case the above patterns miss anything
%r{\.rb\z} => :backend,
- %r{\.(md|txt)\z} => :docs,
+ %r{\.(md|txt)\z} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\.js\z} => :frontend
}.freeze
- # rubocop:enable Style/RegexpLiteral
end
end
end
diff --git a/lib/gitlab/data_builder/deployment.rb b/lib/gitlab/data_builder/deployment.rb
new file mode 100644
index 00000000000..f11e032ab84
--- /dev/null
+++ b/lib/gitlab/data_builder/deployment.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DataBuilder
+ module Deployment
+ extend self
+
+ def build(deployment)
+ {
+ object_kind: 'deployment',
+ status: deployment.status,
+ deployable_id: deployment.deployable_id,
+ deployable_url: Gitlab::UrlBuilder.build(deployment.deployable),
+ environment: deployment.environment.name,
+ project: deployment.project.hook_attrs,
+ short_sha: deployment.short_sha,
+ user: deployment.user.hook_attrs,
+ user_url: Gitlab::UrlBuilder.build(deployment.user),
+ commit_url: Gitlab::UrlBuilder.build(deployment.commit),
+ commit_title: deployment.commit.title
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index af385d7d4ca..40bda3410e1 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -58,7 +58,10 @@ module Gitlab
# }
#
# rubocop:disable Metrics/ParameterLists
- def build(project, user, oldrev, newrev, ref, commits = [], message = nil, commits_count: nil, push_options: {})
+ def build(
+ project:, user:, ref:, oldrev: nil, newrev: nil,
+ commits: [], commits_count: nil, message: nil, push_options: {})
+
commits = Array(commits)
# Total commits count
@@ -113,7 +116,12 @@ module Gitlab
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
commits = project.repository.commits(project.default_branch.to_s, limit: 3)
- build(project, user, commits.last&.id, commits.first&.id, ref, commits)
+ build(project: project,
+ user: user,
+ oldrev: commits.last&.id,
+ newrev: commits.first&.id,
+ ref: ref,
+ commits: commits)
end
def sample_data
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index 270cfb89488..369c6b87fb4 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -52,6 +52,19 @@ module Gitlab
end
end
+ # Clears multiple cache keys at once.
+ #
+ # raw_keys - An Array of unique cache keys, without namespaces.
+ #
+ # It returns the number of cache keys cleared. Ex.: 42
+ def clear_multiple(raw_keys)
+ return [] if raw_keys.empty?
+
+ keys = raw_keys.map { |id| cache_key_for(id) }
+
+ Redis::Cache.with { |redis| redis.del(keys) }
+ end
+
def cache_key_for(raw_key)
"#{cache_key_prefix}:#{raw_key}"
end
diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb
index 2770469ca9f..9fc2217ad43 100644
--- a/lib/gitlab/file_detector.rb
+++ b/lib/gitlab/file_detector.rb
@@ -16,6 +16,7 @@ module Gitlab
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: %r{\A\.gitlab/issue_templates/[^/]+\.md\z},
merge_request_template: %r{\A\.gitlab/merge_request_templates/[^/]+\.md\z},
+ metrics_dashboard: %r{\A\.gitlab/dashboards/[^/]+\.yml\z},
xcode_config: %r{\A[^/]*\.(xcodeproj|xcworkspace)(/.+)?\z},
# Configuration files
diff --git a/lib/gitlab/git/object_pool.rb b/lib/gitlab/git/object_pool.rb
index 8eb3c28ab70..d0577d7a4ff 100644
--- a/lib/gitlab/git/object_pool.rb
+++ b/lib/gitlab/git/object_pool.rb
@@ -40,6 +40,10 @@ module Gitlab
@repository ||= Gitlab::Git::Repository.new(storage, relative_path, GL_REPOSITORY, gl_project_path)
end
+ def fetch
+ object_pool_service.fetch(source_repository)
+ end
+
private
def object_pool_service
diff --git a/lib/gitlab/git/pre_receive_error.rb b/lib/gitlab/git/pre_receive_error.rb
index b46d4ba0b02..ef9b1bf5224 100644
--- a/lib/gitlab/git/pre_receive_error.rb
+++ b/lib/gitlab/git/pre_receive_error.rb
@@ -14,7 +14,7 @@ module Gitlab
'GL-HOOK-ERR:' # Messages marked as safe by user
].freeze
- SAFE_MESSAGE_REGEX = /^(#{SAFE_MESSAGE_PREFIXES.join('|')})\s*(?<safe_message>.+)/
+ SAFE_MESSAGE_REGEX = /^(#{SAFE_MESSAGE_PREFIXES.join('|')})\s*(?<safe_message>.+)/.freeze
def initialize(message = '')
super(sanitize(message))
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index be9e926728c..fc9bcbdcca2 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -118,6 +118,12 @@ module Gitlab
gitaly_repository_client.exists?
end
+ def create_repository
+ wrapped_gitaly_errors do
+ gitaly_repository_client.create_repository
+ end
+ end
+
# Returns an Array of branch names
# sorted by name ASC
def branch_names
@@ -231,12 +237,12 @@ module Gitlab
end
end
- def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:)
+ def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:, path: nil)
ref ||= root_ref
commit = Gitlab::Git::Commit.find(self, ref)
return {} if commit.nil?
- prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha)
+ prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha, path: path)
{
'ArchivePrefix' => prefix,
@@ -248,13 +254,14 @@ module Gitlab
# This is both the filename of the archive (missing the extension) and the
# name of the top-level member of the archive under which all files go
- def archive_prefix(ref, sha, project_path, append_sha:)
+ def archive_prefix(ref, sha, project_path, append_sha:, path:)
append_sha = (ref != sha) if append_sha.nil?
formatted_ref = ref.tr('/', '-')
prefix_segments = [project_path, formatted_ref]
prefix_segments << sha if append_sha
+ prefix_segments << path.tr('/', '-').gsub(%r{^/|/$}, '') if path
prefix_segments.join('-')
end
@@ -731,18 +738,29 @@ module Gitlab
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
+ reachable_ref =
+ if source_repository == self
+ source_branch_name
+ else
+ # If a tmp ref was created before for a separate repo comparison (forks),
+ # we're able to short-circuit the tmp ref re-creation:
+ # 1. Take the SHA from the source repo
+ # 2. Read that in the current "target" repo
+ # 3. If that SHA is still known (readable), it means GC hasn't
+ # cleaned it up yet, so we can use it instead re-writing the tmp ref.
+ source_commit_id = source_repository.commit(source_branch_name)&.sha
+ commit(source_commit_id)&.sha if source_commit_id
+ end
+
+ return compare(target_branch_name, reachable_ref, straight: straight) if reachable_ref
+
tmp_ref = "refs/tmp/#{SecureRandom.hex}"
return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref)
- Gitlab::Git::Compare.new(
- self,
- target_branch_name,
- tmp_ref,
- straight: straight
- )
+ compare(target_branch_name, tmp_ref, straight: straight)
ensure
- delete_refs(tmp_ref)
+ delete_refs(tmp_ref) if tmp_ref
end
def write_ref(ref_path, ref, old_ref: nil)
@@ -816,7 +834,8 @@ module Gitlab
gitaly_repository_client.create_from_snapshot(url, auth)
end
- def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
+ # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
+ def rebase_deprecated(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
wrapped_gitaly_errors do
gitaly_operation_client.user_rebase(user, rebase_id,
branch: branch,
@@ -826,6 +845,20 @@ module Gitlab
end
end
+ def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:, &block)
+ wrapped_gitaly_errors do
+ gitaly_operation_client.rebase(
+ user,
+ rebase_id,
+ branch: branch,
+ branch_sha: branch_sha,
+ remote_repository: remote_repository,
+ remote_branch: remote_branch,
+ &block
+ )
+ end
+ end
+
def rebase_in_progress?(rebase_id)
wrapped_gitaly_errors do
gitaly_repository_client.rebase_in_progress?(rebase_id)
@@ -889,6 +922,12 @@ module Gitlab
end
end
+ def disconnect_alternates
+ wrapped_gitaly_errors do
+ gitaly_repository_client.disconnect_alternates
+ end
+ end
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path, @gl_repository, @gl_project_path)
end
@@ -998,6 +1037,13 @@ module Gitlab
private
+ def compare(base_ref, head_ref, straight:)
+ Gitlab::Git::Compare.new(self,
+ base_ref,
+ head_ref,
+ straight: straight)
+ end
+
def empty_diff_stats
Gitlab::Git::DiffStatsCollection.new([])
end
diff --git a/lib/gitlab/git/repository_cleaner.rb b/lib/gitlab/git/repository_cleaner.rb
index 2d1d8435cf3..9dd0ddfb44b 100644
--- a/lib/gitlab/git/repository_cleaner.rb
+++ b/lib/gitlab/git/repository_cleaner.rb
@@ -12,9 +12,9 @@ module Gitlab
@repository = repository
end
- def apply_bfg_object_map(io)
+ def apply_bfg_object_map_stream(io, &blk)
wrapped_gitaly_errors do
- gitaly_cleanup_client.apply_bfg_object_map(io)
+ gitaly_cleanup_client.apply_bfg_object_map_stream(io, &blk)
end
end
diff --git a/lib/gitlab/git/rugged_impl/commit.rb b/lib/gitlab/git/rugged_impl/commit.rb
index f6777dfa0c3..bce4fa14fb4 100644
--- a/lib/gitlab/git/rugged_impl/commit.rb
+++ b/lib/gitlab/git/rugged_impl/commit.rb
@@ -21,6 +21,17 @@ module Gitlab
nil
end
+ # This needs to return an array of Gitlab::Git:Commit objects
+ # instead of Rugged::Commit objects to ensure upstream models
+ # operate on a consistent interface. Unlike
+ # Gitlab::Git::Commit.find, Gitlab::Git::Commit.batch_by_oid
+ # doesn't attempt to decorate the result.
+ def rugged_batch_by_oid(repo, oids)
+ oids.map { |oid| rugged_find(repo, oid) }
+ .compact
+ .map { |commit| decorate(repo, commit) }
+ end
+
override :find_commit
def find_commit(repo, commit_id)
if Feature.enabled?(:rugged_find_commit)
@@ -29,6 +40,15 @@ module Gitlab
super
end
end
+
+ override :batch_by_oid
+ def batch_by_oid(repo, oids)
+ if Feature.enabled?(:rugged_list_commits_by_oid)
+ rugged_batch_by_oid(repo, oids)
+ else
+ super
+ end
+ end
end
extend ::Gitlab::Utils::Override
diff --git a/lib/gitlab/git/rugged_impl/repository.rb b/lib/gitlab/git/rugged_impl/repository.rb
index c0a91f59ab9..e91b0ddcd31 100644
--- a/lib/gitlab/git/rugged_impl/repository.rb
+++ b/lib/gitlab/git/rugged_impl/repository.rb
@@ -12,7 +12,7 @@ module Gitlab
module Repository
extend ::Gitlab::Utils::Override
- FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry).freeze
+ FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry rugged_list_commits_by_oid).freeze
def alternate_object_directories
relative_object_directories.map { |d| File.join(path, d) }
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index a0dd4a24363..c1bcd8e934a 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -86,9 +86,14 @@ module Gitlab
end
end
- def pages(limit: 0, sort: nil, direction_desc: false)
+ def list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
wrapped_gitaly_errors do
- gitaly_get_all_pages(limit: limit, sort: sort, direction_desc: direction_desc)
+ gitaly_list_pages(
+ limit: limit,
+ sort: sort,
+ direction_desc: direction_desc,
+ load_content: load_content
+ )
end
end
@@ -168,10 +173,17 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
- def gitaly_get_all_pages(limit: 0, sort: nil, direction_desc: false)
- gitaly_wiki_client.get_all_pages(
- limit: limit, sort: sort, direction_desc: direction_desc
- ).map do |wiki_page, version|
+ def gitaly_list_pages(limit: 0, sort: nil, direction_desc: false, load_content: false)
+ params = { limit: limit, sort: sort, direction_desc: direction_desc }
+
+ gitaly_pages =
+ if load_content
+ gitaly_wiki_client.load_all_pages(params)
+ else
+ gitaly_wiki_client.list_all_pages(params)
+ end
+
+ gitaly_pages.map do |wiki_page, version|
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index cb80ed64eff..4b626509008 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -85,7 +85,7 @@ module Gitlab
check_push_access!
end
- ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd))
+ success_result(cmd)
end
def guest_can_download_code?
@@ -365,6 +365,10 @@ module Gitlab
protected
+ def success_result(cmd)
+ ::Gitlab::GitAccessResult::Success.new(console_messages: check_for_console_messages(cmd))
+ end
+
def changes_list
@changes_list ||= Gitlab::ChangesList.new(changes == ANY ? [] : changes)
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 35565b68388..05e06eec012 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -26,11 +26,15 @@ module Gitlab
end
end
- PEM_REGEX = /\-+BEGIN CERTIFICATE\-+.+?\-+END CERTIFICATE\-+/m
+ PEM_REGEX = /\-+BEGIN CERTIFICATE\-+.+?\-+END CERTIFICATE\-+/m.freeze
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'
MAXIMUM_GITALY_CALLS = 30
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
+ SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze
+ # Server feature flags should use '_' to separate words.
+ SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE, 'delta_islands'].freeze
+
MUTEX = Mutex.new
define_histogram :gitaly_controller_action_duration_seconds do
@@ -52,9 +56,9 @@ module Gitlab
end
def self.interceptors
- return [] unless Gitlab::Tracing.enabled?
+ return [] unless Labkit::Tracing.enabled?
- [Gitlab::Tracing::GRPCInterceptor.instance]
+ [Labkit::Tracing::GRPCInterceptor.instance]
end
private_class_method :interceptors
@@ -165,7 +169,10 @@ module Gitlab
current_transaction_labels.merge(gitaly_service: service.to_s, rpc: rpc.to_s),
duration)
- add_call_details(feature: "#{service}##{rpc}", duration: duration, request: request_hash, rpc: rpc)
+ if peek_enabled?
+ add_call_details(feature: "#{service}##{rpc}", duration: duration, request: request_hash, rpc: rpc,
+ backtrace: Gitlab::Profiler.clean_backtrace(caller))
+ end
end
def self.query_time
@@ -215,7 +222,8 @@ module Gitlab
feature = feature_stack && feature_stack[0]
metadata['call_site'] = feature.to_s if feature
metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage
- metadata['x-gitlab-correlation-id'] = Gitlab::CorrelationId.current_id if Gitlab::CorrelationId.current_id
+ metadata['x-gitlab-correlation-id'] = Labkit::Correlation::CorrelationId.current_id if Labkit::Correlation::CorrelationId.current_id
+ metadata['gitaly-session-id'] = session_id if feature_enabled?(SERVER_FEATURE_CATFILE_CACHE)
metadata.merge!(server_feature_flags)
@@ -232,7 +240,9 @@ module Gitlab
result
end
- SERVER_FEATURE_FLAGS = %w[].freeze
+ def self.session_id
+ Gitlab::SafeRequestStore[:gitaly_session_id] ||= SecureRandom.uuid
+ end
def self.server_feature_flags
SERVER_FEATURE_FLAGS.map do |f|
@@ -350,15 +360,17 @@ module Gitlab
Gitlab::SafeRequestStore["gitaly_call_permitted"] = 0
end
- def self.add_call_details(details)
- return unless Gitlab::SafeRequestStore[:peek_enabled]
+ def self.peek_enabled?
+ Gitlab::SafeRequestStore[:peek_enabled]
+ end
+ def self.add_call_details(details)
Gitlab::SafeRequestStore['gitaly_call_details'] ||= []
Gitlab::SafeRequestStore['gitaly_call_details'] << details
end
def self.list_call_details
- return [] unless Gitlab::SafeRequestStore[:peek_enabled]
+ return [] unless peek_enabled?
Gitlab::SafeRequestStore['gitaly_call_details'] || []
end
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 6b8e58e6199..8ccefb00d20 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -55,13 +55,13 @@ module Gitlab
def get_blobs(revision_paths, limit = -1)
return [] if revision_paths.empty?
- revision_paths.map! do |rev, path|
+ request_revision_paths = revision_paths.map do |rev, path|
Gitaly::GetBlobsRequest::RevisionPath.new(revision: rev, path: encode_binary(path))
end
request = Gitaly::GetBlobsRequest.new(
repository: @gitaly_repo,
- revision_paths: revision_paths,
+ revision_paths: request_revision_paths,
limit: limit
)
diff --git a/lib/gitlab/gitaly_client/cleanup_service.rb b/lib/gitlab/gitaly_client/cleanup_service.rb
index 3e8d6a773ca..a56bc35f6d7 100644
--- a/lib/gitlab/gitaly_client/cleanup_service.rb
+++ b/lib/gitlab/gitaly_client/cleanup_service.rb
@@ -12,25 +12,32 @@ module Gitlab
@storage = repository.storage
end
- def apply_bfg_object_map(io)
- first_request = Gitaly::ApplyBfgObjectMapRequest.new(repository: gitaly_repo)
+ def apply_bfg_object_map_stream(io, &blk)
+ responses = GitalyClient.call(
+ storage,
+ :cleanup_service,
+ :apply_bfg_object_map_stream,
+ build_object_map_enum(io),
+ timeout: GitalyClient.no_timeout
+ )
+
+ responses.each(&blk)
+ end
+
+ private
- enum = Enumerator.new do |y|
- y.yield first_request
+ def build_object_map_enum(io)
+ Enumerator.new do |y|
+ # First request. For simplicity, doesn't include any object map data
+ y << Gitaly::ApplyBfgObjectMapStreamRequest.new(repository: gitaly_repo)
+ # Now stream the BFG object map file to gitaly in chunks
while data = io.read(RepositoryService::MAX_MSG_SIZE)
- y.yield Gitaly::ApplyBfgObjectMapRequest.new(object_map: data)
+ y << Gitaly::ApplyBfgObjectMapStreamRequest.new(object_map: data)
+
break if io&.eof?
end
end
-
- GitalyClient.call(
- storage,
- :cleanup_service,
- :apply_bfg_object_map,
- enum,
- timeout: GitalyClient.no_timeout
- )
end
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 0d5debfcd01..2896b7e1ce0 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -174,7 +174,7 @@ module Gitlab
response.each_with_object({}) do |gitaly_response, hsh|
gitaly_response.commits.each do |commit_for_tree|
- hsh[commit_for_tree.path] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit)
+ hsh[commit_for_tree.path_bytes] = Gitlab::Git::Commit.new(@repository, commit_for_tree.commit)
end
end
end
diff --git a/lib/gitlab/gitaly_client/object_pool_service.rb b/lib/gitlab/gitaly_client/object_pool_service.rb
index ce1fb4d68ae..d7fac26bc13 100644
--- a/lib/gitlab/gitaly_client/object_pool_service.rb
+++ b/lib/gitlab/gitaly_client/object_pool_service.rb
@@ -33,6 +33,15 @@ module Gitlab
GitalyClient.call(storage, :object_pool_service, :link_repository_to_object_pool,
request, timeout: GitalyClient.fast_timeout)
end
+
+ def fetch(repository)
+ request = Gitaly::FetchIntoObjectPoolRequest.new(
+ object_pool: object_pool,
+ origin: repository.gitaly_repository
+ )
+
+ GitalyClient.call(storage, :object_pool_service, :fetch_into_object_pool, request)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index b0f328ce3d4..e4a59ee3f9b 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -197,6 +197,7 @@ module Gitlab
start_repository: start_repository)
end
+ # DEPRECATED: https://gitlab.com/gitlab-org/gitaly/issues/1628
def user_rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
request = Gitaly::UserRebaseRequest.new(
repository: @gitaly_repo,
@@ -225,6 +226,49 @@ module Gitlab
end
end
+ def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
+ request_enum = QueueEnumerator.new
+ rebase_sha = nil
+
+ response_enum = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_rebase_confirmable,
+ request_enum.each,
+ remote_storage: remote_repository.storage
+ )
+
+ # First request
+ request_enum.push(
+ Gitaly::UserRebaseConfirmableRequest.new(
+ header: Gitaly::UserRebaseConfirmableRequest::Header.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ rebase_id: rebase_id.to_s,
+ branch: encode_binary(branch),
+ branch_sha: branch_sha,
+ remote_repository: remote_repository.gitaly_repository,
+ remote_branch: encode_binary(remote_branch)
+ )
+ )
+ )
+
+ perform_next_gitaly_rebase_request(response_enum) do |response|
+ rebase_sha = response.rebase_sha
+ end
+
+ yield rebase_sha
+
+ # Second request confirms with gitaly to finalize the rebase
+ request_enum.push(Gitaly::UserRebaseConfirmableRequest.new(apply: true))
+
+ perform_next_gitaly_rebase_request(response_enum)
+
+ rebase_sha
+ ensure
+ request_enum.close
+ end
+
def user_squash(user, squash_id, branch, start_sha, end_sha, author, message)
request = Gitaly::UserSquashRequest.new(
repository: @gitaly_repo,
@@ -346,6 +390,20 @@ module Gitlab
private
+ def perform_next_gitaly_rebase_request(response_enum)
+ response = response_enum.next
+
+ if response.pre_receive_error.present?
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
+ elsif response.git_error.present?
+ raise Gitlab::Git::Repository::GitError, response.git_error
+ end
+
+ yield response if block_given?
+
+ response
+ end
+
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
request_class = "Gitaly::User#{rpc.to_s.camelcase}Request".constantize
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 6f6698607d9..b7d509dfa48 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -239,6 +239,12 @@ module Gitlab
messages
end
+ def pack_refs
+ request = Gitaly::PackRefsRequest.new(repository: @gitaly_repo)
+
+ GitalyClient.call(@storage, :ref_service, :pack_refs, request)
+ end
+
private
def consume_refs_response(response)
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 74aae4a8e97..68b17e86608 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -331,6 +331,14 @@ module Gitlab
search_results_from_response(response)
end
+ def disconnect_alternates
+ request = Gitaly::DisconnectGitAlternatesRequest.new(
+ repository: @gitaly_repo
+ )
+
+ GitalyClient.call(@storage, :object_pool_service, :disconnect_git_alternates, request)
+ end
+
private
def search_results_from_response(gitaly_response)
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index e036cdcd800..ce9faad825c 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -87,7 +87,27 @@ module Gitlab
wiki_page_from_iterator(response)
end
- def get_all_pages(limit: 0, sort: nil, direction_desc: false)
+ def list_all_pages(limit: 0, sort: nil, direction_desc: false)
+ sort_value = Gitaly::WikiListPagesRequest::SortBy.resolve(sort.to_s.upcase.to_sym)
+
+ params = { repository: @gitaly_repo, limit: limit, direction_desc: direction_desc }
+ params[:sort] = sort_value if sort_value
+
+ request = Gitaly::WikiListPagesRequest.new(params)
+ stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_list_pages, request, timeout: GitalyClient.medium_timeout)
+ stream.each_with_object([]) do |message, pages|
+ page = message.page
+
+ next unless page
+
+ wiki_page = GitalyClient::WikiPage.new(page.to_h)
+ version = new_wiki_page_version(page.version)
+
+ pages << [wiki_page, version]
+ end
+ end
+
+ def load_all_pages(limit: 0, sort: nil, direction_desc: false)
sort_value = Gitaly::WikiGetAllPagesRequest::SortBy.resolve(sort.to_s.upcase.to_sym)
params = { repository: @gitaly_repo, limit: limit, direction_desc: direction_desc }
@@ -95,6 +115,7 @@ module Gitlab
request = Gitaly::WikiGetAllPagesRequest.new(params)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
+
pages = []
loop do
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 656d46b6a7d..a468f6d8821 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -53,6 +53,7 @@ module Gitlab
description: description,
milestone_id: milestone_finder.id_for(issue),
state: issue.state,
+ state_id: ::Issue.available_states[issue.state],
created_at: issue.created_at,
updated_at: issue.updated_at
}
diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb
index 1b293ddc7c7..377e873d24d 100644
--- a/lib/gitlab/github_import/importer/pull_request_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -55,6 +55,7 @@ module Gitlab
source_branch: pull_request.formatted_source_branch,
target_branch: pull_request.target_branch,
state: pull_request.state,
+ state_id: ::MergeRequest.available_states[pull_request.state],
milestone_id: milestone_finder.id_for(pull_request),
author_id: author_id,
assignee_id: user_finder.assignee_id_for(pull_request),
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
index be1334ca98a..d336b1ba797 100644
--- a/lib/gitlab/github_import/representation/diff_note.rb
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -13,7 +13,7 @@ module Gitlab
:diff_hunk, :author, :note, :created_at, :updated_at,
:github_id
- NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i
+ NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i.freeze
# Builds a diff note from a GitHub API response.
#
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
index 070e3b2db8d..5b98ce7d5ed 100644
--- a/lib/gitlab/github_import/representation/note.rb
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -12,7 +12,7 @@ module Gitlab
expose_attribute :noteable_id, :noteable_type, :author, :note,
:created_at, :updated_at, :github_id
- NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i
+ NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i.freeze
# Builds a note from a GitHub API response.
#
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index a56ca1e39e7..04dabe423e8 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
+ include Singleton
+
PROJECT = RepoType.new(
name: :project,
access_checker_class: Gitlab::GitAccess,
@@ -19,7 +21,7 @@ module Gitlab
}.freeze
def self.types
- TYPES
+ instance.types
end
def self.parse(gl_repository)
@@ -39,5 +41,11 @@ module Gitlab
def self.default_type
PROJECT
end
+
+ def types
+ TYPES
+ end
+
+ private_class_method :instance
end
end
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 7abe6c29a25..19915980d7f 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Gitlab
- module GlRepository
+ class GlRepository
class RepoType
attr_reader :name,
:access_checker_class,
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index e00309e7946..582c3065189 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -15,7 +15,12 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled
+
+ if Gitlab::CurrentSettings.clientside_sentry_enabled
+ gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn
+ gon.sentry_environment = Gitlab.config.sentry.environment
+ end
+
gon.gitlab_url = Gitlab.config.gitlab.url
gon.revision = Gitlab.revision
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
diff --git a/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
index fa4c5d86d44..bbe4b24c7d4 100644
--- a/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/correlation_id_logger.rb
@@ -6,7 +6,7 @@ module Gitlab
module Loggers
class CorrelationIdLogger < ::GrapeLogging::Loggers::Base
def parameters(_, _)
- { Gitlab::CorrelationId::LOG_KEY => Gitlab::CorrelationId.current_id }
+ { Labkit::Correlation::CorrelationId::LOG_KEY => Labkit::Correlation::CorrelationId.current_id }
end
end
end
diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb
index f62813db82c..f8d0208e275 100644
--- a/lib/gitlab/graphql/authorize.rb
+++ b/lib/gitlab/graphql/authorize.rb
@@ -8,7 +8,7 @@ module Gitlab
extend ActiveSupport::Concern
def self.use(schema_definition)
- schema_definition.instrument(:field, Instrumentation.new)
+ schema_definition.instrument(:field, Instrumentation.new, after_built_ins: true)
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
index 8deff79fc84..619ce100421 100644
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -15,15 +15,10 @@ module Gitlab
def authorized_resolve
proc do |parent_typed_object, args, ctx|
- resolved_obj = @old_resolve_proc.call(parent_typed_object, args, ctx)
- authorizing_obj = authorize_against(parent_typed_object)
- checker = build_checker(ctx[:current_user], authorizing_obj)
-
- if resolved_obj.respond_to?(:then)
- resolved_obj.then(&checker)
- else
- checker.call(resolved_obj)
- end
+ resolved_type = @old_resolve_proc.call(parent_typed_object, args, ctx)
+ authorizing_object = authorize_against(parent_typed_object, resolved_type)
+
+ filter_allowed(ctx[:current_user], resolved_type, authorizing_object)
end
end
@@ -38,7 +33,7 @@ module Gitlab
type = @field.type
# When the return type of @field is a collection, find the singular type
- if type.get_field('edges')
+ if @field.connection?
type = node_type_for_relay_connection(type)
elsif type.list?
type = node_type_for_basic_connection(type)
@@ -52,43 +47,60 @@ module Gitlab
Array.wrap(@field.metadata[:authorize])
end
- # If it's a built-in/scalar type, authorize using its parent object.
- # nil means authorize using the resolved object
- def authorize_against(parent_typed_object)
- parent_typed_object.object if built_in_type? && parent_typed_object.respond_to?(:object)
+ def authorize_against(parent_typed_object, resolved_type)
+ if scalar_type?
+ # The field is a built-in/scalar type, or a list of scalars
+ # authorize using the parent's object
+ parent_typed_object.object
+ elsif resolved_type.respond_to?(:object)
+ # The field is a type representing a single object, we'll authorize
+ # against the object directly
+ resolved_type.object
+ elsif @field.connection? || resolved_type.is_a?(Array)
+ # The field is a connection or a list of non-built-in types, we'll
+ # authorize each element when rendering
+ nil
+ else
+ # Resolved type is a single object that might not be loaded yet by
+ # the batchloader, we'll authorize that
+ resolved_type
+ end
end
- def build_checker(current_user, authorizing_obj)
- lambda do |resolved_obj|
- # Load the elements if they were not loaded by BatchLoader yet
- resolved_obj = resolved_obj.sync if resolved_obj.respond_to?(:sync)
-
- check = lambda do |object|
- authorizations.all? do |ability|
- Ability.allowed?(current_user, ability, authorizing_obj || object)
- end
+ def filter_allowed(current_user, resolved_type, authorizing_object)
+ if authorizing_object
+ # Authorizing fields representing scalars, or a simple field with an object
+ resolved_type if allowed_access?(current_user, authorizing_object)
+ elsif @field.connection?
+ # A connection with pagination, modify the visible nodes in on the
+ # connection type in place
+ resolved_type.edge_nodes.to_a.keep_if { |node| allowed_access?(current_user, node) }
+ resolved_type
+ elsif resolved_type.is_a? Array
+ # A simple list of rendered types each object being an object to authorize
+ resolved_type.select do |single_object_type|
+ allowed_access?(current_user, single_object_type.object)
end
+ elsif resolved_type.nil?
+ # We're not rendering anything, for example when a record was not found
+ # no need to do anything
+ else
+ raise "Can't authorize #{@field}"
+ end
+ end
- case resolved_obj
- when Array, ActiveRecord::Relation
- resolved_obj.select(&check)
- else
- resolved_obj if check.call(resolved_obj)
- end
+ def allowed_access?(current_user, object)
+ object = object.sync if object.respond_to?(:sync)
+
+ authorizations.all? do |ability|
+ Ability.allowed?(current_user, ability, object)
end
end
# Returns the singular type for relay connections.
# This will be the type class of edges.node
def node_type_for_relay_connection(type)
- type = type.get_field('edges').type.unwrap.get_field('node')&.type
-
- if type.nil?
- raise Gitlab::Graphql::Errors::ConnectionDefinitionError,
- 'Connection Type must conform to the Relay Cursor Connections Specification'
- end
-
- type
+ type.unwrap.get_field('edges').type.unwrap.get_field('node').type
end
# Returns the singular type for basic connections, for example `[Types::ProjectType]`
@@ -96,8 +108,8 @@ module Gitlab
type.unwrap
end
- def built_in_type?
- GraphQL::Schema::BUILT_IN_TYPES.has_value?(node_type_for_basic_connection(@field.type))
+ def scalar_type?
+ node_type_for_basic_connection(@field.type).kind.scalar?
end
end
end
diff --git a/lib/gitlab/graphql/connections/keyset_connection.rb b/lib/gitlab/graphql/connections/keyset_connection.rb
index 851054c0393..715963a44c1 100644
--- a/lib/gitlab/graphql/connections/keyset_connection.rb
+++ b/lib/gitlab/graphql/connections/keyset_connection.rb
@@ -22,8 +22,17 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable 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
@@ -31,12 +40,9 @@ module Gitlab
if last
sliced_nodes.last(limit_value)
else
- sliced_nodes.limit(limit_value)
+ sliced_nodes.limit(limit_value) # rubocop: disable CodeReuse/ActiveRecord
end
end
- # rubocop: enable CodeReuse/ActiveRecord
-
- private
def before_slice
if sort_direction == :asc
diff --git a/lib/gitlab/graphql/errors.rb b/lib/gitlab/graphql/errors.rb
index bcbba72e017..fe74549e322 100644
--- a/lib/gitlab/graphql/errors.rb
+++ b/lib/gitlab/graphql/errors.rb
@@ -6,7 +6,6 @@ module Gitlab
BaseError = Class.new(GraphQL::ExecutionError)
ArgumentError = Class.new(BaseError)
ResourceNotAvailable = Class.new(BaseError)
- ConnectionDefinitionError = Class.new(BaseError)
end
end
end
diff --git a/lib/gitlab/graphql/generic_tracing.rb b/lib/gitlab/graphql/generic_tracing.rb
new file mode 100644
index 00000000000..936b22d5afa
--- /dev/null
+++ b/lib/gitlab/graphql/generic_tracing.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# This class is used as a hook to observe graphql runtime events. From this
+# hook both gitlab metrics and opentracking measurements are generated
+
+module Gitlab
+ module Graphql
+ class GenericTracing < GraphQL::Tracing::PlatformTracing
+ self.platform_keys = {
+ 'lex' => 'graphql.lex',
+ 'parse' => 'graphql.parse',
+ 'validate' => 'graphql.validate',
+ 'analyze_query' => 'graphql.analyze',
+ 'analyze_multiplex' => 'graphql.analyze',
+ 'execute_multiplex' => 'graphql.execute',
+ 'execute_query' => 'graphql.execute',
+ 'execute_query_lazy' => 'graphql.execute',
+ 'execute_field' => 'graphql.execute',
+ 'execute_field_lazy' => 'graphql.execute'
+ }
+
+ def platform_field_key(type, field)
+ "#{type.name}.#{field.name}"
+ end
+
+ def platform_trace(platform_key, key, data, &block)
+ tags = { platform_key: platform_key, key: key }
+ start = Gitlab::Metrics::System.monotonic_time
+
+ with_labkit_tracing(tags, &block)
+ ensure
+ duration = Gitlab::Metrics::System.monotonic_time - start
+
+ graphql_duration_seconds.observe(tags, duration)
+ end
+
+ private
+
+ def with_labkit_tracing(tags, &block)
+ return yield unless Labkit::Tracing.enabled?
+
+ name = "#{tags[:platform_key]}.#{tags[:key]}"
+ span_tags = {
+ 'component' => 'web',
+ 'span.kind' => 'server'
+ }.merge(tags.stringify_keys)
+
+ Labkit::Tracing.with_tracing(operation_name: name, tags: span_tags, &block)
+ end
+
+ def graphql_duration_seconds
+ @graphql_duration_seconds ||= Gitlab::Metrics.histogram(
+ :graphql_duration_seconds,
+ 'GraphQL execution time'
+ )
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/graphql/tracing.rb b/lib/gitlab/graphql/tracing.rb
deleted file mode 100644
index 6b505e4262b..00000000000
--- a/lib/gitlab/graphql/tracing.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Graphql
- class Tracing < GraphQL::Tracing::PlatformTracing
- self.platform_keys = {
- 'lex' => 'graphql.lex',
- 'parse' => 'graphql.parse',
- 'validate' => 'graphql.validate',
- 'analyze_query' => 'graphql.analyze',
- 'analyze_multiplex' => 'graphql.analyze',
- 'execute_multiplex' => 'graphql.execute',
- 'execute_query' => 'graphql.execute',
- 'execute_query_lazy' => 'graphql.execute',
- 'execute_field' => 'graphql.execute',
- 'execute_field_lazy' => 'graphql.execute'
- }
-
- def platform_field_key(type, field)
- "#{type.name}.#{field.name}"
- end
-
- def platform_trace(platform_key, key, data, &block)
- start = Gitlab::Metrics::System.monotonic_time
-
- yield
- ensure
- duration = Gitlab::Metrics::System.monotonic_time - start
-
- graphql_duration_seconds.observe({ platform_key: platform_key, key: key }, duration)
- end
-
- private
-
- def graphql_duration_seconds
- @graphql_duration_seconds ||= Gitlab::Metrics.histogram(
- :graphql_duration_seconds,
- 'GraphQL execution time'
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 7255293b194..334642f252e 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -2,6 +2,8 @@
module Gitlab
class GroupSearchResults < SearchResults
+ attr_reader :group
+
def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
@@ -26,5 +28,9 @@ module Gitlab
.where(id: groups.select('members.user_id'))
end
# rubocop:enable CodeReuse/ActiveRecord
+
+ def issuable_params
+ super.merge(group_id: group.id)
+ end
end
end
diff --git a/lib/gitlab/health_checks/metric.rb b/lib/gitlab/health_checks/metric.rb
index 62a5216d159..184083de2bc 100644
--- a/lib/gitlab/health_checks/metric.rb
+++ b/lib/gitlab/health_checks/metric.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab::HealthChecks
diff --git a/lib/gitlab/health_checks/result.rb b/lib/gitlab/health_checks/result.rb
index d32a6980eb8..4586b1d94a7 100644
--- a/lib/gitlab/health_checks/result.rb
+++ b/lib/gitlab/health_checks/result.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab::HealthChecks
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index fcf6a25ab00..acb7f225b17 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -30,10 +30,7 @@ module Gitlab
def handle_response_error(response)
unless response.success?
- error_code = response.dig('Error', 'Code') || response.code
- error_message = response.dig('Error', 'Message') || response.message
-
- raise StrategyError.new("Error uploading the project. Code #{error_code}: #{error_message}")
+ raise StrategyError.new("Error uploading the project. Code #{response.code}: #{response.message}")
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index ce268793128..c6d4fda4af5 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -75,6 +75,7 @@ project_tree:
- :project_badges
- :ci_cd_settings
- :error_tracking_setting
+ - :metrics_setting
# Only include the following attributes for the models specified.
included_attributes:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 61a1aa6da5a..e1e70a008d9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -25,7 +25,8 @@ module Gitlab
metrics: 'MergeRequest::Metrics',
ci_cd_settings: 'ProjectCiCdSetting',
error_tracking_setting: 'ErrorTracking::ProjectErrorTrackingSetting',
- links: 'Releases::Link' }.freeze
+ links: 'Releases::Link',
+ metrics_setting: 'ProjectMetricsSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id merged_by_id latest_closed_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
diff --git a/lib/gitlab/json_logger.rb b/lib/gitlab/json_logger.rb
index a5a5759cc89..ab34fb03158 100644
--- a/lib/gitlab/json_logger.rb
+++ b/lib/gitlab/json_logger.rb
@@ -10,7 +10,7 @@ module Gitlab
data = {}
data[:severity] = severity
data[:time] = timestamp.utc.iso8601(3)
- data[Gitlab::CorrelationId::LOG_KEY] = Gitlab::CorrelationId.current_id
+ data[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
case message
when String
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index 7dfd9ed4f35..ff1dadf9247 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -22,6 +22,13 @@ module Gitlab
alias_method :update, :install
+ def uninstall(command)
+ namespace.ensure_exists!
+
+ delete_pod!(command.pod_name)
+ kubeclient.create_pod(command.pod_resource)
+ end
+
##
# Returns Pod phase
#
diff --git a/lib/gitlab/kubernetes/helm/delete_command.rb b/lib/gitlab/kubernetes/helm/delete_command.rb
new file mode 100644
index 00000000000..876994d2678
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/delete_command.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module Helm
+ class DeleteCommand
+ include BaseCommand
+ include ClientCommand
+
+ attr_accessor :name, :files
+
+ def initialize(name:, rbac:, files:)
+ @name = name
+ @files = files
+ @rbac = rbac
+ end
+
+ def generate_script
+ super + [
+ init_command,
+ wait_for_tiller_command,
+ delete_command
+ ].compact.join("\n")
+ end
+
+ def pod_name
+ "uninstall-#{name}"
+ end
+
+ def rbac?
+ @rbac
+ end
+
+ private
+
+ def delete_command
+ command = ['helm', 'delete', '--purge', name] + optional_tls_flags
+
+ command.shelljoin
+ end
+
+ def optional_tls_flags
+ return [] unless files.key?(:'ca.pem')
+
+ [
+ '--tls',
+ '--tls-ca-cert', "#{files_dir}/ca.pem",
+ '--tls-cert', "#{files_dir}/cert.pem",
+ '--tls-key', "#{files_dir}/key.pem"
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/base_service.rb b/lib/gitlab/metrics/dashboard/base_service.rb
new file mode 100644
index 00000000000..94aabd0466c
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/base_service.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+module Gitlab
+ module Metrics
+ module Dashboard
+ class BaseService < ::BaseService
+ DASHBOARD_LAYOUT_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ def get_dashboard
+ return error("#{dashboard_path} could not be found.", :not_found) unless path_available?
+
+ success(dashboard: process_dashboard)
+ rescue DASHBOARD_LAYOUT_ERROR => e
+ error(e.message, :unprocessable_entity)
+ end
+
+ # Summary of all known dashboards for the service.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def all_dashboard_paths(_project)
+ raise NotImplementedError
+ end
+
+ private
+
+ # Returns a new dashboard Hash, supplemented with DB info
+ def process_dashboard
+ Gitlab::Metrics::Dashboard::Processor
+ .new(project, params[:environment], raw_dashboard)
+ .process(insert_project_metrics: insert_project_metrics?)
+ end
+
+ # @return [String] Relative filepath of the dashboard yml
+ def dashboard_path
+ params[:dashboard_path]
+ end
+
+ # Returns an un-processed dashboard from the cache.
+ def raw_dashboard
+ Rails.cache.fetch(cache_key) { get_raw_dashboard }
+ end
+
+ # @return [Hash] an unmodified dashboard
+ def get_raw_dashboard
+ raise NotImplementedError
+ end
+
+ # @return [String]
+ def cache_key
+ raise NotImplementedError
+ end
+
+ # Determines whether custom metrics should be included
+ # in the processed output.
+ def insert_project_metrics?
+ false
+ end
+
+ # Checks if dashboard path exists or should be rejected
+ # as a result of file-changes to the project repository.
+ # @return [Boolean]
+ def path_available?
+ available_paths = Gitlab::Metrics::Dashboard::Finder.find_all_paths(project)
+
+ available_paths.any? do |path_params|
+ path_params[:path] == dashboard_path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/finder.rb b/lib/gitlab/metrics/dashboard/finder.rb
new file mode 100644
index 00000000000..4a41590f000
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/finder.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# Returns DB-supplmented dashboard info for determining
+# the layout of UI. Intended entry-point for the Metrics::Dashboard
+# module.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Finder
+ class << self
+ # Returns a formatted dashboard packed with DB info.
+ # @return [Hash]
+ def find(project, user, environment, dashboard_path = nil)
+ service = system_dashboard?(dashboard_path) ? system_service : project_service
+
+ service
+ .new(project, user, environment: environment, dashboard_path: dashboard_path)
+ .get_dashboard
+ end
+
+ # Summary of all known dashboards.
+ # @return [Array<Hash>] ex) [{ path: String, default: Boolean }]
+ def find_all_paths(project)
+ project.repository.metrics_dashboard_paths
+ end
+
+ # Summary of all known dashboards. Used to populate repo cache.
+ # Prefer #find_all_paths.
+ def find_all_paths_from_source(project)
+ system_service.all_dashboard_paths(project)
+ .+ project_service.all_dashboard_paths(project)
+ end
+
+ private
+
+ def system_service
+ Gitlab::Metrics::Dashboard::SystemDashboardService
+ end
+
+ def project_service
+ Gitlab::Metrics::Dashboard::ProjectDashboardService
+ end
+
+ def system_dashboard?(filepath)
+ !filepath || system_service.system_dashboard?(filepath)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
new file mode 100644
index 00000000000..dd986020693
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ # Responsible for processesing a dashboard hash, inserting
+ # relevant DB records & sorting for proper rendering in
+ # the UI. These includes shared metric info, custom metrics
+ # info, and alerts (only in EE).
+ class Processor
+ SYSTEM_SEQUENCE = [
+ Stages::CommonMetricsInserter,
+ Stages::ProjectMetricsInserter,
+ Stages::Sorter
+ ].freeze
+
+ PROJECT_SEQUENCE = [
+ Stages::CommonMetricsInserter,
+ Stages::Sorter
+ ].freeze
+
+ def initialize(project, environment, dashboard)
+ @project = project
+ @environment = environment
+ @dashboard = dashboard
+ end
+
+ # Returns a new dashboard hash with the results of
+ # running transforms on the dashboard.
+ def process(insert_project_metrics:)
+ @dashboard.deep_symbolize_keys.tap do |dashboard|
+ sequence(insert_project_metrics).each do |stage|
+ stage.new(@project, @environment, dashboard).transform!
+ end
+ end
+ end
+
+ private
+
+ def sequence(insert_project_metrics)
+ insert_project_metrics ? SYSTEM_SEQUENCE : PROJECT_SEQUENCE
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/project_dashboard_service.rb b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
new file mode 100644
index 00000000000..fdffd067c93
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/project_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Searches a projects repository for a metrics dashboard and formats the output.
+# Expects any custom dashboards will be located in `.gitlab/dashboards`
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class ProjectDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ DASHBOARD_ROOT = ".gitlab/dashboards"
+
+ class << self
+ def all_dashboard_paths(project)
+ file_finder(project)
+ .list_files_for(DASHBOARD_ROOT)
+ .map do |filepath|
+ Rails.cache.delete(cache_key(project.id, filepath))
+
+ { path: filepath, default: false }
+ end
+ end
+
+ def file_finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, DASHBOARD_ROOT, '.yml')
+ end
+
+ def cache_key(id, dashboard_path)
+ "project_#{id}_metrics_dashboard_#{dashboard_path}"
+ end
+ end
+
+ private
+
+ # Searches the project repo for a custom-defined dashboard.
+ def get_raw_dashboard
+ yml = self.class.file_finder(project).read(dashboard_path)
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ self.class.cache_key(project.id, dashboard_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/base_stage.rb b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
new file mode 100644
index 00000000000..a6d1f974556
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/base_stage.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class BaseStage
+ DashboardLayoutError = Class.new(StandardError)
+
+ DEFAULT_PANEL_TYPE = 'area-chart'
+
+ attr_reader :project, :environment, :dashboard
+
+ def initialize(project, environment, dashboard)
+ @project = project
+ @environment = environment
+ @dashboard = dashboard
+ end
+
+ # Entry-point to the stage
+ def transform!
+ raise NotImplementedError
+ end
+
+ protected
+
+ def missing_panel_groups!
+ raise DashboardLayoutError.new('Top-level key :panel_groups must be an array')
+ end
+
+ def missing_panels!
+ raise DashboardLayoutError.new('Each "panel_group" must define an array :panels')
+ end
+
+ def missing_metrics!
+ raise DashboardLayoutError.new('Each "panel" must define an array :metrics')
+ end
+
+ def for_metrics
+ missing_panel_groups! unless dashboard[:panel_groups].is_a?(Array)
+
+ dashboard[:panel_groups].each do |panel_group|
+ missing_panels! unless panel_group[:panels].is_a?(Array)
+
+ panel_group[:panels].each do |panel|
+ missing_metrics! unless panel[:metrics].is_a?(Array)
+
+ panel[:metrics].each do |metric|
+ yield metric
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
new file mode 100644
index 00000000000..188912bedb4
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/common_metrics_inserter.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class CommonMetricsInserter < BaseStage
+ # For each metric in the dashboard config, attempts to
+ # find a corresponding database record. If found,
+ # includes the record's id in the dashboard config.
+ def transform!
+ common_metrics = ::PrometheusMetric.common
+
+ for_metrics do |metric|
+ metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
+ metric[:metric_id] = metric_record.id if metric_record
+ end
+ 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
new file mode 100644
index 00000000000..221610a14d1
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/project_metrics_inserter.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class ProjectMetricsInserter < BaseStage
+ # Inserts project-specific metrics into the dashboard
+ # config. If there are no project-specific metrics,
+ # this will have no effect.
+ def transform!
+ project.prometheus_metrics.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)
+ end
+ end
+
+ private
+
+ # Looks for a panel_group corresponding to the
+ # provided metric object. If unavailable, inserts one.
+ # @param panel_groups [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_panel_group(panel_groups, metric)
+ panel_group = find_panel_group(panel_groups, metric)
+ return panel_group if panel_group
+
+ panel_group = new_panel_group(metric)
+ panel_groups << panel_group
+
+ panel_group
+ end
+
+ # Looks for a panel corresponding to the provided
+ # metric object. If unavailable, inserts one.
+ # @param panels [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_panel(panels, metric)
+ panel = find_panel(panels, metric)
+ return panel if panel
+
+ panel = new_panel(metric)
+ panels << panel
+
+ panel
+ end
+
+ # Looks for a metric corresponding to the provided
+ # metric object. If unavailable, inserts one.
+ # @param metrics [Array<Hash>]
+ # @param metric [PrometheusMetric]
+ def find_or_create_metric(metrics, metric)
+ target_metric = find_metric(metrics, metric)
+ return target_metric if target_metric
+
+ target_metric = new_metric(metric)
+ metrics << target_metric
+
+ target_metric
+ end
+
+ def find_panel_group(panel_groups, metric)
+ return unless panel_groups
+
+ panel_groups.find { |group| group[:group] == metric.group_title }
+ end
+
+ def find_panel(panels, metric)
+ return unless panels
+
+ panel_identifiers = [DEFAULT_PANEL_TYPE, metric.title, metric.y_label]
+ panels.find { |panel| panel.values_at(:type, :title, :y_label) == panel_identifiers }
+ end
+
+ def find_metric(metrics, metric)
+ return unless metrics
+
+ metrics.find { |m| m[:id] == metric.identifier }
+ end
+
+ def new_panel_group(metric)
+ {
+ group: metric.group_title,
+ priority: metric.priority,
+ panels: []
+ }
+ end
+
+ def new_panel(metric)
+ {
+ type: DEFAULT_PANEL_TYPE,
+ title: metric.title,
+ y_label: metric.y_label,
+ metrics: []
+ }
+ end
+
+ def new_metric(metric)
+ metric.queries.first.merge(metric_id: metric.id)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/stages/sorter.rb b/lib/gitlab/metrics/dashboard/stages/sorter.rb
new file mode 100644
index 00000000000..ba5aa78059c
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/sorter.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class Sorter < BaseStage
+ def transform!
+ missing_panel_groups! unless dashboard[:panel_groups].is_a? Array
+
+ sort_groups!
+ sort_panels!
+ end
+
+ private
+
+ # Sorts the groups in the dashboard by the :priority key
+ def sort_groups!
+ dashboard[:panel_groups] = dashboard[:panel_groups].sort_by { |group| -group[:priority].to_i }
+ end
+
+ # Sorts the panels in the dashboard by the :weight key
+ def sort_panels!
+ dashboard[:panel_groups].each do |group|
+ missing_panels! unless group[:panels].is_a? Array
+
+ group[:panels] = group[:panels].sort_by { |panel| -panel[:weight].to_i }
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/system_dashboard_service.rb b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
new file mode 100644
index 00000000000..67509ed4230
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/system_dashboard_service.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# Fetches the system metrics dashboard and formats the output.
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class SystemDashboardService < Gitlab::Metrics::Dashboard::BaseService
+ SYSTEM_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
+
+ class << self
+ def all_dashboard_paths(_project)
+ [{
+ path: SYSTEM_DASHBOARD_PATH,
+ default: true
+ }]
+ end
+
+ def system_dashboard?(filepath)
+ filepath == SYSTEM_DASHBOARD_PATH
+ end
+ end
+
+ private
+
+ def dashboard_path
+ SYSTEM_DASHBOARD_PATH
+ end
+
+ # Returns the base metrics shipped with every GitLab service.
+ def get_raw_dashboard
+ yml = File.read(Rails.root.join(dashboard_path))
+
+ YAML.safe_load(yml)
+ end
+
+ def cache_key
+ "metrics_dashboard_#{dashboard_path}"
+ end
+
+ def insert_project_metrics?
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index 9e4d70a71ff..30f181542be 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,7 +4,7 @@ module Gitlab
module Metrics
# Class for storing details of a single metric (label, value, etc).
class Metric
- JITTER_RANGE = 0.000001..0.001
+ JITTER_RANGE = (0.000001..0.001).freeze
attr_reader :series, :values, :tags, :type
diff --git a/lib/gitlab/middleware/correlation_id.rb b/lib/gitlab/middleware/correlation_id.rb
index 80dddc41c12..fffd5da827f 100644
--- a/lib/gitlab/middleware/correlation_id.rb
+++ b/lib/gitlab/middleware/correlation_id.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def call(env)
- ::Gitlab::CorrelationId.use_id(correlation_id(env)) do
+ ::Labkit::Correlation::CorrelationId.use_id(correlation_id(env)) do
@app.call(env)
end
end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index 83c52a6c6e0..8e17073abab 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -3,7 +3,7 @@
module Gitlab
module Middleware
class ReadOnly
- API_VERSIONS = (3..4)
+ API_VERSIONS = (3..4).freeze
def self.internal_routes
@internal_routes ||=
diff --git a/lib/gitlab/middleware/release_env.rb b/lib/gitlab/middleware/release_env.rb
index 849cf8f759b..0719fb2e8c6 100644
--- a/lib/gitlab/middleware/release_env.rb
+++ b/lib/gitlab/middleware/release_env.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab
diff --git a/lib/gitlab/namespaced_session_store.rb b/lib/gitlab/namespaced_session_store.rb
new file mode 100644
index 00000000000..34520078bfb
--- /dev/null
+++ b/lib/gitlab/namespaced_session_store.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class NamespacedSessionStore
+ delegate :[], :[]=, to: :store
+
+ def initialize(key)
+ @key = key
+ end
+
+ def initiated?
+ !Session.current.nil?
+ end
+
+ def store
+ return unless Session.current
+
+ Session.current[@key] ||= {}
+ Session.current[@key]
+ end
+ end
+end
diff --git a/lib/gitlab/performance_bar/peek_query_tracker.rb b/lib/gitlab/performance_bar/peek_query_tracker.rb
index ac392432427..16c16aa0265 100644
--- a/lib/gitlab/performance_bar/peek_query_tracker.rb
+++ b/lib/gitlab/performance_bar/peek_query_tracker.rb
@@ -26,11 +26,7 @@ module Gitlab
subscribe('sql.active_record') do |_, start, finish, _, data|
if Gitlab::SafeRequestStore.store[:peek_enabled]
- # data[:cached] is only available starting from Rails 5.1.0
- # https://github.com/rails/rails/blob/v5.1.0/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb#L113
- # Before that, data[:name] was set to 'CACHE'
- # https://github.com/rails/rails/blob/v4.2.9/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb#L80
- unless data.fetch(:cached, data[:name] == 'CACHE')
+ unless data[:cached]
track_query(data[:sql].strip, data[:binds], start, finish)
end
end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 93a9fcf1591..890228e5e78 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -16,7 +16,11 @@ module Gitlab
ee/lib/gitlab/middleware/
lib/gitlab/performance_bar/
lib/gitlab/request_profiler/
+ lib/gitlab/query_limiting/
+ lib/gitlab/tracing/
lib/gitlab/profiler.rb
+ lib/gitlab/correlation_id.rb
+ lib/gitlab/webpack/dev_server_middleware.rb
].freeze
# Takes a URL to profile (can be a fully-qualified URL, or an absolute path)
@@ -69,7 +73,7 @@ module Gitlab
result = with_custom_logger(logger) do
with_user(user) do
- RubyProf.profile { app.public_send(verb, url, post_data, headers) } # rubocop:disable GitlabSecurity/PublicSend
+ RubyProf.profile { app.public_send(verb, url, params: post_data, headers: headers) } # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 58f06b6708c..78337518988 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -145,5 +145,9 @@ module Gitlab
def repository_wiki_ref
@repository_wiki_ref ||= repository_ref || project.wiki.default_branch
end
+
+ def issuable_params
+ super.merge(project_id: project.id)
+ end
end
end
diff --git a/lib/gitlab/prometheus/query_variables.rb b/lib/gitlab/prometheus/query_variables.rb
index 1cc85d4b4a6..dca09aef47d 100644
--- a/lib/gitlab/prometheus/query_variables.rb
+++ b/lib/gitlab/prometheus/query_variables.rb
@@ -4,9 +4,13 @@ module Gitlab
module Prometheus
module QueryVariables
def self.call(environment)
+ deployment_platform = environment.deployment_platform
+ namespace = deployment_platform&.namespace_for(environment.project) ||
+ deployment_platform&.actual_namespace || ''
+
{
ci_environment_slug: environment.slug,
- kube_namespace: environment.deployment_platform&.actual_namespace || '',
+ kube_namespace: namespace,
environment_filter: %{container_name!="POD",environment="#{environment.slug}"}
}
end
diff --git a/lib/gitlab/push_options.rb b/lib/gitlab/push_options.rb
index 810aba436cc..3137676ba4b 100644
--- a/lib/gitlab/push_options.rb
+++ b/lib/gitlab/push_options.rb
@@ -15,7 +15,7 @@ module Gitlab
mr: :merge_request
}).freeze
- OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/
+ OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/.freeze
attr_reader :options
diff --git a/lib/gitlab/quick_actions/commit_actions.rb b/lib/gitlab/quick_actions/commit_actions.rb
index 62c0fbb5afd..1018910e8e9 100644
--- a/lib/gitlab/quick_actions/commit_actions.rb
+++ b/lib/gitlab/quick_actions/commit_actions.rb
@@ -8,10 +8,13 @@ module Gitlab
included do
# Commit only quick actions definitions
- desc 'Tag this commit.'
+ desc _('Tag this commit.')
explanation do |tag_name, message|
- with_message = %{ with "#{message}"} if message.present?
- "Tags this commit to #{tag_name}#{with_message}."
+ if message.present?
+ _("Tags this commit to %{tag_name} with \"%{message}\".") % { tag_name: tag_name, message: message }
+ else
+ _("Tags this commit to %{tag_name}.") % { tag_name: tag_name }
+ end
end
params 'v1.2.3 <message>'
parse_params do |tag_name_and_message|
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index ad2e15d19fa..572c55efcc2 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -43,9 +43,9 @@ module Gitlab
@updates[:state_event] = 'reopen'
end
- desc 'Change title'
+ desc _('Change title')
explanation do |title_param|
- "Changes the title to \"#{title_param}\"."
+ _("Changes the title to \"%{title_param}\".") % { title_param: title_param }
end
params '<New title>'
types Issuable
@@ -57,7 +57,7 @@ module Gitlab
@updates[:title] = title_param
end
- desc 'Add label(s)'
+ desc _('Add label(s)')
explanation do |labels_param|
labels = find_label_references(labels_param)
@@ -81,13 +81,13 @@ module Gitlab
end
end
- desc 'Remove all or specific label(s)'
+ desc _('Remove all or specific label(s)')
explanation do |labels_param = nil|
if labels_param.present?
labels = find_label_references(labels_param)
"Removes #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
else
- 'Removes all labels.'
+ _('Removes all labels.')
end
end
params '~label1 ~"label 2"'
@@ -112,7 +112,7 @@ module Gitlab
end
end
- desc 'Replace all label(s)'
+ desc _('Replace all label(s)')
explanation do |labels_param|
labels = find_label_references(labels_param)
"Replaces all labels with #{labels.join(' ')} #{'label'.pluralize(labels.count)}." if labels.any?
@@ -135,8 +135,8 @@ module Gitlab
end
end
- desc 'Add a todo'
- explanation 'Adds a todo.'
+ desc _('Add a todo')
+ explanation _('Adds a todo.')
types Issuable
condition do
quick_action_target.persisted? &&
@@ -146,8 +146,8 @@ module Gitlab
@updates[:todo_event] = 'add'
end
- desc 'Mark todo as done'
- explanation 'Marks todo as done.'
+ desc _('Mark todo as done')
+ explanation _('Marks todo as done.')
types Issuable
condition do
quick_action_target.persisted? &&
@@ -157,7 +157,7 @@ module Gitlab
@updates[:todo_event] = 'done'
end
- desc 'Subscribe'
+ desc _('Subscribe')
explanation do
"Subscribes to this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
end
@@ -170,7 +170,7 @@ module Gitlab
@updates[:subscription_event] = 'subscribe'
end
- desc 'Unsubscribe'
+ desc _('Unsubscribe')
explanation do
"Unsubscribes from this #{quick_action_target.to_ability_name.humanize(capitalize: false)}."
end
@@ -183,9 +183,9 @@ module Gitlab
@updates[:subscription_event] = 'unsubscribe'
end
- desc 'Toggle emoji award'
+ desc _('Toggle emoji award')
explanation do |name|
- "Toggles :#{name}: emoji award." if name
+ _("Toggles :%{name}: emoji award.") % { name: name } if name
end
params ':emoji:'
types Issuable
@@ -202,14 +202,14 @@ module Gitlab
end
end
- desc "Append the comment with #{SHRUG}"
+ desc _("Append the comment with %{shrug}") % { shrug: SHRUG }
params '<Comment>'
types Issuable
substitution :shrug do |comment|
"#{comment} #{SHRUG}"
end
- desc "Append the comment with #{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 1f08e8740a2..85e62f950c8 100644
--- a/lib/gitlab/quick_actions/issue_actions.rb
+++ b/lib/gitlab/quick_actions/issue_actions.rb
@@ -8,9 +8,9 @@ module Gitlab
included do
# Issue only quick actions definition
- desc 'Set due date'
+ desc _('Set due date')
explanation do |due_date|
- "Sets the due date to #{due_date.to_s(:medium)}." if due_date
+ _("Sets the due date to %{due_date}.") % { due_date: due_date.strftime('%b %-d, %Y') } if due_date
end
params '<in 2 days | this Friday | December 31st>'
types Issue
@@ -25,8 +25,8 @@ module Gitlab
@updates[:due_date] = due_date if due_date
end
- desc 'Remove due date'
- explanation 'Removes the due date.'
+ desc _('Remove due date')
+ explanation _('Removes the due date.')
types Issue
condition do
quick_action_target.persisted? &&
@@ -38,10 +38,10 @@ module Gitlab
@updates[:due_date] = nil
end
- desc 'Move issue from one column of the board to another'
+ desc _('Move issue from one column of the board to another')
explanation do |target_list_name|
label = find_label_references(target_list_name).first
- "Moves issue to #{label} column in the board." if label
+ _("Moves issue to %{label} column in the board.") % { label: label } if label
end
params '~"Target column"'
types Issue
@@ -66,9 +66,9 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- desc 'Mark this issue as a duplicate of another issue'
+ desc _('Mark this issue as a duplicate of another issue')
explanation do |duplicate_reference|
- "Marks this issue as a duplicate of #{duplicate_reference}."
+ _("Marks this issue as a duplicate of %{duplicate_reference}.") % { duplicate_reference: duplicate_reference }
end
params '#issue'
types Issue
@@ -84,9 +84,9 @@ module Gitlab
end
end
- desc 'Move this issue to another project.'
+ desc _('Move this issue to another project.')
explanation do |path_to_project|
- "Moves this issue to #{path_to_project}."
+ _("Moves this issue to %{path_to_project}.") % { path_to_project: path_to_project }
end
params 'path/to/project'
types Issue
@@ -102,9 +102,9 @@ module Gitlab
end
end
- desc 'Make issue confidential.'
+ desc _('Make issue confidential.')
explanation do
- 'Makes this issue confidential'
+ _('Makes this issue confidential')
end
types Issue
condition do
@@ -114,10 +114,13 @@ module Gitlab
@updates[:confidential] = true
end
- desc 'Create a merge request.'
+ desc _('Create a merge request.')
explanation do |branch_name = nil|
- branch_text = branch_name ? "branch '#{branch_name}'" : 'a branch'
- "Creates #{branch_text} and a merge request to resolve this issue"
+ if branch_name
+ _("Creates branch '%{branch_name}' and a merge request to resolve this issue") % { branch_name: branch_name }
+ else
+ "Creates a branch and a merge request to resolve this issue"
+ end
end
params "<branch name>"
types Issue
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index 08872eda410..1cd158db2b0 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -8,7 +8,7 @@ module Gitlab
included do
# Issue, MergeRequest: quick actions definitions
- desc 'Assign'
+ desc _('Assign')
# rubocop: disable CodeReuse/ActiveRecord
explanation do |users|
users = quick_action_target.allows_multiple_assignees? ? users : users.take(1)
@@ -38,9 +38,9 @@ module Gitlab
desc do
if quick_action_target.allows_multiple_assignees?
- 'Remove all or specific assignee(s)'
+ _('Remove all or specific assignee(s)')
else
- 'Remove assignee'
+ _('Remove assignee')
end
end
explanation do |users = nil|
@@ -70,9 +70,9 @@ module Gitlab
end
end
- desc 'Set milestone'
+ desc _('Set milestone')
explanation do |milestone|
- "Sets the milestone to #{milestone.to_reference}." if milestone
+ _("Sets the milestone to %{milestone_reference}.") % { milestone_reference: milestone.to_reference } if milestone
end
params '%"milestone"'
types Issue, MergeRequest
@@ -88,9 +88,9 @@ module Gitlab
@updates[:milestone_id] = milestone.id if milestone
end
- desc 'Remove milestone'
+ desc _('Remove milestone')
explanation do
- "Removes #{quick_action_target.milestone.to_reference(format: :name)} milestone."
+ _("Removes %{milestone_reference} milestone.") % { milestone_reference: quick_action_target.milestone.to_reference(format: :name) }
end
types Issue, MergeRequest
condition do
@@ -102,9 +102,9 @@ module Gitlab
@updates[:milestone_id] = nil
end
- desc 'Copy labels and milestone from other issue or merge request'
+ desc _('Copy labels and milestone from other issue or merge request')
explanation do |source_issuable|
- "Copy labels and milestone from #{source_issuable.to_reference}."
+ _("Copy labels and milestone from %{source_issuable_reference}.") % { source_issuable_reference: source_issuable.to_reference }
end
params '#issue | !merge_request'
types Issue, MergeRequest
@@ -122,11 +122,11 @@ module Gitlab
end
end
- desc 'Set time estimate'
+ desc _('Set time estimate')
explanation do |time_estimate|
time_estimate = Gitlab::TimeTrackingFormatter.output(time_estimate)
- "Sets time estimate to #{time_estimate}." if time_estimate
+ _("Sets time estimate to %{time_estimate}.") % { time_estimate: time_estimate } if time_estimate
end
params '<1w 3d 2h 14m>'
types Issue, MergeRequest
@@ -142,18 +142,18 @@ module Gitlab
end
end
- desc 'Add or subtract spent time'
+ desc _('Add or subtract spent time')
explanation do |time_spent, time_spent_date|
if time_spent
if time_spent > 0
- verb = 'Adds'
+ verb = _('Adds')
value = time_spent
else
- verb = 'Subtracts'
+ verb = _('Subtracts')
value = -time_spent
end
- "#{verb} #{Gitlab::TimeTrackingFormatter.output(value)} spent time."
+ _("%{verb} %{time_spent_value} spent time.") % { verb: verb, time_spent_value: Gitlab::TimeTrackingFormatter.output(value) }
end
end
params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>'
@@ -174,8 +174,8 @@ module Gitlab
end
end
- desc 'Remove time estimate'
- explanation 'Removes time estimate.'
+ desc _('Remove time estimate')
+ explanation _('Removes time estimate.')
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
@@ -185,8 +185,8 @@ module Gitlab
@updates[:time_estimate] = 0
end
- desc 'Remove spent time'
- explanation 'Removes spent time.'
+ desc _('Remove spent time')
+ explanation _('Removes spent time.')
condition do
quick_action_target.persisted? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
@@ -196,8 +196,8 @@ module Gitlab
@updates[:spend_time] = { duration: :reset, user_id: current_user.id }
end
- desc "Lock the discussion"
- explanation "Locks the discussion"
+ desc _("Lock the discussion")
+ explanation _("Locks the discussion")
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
@@ -208,8 +208,8 @@ module Gitlab
@updates[:discussion_locked] = true
end
- desc "Unlock the discussion"
- explanation "Unlocks the discussion"
+ desc _("Unlock the discussion")
+ explanation _("Unlocks the discussion")
types Issue, MergeRequest
condition do
quick_action_target.persisted? &&
diff --git a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
index f5176376a60..4a62e83e8e9 100644
--- a/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
+++ b/lib/gitlab/quick_actions/spend_time_and_date_separator.rb
@@ -11,7 +11,7 @@ module Gitlab
# if date doesn't present return time with current date
# in other cases return nil
class SpendTimeAndDateSeparator
- DATE_REGEX = %r{(\d{2,4}[/\-.]\d{1,2}[/\-.]\d{1,2})}
+ DATE_REGEX = %r{(\d{2,4}[/\-.]\d{1,2}[/\-.]\d{1,2})}.freeze
def initialize(spend_command_arg)
@spend_arg = spend_command_arg
diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb
index 0d4e6be2129..98f78c5e74b 100644
--- a/lib/gitlab/sanitizers/svg.rb
+++ b/lib/gitlab/sanitizers/svg.rb
@@ -9,7 +9,7 @@ module Gitlab
class Scrubber < Loofah::Scrubber
# http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#embedding-custom-non-visible-data-with-the-data-*-attributes
- DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u
+ DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u.freeze
def scrub(node)
unless Whitelist::ALLOWED_ELEMENTS.include?(node.name)
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 8988b9ad7be..4a097a00101 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,6 +2,8 @@
module Gitlab
class SearchResults
+ COUNT_LIMIT = 1001
+
attr_reader :current_user, :query, :per_page
# Limit search results by passed projects
@@ -25,29 +27,26 @@ module Gitlab
def objects(scope, page = nil, without_count = true)
collection = case scope
when 'projects'
- projects.page(page).per(per_page)
+ projects
when 'issues'
- issues.page(page).per(per_page)
+ issues
when 'merge_requests'
- merge_requests.page(page).per(per_page)
+ merge_requests
when 'milestones'
- milestones.page(page).per(per_page)
+ milestones
when 'users'
- users.page(page).per(per_page)
+ users
else
- Kaminari.paginate_array([]).page(page).per(per_page)
- end
+ Kaminari.paginate_array([])
+ end.page(page).per(per_page)
without_count ? collection.without_count : collection
end
- # rubocop: disable CodeReuse/ActiveRecord
def limited_projects_count
- @limited_projects_count ||= projects.limit(count_limit).count
+ @limited_projects_count ||= limited_count(projects)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_issues_count
return @limited_issues_count if @limited_issues_count
@@ -56,35 +55,28 @@ module Gitlab
# and confidential issues user has access to, is too complex.
# It's faster to try to fetch all public issues first, then only
# if necessary try to fetch all issues.
- sum = issues(public_only: true).limit(count_limit).count
- @limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum
+ sum = limited_count(issues(public_only: true))
+ @limited_issues_count = sum < count_limit ? limited_count(issues) : sum
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_merge_requests_count
- @limited_merge_requests_count ||= merge_requests.limit(count_limit).count
+ @limited_merge_requests_count ||= limited_count(merge_requests)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_milestones_count
- @limited_milestones_count ||= milestones.limit(count_limit).count
+ @limited_milestones_count ||= limited_count(milestones)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop:disable CodeReuse/ActiveRecord
def limited_users_count
- @limited_users_count ||= users.limit(count_limit).count
+ @limited_users_count ||= limited_count(users)
end
- # rubocop:enable CodeReuse/ActiveRecord
def single_commit_result?
false
end
def count_limit
- 1001
+ COUNT_LIMIT
end
def users
@@ -99,49 +91,33 @@ module Gitlab
limit_projects.search(query)
end
- # rubocop: disable CodeReuse/ActiveRecord
def issues(finder_params = {})
- issues = IssuesFinder.new(current_user, finder_params).execute
+ issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
+
unless default_project_filter
- issues = issues.where(project_id: project_ids_relation)
+ issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end
- issues =
- if query =~ /#(\d+)\z/
- issues.where(iid: $1)
- else
- issues.full_search(query)
- end
-
- issues.reorder('updated_at DESC')
+ issues
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def milestones
milestones = Milestone.where(project_id: project_ids_relation)
milestones = milestones.search(query)
- milestones.reorder('updated_at DESC')
+ milestones.reorder('milestones.updated_at DESC')
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def merge_requests
- merge_requests = MergeRequestsFinder.new(current_user).execute
+ merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
+
unless default_project_filter
merge_requests = merge_requests.in_projects(project_ids_relation)
end
- merge_requests =
- if query =~ /[#!](\d+)\z/
- merge_requests.where(iid: $1)
- else
- merge_requests.full_search(query)
- end
-
- merge_requests.reorder('updated_at DESC')
+ merge_requests
end
- # rubocop: enable CodeReuse/ActiveRecord
def default_scope
'projects'
@@ -152,5 +128,23 @@ module Gitlab
limit_projects.select(:id).reorder(nil)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def issuable_params
+ {}.tap do |params|
+ params[:sort] = 'updated_desc'
+
+ if query =~ /#(\d+)\z/
+ params[:iids] = $1
+ else
+ params[:search] = query
+ end
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def limited_count(relation)
+ relation.reorder(nil).limit(count_limit).size
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 956c16117f5..356e6445e0e 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -45,7 +45,7 @@ module Gitlab
context # Make sure we've set everything we know in the context
tags = {
- Gitlab::CorrelationId::LOG_KEY.to_sym => Gitlab::CorrelationId.current_id
+ Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id
}
Raven.capture_exception(exception, tags: tags, extra: extra)
diff --git a/lib/gitlab/session.rb b/lib/gitlab/session.rb
new file mode 100644
index 00000000000..7487ba04a6d
--- /dev/null
+++ b/lib/gitlab/session.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class Session
+ STORE_KEY = :session_storage
+
+ class << self
+ def with_session(session)
+ old = self.current
+ self.current = session
+ yield
+ ensure
+ self.current = old
+ end
+
+ def current
+ Thread.current[STORE_KEY]
+ end
+
+ protected
+
+ def current=(value)
+ Thread.current[STORE_KEY] = value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb
index 747cb0f9142..f7b08d58e49 100644
--- a/lib/gitlab/sherlock/middleware.rb
+++ b/lib/gitlab/sherlock/middleware.rb
@@ -4,9 +4,9 @@ module Gitlab
module Sherlock
# Rack middleware used for tracking request metrics.
class Middleware
- CONTENT_TYPES = %r{text/html|application/json}i
+ CONTENT_TYPES = %r{text/html|application/json}i.freeze
- IGNORE_PATHS = %r{^/sherlock}
+ IGNORE_PATHS = %r{^/sherlock}.freeze
def initialize(app)
@app = app
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
index 11561eec32a..159ce27e702 100644
--- a/lib/gitlab/sherlock/query.rb
+++ b/lib/gitlab/sherlock/query.rb
@@ -15,7 +15,7 @@ module Gitlab
|GROUP\s+BY
|ORDER\s+BY
|LIMIT
- |OFFSET)\s+}ix # Vim indent breaks when this is on a newline :<
+ |OFFSET)\s+}ix.freeze # Vim indent breaks when this is on a newline :<
# Creates a new Query using a String and a separate Array of bindings.
#
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index fb303e3fb0c..c102fa14cfc 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -7,7 +7,7 @@ module Gitlab
module SidekiqConfig
QUEUE_CONFIG_PATHS = %w[app/workers/all_queues.yml ee/app/workers/all_queues.yml].freeze
- # This method is called by `bin/sidekiq-cluster` in EE, which runs outside
+ # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
def self.worker_queues(rails_path = Rails.root.to_s)
@worker_queues ||= {}
@@ -19,7 +19,7 @@ module Gitlab
end
end
- # This method is called by `bin/sidekiq-cluster` in EE, which runs outside
+ # This method is called by `ee/bin/sidekiq-cluster` in EE, which runs outside
# of bundler/Rails context, so we cannot use any gem or Rails methods.
def self.expand_queues(queues, all_queues = self.worker_queues)
return [] if queues.empty?
diff --git a/lib/gitlab/sidekiq_middleware/correlation_injector.rb b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
index b807b3a03ed..1539fd706ab 100644
--- a/lib/gitlab/sidekiq_middleware/correlation_injector.rb
+++ b/lib/gitlab/sidekiq_middleware/correlation_injector.rb
@@ -4,8 +4,8 @@ module Gitlab
module SidekiqMiddleware
class CorrelationInjector
def call(worker_class, job, queue, redis_pool)
- job[Gitlab::CorrelationId::LOG_KEY] ||=
- Gitlab::CorrelationId.current_or_new_id
+ job[Labkit::Correlation::CorrelationId::LOG_KEY] ||=
+ Labkit::Correlation::CorrelationId.current_or_new_id
yield
end
diff --git a/lib/gitlab/sidekiq_middleware/correlation_logger.rb b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
index cb8ff4a6284..cffc4483573 100644
--- a/lib/gitlab/sidekiq_middleware/correlation_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/correlation_logger.rb
@@ -4,9 +4,9 @@ module Gitlab
module SidekiqMiddleware
class CorrelationLogger
def call(worker, job, queue)
- correlation_id = job[Gitlab::CorrelationId::LOG_KEY]
+ correlation_id = job[Labkit::Correlation::CorrelationId::LOG_KEY]
- Gitlab::CorrelationId.use_id(correlation_id) do
+ Labkit::Correlation::CorrelationId.use_id(correlation_id) do
yield
end
end
diff --git a/lib/gitlab/slash_commands/result.rb b/lib/gitlab/slash_commands/result.rb
index 607c9c8dec1..a66a2e0726b 100644
--- a/lib/gitlab/slash_commands/result.rb
+++ b/lib/gitlab/slash_commands/result.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module Gitlab
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index b698391c8bd..fd108b4c124 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -6,7 +6,7 @@ module Gitlab
extend ActiveSupport::Concern
MIN_CHARS_FOR_PARTIAL_MATCHING = 3
- REGEX_QUOTED_WORD = /(?<=\A| )"[^"]+"(?= |\z)/
+ REGEX_QUOTED_WORD = /(?<=\A| )"[^"]+"(?= |\z)/.freeze
class_methods do
def fuzzy_search(query, columns)
diff --git a/lib/gitlab/tracing/common.rb b/lib/gitlab/tracing/common.rb
deleted file mode 100644
index 3a08ede8138..00000000000
--- a/lib/gitlab/tracing/common.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'opentracing'
-
-module Gitlab
- module Tracing
- module Common
- def tracer
- OpenTracing.global_tracer
- end
-
- # Convience method for running a block with a span
- def in_tracing_span(operation_name:, tags:, child_of: nil)
- scope = tracer.start_active_span(
- operation_name,
- child_of: child_of,
- tags: tags
- )
- span = scope.span
-
- # Add correlation details to the span if we have them
- correlation_id = Gitlab::CorrelationId.current_id
- if correlation_id
- span.set_tag('correlation_id', correlation_id)
- end
-
- begin
- yield span
- rescue => e
- log_exception_on_span(span, e)
- raise e
- ensure
- scope.close
- end
- end
-
- def postnotify_span(operation_name, start_time, end_time, tags: nil, child_of: nil, exception: nil)
- span = OpenTracing.start_span(operation_name, start_time: start_time, tags: tags, child_of: child_of)
-
- log_exception_on_span(span, exception) if exception
-
- span.finish(end_time: end_time)
- end
-
- def log_exception_on_span(span, exception)
- span.set_tag('error', true)
- span.log_kv(kv_tags_for_exception(exception))
- end
-
- def kv_tags_for_exception(exception)
- case exception
- when Exception
- {
- 'event': 'error',
- 'error.kind': exception.class.to_s,
- 'message': Gitlab::UrlSanitizer.sanitize(exception.message),
- 'stack': exception.backtrace&.join("\n")
- }
- else
- {
- 'event': 'error',
- 'error.kind': exception.class.to_s,
- 'error.object': Gitlab::UrlSanitizer.sanitize(exception.to_s)
- }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/factory.rb b/lib/gitlab/tracing/factory.rb
deleted file mode 100644
index fc714164353..00000000000
--- a/lib/gitlab/tracing/factory.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require "cgi"
-
-module Gitlab
- module Tracing
- class Factory
- OPENTRACING_SCHEME = "opentracing"
-
- def self.create_tracer(service_name, connection_string)
- return unless connection_string.present?
-
- begin
- opentracing_details = parse_connection_string(connection_string)
- driver_name = opentracing_details[:driver_name]
-
- case driver_name
- when "jaeger"
- JaegerFactory.create_tracer(service_name, opentracing_details[:options])
- else
- raise "Unknown driver: #{driver_name}"
- end
- rescue => e
- # Can't create the tracer? Warn and continue sans tracer
- warn "Unable to instantiate tracer: #{e}"
- nil
- end
- end
-
- def self.parse_connection_string(connection_string)
- parsed = URI.parse(connection_string)
-
- unless valid_uri?(parsed)
- raise "Invalid tracing connection string"
- end
-
- {
- driver_name: parsed.host,
- options: parse_query(parsed.query)
- }
- end
- private_class_method :parse_connection_string
-
- def self.parse_query(query)
- return {} unless query
-
- CGI.parse(query).symbolize_keys.transform_values(&:first)
- end
- private_class_method :parse_query
-
- def self.valid_uri?(uri)
- return false unless uri
-
- uri.scheme == OPENTRACING_SCHEME &&
- uri.host.to_s =~ /^[a-z0-9_]+$/ &&
- uri.path.empty?
- end
- private_class_method :valid_uri?
- end
- end
-end
diff --git a/lib/gitlab/tracing/grpc_interceptor.rb b/lib/gitlab/tracing/grpc_interceptor.rb
deleted file mode 100644
index 6c2aab73125..00000000000
--- a/lib/gitlab/tracing/grpc_interceptor.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require 'opentracing'
-require 'grpc'
-
-module Gitlab
- module Tracing
- class GRPCInterceptor < GRPC::ClientInterceptor
- include Common
- include Singleton
-
- def request_response(request:, call:, method:, metadata:)
- wrap_with_tracing(method, 'unary', metadata) do
- yield
- end
- end
-
- def client_streamer(requests:, call:, method:, metadata:)
- wrap_with_tracing(method, 'client_stream', metadata) do
- yield
- end
- end
-
- def server_streamer(request:, call:, method:, metadata:)
- wrap_with_tracing(method, 'server_stream', metadata) do
- yield
- end
- end
-
- def bidi_streamer(requests:, call:, method:, metadata:)
- wrap_with_tracing(method, 'bidi_stream', metadata) do
- yield
- end
- end
-
- private
-
- def wrap_with_tracing(method, grpc_type, metadata)
- tags = {
- 'component' => 'grpc',
- 'span.kind' => 'client',
- 'grpc.method' => method,
- 'grpc.type' => grpc_type
- }
-
- in_tracing_span(operation_name: "grpc:#{method}", tags: tags) do |span|
- OpenTracing.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, metadata)
-
- yield
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/jaeger_factory.rb b/lib/gitlab/tracing/jaeger_factory.rb
deleted file mode 100644
index 93520d5667b..00000000000
--- a/lib/gitlab/tracing/jaeger_factory.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'jaeger/client'
-
-module Gitlab
- module Tracing
- class JaegerFactory
- # When the probabilistic sampler is used, by default 0.1% of requests will be traced
- DEFAULT_PROBABILISTIC_RATE = 0.001
-
- # The default port for the Jaeger agent UDP listener
- DEFAULT_UDP_PORT = 6831
-
- # Reduce this from default of 10 seconds as the Ruby jaeger
- # client doesn't have overflow control, leading to very large
- # messages which fail to send over UDP (max packet = 64k)
- # Flush more often, with smaller packets
- FLUSH_INTERVAL = 5
-
- def self.create_tracer(service_name, options)
- kwargs = {
- service_name: service_name,
- sampler: get_sampler(options[:sampler], options[:sampler_param]),
- reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint])
- }.compact
-
- extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug) # rubocop: disable CodeReuse/ActiveRecord
- if extra_params.present?
- message = "jaeger tracer: invalid option: #{extra_params.keys.join(", ")}"
-
- if options[:strict_parsing]
- raise message
- else
- warn message
- end
- end
-
- Jaeger::Client.build(kwargs)
- end
-
- def self.get_sampler(sampler_type, sampler_param)
- case sampler_type
- when "probabilistic"
- sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE
- Jaeger::Samplers::Probabilistic.new(rate: sampler_rate)
- when "const"
- const_value = sampler_param == "1"
- Jaeger::Samplers::Const.new(const_value)
- else
- nil
- end
- end
- private_class_method :get_sampler
-
- def self.get_reporter(service_name, http_endpoint, udp_endpoint)
- encoder = Jaeger::Encoders::ThriftEncoder.new(service_name: service_name)
-
- if http_endpoint.present?
- sender = get_http_sender(encoder, http_endpoint)
- elsif udp_endpoint.present?
- sender = get_udp_sender(encoder, udp_endpoint)
- else
- return
- end
-
- Jaeger::Reporters::RemoteReporter.new(
- sender: sender,
- flush_interval: FLUSH_INTERVAL
- )
- end
- private_class_method :get_reporter
-
- def self.get_http_sender(encoder, address)
- Jaeger::HttpSender.new(
- url: address,
- encoder: encoder,
- logger: Logger.new(STDOUT)
- )
- end
- private_class_method :get_http_sender
-
- def self.get_udp_sender(encoder, address)
- pair = address.split(":", 2)
- host = pair[0]
- port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT
-
- Jaeger::UdpSender.new(
- host: host,
- port: port,
- encoder: encoder,
- logger: Logger.new(STDOUT)
- )
- end
- private_class_method :get_udp_sender
- end
- end
-end
diff --git a/lib/gitlab/tracing/rack_middleware.rb b/lib/gitlab/tracing/rack_middleware.rb
deleted file mode 100644
index e6a31293f7b..00000000000
--- a/lib/gitlab/tracing/rack_middleware.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'opentracing'
-
-module Gitlab
- module Tracing
- class RackMiddleware
- include Common
-
- REQUEST_METHOD = 'REQUEST_METHOD'
-
- def initialize(app)
- @app = app
- end
-
- def call(env)
- method = env[REQUEST_METHOD]
-
- context = tracer.extract(OpenTracing::FORMAT_RACK, env)
- tags = {
- 'component' => 'rack',
- 'span.kind' => 'server',
- 'http.method' => method,
- 'http.url' => self.class.build_sanitized_url_from_env(env)
- }
-
- in_tracing_span(operation_name: "http:#{method}", child_of: context, tags: tags) do |span|
- @app.call(env).tap do |status_code, _headers, _body|
- span.set_tag('http.status_code', status_code)
- end
- end
- end
-
- # Generate a sanitized (safe) request URL from the rack environment
- def self.build_sanitized_url_from_env(env)
- request = ActionDispatch::Request.new(env)
-
- original_url = request.original_url
- uri = URI.parse(original_url)
- uri.query = request.filtered_parameters.to_query if uri.query.present?
-
- uri.to_s
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/rails/action_view_subscriber.rb b/lib/gitlab/tracing/rails/action_view_subscriber.rb
deleted file mode 100644
index 88816e1fb32..00000000000
--- a/lib/gitlab/tracing/rails/action_view_subscriber.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Tracing
- module Rails
- class ActionViewSubscriber
- include RailsCommon
-
- COMPONENT_TAG = 'ActionView'
- RENDER_TEMPLATE_NOTIFICATION_TOPIC = 'render_template.action_view'
- RENDER_COLLECTION_NOTIFICATION_TOPIC = 'render_collection.action_view'
- RENDER_PARTIAL_NOTIFICATION_TOPIC = 'render_partial.action_view'
-
- # Instruments Rails ActionView events for opentracing.
- # Returns a lambda, which, when called will unsubscribe from the notifications
- def self.instrument
- subscriber = new
-
- subscriptions = [
- ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
- subscriber.notify_render_template(start, finish, payload)
- end,
- ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
- subscriber.notify_render_collection(start, finish, payload)
- end,
- ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
- subscriber.notify_render_partial(start, finish, payload)
- end
- ]
-
- create_unsubscriber subscriptions
- end
-
- # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
- def notify_render_template(start, finish, payload)
- generate_span_for_notification("render_template", start, finish, payload, tags_for_render_template(payload))
- end
-
- def notify_render_collection(start, finish, payload)
- generate_span_for_notification("render_collection", start, finish, payload, tags_for_render_collection(payload))
- end
-
- def notify_render_partial(start, finish, payload)
- generate_span_for_notification("render_partial", start, finish, payload, tags_for_render_partial(payload))
- end
-
- private
-
- def tags_for_render_template(payload)
- {
- 'component' => COMPONENT_TAG,
- 'template.id' => payload[:identifier],
- 'template.layout' => payload[:layout]
- }
- end
-
- def tags_for_render_collection(payload)
- {
- 'component' => COMPONENT_TAG,
- 'template.id' => payload[:identifier],
- 'template.count' => payload[:count] || 0,
- 'template.cache.hits' => payload[:cache_hits] || 0
- }
- end
-
- def tags_for_render_partial(payload)
- {
- 'component' => COMPONENT_TAG,
- 'template.id' => payload[:identifier]
- }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/rails/active_record_subscriber.rb b/lib/gitlab/tracing/rails/active_record_subscriber.rb
deleted file mode 100644
index 32f5658e57e..00000000000
--- a/lib/gitlab/tracing/rails/active_record_subscriber.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Tracing
- module Rails
- class ActiveRecordSubscriber
- include RailsCommon
-
- ACTIVE_RECORD_NOTIFICATION_TOPIC = 'sql.active_record'
- OPERATION_NAME_PREFIX = 'active_record:'
- DEFAULT_OPERATION_NAME = 'sqlquery'
-
- # Instruments Rails ActiveRecord events for opentracing.
- # Returns a lambda, which, when called will unsubscribe from the notifications
- def self.instrument
- subscriber = new
-
- subscription = ActiveSupport::Notifications.subscribe(ACTIVE_RECORD_NOTIFICATION_TOPIC) do |_, start, finish, _, payload|
- subscriber.notify(start, finish, payload)
- end
-
- create_unsubscriber [subscription]
- end
-
- # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html
- def notify(start, finish, payload)
- generate_span_for_notification(notification_name(payload), start, finish, payload, tags_for_notification(payload))
- end
-
- private
-
- def notification_name(payload)
- OPERATION_NAME_PREFIX + (payload[:name].presence || DEFAULT_OPERATION_NAME)
- end
-
- def tags_for_notification(payload)
- {
- 'component' => 'ActiveRecord',
- 'span.kind' => 'client',
- 'db.type' => 'sql',
- 'db.connection_id' => payload[:connection_id],
- 'db.cached' => payload[:cached] || false,
- 'db.statement' => payload[:sql]
- }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/rails/rails_common.rb b/lib/gitlab/tracing/rails/rails_common.rb
deleted file mode 100644
index 88e914f62f8..00000000000
--- a/lib/gitlab/tracing/rails/rails_common.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Tracing
- module Rails
- module RailsCommon
- extend ActiveSupport::Concern
- include Gitlab::Tracing::Common
-
- class_methods do
- def create_unsubscriber(subscriptions)
- -> { subscriptions.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) } }
- end
- end
-
- def generate_span_for_notification(operation_name, start, finish, payload, tags)
- exception = payload[:exception]
-
- postnotify_span(operation_name, start, finish, tags: tags, exception: exception)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/sidekiq/client_middleware.rb b/lib/gitlab/tracing/sidekiq/client_middleware.rb
deleted file mode 100644
index 2b71c1ea21e..00000000000
--- a/lib/gitlab/tracing/sidekiq/client_middleware.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'opentracing'
-
-module Gitlab
- module Tracing
- module Sidekiq
- class ClientMiddleware
- include SidekiqCommon
-
- SPAN_KIND = 'client'
-
- def call(worker_class, job, queue, redis_pool)
- in_tracing_span(
- operation_name: "sidekiq:#{job['class']}",
- tags: tags_from_job(job, SPAN_KIND)) do |span|
- # Inject the details directly into the job
- tracer.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, job)
-
- yield
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/sidekiq/server_middleware.rb b/lib/gitlab/tracing/sidekiq/server_middleware.rb
deleted file mode 100644
index 5b43c4310e6..00000000000
--- a/lib/gitlab/tracing/sidekiq/server_middleware.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require 'opentracing'
-
-module Gitlab
- module Tracing
- module Sidekiq
- class ServerMiddleware
- include SidekiqCommon
-
- SPAN_KIND = 'server'
-
- def call(worker, job, queue)
- context = tracer.extract(OpenTracing::FORMAT_TEXT_MAP, job)
-
- in_tracing_span(
- operation_name: "sidekiq:#{job['class']}",
- child_of: context,
- tags: tags_from_job(job, SPAN_KIND)) do |span|
- yield
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/tracing/sidekiq/sidekiq_common.rb b/lib/gitlab/tracing/sidekiq/sidekiq_common.rb
deleted file mode 100644
index a911a29d773..00000000000
--- a/lib/gitlab/tracing/sidekiq/sidekiq_common.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Tracing
- module Sidekiq
- module SidekiqCommon
- include Gitlab::Tracing::Common
-
- def tags_from_job(job, kind)
- {
- 'component' => 'sidekiq',
- 'span.kind' => kind,
- 'sidekiq.queue' => job['queue'],
- 'sidekiq.jid' => job['jid'],
- 'sidekiq.retry' => job['retry'].to_s,
- 'sidekiq.args' => job['args']&.join(", ")
- }
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index f86d599e4cb..42cf1ec1f0e 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -30,6 +30,10 @@ module Gitlab
snippet_url(object)
when Milestone
milestone_url(object)
+ when ::Ci::Build
+ project_job_url(object.project, object)
+ when User
+ user_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 75477c7cf18..08156d7ffa6 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -117,9 +117,11 @@ module Gitlab
container_registry_enabled: Gitlab.config.registry.enabled,
gitlab_shared_runners_enabled: Gitlab.config.gitlab_ci.shared_runners_enabled,
gravatar_enabled: Gitlab::CurrentSettings.gravatar_enabled?,
+ influxdb_metrics_enabled: Gitlab::Metrics.influx_metrics_enabled?,
ldap_enabled: Gitlab.config.ldap.enabled,
mattermost_enabled: Gitlab.config.mattermost.enabled,
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?
}
diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb
index f0557f6ad68..ede60c9ab1d 100644
--- a/lib/gitlab/user_extractor.rb
+++ b/lib/gitlab/user_extractor.rb
@@ -7,7 +7,7 @@ module Gitlab
class UserExtractor
# Not using `Devise.email_regexp` to filter out any chars that an email
# does not end with and not pinning the email to a start of end of a string.
- EMAIL_REGEXP = /(?<email>([^@\s]+@[^@\s]+(?<!\W)))/
+ EMAIL_REGEXP = /(?<email>([^@\s]+@[^@\s]+(?<!\W)))/.freeze
USERNAME_REGEXP = User.reference_pattern
def initialize(text)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 0c2acac3d1e..46a7b5b982a 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -63,16 +63,34 @@ module Gitlab
]
end
- def send_git_archive(repository, ref:, format:, append_sha:)
+ def send_git_archive(repository, ref:, format:, append_sha:, path: nil)
+ path_enabled = Feature.enabled?(:git_archive_path, default_enabled: true)
+ path = nil unless path_enabled
+
format ||= 'tar.gz'
format = format.downcase
- params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format, append_sha: append_sha)
- raise "Repository or ref not found" if params.empty?
- params['GitalyServer'] = gitaly_server_hash(repository)
+ metadata = repository.archive_metadata(
+ ref,
+ Gitlab.config.gitlab.repository_downloads_path,
+ format,
+ append_sha: append_sha,
+ path: path
+ )
- # If present DisableCache must be a Boolean. Otherwise workhorse ignores it.
+ raise "Repository or ref not found" if metadata.empty?
+
+ params =
+ if path_enabled
+ send_git_archive_params(repository, metadata, path, archive_format(format))
+ else
+ metadata
+ end
+
+ # If present, DisableCache must be a Boolean. Otherwise
+ # workhorse ignores it.
params['DisableCache'] = true if git_archive_cache_disabled?
+ params['GitalyServer'] = gitaly_server_hash(repository)
[
SEND_DATA_HEADER,
@@ -216,10 +234,19 @@ module Gitlab
protected
+ # This is the outermost encoding of a senddata: header. It is safe for
+ # inclusion in HTTP response headers
def encode(hash)
Base64.urlsafe_encode64(JSON.dump(hash))
end
+ # This is for encoding individual fields inside the senddata JSON that
+ # contain binary data. In workhorse, the corresponding struct field should
+ # be type []byte
+ def encode_binary(binary)
+ Base64.encode64(binary)
+ end
+
def gitaly_server_hash(repository)
{
address: Gitlab::GitalyClient.address(repository.project.repository_storage),
@@ -238,6 +265,34 @@ module Gitlab
def git_archive_cache_disabled?
ENV['WORKHORSE_ARCHIVE_CACHE_DISABLED'].present? || Feature.enabled?(:workhorse_archive_cache_disabled)
end
+
+ def archive_format(format)
+ case format
+ when "tar.bz2", "tbz", "tbz2", "tb2", "bz2"
+ Gitaly::GetArchiveRequest::Format::TAR_BZ2
+ when "tar"
+ Gitaly::GetArchiveRequest::Format::TAR
+ when "zip"
+ Gitaly::GetArchiveRequest::Format::ZIP
+ else
+ Gitaly::GetArchiveRequest::Format::TAR_GZ
+ end
+ end
+
+ def send_git_archive_params(repository, metadata, path, format)
+ {
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => encode_binary(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: format,
+ path: path.presence || ""
+ ).to_proto
+ )
+ }
+ end
end
end
end
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index 2e98227a05e..1b17162f71d 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
unless Rails.env.production?
diff --git a/lib/peek/views/tracing.rb b/lib/peek/views/tracing.rb
index 0de32a8fdda..94726a498ea 100644
--- a/lib/peek/views/tracing.rb
+++ b/lib/peek/views/tracing.rb
@@ -4,9 +4,9 @@ module Peek
module Views
class Tracing < View
def results
- {
- tracing_url: Gitlab::Tracing.tracing_url
- }
+ tracing_url = Labkit::Tracing.tracing_url(Gitlab.process_name)
+
+ { tracing_url: tracing_url }
end
end
end
diff --git a/lib/quality/kubernetes_client.rb b/lib/quality/kubernetes_client.rb
index 2ff9e811425..190b48ba7cb 100644
--- a/lib/quality/kubernetes_client.rb
+++ b/lib/quality/kubernetes_client.rb
@@ -18,6 +18,8 @@ module Quality
'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}")
]
diff --git a/lib/quality/seeders/issues.rb b/lib/quality/seeders/issues.rb
new file mode 100644
index 00000000000..4c8cb6e97cc
--- /dev/null
+++ b/lib/quality/seeders/issues.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# rubocop:disable CodeReuse/ActiveRecord
+module Quality
+ module Seeders
+ class Issues
+ DEFAULT_BACKFILL_WEEKS = 52
+ DEFAULT_AVERAGE_ISSUES_PER_WEEK = 10
+
+ attr_reader :project, :user
+
+ def initialize(project:)
+ @project = project
+ end
+
+ def seed(backfill_weeks: DEFAULT_BACKFILL_WEEKS, average_issues_per_week: DEFAULT_AVERAGE_ISSUES_PER_WEEK)
+ created_at = backfill_weeks.to_i.weeks.ago
+ team = project.team.users
+ created_issues_count = 0
+
+ loop do
+ rand(average_issues_per_week * 2).times do
+ params = {
+ title: FFaker::Lorem.sentence(6),
+ description: FFaker::Lorem.sentence,
+ created_at: created_at + rand(6).days,
+ state: %w[opened closed].sample,
+ milestone: project.milestones.sample,
+ assignee_ids: Array(team.pluck(:id).sample(3)),
+ labels: labels.join(',')
+ }
+ issue = ::Issues::CreateService.new(project, team.sample, params).execute
+
+ if issue.persisted?
+ created_issues_count += 1
+ print '.' # rubocop:disable Rails/Output
+ end
+ end
+
+ created_at += 1.week
+
+ break if created_at > Time.now
+ end
+
+ created_issues_count
+ end
+
+ private
+
+ def labels
+ @labels_pool ||= project.labels.limit(rand(3)).pluck(:title).tap do |labels_array|
+ labels_array.concat(project.group.labels.limit(rand(3)).pluck(:title)) if project.group
+ end
+ end
+ end
+ end
+end
+# rubocop:enable CodeReuse/ActiveRecord
diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb
index 7c3e7759dd0..467711fb74e 100644
--- a/lib/system_check/app/git_version_check.rb
+++ b/lib/system_check/app/git_version_check.rb
@@ -7,7 +7,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.parse('2.18.0')
+ @required_version ||= Gitlab::VersionInfo.parse('2.21.0')
end
def self.current_version
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
index 60e07718338..53da62df176 100644
--- a/lib/system_check/app/ruby_version_check.rb
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -7,7 +7,7 @@ module SystemCheck
set_check_pass -> { "yes (#{self.current_version})" }
def self.required_version
- @required_version ||= Gitlab::VersionInfo.new(2, 3, 5)
+ @required_version ||= Gitlab::VersionInfo.new(2, 5, 3)
end
def self.current_version
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 560a52053d8..8cf7c9e89f0 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -209,7 +209,7 @@ namespace :gemojione do
image.destroy!
end
- EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
+ EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i.freeze
def rename_to_named_emoji_image!(emoji_unicode_string_to_name_map, image_path)
# Rename file from unicode to emoji name
matches = EMOJI_IMAGE_PATH_RE.match(image_path)
diff --git a/lib/tasks/gitlab/seed.rake b/lib/tasks/gitlab/seed.rake
new file mode 100644
index 00000000000..155ba979b36
--- /dev/null
+++ b/lib/tasks/gitlab/seed.rake
@@ -0,0 +1,34 @@
+namespace :gitlab do
+ namespace :seed do
+ desc "GitLab | Seed | Seeds issues"
+ task :issues, [:project_full_path] => :environment do |t, args|
+ projects =
+ if args.project_full_path
+ project = Project.find_by_full_path(args.project_full_path)
+
+ unless project
+ error_message = "Project '#{args.project_full_path}' does not exist!"
+ potential_projects = Project.search(args.project_full_path)
+
+ if potential_projects.present?
+ error_message += " Did you mean '#{potential_projects.first.full_path}'?"
+ end
+
+ puts error_message.color(:red)
+ exit 1
+ end
+
+ [project]
+ else
+ Project.find_each
+ end
+
+ projects.each do |project|
+ puts "\nSeeding issues for the '#{project.full_path}' project"
+ seeder = Quality::Seeders::Issues.new(project: project)
+ issues_created = seeder.seed(backfill_weeks: 5, average_issues_per_week: 2)
+ puts "\n#{issues_created} issues created!"
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 674a1d924f2..5aa048c28a3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -44,6 +44,11 @@ msgstr[1] ""
msgid "%d commits"
msgstr ""
+msgid "%d contribution"
+msgid_plural "%d contributions"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d exporter"
msgid_plural "%d exporters"
msgstr[0] ""
@@ -79,6 +84,11 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "%d more comment"
+msgid_plural "%d more comments"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d staged change"
msgid_plural "%d staged changes"
msgstr[0] ""
@@ -100,7 +110,7 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
-msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
+msgid "%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
msgstr ""
msgid "%{count} more"
@@ -114,6 +124,12 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
+msgid "%{edit_in_new_fork_notice} Try to cherry-pick this commit again."
+msgstr ""
+
+msgid "%{edit_in_new_fork_notice} Try to revert this commit again."
+msgstr ""
+
msgid "%{filePath} deleted"
msgstr ""
@@ -132,6 +148,9 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
+msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
+msgstr ""
+
msgid "%{level_name} is not allowed in a %{group_level_name} group."
msgstr ""
@@ -153,6 +172,12 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{name} contained %{resultsString}"
+msgstr ""
+
+msgid "%{name} found %{resultsString}"
+msgstr ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -209,6 +234,9 @@ msgstr ""
msgid "%{user_name} profile page"
msgstr ""
+msgid "%{verb} %{time_spent_value} spent time."
+msgstr ""
+
msgid "'%{level}' is not a valid visibility level"
msgstr ""
@@ -236,6 +264,9 @@ msgstr ""
msgid "- show less"
msgstr ""
+msgid "0 for unlimited"
+msgstr ""
+
msgid "1 %{type} addition"
msgid_plural "%{count} %{type} additions"
msgstr[0] ""
@@ -282,9 +313,18 @@ msgstr[1] ""
msgid "1 week"
msgstr ""
+msgid "1-9 contributions"
+msgstr ""
+
+msgid "10-19 contributions"
+msgstr ""
+
msgid "1st contribution!"
msgstr ""
+msgid "20-29 contributions"
+msgstr ""
+
msgid "2FA"
msgstr ""
@@ -300,6 +340,9 @@ msgstr ""
msgid "30 minutes"
msgstr ""
+msgid "30+ contributions"
+msgstr ""
+
msgid "403|Please contact your GitLab administrator to get permission."
msgstr ""
@@ -357,19 +400,19 @@ msgstr ""
msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
-msgid "A default branch cannot be chosen for an empty project."
+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 deleted user"
+msgid "A default branch cannot be chosen for an empty project."
msgstr ""
-msgid "A member of GitLab's abuse team will review your report as soon as possible."
+msgid "A deleted user"
msgstr ""
-msgid "A merge commit is created for every merge, and merging is allowed as long as there are no conflicts."
+msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
msgstr ""
-msgid "A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible. This way you could make sure that if this merge request would build, after merging to target branch it would also build."
+msgid "A member of GitLab's abuse team will review your report as soon as possible."
msgstr ""
msgid "A new branch will be created in your fork and a new merge request will be started."
@@ -486,9 +529,15 @@ msgstr ""
msgid "Add a task list"
msgstr ""
+msgid "Add a todo"
+msgstr ""
+
msgid "Add bold text"
msgstr ""
+msgid "Add comment now"
+msgstr ""
+
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr ""
@@ -498,6 +547,9 @@ msgstr ""
msgid "Add italic text"
msgstr ""
+msgid "Add label(s)"
+msgstr ""
+
msgid "Add license"
msgstr ""
@@ -510,12 +562,18 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "Add or subtract spent time"
+msgstr ""
+
msgid "Add reaction"
msgstr ""
msgid "Add to project"
msgstr ""
+msgid "Add to review"
+msgstr ""
+
msgid "Add todo"
msgstr ""
@@ -537,6 +595,12 @@ msgstr ""
msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission"
msgstr ""
+msgid "Adds"
+msgstr ""
+
+msgid "Adds a todo."
+msgstr ""
+
msgid "Admin Area"
msgstr ""
@@ -690,6 +754,9 @@ msgstr ""
msgid "All merge conflicts were resolved. The merge request can now be merged."
msgstr ""
+msgid "All projects"
+msgstr ""
+
msgid "All todos were marked as done."
msgstr ""
@@ -747,9 +814,6 @@ msgstr ""
msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication"
msgstr ""
-msgid "An SSH key will be automatically generated when the form is submitted. For more information, please refer to the documentation."
-msgstr ""
-
msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr ""
@@ -771,6 +835,12 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
+msgid "An error occurred when trying to resolve a comment. Please try again."
+msgstr ""
+
+msgid "An error occurred when trying to resolve a discussion. Please try again."
+msgstr ""
+
msgid "An error occurred while detecting host keys"
msgstr ""
@@ -780,15 +850,24 @@ msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
+msgid "An error occurred while fetching label colors."
+msgstr ""
+
msgid "An error occurred while fetching markdown preview"
msgstr ""
+msgid "An error occurred while fetching projects autocomplete."
+msgstr ""
+
msgid "An error occurred while fetching sidebar data"
msgstr ""
msgid "An error occurred while fetching stages."
msgstr ""
+msgid "An error occurred while fetching the board lists. Please try again."
+msgstr ""
+
msgid "An error occurred while fetching the job log."
msgstr ""
@@ -825,6 +904,12 @@ msgstr ""
msgid "An error occurred while making the request."
msgstr ""
+msgid "An error occurred while moving the issue."
+msgstr ""
+
+msgid "An error occurred while parsing recent searches"
+msgstr ""
+
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -897,6 +982,12 @@ msgstr ""
msgid "Appearance was successfully updated."
msgstr ""
+msgid "Append the comment with %{TABLEFLIP}"
+msgstr ""
+
+msgid "Append the comment with %{shrug}"
+msgstr ""
+
msgid "Application"
msgstr ""
@@ -906,6 +997,9 @@ msgstr ""
msgid "Application settings saved successfully"
msgstr ""
+msgid "Application uninstalled but failed to destroy: %{error_message}"
+msgstr ""
+
msgid "Application was successfully destroyed."
msgstr ""
@@ -924,6 +1018,18 @@ msgstr ""
msgid "Apply suggestion"
msgstr ""
+msgid "Applying command"
+msgstr ""
+
+msgid "Applying command to %{commandDescription}"
+msgstr ""
+
+msgid "Applying multiple commands"
+msgstr ""
+
+msgid "Applying suggestion"
+msgstr ""
+
msgid "Apr"
msgstr ""
@@ -954,6 +1060,9 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
+msgid "Are you sure you want to delete this list?"
+msgstr ""
+
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr ""
@@ -1014,6 +1123,9 @@ msgstr ""
msgid "Assets"
msgstr ""
+msgid "Assign"
+msgstr ""
+
msgid "Assign custom color like #FF0000"
msgstr ""
@@ -1029,6 +1141,12 @@ msgstr ""
msgid "Assign to"
msgstr ""
+msgid "Assign yourself to these issues"
+msgstr ""
+
+msgid "Assign yourself to this issue"
+msgstr ""
+
msgid "Assigned Issues"
msgstr ""
@@ -1134,10 +1252,10 @@ msgstr ""
msgid "Automatically marked as default internal user"
msgstr ""
-msgid "Automatically resolve merge request diff discussions when they become outdated"
+msgid "Automatically resolved"
msgstr ""
-msgid "Automatically resolved"
+msgid "Autosave|Note"
msgstr ""
msgid "Available"
@@ -1251,6 +1369,9 @@ msgstr ""
msgid "Badges|e.g. %{exampleUrl}"
msgstr ""
+msgid "Balsamiq file could not be loaded."
+msgstr ""
+
msgid "BambooService|A continuous integration and build server"
msgstr ""
@@ -1545,6 +1666,9 @@ msgstr ""
msgid "Can't find HEAD commit for this branch"
msgstr ""
+msgid "Can't find variable: ZiteReader"
+msgstr ""
+
msgid "Cancel"
msgstr ""
@@ -1569,6 +1693,9 @@ msgstr ""
msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
msgstr ""
+msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above."
+msgstr ""
+
msgid "Cannot skip two factor authentication setup"
msgstr ""
@@ -1590,6 +1717,9 @@ msgstr ""
msgid "Change this value to influence how frequently the GitLab UI polls for updates."
msgstr ""
+msgid "Change title"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -1614,6 +1744,9 @@ msgstr ""
msgid "Changes suppressed. Click to show."
msgstr ""
+msgid "Changes the title to \"%{title_param}\"."
+msgstr ""
+
msgid "Charts"
msgstr ""
@@ -1656,6 +1789,9 @@ msgstr ""
msgid "Choose a role permission"
msgstr ""
+msgid "Choose a template"
+msgstr ""
+
msgid "Choose a template..."
msgstr ""
@@ -1677,7 +1813,7 @@ msgstr ""
msgid "Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions."
msgstr ""
-msgid "Choose your merge method, set up a default merge request description template."
+msgid "Choose your merge method, options, checks, and set up a default merge request description template."
msgstr ""
msgid "CiStatusLabel|canceled"
@@ -1749,6 +1885,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "CiVariables|Cannot use Masked Variable with current value"
+msgstr ""
+
msgid "CiVariables|Input variable key"
msgstr ""
@@ -1758,9 +1897,6 @@ msgstr ""
msgid "CiVariables|Remove variable row"
msgstr ""
-msgid "CiVariables|This variable will not be masked"
-msgstr ""
-
msgid "CiVariable|* (All environments)"
msgstr ""
@@ -1851,6 +1987,9 @@ msgstr ""
msgid "Close milestone"
msgstr ""
+msgid "Close sidebar"
+msgstr ""
+
msgid "Closed"
msgstr ""
@@ -1866,9 +2005,15 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|%{title} uninstalled successfully."
+msgstr ""
+
msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr ""
+msgid "ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges."
+msgstr ""
+
msgid "ClusterIntegration|API URL"
msgstr ""
@@ -1884,12 +2029,24 @@ msgstr ""
msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
msgstr ""
+msgid "ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
+msgstr ""
+
msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects."
msgstr ""
+msgid "ClusterIntegration|Adding an integration will share the cluster across all projects."
+msgstr ""
+
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|All data will be deleted and cannot be restored."
+msgstr ""
+
+msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster."
+msgstr ""
+
msgid "ClusterIntegration|Alternatively"
msgstr ""
@@ -1905,6 +2062,9 @@ msgstr ""
msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}"
msgstr ""
+msgid "ClusterIntegration|Any running pipelines will be canceled."
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -2010,6 +2170,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab Runner connects to the repository and executes CI/CD jobs, pushing results back and deploying applications to production."
msgstr ""
+msgid "ClusterIntegration|GitLab-managed cluster"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
@@ -2055,6 +2218,9 @@ msgstr ""
msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}."
msgstr ""
+msgid "ClusterIntegration|Instance cluster"
+msgstr ""
+
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr ""
@@ -2121,6 +2287,9 @@ msgstr ""
msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr ""
+msgid "ClusterIntegration|Learn more about instance Kubernetes clusters"
+msgstr ""
+
msgid "ClusterIntegration|Let's Encrypt"
msgstr ""
@@ -2196,6 +2365,9 @@ msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
+msgid "ClusterIntegration|Request to begin uninstalling failed"
+msgstr ""
+
msgid "ClusterIntegration|Retry update"
msgstr ""
@@ -2250,9 +2422,27 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
+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|The Kubernetes certificate used to authenticate to the cluster."
+msgstr ""
+
+msgid "ClusterIntegration|The URL used to access the Kubernetes API."
+msgstr ""
+
+msgid "ClusterIntegration|The associated IP will be deleted and cannot be restored."
+msgstr ""
+
+msgid "ClusterIntegration|The associated certifcate will be deleted and cannot be restored."
+msgstr ""
+
+msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
+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 ""
@@ -2268,6 +2458,9 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|Uninstall %{appTitle}"
+msgstr ""
+
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
@@ -2295,6 +2488,9 @@ msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
+msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
+msgstr ""
+
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
@@ -2340,9 +2536,15 @@ msgstr ""
msgid "Collapse sidebar"
msgstr ""
+msgid "ComboSearch is not defined"
+msgstr ""
+
msgid "Command line instructions"
msgstr ""
+msgid "Commands applied"
+msgstr ""
+
msgid "Comment"
msgstr ""
@@ -2474,6 +2676,9 @@ msgstr ""
msgid "Configure Gitaly timeouts."
msgstr ""
+msgid "Configure Let's Encrypt"
+msgstr ""
+
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
@@ -2501,6 +2706,9 @@ msgstr ""
msgid "Connect repositories from GitHub"
msgstr ""
+msgid "Connection failure"
+msgstr ""
+
msgid "Container Registry"
msgstr ""
@@ -2600,6 +2808,9 @@ msgstr ""
msgid "ConvDev Index"
msgstr ""
+msgid "Copied"
+msgstr ""
+
msgid "Copy %{http_label} clone URL"
msgstr ""
@@ -2615,9 +2826,6 @@ msgstr ""
msgid "Copy SSH public key"
msgstr ""
-msgid "Copy SSH public key to clipboard"
-msgstr ""
-
msgid "Copy URL to clipboard"
msgstr ""
@@ -2633,6 +2841,12 @@ msgstr ""
msgid "Copy file path to clipboard"
msgstr ""
+msgid "Copy labels and milestone from %{source_issuable_reference}."
+msgstr ""
+
+msgid "Copy labels and milestone from other issue or merge request"
+msgstr ""
+
msgid "Copy link"
msgstr ""
@@ -2642,6 +2856,9 @@ msgstr ""
msgid "Copy secret to clipboard"
msgstr ""
+msgid "Copy source to clipboard"
+msgstr ""
+
msgid "Copy to clipboard"
msgstr ""
@@ -2690,6 +2907,9 @@ msgstr ""
msgid "Create a GitLab account first, and then connect it to your %{label} account."
msgstr ""
+msgid "Create a merge request."
+msgstr ""
+
msgid "Create a new branch"
msgstr ""
@@ -2786,6 +3006,9 @@ msgstr ""
msgid "Created on:"
msgstr ""
+msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue"
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -2867,12 +3090,27 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "DashboardProjects|Trending"
+msgstr ""
+
msgid "Data is still calculating..."
msgstr ""
msgid "Date picker"
msgstr ""
+msgid "DayTitle|F"
+msgstr ""
+
+msgid "DayTitle|M"
+msgstr ""
+
+msgid "DayTitle|S"
+msgstr ""
+
+msgid "DayTitle|W"
+msgstr ""
+
msgid "Debug"
msgstr ""
@@ -3166,6 +3404,9 @@ msgstr ""
msgid "Disabled"
msgstr ""
+msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
+msgstr ""
+
msgid "Discard"
msgstr ""
@@ -3199,6 +3440,9 @@ msgstr ""
msgid "Discuss a specific suggestion or question that needs to be resolved"
msgstr ""
+msgid "Discussion"
+msgstr ""
+
msgid "Dismiss"
msgstr ""
@@ -3211,9 +3455,15 @@ msgstr ""
msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?"
msgstr ""
+msgid "Dockerfile"
+msgstr ""
+
msgid "Domain"
msgstr ""
+msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -3232,19 +3482,10 @@ msgstr ""
msgid "Download export"
msgstr ""
-msgid "Download tar"
+msgid "Download source code"
msgstr ""
-msgid "Download tar.bz2"
-msgstr ""
-
-msgid "Download tar.gz"
-msgstr ""
-
-msgid "Download zip"
-msgstr ""
-
-msgid "DownloadArtifacts|Download"
+msgid "Download this directory"
msgstr ""
msgid "DownloadCommit|Email Patches"
@@ -3622,6 +3863,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching projects"
+msgstr ""
+
msgid "Error fetching refs"
msgstr ""
@@ -3634,6 +3878,9 @@ msgstr ""
msgid "Error loading branches."
msgstr ""
+msgid "Error loading file viewer."
+msgstr ""
+
msgid "Error loading last commit."
msgstr ""
@@ -3652,6 +3899,12 @@ msgstr ""
msgid "Error loading template."
msgstr ""
+msgid "Error loading viewer"
+msgstr ""
+
+msgid "Error occurred when fetching sidebar data"
+msgstr ""
+
msgid "Error occurred when toggling the notification subscription"
msgstr ""
@@ -3685,9 +3938,15 @@ msgstr ""
msgid "Error uploading file"
msgstr ""
+msgid "Error uploading file: %{stripped}"
+msgstr ""
+
msgid "Error while loading the merge request. Please try again."
msgstr ""
+msgid "Error while loading the project data. Please try again."
+msgstr ""
+
msgid "Error while migrating %{upload_id}: %{error_message}"
msgstr ""
@@ -3859,6 +4118,18 @@ msgstr ""
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
msgstr ""
+msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards."
+msgstr ""
+
+msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to"
+msgstr ""
+
+msgid "ExternalMetrics|External Dashboard"
+msgstr ""
+
+msgid "ExternalMetrics|Full dashboard URL"
+msgstr ""
+
msgid "ExternalWikiService|External Wiki"
msgstr ""
@@ -3886,6 +4157,9 @@ msgstr ""
msgid "Failed to check related branches."
msgstr ""
+msgid "Failed to connect to the prometheus server"
+msgstr ""
+
msgid "Failed to create repository via gitlab-shell"
msgstr ""
@@ -3895,6 +4169,9 @@ msgstr ""
msgid "Failed to deploy to"
msgstr ""
+msgid "Failed to install."
+msgstr ""
+
msgid "Failed to load emoji list."
msgstr ""
@@ -3919,6 +4196,9 @@ msgstr ""
msgid "Failed to remove user key."
msgstr ""
+msgid "Failed to save merge conflicts resolutions. Please try again!"
+msgstr ""
+
msgid "Failed to save new settings"
msgstr ""
@@ -3931,13 +4211,19 @@ msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
-msgid "Failed to upload object map file"
+msgid "Failed to update tag!"
msgstr ""
-msgid "Failure"
+msgid "Failed to update."
msgstr ""
-msgid "Fast-forward merge"
+msgid "Failed to upgrade."
+msgstr ""
+
+msgid "Failed to upload object map file"
+msgstr ""
+
+msgid "Failure"
msgstr ""
msgid "Fast-forward merge without a merge commit"
@@ -4086,6 +4372,15 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
+msgid "Fork"
+msgstr ""
+
+msgid "Fork Error!"
+msgstr ""
+
+msgid "Fork project"
+msgstr ""
+
msgid "ForkedFromProjectPath|Forked from"
msgstr ""
@@ -4095,6 +4390,9 @@ msgstr ""
msgid "Forking in progress"
msgstr ""
+msgid "Forking repository"
+msgstr ""
+
msgid "Forks"
msgstr ""
@@ -4257,6 +4555,9 @@ msgstr ""
msgid "Go to project"
msgstr ""
+msgid "Go to your fork"
+msgstr ""
+
msgid "Google Code import"
msgstr ""
@@ -4455,6 +4756,9 @@ msgstr ""
msgid "GroupsTree|Search by name"
msgstr ""
+msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
+msgstr ""
+
msgid "Header logo was successfully removed."
msgstr ""
@@ -4488,12 +4792,15 @@ msgstr ""
msgid "Help page text and support page url."
msgstr ""
-msgid "Here is the public SSH key that needs to be added to the remote server. For more information, please refer to the documentation."
+msgid "Hide archived projects"
msgstr ""
msgid "Hide file browser"
msgstr ""
+msgid "Hide group projects"
+msgstr ""
+
msgid "Hide host keys manual input"
msgstr ""
@@ -4503,6 +4810,9 @@ msgstr ""
msgid "Hide payload"
msgstr ""
+msgid "Hide shared projects"
+msgstr ""
+
msgid "Hide value"
msgid_plural "Hide values"
msgstr[0] ""
@@ -4541,6 +4851,9 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
+msgid "I have read and agree to the Let's Encrypt Terms of Service"
+msgstr ""
+
msgid "ID"
msgstr ""
@@ -4604,6 +4917,12 @@ msgstr ""
msgid "If enabled, access to projects will be validated on an external service using their classification label."
msgstr ""
+msgid "If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}."
+msgstr ""
+
+msgid "If this was a mistake you can leave the %{source_type}."
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
msgstr ""
@@ -4829,6 +5148,9 @@ msgstr ""
msgid "Invalid feature"
msgstr ""
+msgid "Invalid field"
+msgstr ""
+
msgid "Invalid file."
msgstr ""
@@ -5039,6 +5361,12 @@ msgstr ""
msgid "July"
msgstr ""
+msgid "Jump to first unresolved discussion"
+msgstr ""
+
+msgid "Jump to next unresolved discussion"
+msgstr ""
+
msgid "Jun"
msgstr ""
@@ -5072,6 +5400,9 @@ msgstr ""
msgid "Kubernetes configured"
msgstr ""
+msgid "Kubernetes error: %{error_code}"
+msgstr ""
+
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
@@ -5087,6 +5418,9 @@ msgstr ""
msgid "LFSStatus|Enabled"
msgstr ""
+msgid "LICENSE"
+msgstr ""
+
msgid "Label"
msgstr ""
@@ -5108,6 +5442,9 @@ msgstr ""
msgid "LabelSelect|%{labelsString}, and %{remainingLabelCount} more"
msgstr ""
+msgid "LabelSelect|Labels"
+msgstr ""
+
msgid "Labels"
msgstr ""
@@ -5185,6 +5522,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -5221,6 +5561,9 @@ msgstr ""
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
msgstr ""
+msgid "Let's Encrypt does not accept emails on example.com"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
@@ -5271,6 +5614,9 @@ msgstr ""
msgid "Lock not found"
msgstr ""
+msgid "Lock the discussion"
+msgstr ""
+
msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment."
msgstr ""
@@ -5283,6 +5629,9 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
+msgid "Locks the discussion"
+msgstr ""
+
msgid "Logo was successfully removed."
msgstr ""
@@ -5295,9 +5644,15 @@ msgstr ""
msgid "MRDiff|Show full file"
msgstr ""
+msgid "Make issue confidential."
+msgstr ""
+
msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr ""
+msgid "Makes this issue confidential"
+msgstr ""
+
msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
msgstr ""
@@ -5355,6 +5710,12 @@ msgstr ""
msgid "March"
msgstr ""
+msgid "Mark as resolved"
+msgstr ""
+
+msgid "Mark this issue as a duplicate of another issue"
+msgstr ""
+
msgid "Mark todo as done"
msgstr ""
@@ -5367,6 +5728,12 @@ msgstr ""
msgid "Markdown enabled"
msgstr ""
+msgid "Marks this issue as a duplicate of %{duplicate_reference}."
+msgstr ""
+
+msgid "Marks todo as done."
+msgstr ""
+
msgid "Max access level"
msgstr ""
@@ -5409,15 +5776,9 @@ msgstr ""
msgid "Merge Requests"
msgstr ""
-msgid "Merge commit"
-msgstr ""
-
msgid "Merge commit message"
msgstr ""
-msgid "Merge commit with semi-linear history"
-msgstr ""
-
msgid "Merge events"
msgstr ""
@@ -5427,9 +5788,6 @@ msgstr ""
msgid "Merge in progress"
msgstr ""
-msgid "Merge method"
-msgstr ""
-
msgid "Merge request"
msgstr ""
@@ -5442,6 +5800,30 @@ msgstr ""
msgid "Merge when pipeline succeeds"
msgstr ""
+msgid "MergeConflict|Commit to source branch"
+msgstr ""
+
+msgid "MergeConflict|Committing..."
+msgstr ""
+
+msgid "MergeConflict|HEAD//our changes"
+msgstr ""
+
+msgid "MergeConflict|Use ours"
+msgstr ""
+
+msgid "MergeConflict|Use theirs"
+msgstr ""
+
+msgid "MergeConflict|conflict"
+msgstr ""
+
+msgid "MergeConflict|conflicts"
+msgstr ""
+
+msgid "MergeConflict|origin//their changes"
+msgstr ""
+
msgid "MergeRequests|Add a reply"
msgstr ""
@@ -5538,9 +5920,6 @@ msgstr ""
msgid "Metrics|No deployed environments"
msgstr ""
-msgid "Metrics|Not enough data to display"
-msgstr ""
-
msgid "Metrics|Show last"
msgstr ""
@@ -5670,12 +6049,24 @@ msgstr ""
msgid "Move issue"
msgstr ""
+msgid "Move issue from one column of the board to another"
+msgstr ""
+
+msgid "Move this issue to another project."
+msgstr ""
+
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
msgstr ""
msgid "MoveIssue|Cannot move issue to project it originates from!"
msgstr ""
+msgid "Moves issue to %{label} column in the board."
+msgstr ""
+
+msgid "Moves this issue to %{path_to_project}."
+msgstr ""
+
msgid "Multiple model types found: %{model_types}"
msgstr ""
@@ -5685,6 +6076,9 @@ msgstr ""
msgid "Name"
msgstr ""
+msgid "Name has already been taken"
+msgstr ""
+
msgid "Name new label"
msgstr ""
@@ -5825,18 +6219,27 @@ msgstr ""
msgid "Newly registered users will by default be external"
msgstr ""
+msgid "Next"
+msgstr ""
+
msgid "No"
msgstr ""
msgid "No %{providerTitle} repositories available to import"
msgstr ""
+msgid "No Milestone"
+msgstr ""
+
msgid "No Tag"
msgstr ""
msgid "No activities found"
msgstr ""
+msgid "No available namespaces to fork the project."
+msgstr ""
+
msgid "No branches found"
msgstr ""
@@ -5852,13 +6255,13 @@ msgstr ""
msgid "No container images stored for this project. Add one by following the instructions above."
msgstr ""
-msgid "No contributions were found"
+msgid "No contributions"
msgstr ""
-msgid "No details available"
+msgid "No contributions were found"
msgstr ""
-msgid "No due date"
+msgid "No details available"
msgstr ""
msgid "No errors to display."
@@ -5885,9 +6288,6 @@ msgstr ""
msgid "No license. All rights reserved"
msgstr ""
-msgid "No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded. When fast-forward merge is not possible, the user is given the option to rebase."
-msgstr ""
-
msgid "No merge requests found"
msgstr ""
@@ -5900,6 +6300,9 @@ msgstr ""
msgid "No other labels with such name or description"
msgstr ""
+msgid "No parent group"
+msgstr ""
+
msgid "No preview for this file type"
msgstr ""
@@ -5975,6 +6378,9 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Nothing to preview."
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -6082,12 +6488,6 @@ msgstr ""
msgid "Only admins"
msgstr ""
-msgid "Only allow merge requests to be merged if all discussions are resolved"
-msgstr ""
-
-msgid "Only allow merge requests to be merged if the pipeline succeeds"
-msgstr ""
-
msgid "Only mirror protected branches"
msgstr ""
@@ -6136,6 +6536,12 @@ msgstr ""
msgid "Opens in a new window"
msgstr ""
+msgid "Operation failed. Check pod logs for %{pod_name} for more details."
+msgstr ""
+
+msgid "Operation timed out. Check pod logs for %{pod_name} for more details."
+msgstr ""
+
msgid "Operations"
msgstr ""
@@ -6166,6 +6572,12 @@ msgstr ""
msgid "Overview"
msgstr ""
+msgid "Owned by anyone"
+msgstr ""
+
+msgid "Owned by me"
+msgstr ""
+
msgid "Owner"
msgstr ""
@@ -6352,10 +6764,10 @@ msgstr ""
msgid "Pipelines for last year"
msgstr ""
-msgid "Pipelines need to be configured to enable this feature."
+msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines settings for '%{project_name}' were successfully updated."
+msgid "Pipelines|API"
msgstr ""
msgid "Pipelines|Build with confidence"
@@ -6436,6 +6848,9 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
+msgid "Pipeline|Triggerer"
+msgstr ""
+
msgid "Pipeline|Variables"
msgstr ""
@@ -6484,12 +6899,18 @@ msgstr ""
msgid "Play"
msgstr ""
+msgid "Play all manual"
+msgstr ""
+
msgid "Please %{link_to_register} or %{link_to_sign_in} to comment"
msgstr ""
msgid "Please accept the Terms of Service before continuing."
msgstr ""
+msgid "Please add a list to your board first"
+msgstr ""
+
msgid "Please choose a group URL with no special characters."
msgstr ""
@@ -6514,6 +6935,9 @@ msgstr ""
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr ""
+msgid "Please select a file"
+msgstr ""
+
msgid "Please select a group."
msgstr ""
@@ -6550,18 +6974,27 @@ msgstr ""
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr ""
+msgid "Press %{key}-C to copy"
+msgstr ""
+
msgid "Press Enter or click to search"
msgstr ""
msgid "Preview"
msgstr ""
+msgid "Preview Markdown"
+msgstr ""
+
msgid "Preview changes"
msgstr ""
msgid "Preview payload"
msgstr ""
+msgid "Previous Artifacts"
+msgstr ""
+
msgid "Prioritize"
msgstr ""
@@ -6985,15 +7418,120 @@ msgstr ""
msgid "ProjectPage|Project ID: %{project_id}"
msgstr ""
+msgid "ProjectSettings|Additional merge request capabilities that influence how and when merges will be performed"
+msgstr ""
+
+msgid "ProjectSettings|All discussions must be resolved"
+msgstr ""
+
+msgid "ProjectSettings|Automatically resolve merge request diff discussions when they become outdated"
+msgstr ""
+
msgid "ProjectSettings|Badges"
msgstr ""
msgid "ProjectSettings|Customize your project badges."
msgstr ""
+msgid "ProjectSettings|Every merge creates a merge commit"
+msgstr ""
+
+msgid "ProjectSettings|Fast-forward merge"
+msgstr ""
+
+msgid "ProjectSettings|Fast-forward merges only"
+msgstr ""
+
msgid "ProjectSettings|Learn more about badges."
msgstr ""
+msgid "ProjectSettings|Merge checks"
+msgstr ""
+
+msgid "ProjectSettings|Merge commit"
+msgstr ""
+
+msgid "ProjectSettings|Merge commit with semi-linear history"
+msgstr ""
+
+msgid "ProjectSettings|Merge method"
+msgstr ""
+
+msgid "ProjectSettings|Merge options"
+msgstr ""
+
+msgid "ProjectSettings|No merge commits are created"
+msgstr ""
+
+msgid "ProjectSettings|Pipelines must succeed"
+msgstr ""
+
+msgid "ProjectSettings|Pipelines need to be configured to enable this feature."
+msgstr ""
+
+msgid "ProjectSettings|Show link to create/view merge request when pushing from the command line"
+msgstr ""
+
+msgid "ProjectSettings|These checks must pass before merge requests can be merged"
+msgstr ""
+
+msgid "ProjectSettings|This will dictate the commit history when you merge a merge request"
+msgstr ""
+
+msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
+msgstr ""
+
+msgid "ProjectTemplates|.NET Core"
+msgstr ""
+
+msgid "ProjectTemplates|Android"
+msgstr ""
+
+msgid "ProjectTemplates|Go Micro"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/GitBook"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Hexo"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Hugo"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Jekyll"
+msgstr ""
+
+msgid "ProjectTemplates|Netlify/Plain HTML"
+msgstr ""
+
+msgid "ProjectTemplates|NodeJS Express"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/GitBook"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Hexo"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Hugo"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Jekyll"
+msgstr ""
+
+msgid "ProjectTemplates|Pages/Plain HTML"
+msgstr ""
+
+msgid "ProjectTemplates|Ruby on Rails"
+msgstr ""
+
+msgid "ProjectTemplates|Spring"
+msgstr ""
+
+msgid "ProjectTemplates|iOS (Swift)"
+msgstr ""
+
msgid "Projects"
msgstr ""
@@ -7117,6 +7655,9 @@ msgstr ""
msgid "Protected"
msgstr ""
+msgid "Protected Tag"
+msgstr ""
+
msgid "Protip:"
msgstr ""
@@ -7222,6 +7763,9 @@ msgstr ""
msgid "Recent Project Activity"
msgstr ""
+msgid "Recent Searches Service is unavailable"
+msgstr ""
+
msgid "Recent searches"
msgstr ""
@@ -7293,33 +7837,69 @@ msgstr ""
msgid "Remove Runner"
msgstr ""
+msgid "Remove all or specific assignee(s)"
+msgstr ""
+
+msgid "Remove all or specific label(s)"
+msgstr ""
+
+msgid "Remove assignee"
+msgstr ""
+
msgid "Remove avatar"
msgstr ""
+msgid "Remove due date"
+msgstr ""
+
msgid "Remove fork relationship"
msgstr ""
msgid "Remove group"
msgstr ""
+msgid "Remove milestone"
+msgstr ""
+
msgid "Remove priority"
msgstr ""
msgid "Remove project"
msgstr ""
+msgid "Remove spent time"
+msgstr ""
+
msgid "Remove this label? Are you sure?"
msgstr ""
msgid "Remove this label? This will affect all projects within the group. Are you sure?"
msgstr ""
+msgid "Remove time estimate"
+msgstr ""
+
msgid "Removed group can not be restored!"
msgstr ""
msgid "Removed projects cannot be restored!"
msgstr ""
+msgid "Removes %{milestone_reference} milestone."
+msgstr ""
+
+msgid "Removes all labels."
+msgstr ""
+
+msgid "Removes spent time."
+msgstr ""
+
+msgid "Removes the due date."
+msgstr ""
+
+msgid "Removes time estimate."
+msgstr ""
+
msgid "Removing group will cause all child projects and resources to be removed."
msgstr ""
@@ -7341,6 +7921,9 @@ msgstr ""
msgid "Replace"
msgstr ""
+msgid "Replace all label(s)"
+msgstr ""
+
msgid "Reply to comment"
msgstr ""
@@ -7422,6 +8005,9 @@ msgstr ""
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr ""
+msgid "Require users to prove ownership of custom domains"
+msgstr ""
+
msgid "Resend invite"
msgstr ""
@@ -7449,6 +8035,9 @@ msgstr ""
msgid "Resolved all discussions."
msgstr ""
+msgid "Resolved by %{resolvedByName}"
+msgstr ""
+
msgid "Response metrics (AWS ELB)"
msgstr ""
@@ -7598,6 +8187,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
+msgid "Save anyway"
+msgstr ""
+
msgid "Save application"
msgstr ""
@@ -7664,6 +8256,9 @@ msgstr ""
msgid "Search for projects, issues, etc."
msgstr ""
+msgid "Search forks"
+msgstr ""
+
msgid "Search groups"
msgstr ""
@@ -7685,6 +8280,9 @@ msgstr ""
msgid "Search projects"
msgstr ""
+msgid "Search projects..."
+msgstr ""
+
msgid "Search users"
msgstr ""
@@ -7880,9 +8478,15 @@ msgstr ""
msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
msgstr ""
+msgid "Set due date"
+msgstr ""
+
msgid "Set max session time for web terminal."
msgstr ""
+msgid "Set milestone"
+msgstr ""
+
msgid "Set new password"
msgstr ""
@@ -7901,6 +8505,9 @@ msgstr ""
msgid "Set the maximum file size for each job's artifacts"
msgstr ""
+msgid "Set time estimate"
+msgstr ""
+
msgid "Set up CI/CD"
msgstr ""
@@ -7946,6 +8553,15 @@ msgstr ""
msgid "SetStatusModal|What's your status?"
msgstr ""
+msgid "Sets the due date to %{due_date}."
+msgstr ""
+
+msgid "Sets the milestone to %{milestone_reference}."
+msgstr ""
+
+msgid "Sets time estimate to %{time_estimate}."
+msgstr ""
+
msgid "Settings"
msgstr ""
@@ -7964,6 +8580,12 @@ msgstr ""
msgid "Show all activity"
msgstr ""
+msgid "Show archived projects"
+msgstr ""
+
+msgid "Show archived projects only"
+msgstr ""
+
msgid "Show command"
msgstr ""
@@ -7979,9 +8601,6 @@ msgstr ""
msgid "Show latest version"
msgstr ""
-msgid "Show link to create/view merge request when pushing from the command line"
-msgstr ""
-
msgid "Show parent pages"
msgstr ""
@@ -8083,6 +8702,9 @@ msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
+msgid "Something went wrong while adding your award. Please try again."
+msgstr ""
+
msgid "Something went wrong while applying the suggestion. Please try again."
msgstr ""
@@ -8095,6 +8717,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
+msgid "Something went wrong while fetching latest comments."
+msgstr ""
+
msgid "Something went wrong while fetching related merge requests."
msgstr ""
@@ -8227,6 +8852,12 @@ msgstr ""
msgid "SortOptions|Recent sign in"
msgstr ""
+msgid "SortOptions|Sort direction"
+msgstr ""
+
+msgid "SortOptions|Stars"
+msgstr ""
+
msgid "SortOptions|Start later"
msgstr ""
@@ -8320,6 +8951,9 @@ msgstr ""
msgid "Start a new merge request"
msgstr ""
+msgid "Start a review"
+msgstr ""
+
msgid "Start and due date"
msgstr ""
@@ -8419,6 +9053,9 @@ msgstr ""
msgid "Subscribed"
msgstr ""
+msgid "Subtracts"
+msgstr ""
+
msgid "Successfully blocked"
msgstr ""
@@ -8455,6 +9092,12 @@ msgstr ""
msgid "Switch branch/tag"
msgstr ""
+msgid "Switch to GitLab Next"
+msgstr ""
+
+msgid "Switch to the source to copy it to the clipboard"
+msgstr ""
+
msgid "System Hooks"
msgstr ""
@@ -8479,18 +9122,30 @@ msgstr ""
msgid "System metrics (Kubernetes)"
msgstr ""
+msgid "Table of Contents"
+msgstr ""
+
msgid "Tag"
msgstr ""
msgid "Tag list:"
msgstr ""
+msgid "Tag this commit."
+msgstr ""
+
msgid "Tags"
msgstr ""
msgid "Tags feed"
msgstr ""
+msgid "Tags this commit to %{tag_name} with \"%{message}\"."
+msgstr ""
+
+msgid "Tags this commit to %{tag_name}."
+msgstr ""
+
msgid "Tags:"
msgstr ""
@@ -8593,6 +9248,9 @@ msgstr ""
msgid "Test failed."
msgstr ""
+msgid "Test settings and save changes"
+msgstr ""
+
msgid "TestHooks|Ensure one of your projects has merge requests."
msgstr ""
@@ -8751,9 +9409,18 @@ msgstr ""
msgid "The project can be accessed by any logged in user."
msgstr ""
+msgid "The project can be accessed by any user who is logged in."
+msgstr ""
+
+msgid "The project can be accessed by anyone, regardless of authentication."
+msgstr ""
+
msgid "The project can be accessed without any authentication."
msgstr ""
+msgid "The project is accessible only by members of the project. Access must be granted explicitly to each user."
+msgstr ""
+
msgid "The project is still being deleted. Please try again later."
msgstr ""
@@ -8868,6 +9535,9 @@ msgstr ""
msgid "There is already a repository with that name on disk"
msgstr ""
+msgid "There was a problem communicating with your device."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -8892,6 +9562,9 @@ msgstr ""
msgid "There was an error when unsubscribing from this label."
msgstr ""
+msgid "There was an error while fetching cycle analytics data."
+msgstr ""
+
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
@@ -8916,6 +9589,9 @@ msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
+msgid "This also resolves the discussion"
+msgstr ""
+
msgid "This application was created by %{link_to_owner}."
msgstr ""
@@ -8943,6 +9619,12 @@ msgstr ""
msgid "This container registry has been scheduled for deletion."
msgstr ""
+msgid "This device has already been registered with us."
+msgstr ""
+
+msgid "This device has not been registered with us."
+msgstr ""
+
msgid "This diff is collapsed."
msgstr ""
@@ -9385,6 +10067,9 @@ msgstr ""
msgid "Toggle Sidebar"
msgstr ""
+msgid "Toggle backtrace"
+msgstr ""
+
msgid "Toggle comments for this file"
msgstr ""
@@ -9397,6 +10082,9 @@ msgstr ""
msgid "Toggle discussion"
msgstr ""
+msgid "Toggle emoji award"
+msgstr ""
+
msgid "Toggle navigation"
msgstr ""
@@ -9406,6 +10094,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Toggles :%{name}: emoji award."
+msgstr ""
+
msgid "Token"
msgstr ""
@@ -9502,6 +10193,9 @@ msgstr ""
msgid "Try again?"
msgstr ""
+msgid "Try to fork again"
+msgstr ""
+
msgid "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now."
msgstr ""
@@ -9517,12 +10211,18 @@ msgstr ""
msgid "Type"
msgstr ""
+msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
+msgstr ""
+
msgid "Unable to connect to server: %{error}"
msgstr ""
msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
+msgid "Unable to resolve"
+msgstr ""
+
msgid "Unable to schedule a pipeline to run immediately"
msgstr ""
@@ -9547,15 +10247,24 @@ msgstr ""
msgid "Unknown format"
msgstr ""
+msgid "Unknown response text"
+msgstr ""
+
msgid "Unlock"
msgstr ""
+msgid "Unlock the discussion"
+msgstr ""
+
msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
msgstr ""
msgid "Unlocked"
msgstr ""
+msgid "Unlocks the discussion"
+msgstr ""
+
msgid "Unresolve discussion"
msgstr ""
@@ -9874,6 +10583,9 @@ msgstr ""
msgid "View file @ "
msgstr ""
+msgid "View full dashboard"
+msgstr ""
+
msgid "View group labels"
msgstr ""
@@ -9910,6 +10622,9 @@ msgstr ""
msgid "Viewing commit"
msgstr ""
+msgid "Visibility"
+msgstr ""
+
msgid "Visibility and access controls"
msgstr ""
@@ -9937,6 +10652,9 @@ msgstr ""
msgid "VisibilityLevel|Unknown"
msgstr ""
+msgid "Wait for the source to load to copy it to the clipboard"
+msgstr ""
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
@@ -9967,13 +10685,13 @@ msgstr ""
msgid "Webhooks Help"
msgstr ""
-msgid "When a runner is locked, it cannot be assigned to other projects"
+msgid "Welcome to your Issue Board!"
msgstr ""
-msgid "When enabled, users cannot use GitLab until the terms have been accepted."
+msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
-msgid "When fast-forward merge is not possible, the user is given the option to rebase."
+msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "When leaving the URL blank, classification labels can still be specified without disabling cross project features or performing external authorization checks."
@@ -10164,6 +10882,9 @@ msgstr ""
msgid "Yes"
msgstr ""
+msgid "Yes or No"
+msgstr ""
+
msgid "Yes, add it"
msgstr ""
@@ -10182,6 +10903,9 @@ msgstr ""
msgid "You are attempting to delete a file that has been previously updated."
msgstr ""
+msgid "You are attempting to update a file that has changed since you started editing it."
+msgstr ""
+
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -10209,6 +10933,12 @@ msgstr ""
msgid "You can also create a project from the command line."
msgstr ""
+msgid "You can also press &#8984;-Enter"
+msgstr ""
+
+msgid "You can also press Ctrl-Enter"
+msgstr ""
+
msgid "You can also star a label to make it a priority label."
msgstr ""
@@ -10284,6 +11014,9 @@ msgstr ""
msgid "You do not have any subscriptions yet"
msgstr ""
+msgid "You do not have permission to leave this %{namespaceType}."
+msgstr ""
+
msgid "You don't have any applications"
msgstr ""
@@ -10293,6 +11026,12 @@ msgstr ""
msgid "You don't have any deployments right now."
msgstr ""
+msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
+msgstr ""
+
+msgid "You have been granted %{access_level} access to the %{source_name} %{source_type}."
+msgstr ""
+
msgid "You have been granted %{member_human_access} access to %{label}."
msgstr ""
@@ -10320,6 +11059,9 @@ msgstr ""
msgid "You must have maintainer access to force delete a lock"
msgstr ""
+msgid "You must have permission to create a project in a namespace before forking."
+msgstr ""
+
msgid "You need permission."
msgstr ""
@@ -10335,6 +11077,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
+msgstr ""
+
msgid "You will lose all changes you've made to this file. This action cannot be undone."
msgstr ""
@@ -10464,6 +11209,9 @@ msgstr ""
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
msgstr ""
+msgid "Your message here"
+msgstr ""
+
msgid "Your name"
msgstr ""
@@ -10660,6 +11408,9 @@ msgstr ""
msgid "it is too large"
msgstr ""
+msgid "jigsaw is not defined"
+msgstr ""
+
msgid "latest"
msgstr ""
@@ -10758,6 +11509,9 @@ msgstr ""
msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
msgstr ""
+msgid "mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result"
+msgstr ""
+
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr ""
@@ -10851,6 +11605,9 @@ msgstr ""
msgid "mrWidget|The source branch will not be deleted"
msgstr ""
+msgid "mrWidget|The target branch has advanced, which invalidates the merge request pipeline. Please update the source branch and retry merging"
+msgstr ""
+
msgid "mrWidget|There are merge conflicts"
msgstr ""
@@ -10902,6 +11659,9 @@ msgstr ""
msgid "new merge request"
msgstr ""
+msgid "no contributions"
+msgstr ""
+
msgid "none"
msgstr ""
@@ -10971,7 +11731,7 @@ msgstr[1] ""
msgid "score"
msgstr ""
-msgid "should be higher than %{access} inherited membership from group %{group_name}"
+msgid "should be greater than or equal to %{access} inherited membership from group %{group_name}"
msgstr ""
msgid "show less"
@@ -10980,6 +11740,9 @@ msgstr ""
msgid "sign in"
msgstr ""
+msgid "sort:"
+msgstr ""
+
msgid "source"
msgstr ""
diff --git a/package.json b/package.json
index 115f6fd1e8d..eb557101662 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,7 @@
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
- "jest": "BABEL_ENV=jest jest",
- "jest-debug": "BABEL_ENV=jest node --inspect-brk node_modules/.bin/jest --runInBand",
+ "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
@@ -33,8 +32,8 @@
"@babel/plugin-syntax-import-meta": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.9.0",
- "@gitlab/svgs": "^1.59.0",
- "@gitlab/ui": "^3.2.0",
+ "@gitlab/svgs": "^1.60.0",
+ "@gitlab/ui": "^3.10.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-upload-client": "^10.0.0",
@@ -90,7 +89,7 @@
"jszip-utils": "^0.0.2",
"katex": "^0.10.0",
"marked": "^0.3.12",
- "mermaid": "^8.0.0-rc.8",
+ "mermaid": "^8.0.0",
"monaco-editor": "^0.15.6",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
diff --git a/qa/Gemfile b/qa/Gemfile
index 38e95ba2d65..64215b24cf1 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -7,6 +7,6 @@ gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12'
gem 'airborne', '~> 0.2.13'
-gem 'nokogiri', '~> 1.10.1'
+gem 'nokogiri', '~> 1.10.3'
gem 'rspec-retry', '~> 0.6.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 9d3d42fb6ae..a06c88b6f0a 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -49,7 +49,7 @@ GEM
mini_portile2 (2.4.0)
minitest (5.11.1)
netrc (0.11.0)
- nokogiri (1.10.2)
+ nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
pry (0.11.3)
coderay (~> 1.1.0)
@@ -102,7 +102,7 @@ DEPENDENCIES
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
faker (~> 1.6, >= 1.6.6)
- nokogiri (~> 1.10.1)
+ nokogiri (~> 1.10.3)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rspec (~> 3.7)
diff --git a/qa/qa.rb b/qa/qa.rb
index 672f4aa928a..f580691f952 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -275,6 +275,7 @@ module QA
module Layout
autoload :Banner, 'qa/page/layout/banner'
+ autoload :PerformanceBar, 'qa/page/layout/performance_bar'
end
module Label
@@ -293,10 +294,12 @@ module QA
module Settings
autoload :Repository, 'qa/page/admin/settings/repository'
autoload :General, 'qa/page/admin/settings/general'
+ autoload :MetricsAndProfiling, 'qa/page/admin/settings/metrics_and_profiling'
module Component
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'
end
end
end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index b3bad40a90f..84353e3d0c7 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -251,7 +251,7 @@ module QA
end
def netrc_already_contains_content?
- read_netrc_content.grep(/^#{netrc_content}$/).any?
+ read_netrc_content.grep(/^#{Regexp.escape(netrc_content)}$/).any?
end
end
end
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index 25564f2dc6e..eea5717f5a7 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -10,6 +10,7 @@ module QA
element :admin_settings_item
element :admin_settings_repository_item
element :admin_settings_general_item
+ element :admin_settings_metrics_and_profiling_item
end
def go_to_repository_settings
@@ -28,6 +29,14 @@ module QA
end
end
+ def go_to_metrics_and_profiling_settings
+ hover_settings do
+ within_submenu do
+ click_element :admin_settings_metrics_and_profiling_item
+ end
+ end
+ end
+
private
def hover_settings
diff --git a/qa/qa/page/admin/settings/component/performance_bar.rb b/qa/qa/page/admin/settings/component/performance_bar.rb
new file mode 100644
index 00000000000..bc29efb64c0
--- /dev/null
+++ b/qa/qa/page/admin/settings/component/performance_bar.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ module Settings
+ module Component
+ class PerformanceBar < Page::Base
+ view 'app/views/admin/application_settings/_performance_bar.html.haml' do
+ element :enable_performance_bar_checkbox
+ element :save_changes_button
+ end
+
+ def enable_performance_bar
+ click_element :enable_performance_bar_checkbox
+ Capybara.current_session.driver.browser.manage.add_cookie(name: 'perf_bar_enabled', value: 'true')
+ end
+
+ def save_settings
+ click_element :save_changes_button
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/admin/settings/metrics_and_profiling.rb b/qa/qa/page/admin/settings/metrics_and_profiling.rb
new file mode 100644
index 00000000000..e10a92d7a54
--- /dev/null
+++ b/qa/qa/page/admin/settings/metrics_and_profiling.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Admin
+ module Settings
+ class MetricsAndProfiling < Page::Base
+ include QA::Page::Settings::Common
+
+ view 'app/views/admin/application_settings/metrics_and_profiling.html.haml' do
+ element :performance_bar_settings
+ end
+
+ def expand_performance_bar(&block)
+ expand_section(:performance_bar_settings) do
+ Component::PerformanceBar.perform(&block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb
index f5add6bc9b5..07e191f1c9b 100644
--- a/qa/qa/page/component/note.rb
+++ b/qa/qa/page/component/note.rb
@@ -15,7 +15,7 @@ module QA
element :reply_comment_button
end
- base.view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do
+ base.view 'app/assets/javascripts/notes/components/discussion_actions.vue' do
element :discussion_reply
end
diff --git a/qa/qa/page/layout/performance_bar.rb b/qa/qa/page/layout/performance_bar.rb
new file mode 100644
index 00000000000..0cfef6a5705
--- /dev/null
+++ b/qa/qa/page/layout/performance_bar.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Layout
+ class PerformanceBar < Page::Base
+ view 'app/assets/javascripts/performance_bar/components/performance_bar_app.vue' do
+ element :performance_bar
+ end
+
+ view 'app/assets/javascripts/performance_bar/components/detailed_metric.vue' do
+ element :performance_bar_detailed_metric
+ end
+
+ view 'app/assets/javascripts/performance_bar/components/request_selector.vue' do
+ element :performance_bar_request
+ end
+
+ def has_performance_bar?
+ has_element?(:performance_bar)
+ end
+
+ def has_detailed_metrics?
+ all_elements(:performance_bar_detailed_metric).all? do |metric|
+ metric.has_text?(%r{\d+ms / \d+})
+ end
+ end
+
+ def has_request_for?(path)
+ has_element?(:performance_bar_request, text: path)
+ 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 44a62cf6ccf..b8c5c563da6 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module QA
diff --git a/qa/qa/runtime/key/ecdsa.rb b/qa/qa/runtime/key/ecdsa.rb
index 6e5c0a2deca..46a1e5f54cf 100644
--- a/qa/qa/runtime/key/ecdsa.rb
+++ b/qa/qa/runtime/key/ecdsa.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module QA
diff --git a/qa/qa/runtime/key/ed25519.rb b/qa/qa/runtime/key/ed25519.rb
index 53b556d744d..3a3567d55da 100644
--- a/qa/qa/runtime/key/ed25519.rb
+++ b/qa/qa/runtime/key/ed25519.rb
@@ -1,4 +1,3 @@
-# rubocop:disable Naming/FileName
# frozen_string_literal: true
module QA
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index 71c06b3410f..cf225a639b6 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -7,16 +7,12 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- # TODO, since `Signed in successfully` message was removed
- # this is the only way to tell if user is signed in correctly.
- #
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
Page::Main::Menu.perform do |menu|
menu.sign_out
- expect(menu).not_to have_personal_area
end
Page::Main::Login.perform do |form|
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
index a397df03bd2..72dde4e5bd8 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb
@@ -7,9 +7,6 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- # TODO, since `Signed in successfully` message was removed
- # this is the only way to tell if user is signed in correctly.
- #
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
index a118176eb8a..15cd59f041b 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Manage', :orchestrated, :oauth do
+ # https://gitlab.com/gitlab-org/quality/nightly/issues/100
+ context 'Manage', :orchestrated, :oauth, :quarantine do
describe 'OAuth login' do
it 'User logs in to GitLab with GitHub OAuth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
index 185837edacf..a0e3fe0d91a 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb
@@ -7,9 +7,6 @@ module QA
Resource::User.fabricate_via_browser_ui!
- # TODO, since `Signed in successfully` message was removed
- # this is the only way to tell if user is signed in correctly.
- #
Page::Main::Menu.perform do |menu|
expect(menu).to have_personal_area
end
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 de5c535c757..10bba98f704 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
@@ -2,7 +2,8 @@
module QA
context 'Create' do
- describe 'File templates' do
+ # Issue: https://gitlab.com/gitlab-org/quality/nightly/issues/97
+ describe 'File templates', :quarantine do
include Runtime::Fixtures
def login
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 daeee665c93..c2c2b6da90a 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
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/62
+ context 'Create', :quarantine do
describe 'Create, list, and delete branches via web' do
master_branch = 'master'
second_branch = 'second-branch'
diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
index 309ae6cd986..e689ba4c69c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb
@@ -3,11 +3,6 @@
module QA
context 'Create' do
describe 'Wiki management' do
- def validate_content(content)
- expect(page).to have_content('Wiki was successfully updated')
- expect(page).to have_content(/#{content}/)
- end
-
it 'user creates, edits, clones, and pushes to the wiki' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
@@ -38,6 +33,11 @@ module QA
expect(page).to have_content('My Third Wiki Content')
end
+
+ def validate_content(content)
+ expect(page).to have_content('Wiki was successfully updated')
+ expect(page).to have_content(/#{content}/)
+ end
end
end
end
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
new file mode 100644
index 00000000000..fc4ff364fd4
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/non_devops/performance_bar_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Performance bar' do
+ context 'when logged in as an admin user' do
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_admin_credentials)
+ Page::Main::Menu.perform(&:click_admin_area)
+ Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings)
+
+ Page::Admin::Settings::MetricsAndProfiling.perform do |setting|
+ setting.expand_performance_bar do |page|
+ page.enable_performance_bar
+ page.save_settings
+ end
+ end
+ end
+
+ it 'shows results for the original request and AJAX requests' do
+ # Issue pages always make AJAX requests
+ Resource::Issue.fabricate! do |issue|
+ issue.title = 'Performance bar test'
+ end
+
+ Page::Layout::PerformanceBar.perform do |page|
+ expect(page).to have_performance_bar
+ expect(page).to have_detailed_metrics
+ expect(page).to have_request_for('realtime_changes') # Always requested on issue pages
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
index 52cb05fcd13..ca0ce32e74f 100644
--- a/qa/qa/specs/helpers/quarantine.rb
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -20,6 +20,14 @@ module QA::Specs::Helpers
end
end
+ # Skip the entire context if a context is quarantined. This avoids running
+ # before blocks unnecessarily.
+ def skip_or_run_quarantined_contexts(filters, example)
+ return unless example.metadata.key?(:quarantine)
+
+ skip_or_run_quarantined_tests_or_contexts(filters, example)
+ end
+
# Skip tests in quarantine unless we explicitly focus on them.
def skip_or_run_quarantined_tests_or_contexts(filters, example)
if filters.key?(:quarantine)
@@ -39,14 +47,6 @@ module QA::Specs::Helpers
end
end
- # Skip the entire context if a context is quarantined. This avoids running
- # before blocks unnecessarily.
- def skip_or_run_quarantined_contexts(filters, example)
- return unless example.metadata.key?(:quarantine)
-
- skip_or_run_quarantined_tests_or_contexts(filters, example)
- end
-
def filters_other_than_quarantine(filter)
filter.reject { |key, _| key == :quarantine }
end
diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb
index 6d8f9aa7c12..120ba6e6c06 100644
--- a/qa/qa/vendor/github/page/login.rb
+++ b/qa/qa/vendor/github/page/login.rb
@@ -12,9 +12,7 @@ module QA
fill_in 'password', with: QA::Runtime::Env.github_password
click_on 'Sign in'
- unless has_no_text?("Authorize GitLab-OAuth")
- click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
- end
+ click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa')
end
end
end
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index 776dce31d4f..5198eb6f58b 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -116,6 +116,15 @@ describe QA::Git::Repository do
expect(File.read(File.join(tmp_netrc_dir, '.netrc')))
.to eq("machine foo login user password foo\n")
end
+
+ it 'adds credentials with special characters' do
+ password = %q[!"#$%&')(*+,-./:;<=>?]
+ repository.username = 'user'
+ repository.password = password
+
+ expect(File.read(File.join(tmp_netrc_dir, '.netrc')))
+ .to eq("machine foo login user password #{password}\n")
+ end
end
end
end
diff --git a/rubocop/cop/gitlab/finder_with_find_by.rb b/rubocop/cop/gitlab/finder_with_find_by.rb
index f45a37ddc06..764a5073143 100644
--- a/rubocop/cop/gitlab/finder_with_find_by.rb
+++ b/rubocop/cop/gitlab/finder_with_find_by.rb
@@ -2,7 +2,7 @@ module RuboCop
module Cop
module Gitlab
class FinderWithFindBy < RuboCop::Cop::Cop
- FIND_PATTERN = /\Afind(_by\!?)?\z/
+ FIND_PATTERN = /\Afind(_by\!?)?\z/.freeze
ALLOWED_MODULES = ['FinderMethods'].freeze
def message(used_method)
@@ -36,7 +36,7 @@ module RuboCop
def find_on_execute?(node)
chained_on_node = node.descendants.first
node.method_name.to_s =~ FIND_PATTERN &&
- chained_on_node&.method_name == :execute
+ chained_on_node.is_a?(RuboCop::AST::SendNode) && chained_on_node.method_name == :execute
end
def allowed_module?(node)
diff --git a/rubocop/cop/include_action_view_context.rb b/rubocop/cop/include_action_view_context.rb
new file mode 100644
index 00000000000..14662a33e95
--- /dev/null
+++ b/rubocop/cop/include_action_view_context.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative '../spec_helpers'
+
+module RuboCop
+ module Cop
+ # Cop that makes sure workers include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`.
+ class IncludeActionViewContext < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG = 'Include `::Gitlab::ActionViewOutput::Context`, not `ActionView::Context`, for Rails 5.'.freeze
+
+ def_node_matcher :includes_action_view_context?, <<~PATTERN
+ (send nil? :include (const (const nil? :ActionView) :Context))
+ PATTERN
+
+ def on_send(node)
+ return if in_spec?(node)
+ return unless includes_action_view_context?(node)
+
+ add_offense(node.arguments.first, location: :expression)
+ end
+
+ def autocorrect(node)
+ lambda do |corrector|
+ corrector.replace(node.source_range, '::Gitlab::ActionViewOutput::Context')
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/qa_helpers.rb b/rubocop/qa_helpers.rb
index f4adf7f4e9f..95875d64727 100644
--- a/rubocop/qa_helpers.rb
+++ b/rubocop/qa_helpers.rb
@@ -5,7 +5,7 @@ module RuboCop
def in_qa_file?(node)
path = node.location.expression.source_buffer.name
- path.start_with?(File.join(Dir.pwd, 'qa'))
+ path.start_with?(File.join(RuboCop::PathUtil.pwd, 'qa'))
end
end
end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 50eab6f9270..e2a19978839 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,9 +1,9 @@
-# rubocop:disable Naming/FileName
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
require_relative 'cop/gitlab/httparty'
require_relative 'cop/gitlab/finder_with_find_by'
require_relative 'cop/gitlab/union'
+require_relative 'cop/include_action_view_context'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/safe_params'
require_relative 'cop/active_record_association_reload'
diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb
index 63c1b975a65..ecd77c4351d 100644
--- a/rubocop/spec_helpers.rb
+++ b/rubocop/spec_helpers.rb
@@ -6,14 +6,16 @@ module RuboCop
# Returns true if the given node originated from the spec directory.
def in_spec?(node)
path = node.location.expression.source_buffer.name
+ pwd = RuboCop::PathUtil.pwd
!SPEC_HELPERS.include?(File.basename(path)) &&
- path.start_with?(File.join(Dir.pwd, 'spec'), File.join(Dir.pwd, 'ee', 'spec'))
+ path.start_with?(File.join(pwd, 'spec'), File.join(pwd, 'ee', 'spec'))
end
def migration_directories
@migration_directories ||= MIGRATION_SPEC_DIRECTORIES.map do |dir|
- [File.join(Dir.pwd, dir), File.join(Dir.pwd, 'ee', dir)]
+ pwd = RuboCop::PathUtil.pwd
+ [File.join(pwd, dir), File.join(pwd, 'ee', dir)]
end.flatten
end
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index bc73225c1bf..2055ce7f09d 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -35,7 +35,7 @@ fi
# Do not use 'README.md', instead use 'index.md'
# Number of 'README.md's as of 2018-03-26
-NUMBER_READMES_CE=43
+NUMBER_READMES_CE=46
NUMBER_READMES_EE=46
FIND_READMES=$(find doc/ -name "README.md" | wc -l)
echo '=> Checking for new README.md files...'
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 22e47ce2c0a..9455e462617 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -1,26 +1,6 @@
[[ "$TRACE" ]] && set -x
export TILLER_NAMESPACE="$KUBE_NAMESPACE"
-function echoerr() {
- local header="${2}"
-
- if [ -n "${header}" ]; then
- printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
- else
- printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
- fi
-}
-
-function echoinfo() {
- local header="${2}"
-
- if [ -n "${header}" ]; then
- printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2;
- else
- printf "\033[0;33m%s\n\033[0m" "${1}" >&2;
- fi
-}
-
function deployExists() {
local namespace="${1}"
local deploy="${2}"
@@ -68,7 +48,7 @@ function delete() {
echoinfo "Deleting release '$name'..." true
- helm delete --purge "$name" || true
+ helm delete --purge "$name"
}
function cleanup() {
@@ -81,8 +61,8 @@ function cleanup() {
kubectl -n "$KUBE_NAMESPACE" delete \
ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa \
- -l release="$CI_ENVIRONMENT_SLUG" \
- || true
+ --now --ignore-not-found --include-uninitialized \
+ -l release="$CI_ENVIRONMENT_SLUG"
}
function get_pod() {
@@ -109,7 +89,7 @@ function perform_review_app_deployment() {
ensure_namespace
install_tiller
install_external_dns
- time deploy || true
+ time deploy
wait_for_review_app_to_be_accessible
add_license
}
@@ -236,6 +216,7 @@ HELM_CMD=$(cat << EOF
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=tls-cert \
--set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10"
+ --set nginx-ingress.controller.service.enableHttp=false \
--set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \
--set nginx-ingress.controller.resources.requests.memory=440M \
--set nginx-ingress.controller.replicaCount=2 \
@@ -270,7 +251,7 @@ EOF
echoinfo "Deploying with:"
echoinfo "${HELM_CMD}"
- eval $HELM_CMD
+ eval $HELM_CMD || true
}
function wait_for_review_app_to_be_accessible() {
@@ -328,80 +309,3 @@ function add_license() {
puts "License added";
'
}
-
-function get_job_id() {
- local job_name="${1}"
- local query_string="${2:+&${2}}"
-
- local max_page=3
- local page=1
-
- while true; do
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
- echoinfo "GET ${url}"
-
- local job_id
- job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
- [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
-
- let "page++"
- done
-
- if [[ "${job_id}" == "" ]]; then
- echoerr "The '${job_name}' job ID couldn't be retrieved!"
- else
- echoinfo "The '${job_name}' job ID is ${job_id}"
- echo "${job_id}"
- fi
-}
-
-function play_job() {
- local job_name="${1}"
- local job_id
- job_id=$(get_job_id "${job_name}" "scope=manual");
- if [ -z "${job_id}" ]; then return; fi
-
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
- echoinfo "POST ${url}"
-
- local job_url
- job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url")
- echoinfo "Manual job '${job_name}' started at: ${job_url}"
-}
-
-function wait_for_job_to_be_done() {
- local job_name="${1}"
- local query_string="${2}"
- local job_id
- job_id=$(get_job_id "${job_name}" "${query_string}")
- if [ -z "${job_id}" ]; then return; fi
-
- echoinfo "Waiting for the '${job_name}' job to finish..."
-
- local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}"
- echoinfo "GET ${url}"
-
- # In case the job hasn't finished yet. Keep trying until the job times out.
- local interval=30
- local elapsed_seconds=0
- while true; do
- local job_status
- job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".status" | sed -e s/\"//g)
- [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
-
- printf "."
- let "elapsed_seconds+=interval"
- sleep ${interval}
- done
-
- local elapsed_minutes=$((elapsed_seconds / 60))
- echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes."
-
- if [[ "${job_status}" == "failed" ]]; then
- echoerr "The '${job_name}' failed."
- elif [[ "${job_status}" == "manual" ]]; then
- echoinfo "The '${job_name}' is manual."
- else
- echoinfo "The '${job_name}' passed."
- fi
-}
diff --git a/scripts/utils.sh b/scripts/utils.sh
index 2d2ba115563..4a6567b8a62 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -1,4 +1,4 @@
-retry() {
+function retry() {
if eval "$@"; then
return 0
fi
@@ -13,15 +13,15 @@ retry() {
return 1
}
-setup_db_user_only() {
+function setup_db_user_only() {
if [ "$GITLAB_DATABASE" = "postgresql" ]; then
- . scripts/create_postgres_user.sh
+ source scripts/create_postgres_user.sh
else
- . scripts/create_mysql_user.sh
+ source scripts/create_mysql_user.sh
fi
}
-setup_db() {
+function setup_db() {
setup_db_user_only
bundle exec rake db:drop db:create db:schema:load db:migrate
@@ -30,3 +30,129 @@ setup_db() {
bundle exec rake add_limits_mysql
fi
}
+
+function install_api_client_dependencies_with_apk() {
+ apk add --update openssl curl jq
+}
+
+function install_api_client_dependencies_with_apt() {
+ apt update && apt install jq -y
+}
+
+function install_gitlab_gem() {
+ gem install gitlab --no-document
+}
+
+function echoerr() {
+ local header="${2}"
+
+ if [ -n "${header}" ]; then
+ printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
+ else
+ printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
+ fi
+}
+
+function echoinfo() {
+ local header="${2}"
+
+ if [ -n "${header}" ]; then
+ printf "\n\033[0;33m** %s **\n\033[0m" "${1}" >&2;
+ else
+ printf "\033[0;33m%s\n\033[0m" "${1}" >&2;
+ fi
+}
+
+function get_job_id() {
+ local job_name="${1}"
+ local query_string="${2:+&${2}}"
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ local max_page=3
+ local page=1
+
+ while true; do
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}"
+ echoinfo "GET ${url}"
+
+ local job_id
+ job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last")
+ [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break
+
+ let "page++"
+ done
+
+ if [[ "${job_id}" == "" ]]; then
+ echoerr "The '${job_name}' job ID couldn't be retrieved!"
+ else
+ echoinfo "The '${job_name}' job ID is ${job_id}"
+ echo "${job_id}"
+ fi
+}
+
+function play_job() {
+ local job_name="${1}"
+ local job_id
+ job_id=$(get_job_id "${job_name}" "scope=manual");
+ if [ -z "${job_id}" ]; then return; fi
+
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play"
+ echoinfo "POST ${url}"
+
+ local job_url
+ job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".web_url")
+ echoinfo "Manual job '${job_name}' started at: ${job_url}"
+}
+
+function wait_for_job_to_be_done() {
+ local job_name="${1}"
+ local query_string="${2}"
+ local job_id
+ job_id=$(get_job_id "${job_name}" "${query_string}")
+ if [ -z "${job_id}" ]; then return; fi
+
+ local api_token="${API_TOKEN-${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}}"
+ if [ -z "${api_token}" ]; then
+ echoerr "Please provide an API token with \$API_TOKEN or \$GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN."
+ return
+ fi
+
+ echoinfo "Waiting for the '${job_name}' job to finish..."
+
+ local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}"
+ echoinfo "GET ${url}"
+
+ # In case the job hasn't finished yet. Keep trying until the job times out.
+ local interval=30
+ local elapsed_seconds=0
+ while true; do
+ local job_status
+ job_status=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${api_token}" "${url}" | jq ".status" | sed -e s/\"//g)
+ [[ "${job_status}" == "pending" || "${job_status}" == "running" ]] || break
+
+ printf "."
+ let "elapsed_seconds+=interval"
+ sleep ${interval}
+ done
+
+ local elapsed_minutes=$((elapsed_seconds / 60))
+ echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes."
+
+ if [[ "${job_status}" == "failed" ]]; then
+ echoerr "The '${job_name}' failed."
+ elif [[ "${job_status}" == "manual" ]]; then
+ echoinfo "The '${job_name}' is manual."
+ else
+ echoinfo "The '${job_name}' passed."
+ fi
+}
diff --git a/security.txt b/security.txt
new file mode 100644
index 00000000000..f7adb43fda6
--- /dev/null
+++ b/security.txt
@@ -0,0 +1,6 @@
+Contact: security@gitlab.com
+Acknowledgments: https://about.gitlab.com/security/vulnerability-acknowledgements/
+Preferred-Languages: en
+Canonical: https://about.gitlab.com/security/disclosure/
+Policy: https://hackerone.com/gitlab
+Hiring: https://about.gitlab.com/jobs/
diff --git a/spec/controllers/admin/clusters/applications_controller_spec.rb b/spec/controllers/admin/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..76f261e7d3f
--- /dev/null
+++ b/spec/controllers/admin/clusters/applications_controller_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ shared_examples 'a secure endpoint' do
+ it { expect { subject }.to be_allowed_for(:admin) }
+ it { expect { subject }.to be_denied_for(:user) }
+ it { expect { subject }.to be_denied_for(:external) }
+
+ context 'when instance clusters are disabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it 'returns 404' do
+ is_expected.to have_http_status(:not_found)
+ end
+ end
+ end
+
+ let(:cluster) { create(:cluster, :instance, :provided_by_gcp) }
+
+ describe 'POST create' do
+ subject do
+ post :create, params: params
+ end
+
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { subject }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { subject }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ is_expected.to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:clusters_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ is_expected.to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+
+ describe 'PATCH update' do
+ subject do
+ patch :update, params: params
+ end
+
+ let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } }
+
+ describe 'functionality' do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_cert_manager).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterPatchAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+end
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
new file mode 100644
index 00000000000..7b77cb186a4
--- /dev/null
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -0,0 +1,540 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::ClustersController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'GET #index' do
+ def get_index(params = {})
+ get :index, params: params
+ end
+
+ context 'when feature flag is not enabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it 'responds with not found' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(instance_clusters: true)
+ end
+
+ describe 'functionality' do
+ context 'when instance has one or more clusters' do
+ let!(:enabled_cluster) do
+ create(:cluster, :provided_by_gcp, :instance)
+ end
+
+ let!(:disabled_cluster) do
+ create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance)
+ end
+
+ it 'lists available clusters' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { Clusters::Cluster.instance_type.page.total_pages }
+
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance)
+ end
+
+ it 'redirects to the page' do
+ get_index(page: last_page)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
+ end
+ end
+
+ context 'when instance does not have a cluster' do
+ it 'returns an empty state page' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index, partial: :empty_state)
+ expect(assigns(:clusters)).to eq([])
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { expect { get_index }.to be_allowed_for(:admin) }
+ it { expect { get_index }.to be_denied_for(:user) }
+ it { expect { get_index }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #new' do
+ def get_new
+ get :new
+ end
+
+ describe 'functionality for new cluster' do
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ get_new
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path)
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ get_new
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ get_new
+
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'functionality for existing cluster' do
+ it 'has new object' do
+ get_new
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_new }.to be_allowed_for(:admin) }
+ it { expect { get_new }.to be_denied_for(:user) }
+ it { expect { get_new }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST #create_gcp' do
+ let(:legacy_abac_param) { 'true' }
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project-12345',
+ legacy_abac: legacy_abac_param
+ }
+ }
+ }
+ end
+
+ def post_create_gcp
+ post :create_gcp, params: params
+ end
+
+ describe 'functionality' do
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_gcp }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_gcp
+ expect(cluster).to be_kubernetes
+ expect(cluster.provider_gcp).to be_legacy_abac
+ end
+
+ context 'when legacy_abac param is false' do
+ let(:legacy_abac_param) { 'false' }
+
+ it 'creates a new cluster with legacy_abac_disabled' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_gcp }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(Clusters::Cluster.instance_type.first.provider_gcp).not_to be_legacy_abac
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ 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'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it { expect { post_create_gcp }.to be_allowed_for(:admin) }
+ it { expect { post_create_gcp }.to be_denied_for(:user) }
+ it { expect { post_create_gcp }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST #create_user' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test'
+ }
+ }
+ }
+ end
+
+ def post_create_user
+ post :create_user, params: params
+ end
+
+ describe 'functionality' do
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { post_create_user }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ end
+ end
+
+ context 'when creates a RBAC-enabled cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { post_create_user }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { post_create_user }.to be_allowed_for(:admin) }
+ it { expect { post_create_user }.to be_denied_for(:user) }
+ it { expect { post_create_user }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #cluster_status' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, :instance) }
+
+ def get_cluster_status
+ get :cluster_status,
+ params: {
+ id: cluster
+ },
+ format: :json
+ end
+
+ describe 'functionality' do
+ it 'responds with matching schema' do
+ get_cluster_status
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+
+ it 'invokes schedule_status_update on each application' do
+ expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+
+ get_cluster_status
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_cluster_status }.to be_allowed_for(:admin) }
+ it { expect { get_cluster_status }.to be_denied_for(:user) }
+ it { expect { get_cluster_status }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #show' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ def get_show
+ get :show,
+ params: {
+ id: cluster
+ }
+ end
+
+ describe 'functionality' do
+ it 'responds successfully' do
+ get_show
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_show }.to be_allowed_for(:admin) }
+ it { expect { get_show }.to be_denied_for(:user) }
+ it { expect { get_show }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'PUT #update' do
+ def put_update(format: :html)
+ put :update, params: params.merge(
+ id: cluster,
+ format: format
+ )
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, :instance) }
+ let(:domain) { 'test-domain.com' }
+
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ base_domain: domain
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ put_update
+
+ cluster.reload
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.domain).to eq('test-domain.com')
+ end
+
+ context 'when domain is invalid' do
+ let(:domain) { 'http://not-a-valid-domain' }
+
+ it 'does not update cluster attributes' do
+ put_update
+
+ cluster.reload
+ expect(response).to render_template(:show)
+ expect(cluster.name).not_to eq('my-new-cluster-name')
+ expect(cluster.domain).not_to eq('test-domain.com')
+ end
+ end
+
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ domain: domain
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ put_update(format: :json)
+
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: ''
+ }
+ }
+ end
+
+ it 'rejects changes' do
+ put_update(format: :json)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { expect { put_update }.to be_allowed_for(:admin) }
+ it { expect { put_update }.to be_denied_for(:user) }
+ it { expect { put_update }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
+
+ def delete_destroy
+ delete :destroy,
+ params: {
+ id: cluster
+ }
+ end
+
+ describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, :instance) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, :instance) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
+
+ it { expect { delete_destroy }.to be_allowed_for(:admin) }
+ it { expect { delete_destroy }.to be_denied_for(:user) }
+ it { expect { delete_destroy }.to be_denied_for(:external) }
+ end
+ end
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index a5ecb475ce3..7296a4b4526 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -462,7 +462,7 @@ describe ApplicationController do
end
it 'does log correlation id' do
- Gitlab::CorrelationId.use_id('new-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-id') do
get :index
end
diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
new file mode 100644
index 00000000000..e6a6702fdea
--- /dev/null
+++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnforcesAdminAuthentication do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ controller(ApplicationController) do
+ # `described_class` is not available in this context
+ include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass
+
+ def index
+ head :ok
+ end
+ end
+
+ describe 'authenticate_admin!' do
+ context 'as an admin' do
+ let(:user) { create(:admin) }
+
+ it 'renders ok' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'as a user' do
+ it 'renders a 404' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index f9d15d04719..fb2cd5ca955 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -108,51 +108,77 @@ describe IssuableCollections do
end
describe '#finder_options' do
- let(:params) do
- {
- assignee_id: '1',
- assignee_username: 'user1',
- author_id: '2',
- author_username: 'user2',
- authorized_only: 'yes',
- confidential: true,
- due_date: '2017-01-01',
- group_id: '3',
- iids: '4',
- label_name: ['foo'],
- milestone_title: 'bar',
- my_reaction_emoji: 'thumbsup',
- non_archived: 'true',
- project_id: '5',
- scope: 'all',
- search: 'baz',
- sort: 'priority',
- state: 'opened',
- invalid_param: 'invalid_param'
- }
- end
-
- it 'only allows whitelisted params' do
+ before do
allow(controller).to receive(:cookies).and_return({})
allow(controller).to receive(:current_user).and_return(nil)
+ end
+
+ subject { controller.send(:finder_options).to_h }
+
+ context 'scalar params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'yes',
+ confidential: true,
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'confidential' => true,
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'due_date' => '2017-01-01',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
+
+ is_expected.not_to include('invalid_param')
+ end
+ end
+
+ context 'array params' do
+ let(:params) do
+ {
+ assignee_username: %w[user1 user2],
+ label_name: %w[label1 label2],
+ invalid_param: 'invalid_param',
+ invalid_array: ['param']
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'label_name' => %w[label1 label2],
+ 'assignee_username' => %w[user1 user2]
+ })
- finder_options = controller.send(:finder_options)
-
- expect(finder_options).to eq(ActionController::Parameters.new({
- 'assignee_id' => '1',
- 'assignee_username' => 'user1',
- 'author_id' => '2',
- 'author_username' => 'user2',
- 'confidential' => true,
- 'label_name' => ['foo'],
- 'milestone_title' => 'bar',
- 'my_reaction_emoji' => 'thumbsup',
- 'due_date' => '2017-01-01',
- 'scope' => 'all',
- 'search' => 'baz',
- 'sort' => 'priority',
- 'state' => 'opened'
- }).permit!)
+ is_expected.not_to include('invalid_param', 'invalid_array')
+ end
end
end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index e5180ec5c5c..7349cb7094c 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -189,6 +189,7 @@ describe Groups::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
@@ -218,6 +219,7 @@ describe Groups::ClustersController do
expect(cluster).to be_gcp
expect(cluster).to be_kubernetes
expect(cluster.provider_gcp).to be_legacy_abac
+ expect(cluster).to be_managed
end
context 'when legacy_abac param is false' do
@@ -278,6 +280,7 @@ describe Groups::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test'
@@ -303,6 +306,7 @@ describe Groups::ClustersController do
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(cluster).to be_user
expect(cluster).to be_kubernetes
+ expect(cluster).to be_managed
end
end
@@ -334,6 +338,29 @@ describe Groups::ClustersController do
expect(cluster).to be_platform_kubernetes_rbac
end
end
+
+ context 'when creates a user-managed cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ managed: '0',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new user-managed cluster' do
+ go
+
+ cluster = group.clusters.first
+ expect(cluster.managed?).to be_falsy
+ end
+ end
end
describe 'security' do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 0af0eb4f942..6e374a8daa7 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -9,10 +9,14 @@ describe OmniauthCallbacksController, type: :controller do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- mock_auth_hash(provider.to_s, +extern_uid, user.email)
+ @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, +extern_uid, user.email)
stub_omniauth_provider(provider, context: request)
end
+ after do
+ Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
+ end
+
context 'when the user is on the last sign in attempt' do
let(:extern_uid) { 'my-uid' }
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index cd1a01f8acc..70b34f071c8 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -145,4 +145,66 @@ describe Projects::Clusters::ApplicationsController do
it_behaves_like 'a secure endpoint'
end
end
+
+ describe 'DELETE destroy' do
+ subject do
+ delete :destroy, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id } }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_prometheus).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index d94c18ddc02..8d37bd82d21 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -165,6 +165,7 @@ describe Projects::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
@@ -191,6 +192,7 @@ describe Projects::ClustersController do
expect(project.clusters.first).to be_gcp
expect(project.clusters.first).to be_kubernetes
expect(project.clusters.first.provider_gcp).to be_legacy_abac
+ expect(project.clusters.first.managed?).to be_truthy
end
context 'when legacy_abac param is false' do
@@ -251,6 +253,7 @@ describe Projects::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test',
@@ -302,9 +305,35 @@ describe Projects::ClustersController do
expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
- expect(project.clusters.first).to be_user
- expect(project.clusters.first).to be_kubernetes
- expect(project.clusters.first).to be_platform_kubernetes_rbac
+ cluster = project.clusters.first
+
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+
+ context 'when creates a user-managed cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ managed: '0',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ namespace: 'aaa',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new user-managed cluster' do
+ go
+ cluster = project.clusters.first
+
+ expect(cluster.managed?).to be_falsy
end
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 1818809518d..92380a2bf09 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -82,7 +82,7 @@ describe Projects::CompareController do
show_request
expect(response).to be_success
- expect(assigns(:diffs).diff_files.to_a).to eq([])
+ expect(assigns(:diffs)).to eq([])
expect(assigns(:commits)).to eq([])
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 75158f2e8e0..cf23d937037 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -342,11 +342,9 @@ describe Projects::EnvironmentsController do
end
context 'when environment has no metrics' do
- before do
- expect(environment).to receive(:metrics).and_return(nil)
- end
-
it 'returns a metrics page' do
+ expect(environment).not_to receive(:metrics)
+
get :metrics, params: environment_params
expect(response).to be_ok
@@ -354,6 +352,8 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
+ expect(environment).to receive(:metrics).and_return(nil)
+
get :metrics, params: environment_params(format: :json)
expect(response).to have_gitlab_http_status(204)
@@ -461,6 +461,120 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'metrics_dashboard' do
+ context 'when prometheus endpoint is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_use_prometheus_endpoint: false)
+ end
+
+ it 'responds with status code 403' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples_for '200 response' do |contains_all_dashboards: false|
+ let(:expected_keys) { %w(dashboard status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns a json representation of the environment dashboard' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ expect(json_response['dashboard']).to be_an_instance_of(Hash)
+ end
+ end
+
+ shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
+ let(:expected_keys) { %w(message status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns an error response' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(status_code)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ end
+ end
+
+ shared_examples_for 'has all dashboards' do
+ it 'includes an index of all available dashboards' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(json_response.keys).to include('all_dashboards')
+ expect(json_response['all_dashboards']).to be_an_instance_of(Array)
+ expect(json_response['all_dashboards']).to all( include('path', 'default') )
+ end
+ end
+
+ context 'when multiple dashboards is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
+ end
+
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response'
+
+ context 'when the dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }
+
+ it_behaves_like '200 response'
+ end
+ end
+
+ context 'when multiple dashboards is enabled' do
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+
+ context 'when a dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }
+
+ context 'when the dashboard is available' do
+ let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
+ let(:project) { create(:project, :custom_repo, files: dashboard_file) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'error response', :not_found, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+ end
+ end
+ end
+
describe 'GET #search' do
before do
create(:environment, name: 'staging', project: project)
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index eb8983a7633..850ef9c92fb 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -91,7 +91,7 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123', variable_type: 'file' }]
})
end
@@ -105,6 +105,7 @@ describe Projects::PipelineSchedulesController do
Ci::PipelineScheduleVariable.last.tap do |v|
expect(v.key).to eq("AAA")
expect(v.value).to eq("AAA123")
+ expect(v.variable_type).to eq("file")
end
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 fc9a0adeed2..db53e5bc8a4 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -191,6 +191,15 @@ describe Projects::Settings::CiCdController do
expect(project.build_timeout).to eq(5400)
end
end
+
+ context 'when build_timeout_human_readable is invalid' do
+ let(:params) { { build_timeout_human_readable: '5m' } }
+
+ it 'set specified timeout' do
+ expect(subject).to set_flash[:alert]
+ expect(response).to redirect_to(namespace_project_settings_ci_cd_path)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index 02a392f23c2..aa9cd41ed19 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -11,15 +11,118 @@ describe Projects::Settings::OperationsController do
project.add_maintainer(user)
end
- context 'error tracking' do
- describe 'GET #show' do
- it 'renders show template' do
+ shared_examples 'PATCHable' do
+ let(:operations_update_service) { instance_double(::Projects::Operations::UpdateService) }
+ let(:operations_url) { project_settings_operations_url(project) }
+
+ let(:permitted_params) do
+ ActionController::Parameters.new(params).permit!
+ end
+
+ context 'format json' do
+ context 'when update succeeds' do
+ it 'returns success status' do
+ stub_operations_update_service_returning(status: :success)
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('status' => 'success')
+ expect(flash[:notice]).to eq('Your changes have been saved')
+ end
+ end
+
+ context 'when update fails' do
+ it 'returns error' do
+ stub_operations_update_service_returning(
+ status: :error,
+ message: 'error message'
+ )
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+ end
+
+ private
+
+ def stub_operations_update_service_returning(return_value = {})
+ expect(::Projects::Operations::UpdateService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(operations_update_service)
+ expect(operations_update_service).to receive(:execute)
+ .and_return(return_value)
+ end
+ end
+
+ describe 'GET #show' do
+ it 'renders show template' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
+
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
get :show, params: project_params(project)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'renders 404' do
+ patch :update, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signup page' do
+ patch :update, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
end
+ end
+ end
+ context 'error tracking' do
+ describe 'GET #show' do
context 'with existing setting' do
let!(:error_tracking_setting) do
create(:project_error_tracking_setting, project: project)
@@ -40,37 +143,10 @@ describe Projects::Settings::OperationsController do
expect(controller.helpers.error_tracking_setting).to be_new_record
end
end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- get :show, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- get :show, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
end
describe 'PATCH #update' do
- let(:operations_update_service) { spy(:operations_update_service) }
- let(:operations_url) { project_settings_operations_url(project) }
-
- let(:error_tracking_params) do
+ let(:params) do
{
error_tracking_setting_attributes: {
enabled: '1',
@@ -86,79 +162,21 @@ describe Projects::Settings::OperationsController do
}
end
- let(:error_tracking_permitted) do
- ActionController::Parameters.new(error_tracking_params).permit!
- end
-
- context 'format json' do
- context 'when update succeeds' do
- before do
- stub_operations_update_service_returning(status: :success)
- end
-
- it 'returns success status' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq('status' => 'success')
- expect(flash[:notice]).to eq('Your changes have been saved')
- end
- end
-
- context 'when update fails' do
- before do
- stub_operations_update_service_returning(
- status: :error,
- message: 'error message'
- )
- end
-
- it 'returns error' do
- patch :update,
- params: project_params(project, error_tracking_params),
- format: :json
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).not_to be_nil
- end
- end
- end
-
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
-
- it 'renders 404' do
- patch :update, params: project_params(project)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- patch :update, params: project_params(project)
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
+ it_behaves_like 'PATCHable'
end
+ end
- private
+ context 'metrics dashboard setting' do
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'https://gitlab.com'
+ }
+ }
+ end
- def stub_operations_update_service_returning(return_value = {})
- expect(::Projects::Operations::UpdateService)
- .to receive(:new).with(project, user, error_tracking_permitted)
- .and_return(operations_update_service)
- expect(operations_update_service).to receive(:execute)
- .and_return(return_value)
+ it_behaves_like 'PATCHable'
end
end
diff --git a/spec/controllers/projects/stages_controller_spec.rb b/spec/controllers/projects/stages_controller_spec.rb
new file mode 100644
index 00000000000..a91e3523fd7
--- /dev/null
+++ b/spec/controllers/projects/stages_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::StagesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'POST #play_manual.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:stage_name) { 'test' }
+
+ before do
+ create_manual_build(pipeline, 'test', 'rspec 1/2')
+ create_manual_build(pipeline, 'test', 'rspec 2/2')
+
+ pipeline.reload
+ end
+
+ context 'when user does not have access' do
+ it 'returns not authorized' do
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user has access' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when the stage does not exists' do
+ let(:stage_name) { 'deploy' }
+
+ it 'fails to play all manual' do
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the stage exists' do
+ it 'starts all manual jobs' do
+ expect(pipeline.builds.manual.count).to eq(2)
+
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(pipeline.builds.manual.count).to eq(0)
+ end
+ end
+ end
+
+ def play_manual_stage!
+ post :play_manual, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ stage_name: stage_name
+ }, format: :json
+ end
+
+ def create_manual_build(pipeline, stage, name)
+ create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name)
+ end
+ end
+end
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index e0a6fc52ee9..f2e0b5e5c1d 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -19,6 +19,18 @@ describe Projects::WikisController do
destroy_page(wiki_title)
end
+ describe 'GET #pages' do
+ subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
+
+ it 'does not load the pages content' do
+ expect(controller).to receive(:load_wiki).and_return(project_wiki)
+
+ expect(project_wiki).to receive(:list_pages).twice.and_call_original
+
+ subject
+ end
+ end
+
describe 'GET #show' do
render_views
@@ -28,9 +40,9 @@ describe Projects::WikisController do
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call
- expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original
# Sidebar entries
- expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
subject
@@ -104,7 +116,7 @@ describe Projects::WikisController do
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -138,7 +150,7 @@ describe Projects::WikisController do
allow(controller).to receive(:valid_encoding?).and_return(false)
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -148,7 +160,7 @@ describe Projects::WikisController do
it 'updates the page' do
subject
- wiki_page = project_wiki.pages.first
+ wiki_page = project_wiki.list_pages(load_content: true).first
expect(wiki_page.title).to eq new_title
expect(wiki_page.content).to eq new_content
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 9c60f0fcd4d..4634d1d4bb3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -11,6 +11,30 @@ describe SearchController do
sign_in(user)
end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
+
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
+
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
+ end
+ end
+
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
diff --git a/spec/db/importers/common_metrics_importer_spec.rb b/spec/db/importers/common_metrics_importer_spec.rb
index 6133b17ac61..a717c8cd04d 100644
--- a/spec/db/importers/common_metrics_importer_spec.rb
+++ b/spec/db/importers/common_metrics_importer_spec.rb
@@ -23,10 +23,10 @@ describe Importers::CommonMetricsImporter do
subject { described_class.new }
context "does import common_metrics.yml" do
- let(:groups) { subject.content }
- let(:metrics) { groups.map { |group| group['metrics'] }.flatten }
- let(:queries) { metrics.map { |group| group['queries'] }.flatten }
- let(:query_ids) { queries.map { |query| query['id'] } }
+ let(:groups) { subject.content['panel_groups'] }
+ let(:panels) { groups.map { |group| group['panels'] }.flatten }
+ let(:metrics) { panels.map { |group| group['metrics'] }.flatten }
+ let(:metric_ids) { metrics.map { |metric| metric['id'] } }
before do
subject.execute
@@ -36,20 +36,20 @@ describe Importers::CommonMetricsImporter do
expect(PrometheusMetric.common.group(:group).count.count).to eq(groups.count)
end
- it "has the same amount of metrics" do
- expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(metrics.count)
+ it "has the same amount of panels" do
+ expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(panels.count)
end
- it "has the same amount of queries" do
- expect(PrometheusMetric.common.count).to eq(queries.count)
+ it "has the same amount of metrics" do
+ expect(PrometheusMetric.common.count).to eq(metrics.count)
end
it "does not have duplicate IDs" do
- expect(query_ids).to eq(query_ids.uniq)
+ expect(metric_ids).to eq(metric_ids.uniq)
end
it "imports all IDs" do
- expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*query_ids)
+ expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*metric_ids)
end
end
@@ -65,24 +65,26 @@ describe Importers::CommonMetricsImporter do
context 'does import properly all fields' do
let(:query_identifier) { 'response-metric' }
- let(:group) do
+ let(:dashboard) do
{
- group: 'Response metrics (NGINX Ingress)',
- metrics: [{
- title: "Throughput",
- y_label: "Requests / Sec",
- queries: [{
- id: query_identifier,
- query_range: 'my-query',
- unit: 'my-unit',
- label: 'status code'
+ panel_groups: [{
+ group: 'Response metrics (NGINX Ingress)',
+ panels: [{
+ title: "Throughput",
+ y_label: "Requests / Sec",
+ metrics: [{
+ id: query_identifier,
+ query_range: 'my-query',
+ unit: 'my-unit',
+ label: 'status code'
+ }]
}]
}]
}
end
before do
- expect(subject).to receive(:content) { [group.deep_stringify_keys] }
+ expect(subject).to receive(:content) { dashboard.deep_stringify_keys }
end
shared_examples 'stores metric' do
diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb
index 8d29118e310..c85b97fbfc7 100644
--- a/spec/factories/ci/pipeline_schedule_variables.rb
+++ b/spec/factories/ci/pipeline_schedule_variables.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ variable_type 'env_var'
pipeline_schedule factory: :ci_pipeline_schedule
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index fe56ac5b71d..d78f01828d7 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -6,6 +6,11 @@ FactoryBot.define do
status(-2)
end
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
trait :installable do
status 0
end
@@ -30,17 +35,21 @@ FactoryBot.define do
status 5
end
- trait :errored do
- status(-1)
+ trait :update_errored do
+ status(6)
status_reason 'something went wrong'
end
- trait :update_errored do
- status(6)
+ trait :uninstalling do
+ status 7
+ end
+
+ trait :uninstall_errored do
+ status(8)
status_reason 'something went wrong'
end
- trait :timeouted do
+ trait :timed_out do
installing
updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago }
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 97405ec7c58..6eb0194b710 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -65,7 +65,7 @@ FactoryBot.define do
domain 'example.com'
end
- trait :user_managed do
+ trait :not_managed do
managed false
end
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 011c98599a3..db438ad32d3 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :deployment, class: Deployment do
- sha '97de212e80737a608d939f648d959671fb0a0142'
+ sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
ref 'master'
tag false
user nil
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index abf0e6bccb7..e8df5094b83 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -119,7 +119,7 @@ FactoryBot.define do
trait :with_legacy_detached_merge_request_pipeline do
after(:create) do |merge_request|
- merge_request.merge_request_pipelines << create(:ci_pipeline,
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
source: :merge_request_event,
merge_request: merge_request,
project: merge_request.source_project,
@@ -130,7 +130,7 @@ FactoryBot.define do
trait :with_detached_merge_request_pipeline do
after(:create) do |merge_request|
- merge_request.merge_request_pipelines << create(:ci_pipeline,
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
source: :merge_request_event,
merge_request: merge_request,
project: merge_request.source_project,
@@ -147,7 +147,7 @@ FactoryBot.define do
end
after(:create) do |merge_request, evaluator|
- merge_request.merge_request_pipelines << create(:ci_pipeline,
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
source: :merge_request_event,
merge_request: merge_request,
project: merge_request.source_project,
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index b74f72f2bd3..db8384877b0 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -45,6 +45,10 @@ nNp/xedE1YxutQ==
remove_at { 1.day.from_now }
end
+ trait :should_be_removed do
+ remove_at { 1.day.ago }
+ end
+
trait :unverified do
verified_at nil
end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 36e54cf44b4..8cac666069c 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
before(:create) do |pool|
pool.source_project = create(:project, :repository)
+ pool.source_project.update!(pool_repository: pool)
end
trait :scheduled do
diff --git a/spec/factories/project_metrics_settings.rb b/spec/factories/project_metrics_settings.rb
new file mode 100644
index 00000000000..234753f9b87
--- /dev/null
+++ b/spec/factories/project_metrics_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_metrics_setting, class: ProjectMetricsSetting do
+ project
+ external_dashboard_url 'https://grafana.com'
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index ab185ab3972..743ec322885 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -260,6 +260,7 @@ FactoryBot.define do
trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC }
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 04f39b807d7..f9950b5b03f 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -230,6 +230,13 @@ describe 'Admin updates settings' do
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ it 'defaults Deployment events to false for chat notification template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+
+ expect(find_field('Deployment')).not_to be_checked
+ end
end
context 'CI/CD page' do
@@ -368,15 +375,50 @@ describe 'Admin updates settings' do
expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
expect(page).to have_content "Application settings saved successfully"
end
+
+ context 'When pages_auto_ssl is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Change Pages Let's Encrypt settings" do
+ page.within('.as-pages') do
+ fill_in 'Email', with: 'my@test.example.com'
+ check "I have read and agree to the Let's Encrypt Terms of Service"
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.lets_encrypt_notification_email).to eq 'my@test.example.com'
+ expect(Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted).to eq true
+ end
+ end
+
+ context 'When pages_auto_ssl is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Doesn't show Let's Encrypt options" do
+ page.within('.as-pages') do
+ expect(page).not_to have_content('Email')
+ end
+ end
+ end
end
def check_all_events
page.check('Active')
page.check('Push')
- page.check('Tag push')
- page.check('Note')
page.check('Issue')
+ page.check('Confidential issue')
page.check('Merge request')
+ page.check('Note')
+ page.check('Confidential note')
+ page.check('Tag push')
page.check('Pipeline')
+ page.check('Wiki page')
+ page.check('Deployment')
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index b1c6f308bc6..29545779a34 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -34,14 +34,11 @@ describe "Admin::Users" do
expect(page).to have_button('Delete user and contributions')
end
- describe "view extra user information", :js do
- it 'does not have the user popover open' do
+ describe "view extra user information" do
+ it 'shows the user popover on hover', :js, :quarantine do
expect(page).not_to have_selector('#__BV_popover_1__')
- end
- it 'shows the user popover on hover' do
first_user_link = page.first('.js-user-link')
-
first_user_link.hover
expect(page).to have_selector('#__BV_popover_1__')
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 8f34fddbf06..87c0dc40e5c 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -133,7 +133,7 @@ describe 'Issue Boards', :js do
close_dropdown_menu_if_visible
wait_for_requests
- expect(page).to have_content('No assignee')
+ expect(page).to have_content('None')
end
expect(card_two).not_to have_selector('.avatar')
@@ -143,7 +143,7 @@ describe 'Issue Boards', :js do
click_card(card)
page.within(find('.assignee')) do
- expect(page).to have_content('No assignee')
+ expect(page).to have_content('None')
click_button 'assign yourself'
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 9d1c1e3acc7..d1ed64cce7f 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -112,6 +112,14 @@ describe 'Dashboard Projects' do
expect(first('.project-row')).to have_content(project_with_most_stars.title)
end
+
+ it 'shows tabs to filter by all projects or personal' do
+ visit dashboard_projects_path
+ segmented_button = page.find('.filtered-search-nav .button-filter-group')
+
+ expect(segmented_button).to have_content 'All'
+ expect(segmented_button).to have_content 'Personal'
+ end
end
context 'when on Starred projects tab', :js do
@@ -134,6 +142,12 @@ describe 'Dashboard Projects' do
expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1)
expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1)
end
+
+ it 'does not show tabs to filter by all projects or personal' do
+ visit(starred_dashboard_projects_path)
+
+ expect(page).not_to have_content '.filtered-search-nav'
+ end
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index fd8677feab5..d58e3b2841e 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -17,6 +17,26 @@ describe 'Dashboard Todos' do
end
end
+ context 'when the todo references a merge request' do
+ let(:referenced_mr) { create(:merge_request, source_project: project) }
+ let(:note) { create(:note, project: project, note: "Check out #{referenced_mr.to_reference}") }
+ let!(:todo) { create(:todo, :mentioned, user: user, project: project, author: author, note: note) }
+
+ before do
+ sign_in(user)
+ visit dashboard_todos_path
+ end
+
+ it 'renders the mr link with the extra attributes' do
+ link = page.find_link(referenced_mr.to_reference)
+
+ expect(link).not_to be_nil
+ expect(link['data-iid']).to eq(referenced_mr.iid.to_s)
+ expect(link['data-project-path']).to eq(referenced_mr.project.full_path)
+ expect(link['data-mr-title']).to eq(referenced_mr.title)
+ end
+ end
+
context 'User has a todo', :js do
before do
create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index cc86114e436..5b17c49db2d 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe 'Dashboard > User filters projects' do
let(:user) { create(:user) }
- let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+ let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace, created_at: 2.seconds.ago, updated_at: 2.seconds.ago) }
let(:user2) { create(:user) }
- let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+ let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
before do
project.add_maintainer(user)
@@ -14,6 +14,7 @@ describe 'Dashboard > User filters projects' do
describe 'filtering personal projects' do
before do
+ stub_feature_flags(project_list_filter_bar: false)
project2.add_developer(user)
visit dashboard_projects_path
@@ -30,6 +31,7 @@ describe 'Dashboard > User filters projects' do
describe 'filtering starred projects', :js do
before do
+ stub_feature_flags(project_list_filter_bar: false)
user.toggle_star(project)
visit dashboard_projects_path
@@ -42,4 +44,219 @@ describe 'Dashboard > User filters projects' do
expect(page).not_to have_content('You don\'t have starred projects yet')
end
end
+
+ describe 'without search bar', :js do
+ before do
+ stub_feature_flags(project_list_filter_bar: false)
+
+ project2.add_developer(user)
+ visit dashboard_projects_path
+ end
+
+ it 'autocompletes searches upon typing', :js do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord beerus\n'
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+ end
+
+ describe 'with search bar', :js do
+ before do
+ stub_feature_flags(project_list_filter_bar: true)
+
+ project2.add_developer(user)
+ visit dashboard_projects_path
+ end
+
+ # TODO: move these helpers somewhere more useful
+ def click_sort_direction
+ page.find('.filtered-search-block #filtered-search-sorting-dropdown .reverse-sort-btn').click
+ end
+
+ def select_dropdown_option(selector, label)
+ dropdown = page.find(selector)
+ dropdown.click
+
+ dropdown.find('.dropdown-menu a', text: label, match: :first).click
+ end
+
+ def expect_to_see_projects(sorted_projects)
+ list = page.all('.projects-list .project-name').map(&:text)
+ expect(list).to match(sorted_projects)
+ end
+
+ describe 'Search' do
+ it 'executes when the search button is clicked' do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord vegeta\n'
+ find('.filtered-search .btn').click
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+
+ it 'will execute when i press enter' do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord frieza\n'
+ find('#project-filter-form-field').native.send_keys :enter
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+ end
+
+ describe 'Filter' do
+ before do
+ private_project = create(:project, :private, name: 'Private project', namespace: user.namespace)
+ internal_project = create(:project, :internal, name: 'Internal project', namespace: user.namespace)
+
+ private_project.add_maintainer(user)
+ internal_project.add_maintainer(user)
+ end
+
+ it 'filters private projects only' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Private'
+
+ expect(current_url).to match(/visibility_level=0/)
+
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly("Private project", "Treasure", "Victorialand")
+ end
+
+ it 'filters internal projects only' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Internal'
+
+ expect(current_url).to match(/visibility_level=10/)
+
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly('Internal project')
+ end
+
+ it 'filters any project' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Any'
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
+ end
+ end
+
+ describe 'Sorting' do
+ before do
+ [
+ { name: 'Red ribbon army', created_at: 2.days.ago },
+ { name: 'Cell saga', created_at: Time.now },
+ { name: 'Frieza saga', created_at: 10.days.ago }
+ ].each do |item|
+ project = create(:project, name: item[:name], namespace: user.namespace, created_at: item[:created_at])
+ project.add_developer(user)
+ end
+
+ user.toggle_star(project)
+ user.toggle_star(project2)
+ user2.toggle_star(project2)
+ end
+
+ it 'includes sorting direction' do
+ sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown')
+
+ expect(sorting_dropdown).to have_css '.reverse-sort-btn'
+ end
+
+ it 'has all sorting options', :js do
+ sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown')
+ sorting_option_labels = ['Last updated', 'Created date', 'Name', 'Stars']
+
+ sorting_dropdown.click
+
+ sorting_option_labels.each do |label|
+ expect(sorting_dropdown).to have_content(label)
+ end
+ end
+
+ it 'defaults to "Last updated"', :js do
+ page.find('.filtered-search-block #filtered-search-sorting-dropdown').click
+ active_sorting_option = page.first('.filtered-search-block #filtered-search-sorting-dropdown .is-active')
+
+ expect(active_sorting_option).to have_content 'Last updated'
+ end
+
+ context 'Sorting by name' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Name'
+
+ desc = ['Victorialand', 'Treasure', 'Red ribbon army', 'Frieza saga', 'Cell saga']
+ asc = ['Cell saga', 'Frieza saga', 'Red ribbon army', 'Treasure', 'Victorialand']
+
+ click_sort_direction
+
+ expect_to_see_projects(desc)
+
+ click_sort_direction
+
+ expect_to_see_projects(asc)
+ end
+ end
+
+ context 'Sorting by Last updated' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Last updated'
+
+ desc = ["Frieza saga", "Red ribbon army", "Victorialand", "Treasure", "Cell saga"]
+ asc = ["Cell saga", "Treasure", "Victorialand", "Red ribbon army", "Frieza saga"]
+
+ click_sort_direction
+
+ expect_to_see_projects(desc)
+
+ click_sort_direction
+
+ expect_to_see_projects(asc)
+ end
+ end
+
+ context 'Sorting by Created date' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Created date'
+
+ desc = ["Frieza saga", "Red ribbon army", "Victorialand", "Treasure", "Cell saga"]
+ asc = ["Cell saga", "Treasure", "Victorialand", "Red ribbon army", "Frieza saga"]
+
+ click_sort_direction
+
+ expect_to_see_projects(desc)
+
+ click_sort_direction
+
+ expect_to_see_projects(asc)
+ end
+ end
+
+ context 'Sorting by Stars' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Stars'
+
+ desc = ["Red ribbon army", "Cell saga", "Frieza saga", "Victorialand", "Treasure"]
+ asc = ["Treasure", "Victorialand", "Red ribbon army", "Cell saga", "Frieza saga"]
+
+ click_sort_direction
+
+ expect_to_see_projects(desc)
+
+ click_sort_direction
+
+ expect_to_see_projects(asc)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index 7a91c64d7db..439803f9255 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -21,6 +21,20 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
+ it 'guest leaves the group by url param', :js do
+ group.add_guest(user)
+ group.add_owner(other_user)
+
+ visit group_path(group, leave: 1)
+
+ page.accept_confirm
+
+ expect(find('.flash-notice')).to have_content "You left the \"#{group.full_name}\" group"
+ expect(page).to have_content left_group_message(group)
+ expect(current_path).to eq(dashboard_groups_path)
+ expect(group.users).not_to include(user)
+ end
+
it 'guest leaves the group as last member' do
group.add_guest(user)
@@ -32,7 +46,7 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- it 'owner leaves the group if they is not the last owner' do
+ it 'owner leaves the group if they are not the last owner' do
group.add_owner(user)
group.add_owner(other_user)
@@ -44,7 +58,7 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- it 'owner can not leave the group if they is a last owner' do
+ it 'owner can not leave the group if they are the last owner' do
group.add_owner(user)
visit group_path(group)
@@ -56,6 +70,14 @@ describe 'Groups > Members > Leave group' do
expect(find(:css, '.project-members-page li', text: user.name)).not_to have_selector(:css, 'a.btn-remove')
end
+ it 'owner can not leave the group by url param if they are the last owner', :js do
+ group.add_owner(user)
+
+ visit group_path(group, leave: 1)
+
+ expect(find('.flash-alert')).to have_content 'You do not have permission to leave this group'
+ end
+
def left_group_message(group)
"You left the \"#{group.name}\""
end
diff --git a/spec/features/issuables/markdown_references/jira_spec.rb b/spec/features/issuables/markdown_references/jira_spec.rb
index 8eaccfc0949..cc04798248c 100644
--- a/spec/features/issuables/markdown_references/jira_spec.rb
+++ b/spec/features/issuables/markdown_references/jira_spec.rb
@@ -1,6 +1,6 @@
require "rails_helper"
-describe "Jira", :js do
+describe "Jira", :js, :quarantine do
let(:user) { create(:user) }
let(:actual_project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project) }
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index 0601dd47c03..3a46a4e0167 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -86,26 +86,26 @@ describe 'Sort Issuable List' do
expect(last_merge_request).to include(first_created_issuable.title)
end
end
+ end
- context 'custom sorting' do
- let(:issuable_type) { :merge_request }
+ context 'custom sorting' do
+ let(:issuable_type) { :merge_request }
- it 'supports sorting in asc and desc order' do
- visit_merge_requests_with_state(project, 'open')
+ it 'supports sorting in asc and desc order' do
+ visit_merge_requests_with_state(project, 'open')
- page.within('.issues-other-filters') do
- click_button('Created date')
- click_link('Last updated')
- end
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
+ 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 .qa-reverse-sort').click
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
- expect(first_merge_request).to include(first_updated_issuable.title)
- expect(last_merge_request).to include(last_updated_issuable.title)
- end
+ expect(first_merge_request).to include(first_updated_issuable.title)
+ expect(last_merge_request).to include(last_updated_issuable.title)
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 096756f19cc..1f4e9e79179 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -80,7 +80,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -89,7 +89,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -98,7 +98,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -107,7 +107,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -116,7 +116,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
@@ -125,7 +125,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-confidential', visible: true)
- expect_tokens([{ name: 'confidential' }])
+ expect_tokens([{ name: 'Confidential' }])
expect_filtered_search_input_empty
end
end
@@ -137,7 +137,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -147,7 +147,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -157,7 +157,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -167,7 +167,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -177,7 +177,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end
@@ -189,7 +189,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('author')
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -199,7 +199,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('assignee')
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -209,7 +209,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('milestone')
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -219,7 +219,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('label')
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -229,7 +229,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('my-reaction')
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end
@@ -247,7 +247,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-wip', visible: true)
- expect_tokens([{ name: 'wip' }])
+ expect_tokens([{ name: 'WIP' }])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index a9157ccaaa3..86b15b1d980 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -63,125 +63,6 @@ describe 'Issues > User uses quick actions', :js do
it_behaves_like 'duplicate quick action'
it_behaves_like 'create_merge_request quick action'
it_behaves_like 'due quick action'
-
- describe 'move the issue to another project' do
- let(:issue) { create(:issue, project: project) }
-
- context 'when the project is valid' do
- let(:target_project) { create(:project, :public) }
-
- before do
- target_project.add_maintainer(user)
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'moves the issue' do
- add_note("/move #{target_project.full_path}")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'Issues 1'
- end
- end
-
- context 'when the project is valid but the user not authorized' do
- let(:project_unauthorized) { create(:project, :public) }
-
- before do
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'does not move the issue' do
- add_note("/move #{project_unauthorized.full_path}")
-
- wait_for_requests
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_open
- end
- end
-
- context 'when the project is invalid' do
- before do
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'does not move the issue' do
- add_note("/move not/valid")
-
- wait_for_requests
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_open
- end
- end
-
- context 'when the user issues multiple commands' do
- let(:target_project) { create(:project, :public) }
- let(:milestone) { create(:milestone, title: '1.0', project: project) }
- let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
- let(:bug) { create(:label, project: project, title: 'bug') }
- let(:wontfix) { create(:label, project: project, title: 'wontfix') }
- let(:bug_target) { create(:label, project: target_project, title: 'bug') }
- let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') }
-
- before do
- target_project.add_maintainer(user)
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'applies the commands to both issues and moves the issue' do
- add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
-
- visit project_issue_path(project, issue)
- expect(page).to have_content 'Closed'
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
- end
-
- it 'moves the issue and applies the commands to both issues' do
- add_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
-
- visit project_issue_path(project, issue)
- expect(page).to have_content 'Closed'
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
- end
- end
- end
+ it_behaves_like 'move quick action'
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 51508b78649..bc0ec58bd24 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -91,7 +91,7 @@ describe 'Issues' do
click_button 'Save changes'
page.within('.assignee') do
- expect(page).to have_content 'No assignee - assign yourself'
+ expect(page).to have_content 'None - assign yourself'
end
expect(issue.reload.assignees).to be_empty
@@ -465,7 +465,7 @@ describe 'Issues' do
click_link 'Edit'
click_link 'Unassigned'
first('.title').click
- expect(page).to have_content 'No assignee'
+ expect(page).to have_content 'None'
end
# wait_for_requests does not work with vue-resource at the moment
@@ -479,7 +479,7 @@ describe 'Issues' do
visit project_issue_path(project, issue2)
page.within('.assignee') do
- expect(page).to have_content "No assignee"
+ expect(page).to have_content "None"
end
page.within '.assignee' do
@@ -522,7 +522,7 @@ describe 'Issues' do
close_dropdown_menu_if_visible
page.within '.value .assign-yourself' do
- expect(page).to have_content "No assignee"
+ expect(page).to have_content "None"
end
end
end
@@ -775,10 +775,10 @@ describe 'Issues' do
wait_for_requests
- expect(page).to have_no_content 'No due date'
+ expect(page).to have_no_content 'None'
click_link 'remove due date'
- expect(page).to have_content 'No due date'
+ expect(page).to have_content 'None'
end
end
end
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 65de0afae0c..5db54f42264 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
@@ -191,29 +191,119 @@ describe 'Merge request > User creates image diff notes', :js do
end
end
- describe 'image view modes' do
- before do
- visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ shared_examples 'swipe view' do
+ it 'moves the swipe handle' do
+ # Simulate dragging swipe view slider
+ expect { drag_and_drop_by(find('.swipe-bar'), 20, 0) }
+ .to change { find('.swipe-bar')['style'] }
+ .from(a_string_matching('left: 1px'))
end
- it 'resizes image in onion skin view mode' do
- find('.view-modes-menu .onion-skin').click
+ it 'shows both images at the same position' do
+ drag_and_drop_by(find('.swipe-bar'), 40, 0)
- expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ expect(left_position('.frame.added img'))
+ .to eq(left_position('.frame.deleted img'))
end
+ end
- it 'resets onion skin view mode opacity when toggling between view modes' do
- find('.view-modes-menu .onion-skin').click
-
+ shared_examples 'onion skin' do
+ it 'resets opacity when toggling between view modes' do
# Simulate dragging onion-skin slider
drag_and_drop_by(find('.dragger'), -30, 0)
expect(find('.onion-skin-frame .frame.added', visible: false)['style']).not_to match('opacity: 1;')
+ switch_to_swipe_view
+ switch_to_onion_skin
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ end
+ end
+
+ describe 'changes tab image diff' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, target_branch: 'master', source_branch: 'deleted-image-test', author: user) }
+
+ before do
+ visit diffs_project_merge_request_path(project, merge_request)
+ click_link "Changes"
+ end
+
+ def set_image_diff_sources
+ # set path of added and deleted images to something the spec can view
+ page.execute_script("document.querySelector('.frame.added img').src = '/apple-touch-icon.png';")
+ page.execute_script("document.querySelector('.frame.deleted img').src = '/favicon.png';")
+
+ wait_for_requests
+
+ expect(find('.frame.added img', visible: false)['src']).to match('/apple-touch-icon.png')
+ expect(find('.frame.deleted img', visible: false)['src']).to match('/favicon.png')
+ end
+
+ def switch_to_swipe_view
+ # it isn't given the .swipe class in the merge request diff
+ find('.view-modes-menu li:nth-child(2)').click
+ expect(find('.view-modes-menu li.active')).to have_content('Swipe')
+
+ set_image_diff_sources
+ end
+
+ def switch_to_onion_skin
+ # it isn't given the .onion-skin class in the merge request diff
+ find('.view-modes-menu li:nth-child(3)').click
+ expect(find('.view-modes-menu li.active')).to have_content('Onion skin')
+
+ set_image_diff_sources
+ end
+
+ describe 'onion skin' do
+ before do
+ switch_to_onion_skin
+ end
+
+ it_behaves_like 'onion skin'
+ end
+
+ describe 'swipe view' do
+ before do
+ switch_to_swipe_view
+ end
+
+ it_behaves_like 'swipe view'
+ end
+ end
+
+ describe 'image view modes' do
+ before do
+ visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ end
+
+ def switch_to_swipe_view
find('.view-modes-menu .swipe').click
+ end
+
+ def switch_to_onion_skin
find('.view-modes-menu .onion-skin').click
+ end
- expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ describe 'onion skin' do
+ before do
+ switch_to_onion_skin
+ end
+
+ it 'resizes image' do
+ expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ end
+
+ it_behaves_like 'onion skin'
+ end
+
+ describe 'swipe view' do
+ before do
+ switch_to_swipe_view
+ end
+
+ it_behaves_like 'swipe view'
end
end
@@ -232,4 +322,8 @@ describe 'Merge request > User creates image diff notes', :js do
click_button 'Comment'
wait_for_requests
end
+
+ def left_position(element)
+ page.evaluate_script("document.querySelectorAll('#{element}')[0].getBoundingClientRect().left;")
+ end
end
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 40ba676ff92..a32c6bdcf8f 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -670,4 +670,26 @@ describe 'Merge request > User sees merge widget', :js do
end
end
end
+
+ 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) }
+
+ before do
+ project.update(
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ public_builds: false
+ )
+ merge_request.update!(head_pipeline: pipeline)
+ sign_out(:user)
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'renders a CI pipeline error' do
+ within '.ci-widget' do
+ expect(page).to have_content('Could not retrieve the pipeline status.')
+ end
+ end
+ end
end
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 1b5dd6945e0..04c7f4b6c76 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
@@ -121,7 +121,7 @@ describe 'User comments on a diff', :js do
end
context 'multi-line suggestions' do
- it 'suggestion is presented' do
+ before do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
@@ -130,7 +130,9 @@ describe 'User comments on a diff', :js do
end
wait_for_requests
+ end
+ it 'suggestion is presented' do
page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change')
@@ -160,22 +162,24 @@ describe 'User comments on a diff', :js do
end
it 'suggestion is appliable' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ page.within('.diff-discussions') do
+ expect(page).not_to have_content('Applied')
- page.within('.js-discussion-note-form') do
- fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
- click_button('Comment')
- end
+ click_button('Apply suggestion')
+ wait_for_requests
- wait_for_requests
+ expect(page).to have_content('Applied')
+ end
+ end
+ it 'resolves discussion when applied' do
page.within('.diff-discussions') do
- expect(page).not_to have_content('Applied')
+ expect(page).not_to have_content('Unresolve discussion')
click_button('Apply suggestion')
wait_for_requests
- expect(page).to have_content('Applied')
+ expect(page).to have_content('Unresolve discussion')
end
end
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index 8d308729f62..988a8302527 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe 'Merge request > User uses quick actions', :js do
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index f4105730402..5ebfc32952d 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -14,7 +14,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
- :facebook, :cas3, :auth0, :authentiq]
+ :facebook, :cas3, :auth0, :authentiq, :salesforce]
before(:all) do
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index c8dc72a34ec..3e75890725e 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -35,7 +35,7 @@ describe 'Download buttons in branches page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, 'binary-encoding/download', job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index 03cb3530e2b..111972a6b00 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -30,7 +30,7 @@ describe 'Projects > Files > Download buttons in files tree' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index ec3930c26db..2de22582b2c 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -171,7 +171,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', :quarantine do
click_link('.gitignore')
find('.js-edit-blob').click
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 224375daf71..9cf04fe13b4 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -936,8 +936,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
find('.js-cancel-job').click
end
- it 'loads the page and shows all needed controls' do
- expect(page).to have_content 'Retry'
+ it 'loads the page and shows no controls' do
+ expect(page).not_to have_content 'Retry'
end
end
end
@@ -946,7 +946,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Job from project", :js do
before do
job.run!
- job.cancel!
+ job.drop!(:script_failure)
visit project_job_path(project, job)
wait_for_requests
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index d36f043f880..f32b155790f 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -138,29 +138,41 @@ describe 'Prioritize labels' do
end
context 'as a guest' do
- it 'does not prioritize labels' do
+ before do
+ create(:label_priority, project: project, label: bug, priority: 1)
+ create(:label_priority, project: project, label: feature, priority: 2)
+
guest = create(:user)
sign_in guest
visit project_labels_path(project)
+ end
+ it 'cannot prioritize labels' do
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
- expect(page).not_to have_css('.prioritized-labels')
expect(page).not_to have_content 'Star a label'
end
+
+ it 'cannot sort prioritized labels', :js do
+ drag_to(selector: '.prioritized-labels .label-list-item', from_index: 1, to_index: 2)
+
+ page.within('.prioritized-labels') do
+ expect(first('.label-list-item')).to have_content('bug')
+ expect(page.all('.label-list-item').last).to have_content('feature')
+ end
+ end
end
context 'as a non signed in user' do
- it 'does not prioritize labels' do
+ it 'cannot prioritize labels' do
visit project_labels_path(project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
- expect(page).not_to have_css('.prioritized-labels')
expect(page).not_to have_content 'Star a label'
end
end
diff --git a/spec/features/projects/labels/user_views_labels_spec.rb b/spec/features/projects/labels/user_views_labels_spec.rb
index 2c8267764bd..a6f7968c535 100644
--- a/spec/features/projects/labels/user_views_labels_spec.rb
+++ b/spec/features/projects/labels/user_views_labels_spec.rb
@@ -7,6 +7,7 @@ describe "User views labels" do
set(:user) { create(:user) }
let(:label_titles) { %w[bug enhancement feature] }
+ let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) }
before do
label_titles.each { |title| create(:label, project: project, title: title) }
@@ -18,6 +19,10 @@ describe "User views labels" do
end
it "shows all labels" do
+ page.within('.prioritized-labels .manage-labels-list') do
+ expect(page).to have_content('prioritized-label-name')
+ end
+
page.within('.other-labels .manage-labels-list') do
label_titles.each { |title| expect(page).to have_content(title) }
end
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 0ab29660189..a645b917568 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -8,10 +8,17 @@ describe 'Projects > Members > Group member cannot leave group project' do
before do
group.add_developer(user)
sign_in(user)
- visit project_path(project)
end
it 'user does not see a "Leave project" link' do
+ visit project_path(project)
+
expect(page).not_to have_content 'Leave project'
end
+
+ it 'renders a flash message if attempting to leave by url', :js do
+ visit project_path(project, leave: 1)
+
+ expect(find('.flash-alert')).to have_content 'You do not have permission to leave this project'
+ end
end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 94b29de4686..bd2ef9c07c4 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -7,13 +7,24 @@ describe 'Projects > Members > Member leaves project' do
before do
project.add_developer(user)
sign_in(user)
- visit project_path(project)
end
it 'user leaves project' do
+ visit project_path(project)
+
click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey
end
+
+ it 'user leaves project by url param', :js do
+ visit project_path(project, leave: 1)
+
+ page.accept_confirm
+
+ expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project"
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(project.users.exists?(user.id)).to be_falsey
+ end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cf334e1e4da..a1115b514d3 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -115,11 +115,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the running build and shows retry button' do
+ it 'cancels the running build and does not show retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
+ expect(page).not_to have_css('.js-icon-retry')
end
end
end
@@ -133,11 +133,11 @@ describe 'Pipeline', :js do
end
end
- it 'cancels the preparing build and shows retry button' do
+ it 'cancels the preparing build and does not show retry button' do
find('#ci-badge-deploy .ci-action-icon-container').click
page.within('#ci-badge-deploy') do
- expect(page).to have_css('.js-icon-retry')
+ expect(page).not_to have_css('.js-icon-retry')
end
end
end
@@ -236,6 +236,20 @@ describe 'Pipeline', :js do
end
end
+ context 'when the pipeline has manual stage' do
+ before do
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'CentOS')
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'Debian')
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'OpenSUDE')
+
+ visit_pipeline
+ end
+
+ it 'displays play all button' do
+ expect(page).to have_selector('.js-stage-action')
+ end
+ end
+
context 'page tabs' do
before do
visit_pipeline
@@ -331,11 +345,9 @@ describe 'Pipeline', :js do
merge_request.all_pipelines.last
end
- before do
+ it 'shows the pipeline information' do
visit_pipeline
- end
- it 'shows the pipeline information' do
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -347,6 +359,21 @@ describe 'Pipeline', :js do
end
end
+ context 'when source branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.source_branch)
+ end
+
+ it 'does not link to the source branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.source_branch)
+ expect(page).to have_content(merge_request.source_branch)
+ end
+ end
+ end
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
@@ -386,11 +413,11 @@ describe 'Pipeline', :js do
before do
pipeline.update(user: user)
-
- visit_pipeline
end
it 'shows the pipeline information' do
+ visit_pipeline
+
within '.pipeline-info' do
expect(page).to have_content("#{pipeline.statuses.count} jobs " \
"for !#{merge_request.iid} " \
@@ -405,6 +432,21 @@ describe 'Pipeline', :js do
end
end
+ context 'when target branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.target_branch)
+ end
+
+ it 'does not link to the target branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.target_branch)
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+ end
+
context 'when source project is a forked project' do
let(:source_project) { fork_project(project, user, repository: true) }
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 1259ad45791..8c7bc192c50 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -217,5 +217,36 @@ describe 'Projects > Settings > Repository settings' do
expect(RepositoryCleanupWorker.jobs.count).to eq(1)
end
end
+
+ context 'with an existing mirror', :js do
+ let(:mirrored_project) { create(:project, :repository, :remote_mirror) }
+
+ before do
+ mirrored_project.add_maintainer(user)
+
+ visit project_settings_repository_path(mirrored_project)
+ end
+
+ it 'delete remote mirrors' do
+ expect(mirrored_project.remote_mirrors.count).to eq(1)
+
+ find('.js-delete-mirror').click
+ wait_for_requests
+
+ expect(mirrored_project.remote_mirrors.count).to eq(0)
+ end
+ end
+
+ it 'shows a disabled mirror' do
+ create(:remote_mirror, project: project, enabled: false)
+
+ visit project_settings_repository_path(project)
+
+ mirror = find('.qa-mirrored-repository-row')
+
+ expect(mirror).to have_selector('.qa-delete-mirror')
+ expect(mirror).to have_selector('.qa-disabled-mirror-badge')
+ expect(mirror).not_to have_selector('.qa-update-now-button')
+ 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 b1c2bab08c0..28d52f25f56 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
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require 'spec_helper'
describe 'Projects > Settings > User manages merge request settings' do
@@ -30,16 +31,16 @@ describe 'Projects > Settings > User manages merge request settings' do
context 'when Merge Request and Pipelines are initially enabled', :js do
context 'when Pipelines are initially enabled' do
it 'shows the Merge Requests settings' do
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).not_to have_content 'All discussions must be resolved'
end
end
@@ -50,16 +51,16 @@ describe 'Projects > Settings > User manages merge request settings' do
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
end
end
end
@@ -71,16 +72,16 @@ describe 'Projects > Settings > User manages merge request settings' do
end
it 'does not show the Merge Requests settings' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).not_to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
end
end
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index 3a2dcc5aa55..fee5f8001b0 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -35,11 +35,10 @@ describe 'Projects > Show > Download buttons' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
it 'download links have download attribute' do
- expect(page).to have_selector('a', text: 'Download')
page.all('a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 88f3397608f..e9dd1dc0f66 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -20,6 +20,16 @@ describe 'Projects > Show > User manages notifications', :js do
click_notifications_button
expect(find('.update-notification.is-active')).to have_content('On mention')
+ expect(find('.notifications-icon use')[:'xlink:href']).to end_with('#notifications')
+ end
+
+ it 'changes the notification setting to disabled' do
+ click_notifications_button
+ click_link 'Disabled'
+
+ wait_for_requests
+
+ expect(find('.notifications-icon use')[:'xlink:href']).to end_with('#notifications-off')
end
context 'custom notification settings' do
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index fbfd8cee7aa..4c8ec53836a 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -36,7 +36,7 @@ describe 'Download buttons in tags page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{tag}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index bcbba6f14da..aac095bfa6b 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -141,7 +141,7 @@ describe "User creates wiki page" do
end
end
- it_behaves_like 'wiki file attachments'
+ it_behaves_like 'wiki file attachments', :quarantine
end
context "in a group namespace", :js do
@@ -151,7 +151,7 @@ describe "User creates wiki page" do
expect(page).to have_field("wiki[message]", with: "Create home")
end
- it "creates a page from the home page" do
+ it "creates a page from the home page", :quarantine do
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 0aff916ec83..0dbff5a2701 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Branches', :js do
+ include ProtectedBranchHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -150,27 +152,11 @@ describe 'Protected Branches', :js do
end
describe "access control" do
- include_examples "protected branches > access control > CE"
- end
- end
-
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
- def set_defaults
- find(".js-allowed-to-merge").click
- within('.qa-allowed-to-merge-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
- find(".js-allowed-to-push").click
- within('.qa-allowed-to-push-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
+ include_examples "protected branches > access control > CE"
end
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c8e92cd1c07..652542b1719 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Tags', :js do
+ include ProtectedTagHelpers
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
@@ -8,13 +10,6 @@ describe 'Protected Tags', :js do
sign_in(user)
end
- def set_protected_tag_name(tag_name)
- find(".js-protected-tag-select").click
- find(".dropdown-input-field").set(tag_name)
- click_on("Create wildcard #{tag_name}")
- find('.protected-tags-dropdown .dropdown-menu', visible: false)
- end
-
describe "explicit protected tags" do
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
@@ -92,6 +87,10 @@ describe 'Protected Tags', :js do
end
describe "access control" do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
include_examples "protected tags > access control > CE"
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7225ca65492..6d4facd0649 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -14,22 +14,36 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
- it 'finds a page' do
- find('.js-search-project-dropdown').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('.js-search-project-dropdown').click
- page.within('.project-filter') do
- click_link(project.full_name)
- end
+ page.within('.project-filter') do
+ click_link(project.full_name)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').click
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
- fill_in('dashboard_search', with: 'content')
- find('.btn-search').click
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
+ end
+ end
- page.within('.search-filter') do
- click_link('Wiki')
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
end
+ end
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'test_wiki' }
end
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 9d5780d29b0..efba303033b 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -137,7 +137,7 @@ describe 'Login' do
enter_code(user.current_otp)
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
context 'using one-time code' do
@@ -317,7 +317,17 @@ describe 'Login' do
gitlab_sign_in(user)
expect(current_path).to eq root_path
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
+ end
+
+ it 'does not show already signed in message when opening sign in page after login' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+
+ gitlab_sign_in(user)
+ visit new_user_session_path
+
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
end
@@ -579,7 +589,7 @@ describe 'Login' do
click_button 'Accept terms'
expect(current_path).to eq(root_path)
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
it 'does not ask for terms when the user already accepted them' do
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index abd0d6b5185..bcde115b1a6 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -26,9 +26,17 @@ describe Autocomplete::UsersFinder do
it { is_expected.to match_array([project.owner]) }
context 'when author_id passed' do
- let(:params) { { author_id: user2.id } }
+ context 'and author is active' do
+ let(:params) { { author_id: user1.id } }
- it { is_expected.to match_array([project.owner, user2]) }
+ it { is_expected.to match_array([project.owner, user1]) }
+ end
+
+ context 'and author is blocked' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([project.owner]) }
+ end
end
end
@@ -104,9 +112,9 @@ describe Autocomplete::UsersFinder do
end
context 'when filtered by author_id' do
- let(:params) { { author_id: user2.id } }
+ let(:params) { { author_id: user1.id } }
- it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) }
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
end
end
end
diff --git a/spec/finders/cluster_ancestors_finder_spec.rb b/spec/finders/cluster_ancestors_finder_spec.rb
index 332086c42e2..750042b6b54 100644
--- a/spec/finders/cluster_ancestors_finder_spec.rb
+++ b/spec/finders/cluster_ancestors_finder_spec.rb
@@ -8,11 +8,15 @@ describe ClusterAncestorsFinder, '#execute' do
let(:user) { create(:user) }
let!(:project_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :project_type, projects: [project])
+ create(:cluster, :provided_by_user, :project, projects: [project])
end
let!(:group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group])
+ create(:cluster, :provided_by_user, :group, groups: [group])
+ end
+
+ let!(:instance_cluster) do
+ create(:cluster, :provided_by_user, :instance)
end
subject { described_class.new(clusterable, user).execute }
@@ -25,7 +29,7 @@ describe ClusterAncestorsFinder, '#execute' do
end
it 'returns the project clusters followed by group clusters' do
- is_expected.to eq([project_cluster, group_cluster])
+ is_expected.to eq([project_cluster, group_cluster, instance_cluster])
end
context 'nested groups', :nested_groups do
@@ -33,11 +37,11 @@ describe ClusterAncestorsFinder, '#execute' do
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
+ create(:cluster, :provided_by_user, :group, groups: [parent_group])
end
it 'returns the project clusters followed by group clusters ordered ascending the hierarchy' do
- is_expected.to eq([project_cluster, group_cluster, parent_group_cluster])
+ is_expected.to eq([project_cluster, group_cluster, parent_group_cluster, instance_cluster])
end
end
end
@@ -58,7 +62,7 @@ describe ClusterAncestorsFinder, '#execute' do
end
it 'returns the list of group clusters' do
- is_expected.to eq([group_cluster])
+ is_expected.to eq([group_cluster, instance_cluster])
end
context 'nested groups', :nested_groups do
@@ -66,12 +70,21 @@ describe ClusterAncestorsFinder, '#execute' do
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
+ create(:cluster, :provided_by_user, :group, groups: [parent_group])
end
it 'returns the list of group clusters ordered ascending the hierarchy' do
- is_expected.to eq([group_cluster, parent_group_cluster])
+ is_expected.to eq([group_cluster, parent_group_cluster, instance_cluster])
end
end
end
+
+ context 'for an instance' do
+ let(:clusterable) { Clusters::Instance.new }
+ let(:user) { create(:admin) }
+
+ it 'returns the list of instance clusters' do
+ is_expected.to eq([instance_cluster])
+ end
+ end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 6a47cd013f8..89fdaceaa9f 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -641,9 +641,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
@@ -660,9 +658,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 117f4a03735..da5e9dab058 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequestsFinder do
end
context 'filtering by group' do
- it 'includes all merge requests when user has access exceluding merge requests from projects the user does not have access to' do
+ it 'includes all merge requests when user has access excluding merge requests from projects the user does not have access to' do
private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) }
private_project.add_guest(user)
create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 9da07a0b253..695175689b9 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -36,7 +36,8 @@
"external_hostname": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] },
- "update_available": { "type": ["boolean", "null"] }
+ "update_available": { "type": ["boolean", "null"] },
+ "can_uninstall": { "type": "boolean" }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 162fb9c8108..9898819ef75 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -5,7 +5,7 @@
"iid": { "type": "integer" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
- "lock_version": { "type": ["string", "null"] },
+ "lock_version": { "type": ["integer", "null"] },
"milestone_id": { "type": ["string", "null"] },
"title": { "type": "string" },
"moved_to_id": { "type": ["integer", "null"] },
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 88a600398b1..ac0a0314455 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -23,7 +23,7 @@
},
"task_status": { "type": "string" },
"task_status_short": { "type": "string" },
- "lock_version": { "type": ["string", "null"] }
+ "lock_version": { "type": ["integer", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index 6b1cd60c25d..7018cb9a305 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -125,6 +125,7 @@
"test_reports_path": { "type": ["string", "null"] },
"can_receive_suggestion": { "type": "boolean" },
"source_branch_protected": { "type": "boolean" },
- "conflicts_docs_path": { "type": ["string", "null"] }
+ "conflicts_docs_path": { "type": ["string", "null"] },
+ "merge_request_pipelines_docs_path": { "type": ["string", "null"] }
}
}
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index c76c6945117..690c4a7d4e8 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -33,7 +33,7 @@
"additionalProperties": false
},
"variables": {
- "type": ["array", "null"],
+ "type": "array",
"items": { "$ref": "pipeline_schedule_variable.json" }
}
},
diff --git a/spec/fixtures/api/schemas/pipeline_schedule_variable.json b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
index f7ccb2d44a0..022d36cb88c 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule_variable.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
@@ -1,8 +1,14 @@
{
- "type": ["object", "null"],
+ "type": "object",
+ "required": [
+ "key",
+ "value",
+ "variable_type"
+ ],
"properties": {
"key": { "type": "string" },
- "value": { "type": "string" }
+ "value": { "type": "string" },
+ "variable_type": { "type": "string" }
},
"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 6612c2a9911..6ea0781c1ed 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -1,12 +1,33 @@
{
"type": "object",
- "required" : [
- "tag_name",
- "description"
- ],
- "properties" : {
- "tag_name": { "type": ["string", "null"] },
- "description": { "type": "string" }
+ "required": ["name", "tag_name", "commit"],
+ "properties": {
+ "name": { "type": "string" },
+ "tag_name": { "type": "string" },
+ "description": { "type": "string" },
+ "description_html": { "type": "string" },
+ "created_at": { "type": "date" },
+ "commit": {
+ "oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
+ },
+ "author": {
+ "oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }]
+ },
+ "assets": {
+ "required": ["count", "links", "sources"],
+ "properties": {
+ "count": { "type": "integer" },
+ "links": { "$ref": "../../release/links.json" },
+ "sources": {
+ "type": "array",
+ "items": {
+ "format": "zip",
+ "url": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
},
"additionalProperties": false
}
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
new file mode 100644
index 00000000000..e78398ad1d5
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
@@ -0,0 +1,22 @@
+{
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "description_html": { "type": "string" },
+ "created_at": { "type": "date" },
+ "author": {
+ "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
+ },
+ "assets": {
+ "required": ["count", "links"],
+ "properties": {
+ "count": { "type": "integer" },
+ "links": { "$ref": "../../../release/links.json" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json
new file mode 100644
index 00000000000..c13966b28e9
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "release_for_guest.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json
new file mode 100644
index 00000000000..6612c2a9911
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "tag_name",
+ "description"
+ ],
+ "properties" : {
+ "tag_name": { "type": ["string", "null"] },
+ "description": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/releases.json b/spec/fixtures/api/schemas/public_api/v4/releases.json
new file mode 100644
index 00000000000..e26215707fe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/releases.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "release.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
index 10d4edb7ffb..5713ea1f526 100644
--- a/spec/fixtures/api/schemas/public_api/v4/tag.json
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -14,7 +14,7 @@
"release": {
"oneOf": [
{ "type": "null" },
- { "$ref": "release.json" }
+ { "$ref": "release/tag_release.json" }
]
}
},
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
new file mode 100644
index 00000000000..638ecbcc11f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -0,0 +1,36 @@
+dashboard: 'Test Dashboard'
+priority: 1
+panel_groups:
+- group: Group A
+ priority: 1
+ panels:
+ - title: "Super Chart A1"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_a1
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
+ - title: "Super Chart A2"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 2
+ metrics:
+ - id: metric_a2
+ query_range: 'query'
+ label: Legend Label
+ unit: unit
+- group: Group B
+ priority: 10
+ panels:
+ - title: "Super Chart B"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_b
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
new file mode 100644
index 00000000000..1ee1205e29a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": ["dashboard", "priority", "panel_groups"],
+ "properties": {
+ "dashboard": { "type": "string" },
+ "priority": { "type": "number" },
+ "panel_groups": {
+ "type": "array",
+ "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
new file mode 100644
index 00000000000..2d0af57ec2c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "unit",
+ "label"
+ ],
+ "oneOf": [
+ { "required": ["query"] },
+ { "required": ["query_range"] }
+ ],
+ "properties": {
+ "id": { "type": "string" },
+ "query_range": { "type": "string" },
+ "query": { "type": "string" },
+ "unit": { "type": "string" },
+ "label": { "type": "string" },
+ "track": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
new file mode 100644
index 00000000000..d7a390adcdc
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "group",
+ "priority",
+ "panels"
+ ],
+ "properties": {
+ "group": { "type": "string" },
+ "priority": { "type": "number" },
+ "panels": {
+ "type": "array",
+ "items": { "$ref": "panels.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
new file mode 100644
index 00000000000..1548daacd64
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "title",
+ "y_label",
+ "weight",
+ "metrics"
+ ],
+ "properties": {
+ "title": { "type": "string" },
+ "type": { "type": "string" },
+ "y_label": { "type": "string" },
+ "weight": { "type": "number" },
+ "metrics": {
+ "type": "array",
+ "items": { "$ref": "metrics.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 33a35069004..73897107f67 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,16 +1,13 @@
import Clusters from '~/clusters/clusters_bundle';
-import {
- REQUEST_SUBMITTED,
- REQUEST_FAILURE,
- APPLICATION_STATUS,
- INGRESS_DOMAIN_SUFFIX,
-} from '~/clusters/constants';
+import { APPLICATION_STATUS, INGRESS_DOMAIN_SUFFIX, APPLICATIONS } from '~/clusters/constants';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import $ from 'jquery';
+const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
+
describe('Clusters', () => {
setTestTimeout(1000);
@@ -93,7 +90,7 @@ describe('Clusters', () => {
it('does not show alert when things transition from initial null state to something', () => {
cluster.checkForNewInstalls(INITIAL_APP_MAP, {
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' },
+ helm: { status: INSTALLABLE, title: 'Helm Tiller' },
});
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
@@ -105,11 +102,11 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
},
);
@@ -125,13 +122,13 @@ describe('Clusters', () => {
cluster.checkForNewInstalls(
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' },
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: INSTALLABLE, title: 'Ingress' },
},
{
...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' },
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: INSTALLED, title: 'Ingress' },
},
);
@@ -215,73 +212,62 @@ describe('Clusters', () => {
});
describe('installApplication', () => {
- it('tries to install helm', () => {
+ it.each(APPLICATIONS)('tries to install %s', applicationId => {
jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
+ cluster.store.state.applications[applicationId].status = INSTALLABLE;
- cluster.installApplication({ id: 'helm' });
+ cluster.installApplication({ id: applicationId });
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
+ 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);
});
- it('tries to install ingress', () => {
- jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
-
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
-
- cluster.installApplication({ id: 'ingress' });
-
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
- });
+ it('sets error request status when the request fails', () => {
+ jest
+ .spyOn(cluster.service, 'installApplication')
+ .mockRejectedValueOnce(new Error('STUBBED ERROR'));
- it('tries to install runner', () => {
- jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
+ cluster.store.state.applications.helm.status = INSTALLABLE;
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
+ const promise = cluster.installApplication({ id: 'helm' });
- cluster.installApplication({ id: 'runner' });
+ return promise.then(() => {
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
+ expect(cluster.store.state.applications.helm.installFailed).toBe(true);
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ });
});
+ });
- it('tries to install jupyter', () => {
- jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
+ describe('uninstallApplication', () => {
+ it.each(APPLICATIONS)('tries to uninstall %s', applicationId => {
+ jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
- cluster.installApplication({
- id: 'jupyter',
- params: { hostname: cluster.store.state.applications.jupyter.hostname },
- });
+ cluster.store.state.applications[applicationId].status = INSTALLED;
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
- hostname: cluster.store.state.applications.jupyter.hostname,
- });
+ cluster.uninstallApplication({ id: applicationId });
+
+ expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
+ expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
+ expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId);
});
- it('sets error request status when the request fails', () => {
+ it('sets error request status when the uninstall request fails', () => {
jest
- .spyOn(cluster.service, 'installApplication')
+ .spyOn(cluster.service, 'uninstallApplication')
.mockRejectedValueOnce(new Error('STUBBED ERROR'));
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
-
- const promise = cluster.installApplication({ id: 'helm' });
+ cluster.store.state.applications.helm.status = INSTALLED;
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalled();
+ const promise = cluster.uninstallApplication({ id: 'helm' });
return promise.then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
+ expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
+
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
@@ -315,18 +301,16 @@ describe('Clusters', () => {
});
describe('toggleIngressDomainHelpText', () => {
- const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS;
let ingressPreviousState;
let ingressNewState;
beforeEach(() => {
- ingressPreviousState = { status: INSTALLABLE };
- ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' };
+ ingressPreviousState = { externalIp: null };
+ ingressNewState = { externalIp: '127.0.0.1' };
});
- describe(`when ingress application new status is ${INSTALLED}`, () => {
+ describe(`when ingress have an external ip assigned`, () => {
beforeEach(() => {
- ingressNewState.status = INSTALLED;
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
});
@@ -341,31 +325,11 @@ describe('Clusters', () => {
});
});
- describe(`when ingress application new status is different from ${INSTALLED}`, () => {
+ describe(`when ingress does not have an external ip assigned`, () => {
it('hides custom domain help text', () => {
- ingressNewState.status = NOT_INSTALLABLE;
- cluster.ingressDomainHelpText.classList.remove('hide');
-
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
-
- expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
- });
- });
-
- describe('when ingress application new status and old status are the same', () => {
- it('does not display custom domain help text', () => {
- ingressPreviousState.status = INSTALLED;
- ingressNewState.status = ingressPreviousState.status;
-
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
-
- expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
- });
- });
-
- describe(`when ingress new status is ${INSTALLED} and there isn’t an ip assigned`, () => {
- it('does not display custom domain help text', () => {
+ ingressPreviousState.externalIp = '127.0.0.1';
ingressNewState.externalIp = null;
+ cluster.ingressDomainHelpText.classList.remove('hide');
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index b28d0075d06..7c781b72355 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
+import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
+
import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
@@ -80,17 +83,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false);
});
- it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.SCHEDULED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -102,22 +94,12 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has loading "Installing" when REQUEST_SUBMITTED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_SUBMITTED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
- it('has disabled "Installed" when APPLICATION_STATUS.INSTALLED', () => {
+ it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLED,
+ installed: true,
+ uninstallable: false,
});
expect(vm.installButtonLabel).toEqual('Installed');
@@ -125,21 +107,23 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => {
+ it('hides when application is installed and uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATING,
+ status: APPLICATION_STATUS.INSTALLED,
+ installed: true,
+ uninstallable: true,
});
+ const installBtn = vm.$el.querySelector('.js-cluster-application-install-button');
- expect(vm.installButtonLabel).toEqual('Installed');
- expect(vm.installButtonLoading).toEqual(false);
- expect(vm.installButtonDisabled).toEqual(true);
+ expect(installBtn).toBe(null);
});
- it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
+ it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -151,7 +135,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -208,6 +191,60 @@ describe('Application Row', () => {
});
});
+ describe('Uninstall button', () => {
+ it('displays button when app is installed and uninstallable', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ installed: true,
+ uninstallable: true,
+ status: APPLICATION_STATUS.NOT_INSTALLABLE,
+ });
+ const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button');
+
+ expect(uninstallButton).toBeTruthy();
+ });
+
+ it('displays a success toast message if application uninstall was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ uninstallSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.uninstallSuccessful = true;
+
+ return vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner uninstalled successfully.');
+ });
+ });
+ });
+
+ describe('when confirmation modal triggers confirm event', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(ApplicationRow, {
+ propsData: {
+ ...DEFAULT_APPLICATION_STATE,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('triggers uninstallApplication event', () => {
+ jest.spyOn(eventHub, '$emit');
+ wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ });
+ });
+ });
+
describe('Upgrade button', () => {
it('has indeterminate state on page load', () => {
vm = mountComponent(ApplicationRow, {
@@ -230,15 +267,15 @@ describe('Application Row', () => {
expect(upgradeBtn.innerHTML).toContain('Upgrade');
});
- it('has enabled "Retry update" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
+ it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
expect(upgradeBtn).not.toBe(null);
- expect(vm.upgradeFailed).toBe(true);
expect(upgradeBtn.innerHTML).toContain('Retry update');
});
@@ -258,7 +295,8 @@ describe('Application Row', () => {
jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ upgradeAvailable: true,
});
const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
@@ -287,7 +325,8 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
const failureMessage = vm.$el.querySelector(
'.js-cluster-application-upgrade-failure-message',
@@ -298,6 +337,21 @@ describe('Application Row', () => {
'Update failed. Please check the logs and try again.',
);
});
+
+ it('displays a success toast message if application upgrade was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ updateSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.updateSuccessful = true;
+
+ return vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner upgraded successfully.');
+ });
+ });
});
describe('Version', () => {
@@ -305,7 +359,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -321,7 +376,8 @@ describe('Application Row', () => {
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
chartRepo,
version,
});
@@ -335,7 +391,8 @@ describe('Application Row', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
version,
});
const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
@@ -347,60 +404,88 @@ describe('Application Row', () => {
});
describe('Error block', () => {
- it('does not show error block when there is no error', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: null,
- requestStatus: null,
+ describe('when nothing fails', () => {
+ it('does not show error block', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ });
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
+
+ expect(generalErrorMessage).toBeNull();
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
-
- expect(generalErrorMessage).toBeNull();
});
- it('shows status reason when APPLICATION_STATUS.ERROR', () => {
+ describe('when install or uninstall fails', () => {
const statusReason = 'We broke it 0.0';
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
- statusReason,
+ const requestReason = 'We broke the request 0.0';
+
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ statusReason,
+ requestReason,
+ installFailed: true,
+ });
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
- const statusErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-status-error-message',
- );
- expect(generalErrorMessage.textContent.trim()).toEqual(
- `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
- );
+ it('shows status reason if it is available', () => {
+ const statusErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-status-error-message',
+ );
+
+ expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ });
- expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ it('shows request reason if it is available', () => {
+ const requestErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-request-error-message',
+ );
+
+ expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ });
});
- it('shows request reason when REQUEST_FAILURE', () => {
- const requestReason = 'We broke thre request 0.0';
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
- requestReason,
+ describe('when install fails', () => {
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ installFailed: true,
+ });
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
- const requestErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-request-error-message',
- );
- expect(generalErrorMessage.textContent.trim()).toEqual(
- `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
- );
+ it('shows a general message indicating the installation failed', () => {
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(
+ `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
+ );
+ });
+ });
+
+ describe('when uninstall fails', () => {
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ uninstallFailed: true,
+ });
+ });
+
+ it('shows a general message indicating the uninstalling failed', () => {
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
- expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ expect(generalErrorMessage.textContent.trim()).toEqual(
+ `Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
+ );
+ });
});
});
});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 7c54a27d950..8bcf02f0a34 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -75,7 +75,7 @@ describe('Applications', () => {
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
it('renders a row for GitLab Runner', () => {
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
new file mode 100644
index 00000000000..9f9397d4d41
--- /dev/null
+++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js
@@ -0,0 +1,32 @@
+import { shallowMount } from '@vue/test-utils';
+import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+describe('UninstallApplicationButton', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UninstallApplicationButton, {
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | loading | disabled | label
+ ${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
+ ${UPDATING} | ${false} | ${true} | ${'Uninstall'}
+ ${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
+ `('when app status is $status', ({ loading, disabled, status, label }) => {
+ it(`renders a loading=${loading}, disabled=${disabled} button with label="${label}"`, () => {
+ createComponent({ status });
+ expect(wrapper.find(LoadingButton).props()).toMatchObject({ loading, disabled, label });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
new file mode 100644
index 00000000000..04808864fc0
--- /dev/null
+++ b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
+import { GlModal } from '@gitlab/ui';
+import { INGRESS } from '~/clusters/constants';
+
+describe('UninstallApplicationConfirmationModal', () => {
+ let wrapper;
+ const appTitle = 'Ingress';
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UninstallApplicationConfirmationModal, {
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent({ application: INGRESS, applicationTitle: appTitle });
+ });
+
+ it(`renders a modal with a title "Uninstall ${appTitle}"`, () => {
+ expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`);
+ });
+
+ it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => {
+ expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`);
+ });
+
+ describe('when ok button is clicked', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'trackUninstallButtonClick');
+ wrapper.find(GlModal).vm.$emit('ok');
+ });
+
+ it('emits confirm event', () => {
+ expect(wrapper.emitted('confirm')).toBeTruthy();
+ });
+
+ it('calls track uninstall button click mixin', () => {
+ expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS);
+ });
+ });
+
+ it('displays a warning text indicating the app will be uninstalled', () => {
+ expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`);
+ });
+
+ it('displays a custom warning text depending on the application', () => {
+ expect(wrapper.text()).toContain(
+ `The associated load balancer and IP will be deleted and cannot be restored.`,
+ );
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
new file mode 100644
index 00000000000..e057e2ac955
--- /dev/null
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -0,0 +1,162 @@
+import transitionApplicationState from '~/clusters/services/application_state_machine';
+import {
+ APPLICATION_STATUS,
+ UNINSTALL_EVENT,
+ UPDATE_EVENT,
+ INSTALL_EVENT,
+} from '~/clusters/constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+ UNINSTALLING,
+ UNINSTALL_ERRORED,
+} = APPLICATION_STATUS;
+
+const NO_EFFECTS = 'no effects';
+
+describe('applicationStateMachine', () => {
+ const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
+
+ describe(`current state is ${NO_STATUS}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ ${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NO_STATUS,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${NOT_INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NOT_INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UPDATING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true, updateAcknowledged: false }}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UPDATING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UNINSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }}
+ ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UNINSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index b4d1bb710e0..41ad398e924 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -11,6 +11,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'helm',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: null,
+ can_uninstall: false,
},
{
name: 'ingress',
@@ -18,32 +19,38 @@ const CLUSTERS_MOCK_DATA = {
status_reason: 'Cannot connect',
external_ip: null,
external_hostname: null,
+ can_uninstall: false,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
+ can_uninstall: false,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
+ can_uninstall: false,
},
],
},
@@ -113,7 +120,6 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
- requestStatus: null,
requestReason: null,
};
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index 161722ec571..aa926bb36d7 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -1,5 +1,5 @@
import ClustersStore from '~/clusters/stores/clusters_store';
-import { APPLICATION_STATUS } from '~/clusters/constants';
+import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
describe('Clusters Store', () => {
@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
});
describe('updateAppProperty', () => {
- it('should store new request status', () => {
- expect(store.state.applications.helm.requestStatus).toEqual(null);
-
- const newStatus = APPLICATION_STATUS.INSTALLING;
- store.updateAppProperty('helm', 'requestStatus', newStatus);
-
- expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
- });
-
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
@@ -68,66 +59,111 @@ describe('Clusters Store', () => {
title: 'Helm Tiller',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
- requestStatus: null,
requestReason: null,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
ingress: {
title: 'Ingress',
- status: mockResponseData.applications[1].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
- requestStatus: null,
requestReason: null,
externalIp: null,
externalHostname: null,
+ installed: false,
+ installFailed: true,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
- requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
upgradeAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
+ installed: false,
+ installFailed: false,
+ updateAcknowledged: true,
+ updateFailed: false,
+ updateSuccessful: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
prometheus: {
title: 'Prometheus',
- status: mockResponseData.applications[3].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
- requestStatus: null,
requestReason: null,
+ installed: false,
+ installFailed: true,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
- requestStatus: null,
requestReason: null,
hostname: '',
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
- requestStatus: null,
requestReason: null,
hostname: null,
isEditingHostName: false,
externalIp: null,
externalHostname: null,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
cert_manager: {
title: 'Cert-Manager',
- status: mockResponseData.applications[6].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
- requestStatus: null,
requestReason: null,
email: mockResponseData.applications[6].email,
+ installed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
},
});
});
+ describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
+ it('marks application as installed', () => {
+ const mockResponseData =
+ CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+ const runnerAppIndex = 2;
+
+ mockResponseData.applications[runnerAppIndex].status = status;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(store.state.applications[RUNNER].installed).toBe(true);
+ });
+ });
+
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 34df8019a2e..9612162ad0c 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -24,8 +24,9 @@ class CustomEnvironment extends JSDOMEnvironment {
});
const { testEnvironmentOptions } = config;
+ const { IS_EE } = testEnvironmentOptions;
this.global.gon = {
- ee: testEnvironmentOptions.IS_EE,
+ ee: IS_EE,
};
this.rejectedPromises = [];
@@ -33,6 +34,10 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.promiseRejectionHandler = error => {
this.rejectedPromises.push(error);
};
+
+ this.global.fixturesBasePath = `${process.cwd()}/${
+ IS_EE ? 'ee/' : ''
+ }spec/javascripts/fixtures`;
}
async teardown() {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index ed12af925f1..3886853f3c1 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,7 +1,7 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
-import GfmAutoComplete from '~/gfm_auto_complete';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import 'jquery.caret';
import 'at.js';
@@ -94,7 +94,7 @@ describe('GfmAutoComplete', () => {
});
it('should quote if value contains any non-alphanumeric characters', () => {
- expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label\\-20"');
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
});
@@ -102,12 +102,21 @@ describe('GfmAutoComplete', () => {
expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
});
- it('should escape Markdown emphasis characters, except in the first character', () => {
- expect(beforeInsert(atwhoInstance, '@_group')).toEqual('@\\_group');
- expect(beforeInsert(atwhoInstance, '~_bug')).toEqual('~\\_bug');
+ it('escapes Markdown strikethroughs when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a~bug')).toEqual('~"a~bug"');
+ expect(beforeInsert(atwhoInstance, '~a~~bug~~')).toEqual('~"a\\~~bug\\~~"');
+ });
+
+ it('escapes Markdown emphasis when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a_bug_')).toEqual('~a_bug\\_');
+ expect(beforeInsert(atwhoInstance, '~a _bug_')).toEqual('~"a \\_bug\\_"');
+ expect(beforeInsert(atwhoInstance, '~a*bug*')).toEqual('~"a\\*bug\\*"');
+ expect(beforeInsert(atwhoInstance, '~a *bug*')).toEqual('~"a \\*bug\\*"');
+ });
+
+ it('escapes Markdown code spans when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a`bug`')).toEqual('~"a\\`bug\\`"');
expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"');
- expect(beforeInsert(atwhoInstance, '~a ~bug')).toEqual('~"a \\~bug"');
- expect(beforeInsert(atwhoInstance, '~a **bug')).toEqual('~"a \\*\\*bug"');
});
});
@@ -200,6 +209,38 @@ describe('GfmAutoComplete', () => {
});
});
+ describe('DefaultOptions.highlighter', () => {
+ beforeEach(() => {
+ atwhoInstance = { setting: {} };
+ });
+
+ it('should return li if no query is given', () => {
+ const liTag = '<li></li>';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag);
+
+ expect(highlightedTag).toEqual(liTag);
+ });
+
+ it('should highlight search query in li element', () => {
+ const liTag = '<li><img src="" />string</li>';
+ const query = 's';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> <strong>s</strong>tring </li>');
+ });
+
+ it('should highlight search query with special char in li element', () => {
+ const liTag = '<li><img src="" />te.st</li>';
+ const query = '.';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> te<strong>.</strong>st </li>');
+ });
+ });
+
describe('isLoading', () => {
it('should be true with loading data object item', () => {
expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js
index f0351aa31c6..b77bcd6266e 100644
--- a/spec/frontend/helpers/fixtures.js
+++ b/spec/frontend/helpers/fixtures.js
@@ -3,10 +3,8 @@ import path from 'path';
import { ErrorWithStack } from 'jest-util';
-const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures');
-
export function getFixture(relativePath) {
- const absolutePath = path.join(fixturesBasePath, relativePath);
+ const absolutePath = path.join(global.fixturesBasePath, relativePath);
if (!fs.existsSync(absolutePath)) {
throw new ErrorWithStack(
`Fixture file ${relativePath} does not exist.
diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
index 8e8b7f06ca2..6a1a826093c 100644
--- a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
@@ -2,6 +2,9 @@ import createState from '~/ide/stores/modules/file_templates/state';
import * as types from '~/ide/stores/modules/file_templates/mutation_types';
import mutations from '~/ide/stores/modules/file_templates/mutations';
+const mockFileTemplates = [['MIT'], ['CC']];
+const mockTemplateType = 'test';
+
describe('IDE file templates mutations', () => {
let state;
@@ -10,11 +13,21 @@ describe('IDE file templates mutations', () => {
});
describe(`${types.REQUEST_TEMPLATE_TYPES}`, () => {
- it('sets isLoading', () => {
+ it('sets loading to true', () => {
+ state.isLoading = false;
+
mutations[types.REQUEST_TEMPLATE_TYPES](state);
expect(state.isLoading).toBe(true);
});
+
+ it('sets templates to an empty array', () => {
+ state.templates = mockFileTemplates;
+
+ mutations[types.REQUEST_TEMPLATE_TYPES](state);
+
+ expect(state.templates).toEqual([]);
+ });
});
describe(`${types.RECEIVE_TEMPLATE_TYPES_ERROR}`, () => {
@@ -31,29 +44,33 @@ describe('IDE file templates mutations', () => {
it('sets isLoading to false', () => {
state.isLoading = true;
- mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, []);
+ mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates);
expect(state.isLoading).toBe(false);
});
- it('sets templates', () => {
- mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, ['test']);
+ it('sets templates to payload', () => {
+ state.templates = ['test'];
+
+ mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates);
- expect(state.templates).toEqual(['test']);
+ expect(state.templates).toEqual(mockFileTemplates);
});
});
describe(`${types.SET_SELECTED_TEMPLATE_TYPE}`, () => {
- it('sets selectedTemplateType', () => {
- mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type');
+ it('sets templates type to selected type', () => {
+ state.selectedTemplateType = '';
- expect(state.selectedTemplateType).toBe('type');
+ mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType);
+
+ expect(state.selectedTemplateType).toBe(mockTemplateType);
});
- it('clears templates', () => {
- state.templates = ['test'];
+ it('sets templates to empty array', () => {
+ state.templates = mockFileTemplates;
- mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type');
+ mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType);
expect(state.templates).toEqual([]);
});
@@ -61,6 +78,8 @@ describe('IDE file templates mutations', () => {
describe(`${types.SET_UPDATE_SUCCESS}`, () => {
it('sets updateSuccess', () => {
+ state.updateSuccess = false;
+
mutations[types.SET_UPDATE_SUCCESS](state, true);
expect(state.updateSuccess).toBe(true);
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..17a998d0174
--- /dev/null
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,185 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchJobs: jest.fn(),
+ fetchRepos: jest.fn(actions.requestRepos),
+ fetchImport: jest.fn(actions.requestImport),
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ const component = mount(importProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a loading icon whilst repos are loading', () =>
+ vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ }));
+
+ it('renders a table with imported projects and provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ });
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ });
+ });
+
+ it('shows loading spinner when bulk import button is clicked', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ });
+ });
+
+ it('imports provider repos if bulk import button is clicked', () => {
+ mountComponent();
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ });
+ });
+
+ it('polls to update the status of imported projects', () => {
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
new file mode 100644
index 00000000000..f95acc1edd7
--- /dev/null
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -0,0 +1,58 @@
+import Vuex from 'vuex';
+import createStore from '~/import_projects/store';
+import { createLocalVue, mount } from '@vue/test-utils';
+import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportedProjectTableRow', () => {
+ let vm;
+ const project = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'finished',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const component = mount(importedProjectTableRow, {
+ localVue,
+ store: createStore(),
+ propsData: {
+ project: {
+ ...project,
+ },
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders an imported project table row', () => {
+ const providerLink = vm.$el.querySelector('.js-provider-link');
+ const statusObject = STATUS_MAP[project.importStatus];
+
+ expect(vm.$el.classList.contains('js-imported-project')).toBe(true);
+ expect(providerLink.href).toMatch(project.providerLink);
+ expect(providerLink.textContent).toMatch(project.importSource);
+ expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath);
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath);
+ });
+});
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
new file mode 100644
index 00000000000..02c786d8d0b
--- /dev/null
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -0,0 +1,104 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
+import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
+
+describe('ProviderRepoTableRow', () => {
+ let vm;
+ const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
+ const repo = {
+ id: 10,
+ sanitizedName: 'sanitizedName',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchImport,
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+
+ const component = mount(providerRepoTableRow, {
+ localVue,
+ store,
+ propsData: {
+ repo,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a provider repo table row', () => {
+ const providerLink = vm.$el.querySelector('.js-provider-link');
+ const statusObject = STATUS_MAP[STATUSES.NONE];
+
+ expect(vm.$el.classList.contains('js-provider-repo')).toBe(true);
+ expect(providerLink.href).toMatch(repo.providerLink);
+ expect(providerLink.textContent).toMatch(repo.fullName);
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ expect(vm.$el.querySelector('.js-import-button')).not.toBeNull();
+ });
+
+ it('renders a select2 namespace select', () => {
+ const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
+
+ expect(dropdownTrigger).not.toBeNull();
+ expect(dropdownTrigger.classList.contains('select2-container')).toBe(true);
+
+ dropdownTrigger.click();
+
+ expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
+ });
+
+ it('imports repo when clicking import button', () => {
+ vm.$el.querySelector('.js-import-button').click();
+
+ return vm.$nextTick().then(() => {
+ const { calls } = fetchImport.mock;
+
+ // Not using .toBeCalledWith because it expects
+ // an unmatchable and undefined 3rd argument.
+ expect(calls.length).toBe(1);
+ expect(calls[0][1]).toEqual({
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
new file mode 100644
index 00000000000..6a7b90788dd
--- /dev/null
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -0,0 +1,284 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ SET_INITIAL_DATA,
+ REQUEST_REPOS,
+ RECEIVE_REPOS_SUCCESS,
+ RECEIVE_REPOS_ERROR,
+ REQUEST_IMPORT,
+ RECEIVE_IMPORT_SUCCESS,
+ RECEIVE_IMPORT_ERROR,
+ RECEIVE_JOBS_SUCCESS,
+} from '~/import_projects/store/mutation_types';
+import {
+ setInitialData,
+ requestRepos,
+ receiveReposSuccess,
+ receiveReposError,
+ fetchRepos,
+ requestImport,
+ receiveImportSuccess,
+ receiveImportError,
+ fetchImport,
+ receiveJobsSuccess,
+ fetchJobs,
+ clearJobsEtagPoll,
+ stopJobsPolling,
+} from '~/import_projects/store/actions';
+import state from '~/import_projects/store/state';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('import_projects store actions', () => {
+ let localState;
+ const repoId = 1;
+ const repos = [{ id: 1 }, { id: 2 }];
+ const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } };
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('setInitialData', () => {
+ it(`commits ${SET_INITIAL_DATA} mutation`, done => {
+ const initialData = {
+ reposPath: 'reposPath',
+ provider: 'provider',
+ jobsPath: 'jobsPath',
+ importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath',
+ defaultTargetNamespace: 'defaultTargetNamespace',
+ ciCdOnly: 'ciCdOnly',
+ canSelectNamespace: 'canSelectNamespace',
+ };
+
+ testAction(
+ setInitialData,
+ initialData,
+ localState,
+ [{ type: SET_INITIAL_DATA, payload: initialData }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestRepos', () => {
+ it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => {
+ testAction(
+ requestRepos,
+ null,
+ localState,
+ [{ type: REQUEST_REPOS, payload: null }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReposSuccess', () => {
+ it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => {
+ testAction(
+ receiveReposSuccess,
+ repos,
+ localState,
+ [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReposError', () => {
+ it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => {
+ testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done);
+ });
+ });
+
+ describe('fetchRepos', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.reposPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => {
+ const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [
+ { type: 'requestRepos' },
+ {
+ type: 'receiveReposSuccess',
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ {
+ type: 'fetchJobs',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [{ type: 'requestRepos' }, { type: 'receiveReposError' }],
+ done,
+ );
+ });
+ });
+
+ describe('requestImport', () => {
+ it(`commits ${REQUEST_IMPORT} mutation`, done => {
+ testAction(
+ requestImport,
+ repoId,
+ localState,
+ [{ type: REQUEST_IMPORT, payload: repoId }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveImportSuccess', () => {
+ it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => {
+ const payload = { importedProject: { name: 'imported/project' }, repoId: 2 };
+
+ testAction(
+ receiveImportSuccess,
+ payload,
+ localState,
+ [{ type: RECEIVE_IMPORT_SUCCESS, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveImportError', () => {
+ it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => {
+ testAction(
+ receiveImportError,
+ repoId,
+ localState,
+ [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchImport', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.importPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => {
+ const importedProject = { name: 'imported/project' };
+ const importRepoId = importPayload.repo.id;
+ mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
+
+ testAction(
+ fetchImport,
+ importPayload,
+ localState,
+ [],
+ [
+ { type: 'requestImport', payload: importRepoId },
+ {
+ type: 'receiveImportSuccess',
+ payload: {
+ importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }),
+ repoId: importRepoId,
+ },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => {
+ mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
+
+ testAction(
+ fetchImport,
+ importPayload,
+ localState,
+ [],
+ [
+ { type: 'requestImport', payload: importPayload.repo.id },
+ { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('receiveJobsSuccess', () => {
+ it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => {
+ testAction(
+ receiveJobsSuccess,
+ repos,
+ localState,
+ [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobs', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.jobsPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ stopJobsPolling();
+ clearJobsEtagPoll();
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
+ const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
+
+ testAction(
+ fetchJobs,
+ null,
+ localState,
+ [],
+ [
+ {
+ type: 'receiveJobsSuccess',
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/mr_popover/index_spec.js b/spec/frontend/mr_popover/index_spec.js
index 8c33e52a04b..b9db2342687 100644
--- a/spec/frontend/mr_popover/index_spec.js
+++ b/spec/frontend/mr_popover/index_spec.js
@@ -7,18 +7,28 @@ createDefaultClient.default = jest.fn();
describe('initMRPopovers', () => {
let mr1;
let mr2;
+ let mr3;
beforeEach(() => {
setHTMLFixture(`
- <div id="one" class="gfm-merge_request">MR1</div>
- <div id="two" class="gfm-merge_request">MR2</div>
+ <div id="one" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ MR1
+ </div>
+ <div id="two" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ MR2
+ </div>
+ <div id="three" class="gfm-merge_request">
+ MR3
+ </div>
`);
mr1 = document.querySelector('#one');
mr2 = document.querySelector('#two');
+ mr3 = document.querySelector('#three');
mr1.addEventListener = jest.fn();
mr2.addEventListener = jest.fn();
+ mr3.addEventListener = jest.fn();
});
it('does not add the same event listener twice', () => {
@@ -27,4 +37,10 @@ describe('initMRPopovers', () => {
expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
});
+
+ it('does not add listener if it does not have the necessary data attributes', () => {
+ initMRPopovers([mr1, mr2, mr3]);
+
+ expect(mr3.addEventListener).not.toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
new file mode 100644
index 00000000000..0a52c81571e
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -0,0 +1,104 @@
+import createStore from '~/notes/stores';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { discussionMock } from '../../../javascripts/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';
+import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
+import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
+
+describe('DiscussionActions', () => {
+ let wrapper;
+ const createComponentFactory = (shallow = true) => props => {
+ const localVue = createLocalVue();
+ const store = createStore();
+ const mountFn = shallow ? shallowMount : mount;
+
+ wrapper = mountFn(DiscussionActions, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isResolving: false,
+ resolveButtonTitle: 'Resolve discussion',
+ resolveWithIssuePath: '/some/issue/path',
+ shouldShowJumpToNextDiscussion: true,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ const createComponent = createComponentFactory();
+
+ it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => {
+ createComponent();
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true);
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true);
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(true);
+ });
+
+ it('only renders reply placholder if disccusion is not resolvable', () => {
+ const discussion = { ...discussionMock };
+ discussion.resolvable = false;
+ createComponent({ discussion });
+
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false);
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
+ });
+
+ it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => {
+ createComponent({ resolveWithIssuePath: '' });
+
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ });
+
+ it('does not render jump to next discussion button if shouldShowJumpToNextDiscussion is false', () => {
+ createComponent({ shouldShowJumpToNextDiscussion: false });
+
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
+ });
+ });
+
+ describe('events handling', () => {
+ const createComponent = createComponentFactory(false);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits showReplyForm event when clicking on reply placeholder', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(ReplyPlaceholder)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
+ });
+
+ it('emits resolve event when clicking on resolve button', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(ResolveDiscussionButton)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
+ });
+
+ it('emits jumpToNextDiscussion event when clicking on jump to next discussion button', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(JumpToNextDiscussionButton)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('jumpToNextDiscussion');
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
new file mode 100644
index 00000000000..392c1b6533e
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -0,0 +1,139 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import '~/behaviors/markdown/render_gfm';
+import { SYSTEM_NOTE } from '~/notes/constants';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import NoteableNote from '~/notes/components/noteable_note.vue';
+import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import createStore from '~/notes/stores';
+import {
+ noteableDataMock,
+ discussionMock,
+ notesDataMock,
+} from '../../../javascripts/notes/mock_data';
+
+const localVue = createLocalVue();
+
+describe('DiscussionNotes', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ const store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ wrapper = mount(DiscussionNotes, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isExpanded: false,
+ shouldGroupReplies: false,
+ ...props,
+ },
+ scopedSlots: {
+ footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ },
+ slots: {
+ 'avatar-badge': '<span class="avatar-badge-slot-content" />',
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('renders an element for each note in the discussion', () => {
+ createComponent();
+ const notesCount = discussionMock.notes.length;
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(notesCount);
+ });
+
+ it('renders one element if replies groupping is enabled', () => {
+ createComponent({ shouldGroupReplies: true });
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(1);
+ });
+
+ it('uses proper component to render each note type', () => {
+ const discussion = { ...discussionMock };
+ const notesData = [
+ // PlaceholderSystemNote
+ {
+ id: 1,
+ isPlaceholderNote: true,
+ placeholderType: SYSTEM_NOTE,
+ notes: [{ body: 'PlaceholderSystemNote' }],
+ },
+ // PlaceholderNote
+ {
+ id: 2,
+ isPlaceholderNote: true,
+ notes: [{ body: 'PlaceholderNote' }],
+ },
+ // SystemNote
+ {
+ id: 3,
+ system: true,
+ note: 'SystemNote',
+ },
+ // NoteableNote
+ discussion.notes[0],
+ ];
+ discussion.notes = notesData;
+ createComponent({ discussion });
+ const notes = wrapper.findAll('.notes > li');
+
+ expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
+ expect(notes.at(1).is(PlaceholderNote)).toBe(true);
+ expect(notes.at(2).is(SystemNote)).toBe(true);
+ expect(notes.at(3).is(NoteableNote)).toBe(true);
+ });
+
+ it('renders footer scoped slot with showReplies === true when expanded', () => {
+ createComponent({ isExpanded: true });
+ expect(wrapper.text()).toMatch('showReplies:true');
+ });
+
+ it('renders footer scoped slot with showReplies === false when collapsed', () => {
+ createComponent({ isExpanded: false });
+ expect(wrapper.text()).toMatch('showReplies:false');
+ });
+
+ it('passes down avatar-badge slot content', () => {
+ createComponent();
+ expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
+ });
+ });
+
+ describe('componentData', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should return first note object for placeholder note', () => {
+ const data = {
+ isPlaceholderNote: true,
+ notes: [{ body: 'hello world!' }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data.notes[0]);
+ });
+
+ it('should return given note for nonplaceholder notes', () => {
+ const data = {
+ notes: [{ id: 12 }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data);
+ });
+ });
+});
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js
new file mode 100644
index 00000000000..de1dd219fe0
--- /dev/null
+++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js
@@ -0,0 +1,100 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('operation settings external dashboard component', () => {
+ let wrapper;
+ const externalDashboardPath = `http://mock-external-domain.com/external/dashboard/path`;
+ const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
+
+ beforeEach(() => {
+ wrapper = shallowMount(ExternalDashboard, {
+ propsData: {
+ externalDashboardPath,
+ externalDashboardHelpPagePath,
+ },
+ });
+ });
+
+ it('renders header text', () => {
+ expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
+ });
+
+ describe('sub-header', () => {
+ let subHeader;
+
+ beforeEach(() => {
+ subHeader = wrapper.find('.js-section-sub-header');
+ });
+
+ it('renders descriptive text', () => {
+ expect(subHeader.text()).toContain(
+ 'Add a button to the metrics dashboard linking directly to your existing external dashboards.',
+ );
+ });
+
+ it('renders help page link', () => {
+ const link = subHeader.find(GlLink);
+
+ expect(link.text()).toBe('Learn more');
+ expect(link.attributes().href).toBe(externalDashboardHelpPagePath);
+ });
+ });
+
+ describe('form', () => {
+ let form;
+
+ beforeEach(() => {
+ form = wrapper.find('form');
+ });
+
+ describe('external dashboard url', () => {
+ describe('input label', () => {
+ let formGroup;
+
+ beforeEach(() => {
+ formGroup = form.find(GlFormGroup);
+ });
+
+ it('uses label text', () => {
+ expect(formGroup.attributes().label).toBe('Full dashboard URL');
+ });
+
+ it('uses description text', () => {
+ expect(formGroup.attributes().description).toBe(
+ 'Enter the URL of the dashboard you want to link to',
+ );
+ });
+ });
+
+ describe('input field', () => {
+ let input;
+
+ beforeEach(() => {
+ input = form.find(GlFormInput);
+ });
+
+ it('defaults to externalDashboardPath prop', () => {
+ expect(input.attributes().value).toBe(externalDashboardPath);
+ });
+
+ it('uses a placeholder', () => {
+ expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
+ });
+ });
+
+ describe('submit button', () => {
+ let submit;
+
+ beforeEach(() => {
+ submit = form.find(GlButton);
+ });
+
+ it('renders button label', () => {
+ expect(submit.text()).toBe('Save Changes');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 7e9aec84016..f163bdd9913 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -1,6 +1,9 @@
import $ from 'jquery';
import '~/lib/utils/text_utility';
import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
+import { setTestTimeout } from 'helpers/timeout';
+
+setTestTimeout(500);
describe('Abuse Reports', () => {
const FIXTURE = 'abuse_reports/abuse_reports_list.html';
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
new file mode 100644
index 00000000000..d1de98f4a15
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -0,0 +1,114 @@
+import Vue from '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,
+ });
+};
+
+describe('IssueAssigneesComponent', () => {
+ 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);
+ });
+ });
+
+ 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('assigneeCounterLabel', () => {
+ it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => {
+ expect(vm.assigneeCounterLabel).toBe('+3');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('avatarUrlTitle', () => {
+ it('returns string containing 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);
+ });
+
+ it('renders assignee avatars', () => {
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2);
+ });
+
+ 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');
+ });
+
+ it('renders additional assignees count', () => {
+ const avatarCounterEl = vm.$el.querySelector('.avatar-counter');
+
+ expect(avatarCounterEl.innerText.trim()).toBe('+3');
+ expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..2e93ec412b9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mount(Component, {
+ propsData: {
+ milestone,
+ },
+ sync: false,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(false);
+ });
+
+ it('should return `true` when milestone start date is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(true);
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(false);
+ });
+
+ it('should return `true` when milestone due is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(true);
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
+ });
+
+ it('returns empty string when milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toBe('');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
new file mode 100644
index 00000000000..4a8de5fc4f1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -0,0 +1,59 @@
+import Vue from 'vue';
+import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+const IssueWarning = Vue.extend(issueWarning);
+
+function formatWarning(string) {
+ // Replace newlines with a space then replace multiple spaces with one space
+ return string
+ .trim()
+ .replace(/\n/g, ' ')
+ .replace(/\s\s+/g, ' ');
+}
+
+describe('Issue Warning Component', () => {
+ describe('isLocked', () => {
+ it('should render locked issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ });
+
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/lock$/);
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
+ 'This issue is locked. Only project members can comment.',
+ );
+ });
+ });
+
+ describe('isConfidential', () => {
+ it('should render confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isConfidential: true,
+ });
+
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/eye-slash$/);
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
+ 'This is a confidential issue. Your comment will not be visible to the public.',
+ );
+ });
+ });
+
+ describe('isLocked and isConfidential', () => {
+ it('should render locked and confidential issue warning information', () => {
+ const vm = mountComponent(IssueWarning, {
+ isLocked: true,
+ isConfidential: true,
+ });
+
+ expect(vm.$el.querySelector('.icon')).toBeFalsy();
+ expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
+ "This issue is confidential and locked. People without permission will never get a notification and won't be able to comment.",
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
new file mode 100644
index 00000000000..e43d5301a50
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -0,0 +1,198 @@
+import Vue from 'vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { mount, createLocalVue } from '@vue/test-utils';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import {
+ defaultAssignees,
+ defaultMilestone,
+} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
+
+describe('RelatedIssuableItem', () => {
+ let wrapper;
+ const props = {
+ idKey: 1,
+ displayReference: 'gitlab-org/gitlab-test#1',
+ pathIdSeparator: '#',
+ path: `${gl.TEST_HOST}/path`,
+ title: 'title',
+ confidential: true,
+ dueDate: '1990-12-31',
+ weight: 10,
+ createdAt: '2018-12-01T00:00:00.00Z',
+ milestone: defaultMilestone,
+ assignees: defaultAssignees,
+ eventNamespace: 'relatedIssue',
+ };
+ const slots = {
+ dueDate: '<div class="js-due-date-slot"></div>',
+ weight: '<div class="js-weight-slot"></div>',
+ };
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = mount(localVue.extend(RelatedIssuableItem), {
+ localVue,
+ slots,
+ sync: false,
+ propsData: props,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains issuable-info-container class when canReorder is false', () => {
+ expect(wrapper.props('canReorder')).toBe(false);
+ expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
+ });
+
+ it('does not render token state', () => {
+ expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
+ });
+
+ it('does not render remove button', () => {
+ expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
+ });
+
+ describe('token title', () => {
+ it('links to computedPath', () => {
+ expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
+ });
+
+ it('renders confidential icon', () => {
+ expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ });
+
+ it('renders title', () => {
+ expect(wrapper.find('.item-title a').text()).toEqual(props.title);
+ });
+ });
+
+ describe('token state', () => {
+ let tokenState;
+
+ beforeEach(done => {
+ wrapper.setProps({ state: 'opened' });
+
+ Vue.nextTick(() => {
+ tokenState = wrapper.find('.issue-token-state-icon-open');
+
+ done();
+ });
+ });
+
+ it('renders if hasState', () => {
+ expect(tokenState.exists()).toBe(true);
+ });
+
+ it('renders state title', () => {
+ const stateTitle = tokenState.attributes('data-original-title');
+ const formatedCreateDate = formatDate(props.createdAt);
+
+ expect(stateTitle).toContain('<span class="bold">Opened</span>');
+
+ expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
+ });
+
+ it('renders aria label', () => {
+ expect(tokenState.attributes('aria-label')).toEqual('opened');
+ });
+
+ it('renders open icon when open state', () => {
+ expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
+ });
+
+ it('renders close icon when close state', done => {
+ wrapper.setProps({
+ state: 'closed',
+ closedAt: '2018-12-01T00:00:00.00Z',
+ });
+
+ Vue.nextTick(() => {
+ expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
+
+ done();
+ });
+ });
+ });
+
+ describe('token metadata', () => {
+ let tokenMetadata;
+
+ beforeEach(done => {
+ Vue.nextTick(() => {
+ tokenMetadata = wrapper.find('.item-meta');
+
+ done();
+ });
+ });
+
+ it('renders item path and ID', () => {
+ const pathAndID = tokenMetadata.find('.item-path-id').text();
+
+ expect(pathAndID).toContain('gitlab-org/gitlab-test');
+ expect(pathAndID).toContain('#1');
+ });
+
+ it('renders milestone icon and name', () => {
+ const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
+ const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
+
+ expect(milestoneIcon.attributes('href')).toContain('clock');
+ expect(milestoneTitle.text()).toContain('Milestone title');
+ });
+
+ it('renders due date component', () => {
+ expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
+ });
+
+ it('renders weight component', () => {
+ expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
+ });
+ });
+
+ describe('token assignees', () => {
+ it('renders assignees avatars', () => {
+ expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
+ expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
+ });
+ });
+
+ describe('remove button', () => {
+ let removeBtn;
+
+ beforeEach(done => {
+ wrapper.setProps({ canRemove: true });
+ Vue.nextTick(() => {
+ removeBtn = wrapper.find({ ref: 'removeButton' });
+
+ done();
+ });
+ });
+
+ it('renders if canRemove', () => {
+ expect(removeBtn.exists()).toBe(true);
+ });
+
+ it('renders disabled button when removeDisabled', done => {
+ wrapper.vm.removeDisabled = true;
+
+ Vue.nextTick(() => {
+ expect(removeBtn.attributes('disabled')).toEqual('disabled');
+
+ done();
+ });
+ });
+
+ it('triggers onRemoveRequest when clicked', () => {
+ removeBtn.trigger('click');
+
+ const { relatedIssueRemoveRequest } = wrapper.emitted();
+
+ expect(relatedIssueRemoveRequest.length).toBe(1);
+ expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
new file mode 100644
index 00000000000..3b6f67457ad
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -0,0 +1,103 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
+
+const localVue = createLocalVue();
+
+const DEFAULT_PROPS = {
+ canApply: true,
+ isApplied: false,
+ helpPagePath: 'path_to_docs',
+};
+
+describe('Suggestion Diff component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ wrapper = shallowMount(localVue.extend(SuggestionDiffHeader), {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findApplyButton = () => wrapper.find('.qa-apply-btn');
+ const findHeader = () => wrapper.find('.qa-suggestion-diff-header');
+ const findHelpButton = () => wrapper.find('.js-help-btn');
+ const findLoading = () => wrapper.find(GlLoadingIcon);
+
+ it('renders a suggestion header', () => {
+ createComponent();
+
+ const header = findHeader();
+
+ expect(header.exists()).toBe(true);
+ expect(header.html().includes('Suggested change')).toBe(true);
+ });
+
+ it('renders a help button', () => {
+ createComponent();
+
+ expect(findHelpButton().exists()).toBe(true);
+ });
+
+ it('renders an apply button', () => {
+ createComponent();
+
+ const applyBtn = findApplyButton();
+
+ expect(applyBtn.exists()).toBe(true);
+ expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
+ });
+
+ it('does not render an apply button if `canApply` is set to false', () => {
+ createComponent({ canApply: false });
+
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ describe('when apply suggestion is clicked', () => {
+ beforeEach(done => {
+ createComponent();
+
+ findApplyButton().vm.$emit('click');
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('emits apply', () => {
+ expect(wrapper.emittedByOrder()).toEqual([{ name: 'apply', args: [expect.any(Function)] }]);
+ });
+
+ it('hides apply button', () => {
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ it('shows loading', () => {
+ expect(findLoading().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Applying suggestion');
+ });
+
+ it('when callback of apply is called, hides loading', done => {
+ const [callback] = wrapper.emitted().apply[0];
+
+ callback();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findApplyButton().exists()).toBe(true);
+ expect(findLoading().exists()).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
new file mode 100644
index 00000000000..eafff7f681e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -0,0 +1,51 @@
+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';
+
+describe('issue placeholder system note component', () => {
+ let store;
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issuePlaceholderNote);
+ store = createStore();
+ store.dispatch('setUserData', userDataMock);
+ vm = new Component({
+ store,
+ propsData: { note: { body: 'Foo' } },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user information', () => {
+ it('should render user avatar with link', () => {
+ expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
+
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
+ `${userDataMock.avatar_url}?width=40`,
+ );
+ });
+ });
+
+ describe('note content', () => {
+ it('should render note header information', () => {
+ expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(
+ userDataMock.path,
+ );
+
+ expect(
+ vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim(),
+ ).toEqual(`@${userDataMock.username}`);
+ });
+
+ it('should render note body', () => {
+ expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
new file mode 100644
index 00000000000..976e38c15ee
--- /dev/null
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+describe('placeholder system note component', () => {
+ let PlaceholderSystemNote;
+ let vm;
+
+ beforeEach(() => {
+ PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render system note placeholder with plain text', () => {
+ vm = mountComponent(PlaceholderSystemNote, {
+ note: { body: 'This is a placeholder' },
+ });
+
+ expect(vm.$el.tagName).toEqual('LI');
+ expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual(
+ 'This is a placeholder',
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
new file mode 100644
index 00000000000..dc66150ab8d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
+import createStore from '~/notes/stores';
+import initMRPopovers from '~/mr_popover/index';
+
+jest.mock('~/mr_popover/index', () => jest.fn());
+
+describe('system note component', () => {
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ props = {
+ note: {
+ id: '1424',
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'path',
+ path: '/root',
+ },
+ note_html: '<p dir="auto">closed</p>',
+ system_note_icon_name: 'status_closed',
+ created_at: '2017-08-02T10:51:58.559Z',
+ },
+ };
+
+ const store = createStore();
+ store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
+
+ const Component = Vue.extend(issueSystemNote);
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a list item with correct id', () => {
+ expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`);
+ });
+
+ it('should render target class is note is target note', () => {
+ expect(vm.$el.classList).toContain('target');
+ });
+
+ it('should render svg icon', () => {
+ expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined();
+ });
+
+ // Redcarpet Markdown renderer wraps text in `<p>` tags
+ // we need to strip them because they break layout of commit lists in system notes:
+ // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
+ it('removes wrapping paragraph from note HTML', () => {
+ expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
+ });
+
+ it('should initMRPopovers onMount', () => {
+ expect(initMRPopovers).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
new file mode 100644
index 00000000000..691ebe43d6b
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+describe('collapsedCalendarIcon', () => {
+ let vm;
+ beforeEach(() => {
+ const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
+ vm = mountComponent(CollapsedCalendarIcon, {
+ containerClass: 'test-class',
+ text: 'text',
+ showIcon: false,
+ });
+ });
+
+ it('should add class to container', () => {
+ expect(vm.$el.classList.contains('test-class')).toEqual(true);
+ });
+
+ it('should hide calendar icon if showIcon', () => {
+ expect(vm.$el.querySelector('.fa-calendar')).toBeNull();
+ });
+
+ it('should render text', () => {
+ expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
+ });
+
+ it('should emit click event when container is clicked', () => {
+ const click = jest.fn();
+ vm.$on('click', click);
+
+ vm.$el.click();
+
+ expect(click).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
new file mode 100644
index 00000000000..062ebfa01c9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -0,0 +1,86 @@
+import Vue from 'vue';
+import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+describe('collapsedGroupedDatePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
+ vm = mountComponent(CollapsedGroupedDatePicker, {
+ showToggleSidebar: true,
+ });
+ });
+
+ describe('toggleCollapse events', () => {
+ beforeEach(done => {
+ jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
+ vm.minDate = new Date('07/17/2016');
+ Vue.nextTick(done);
+ });
+
+ it('should emit when collapsed-calendar-icon is clicked', () => {
+ vm.$el.querySelector('.sidebar-collapsed-icon').click();
+
+ expect(vm.toggleSidebar).toHaveBeenCalled();
+ });
+ });
+
+ describe('minDate and maxDate', () => {
+ beforeEach(done => {
+ vm.minDate = new Date('07/17/2016');
+ vm.maxDate = new Date('07/17/2017');
+ Vue.nextTick(done);
+ });
+
+ it('should render both collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons.length).toEqual(2);
+ expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
+ expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
+ });
+ });
+
+ describe('minDate', () => {
+ beforeEach(done => {
+ vm.minDate = new Date('07/17/2016');
+ Vue.nextTick(done);
+ });
+
+ it('should render minDate in collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
+ });
+ });
+
+ describe('maxDate', () => {
+ beforeEach(done => {
+ vm.maxDate = new Date('07/17/2017');
+ Vue.nextTick(done);
+ });
+
+ it('should render maxDate in collapsed-calendar-icon', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
+ });
+ });
+
+ describe('no dates', () => {
+ it('should render None', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons.length).toEqual(1);
+ expect(icons[0].innerText.trim()).toEqual('None');
+ });
+
+ it('should have tooltip as `Start and due date`', () => {
+ const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
+
+ expect(icons[0].dataset.originalTitle).toBe('Start and due date');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
new file mode 100644
index 00000000000..5e2bca6efc9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -0,0 +1,121 @@
+import Vue from 'vue';
+import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+describe('sidebarDatePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const SidebarDatePicker = Vue.extend(sidebarDatePicker);
+ vm = mountComponent(SidebarDatePicker, {
+ label: 'label',
+ isLoading: true,
+ });
+ });
+
+ it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
+ const toggleCollapse = jest.fn();
+ vm.$on('toggleCollapse', toggleCollapse);
+
+ vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
+
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+
+ it('should render collapsed-calendar-icon', () => {
+ expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined();
+ });
+
+ it('should render label', () => {
+ expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label');
+ });
+
+ it('should render loading-icon when isLoading', () => {
+ expect(vm.$el.querySelector('.fa-spin')).toBeDefined();
+ });
+
+ it('should render value when not editing', () => {
+ expect(vm.$el.querySelector('.value-content')).toBeDefined();
+ });
+
+ it('should render None if there is no selectedDate', () => {
+ expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
+ });
+
+ it('should render date-picker when editing', done => {
+ vm.editing = true;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.pika-label')).toBeDefined();
+ done();
+ });
+ });
+
+ describe('editable', () => {
+ beforeEach(done => {
+ vm.editable = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render edit button', () => {
+ expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
+ });
+
+ it('should enable editing when edit button is clicked', done => {
+ vm.isLoading = false;
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.title .btn-blank').click();
+
+ expect(vm.editing).toEqual(true);
+ done();
+ });
+ });
+ });
+
+ it('should render date if selectedDate', done => {
+ vm.selectedDate = new Date('07/07/2017');
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
+ done();
+ });
+ });
+
+ describe('selectedDate and editable', () => {
+ beforeEach(done => {
+ vm.selectedDate = new Date('07/07/2017');
+ vm.editable = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render remove button if selectedDate and editable', () => {
+ expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove');
+ });
+
+ it('should emit saveDate when remove button is clicked', () => {
+ const saveDate = jest.fn();
+ vm.$on('saveDate', saveDate);
+
+ vm.$el.querySelector('.value-content .btn-blank').click();
+
+ expect(saveDate).toHaveBeenCalled();
+ });
+ });
+
+ describe('showToggleSidebar', () => {
+ beforeEach(done => {
+ vm.showToggleSidebar = true;
+ Vue.nextTick(done);
+ });
+
+ it('should render toggle-sidebar when showToggleSidebar', () => {
+ expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined();
+ });
+
+ it('should emit toggleCollapse when toggle sidebar is clicked', () => {
+ const toggleCollapse = jest.fn();
+ vm.$on('toggleCollapse', toggleCollapse);
+
+ vm.$el.querySelector('.title .gutter-toggle').click();
+
+ expect(toggleCollapse).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
new file mode 100644
index 00000000000..6aee616c324
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -0,0 +1,134 @@
+import Vue from 'vue';
+
+import LabelsSelect from '~/labels_select';
+import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
+
+import { mount } from '@vue/test-utils';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const createComponent = (config = mockConfig) => {
+ const Component = Vue.extend(baseComponent);
+
+ return mount(Component, {
+ propsData: config,
+ sync: false,
+ });
+};
+
+describe('BaseComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('hiddenInputName', () => {
+ it('returns correct string when showCreate prop is `true`', () => {
+ expect(vm.hiddenInputName).toBe('issue[label_names][]');
+ });
+
+ it('returns correct string when showCreate prop is `false`', () => {
+ wrapper.setProps({ showCreate: false });
+
+ expect(vm.hiddenInputName).toBe('label_id[]');
+ });
+ });
+
+ describe('createLabelTitle', () => {
+ it('returns `Create project label` when `isProject` prop is true', () => {
+ expect(vm.createLabelTitle).toBe('Create project label');
+ });
+
+ it('return `Create group label` when `isProject` prop is false', () => {
+ wrapper.setProps({ isProject: false });
+
+ expect(vm.createLabelTitle).toBe('Create group label');
+ });
+ });
+
+ describe('manageLabelsTitle', () => {
+ it('returns `Manage project labels` when `isProject` prop is true', () => {
+ expect(vm.manageLabelsTitle).toBe('Manage project labels');
+ });
+
+ it('return `Manage group labels` when `isProject` prop is false', () => {
+ wrapper.setProps({ isProject: false });
+
+ expect(vm.manageLabelsTitle).toBe('Manage group labels');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onLabelClick event with label and list of labels as params', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.handleClick(mockLabels[0]);
+
+ expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
+ });
+ });
+
+ describe('handleCollapsedValueClick', () => {
+ it('emits toggleCollapse event on component', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.handleCollapsedValueClick();
+
+ expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
+ });
+ });
+
+ describe('handleDropdownHidden', () => {
+ it('emits onDropdownClose event on component', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.handleDropdownHidden();
+
+ expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
+ expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `block labels`', () => {
+ expect(vm.$el.classList.contains('block')).toBe(true);
+ expect(vm.$el.classList.contains('labels')).toBe(true);
+ });
+
+ it('renders `.selectbox` element', () => {
+ expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
+ expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
+ });
+
+ it('renders `.dropdown` element', () => {
+ expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
+ });
+
+ it('renders `.dropdown-menu` element', () => {
+ const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
+
+ expect(dropdownMenuEl).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
new file mode 100644
index 00000000000..bb33dc6ea0f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -0,0 +1,97 @@
+import Vue from 'vue';
+
+import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const componentConfig = Object.assign({}, mockConfig, {
+ fieldName: 'label_id[]',
+ labels: mockLabels,
+ showExtraOptions: false,
+});
+
+const createComponent = (config = componentConfig) => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns text as `Label` when `labels` prop is empty array', () => {
+ const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] });
+ const vmEmptyLabels = createComponent(mockEmptyLabels);
+
+ expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
+ const mockMoreLabels = Object.assign({}, componentConfig, {
+ labels: mockLabels.concat(mockLabels),
+ });
+ const vmMoreLabels = createComponent(mockMoreLabels);
+
+ expect(vmMoreLabels.dropdownToggleText).toBe(
+ `Foo Label +${mockMoreLabels.labels.length - 1} more`,
+ );
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ const singleLabel = Object.assign({}, componentConfig, {
+ labels: [mockLabels[0]],
+ });
+ const vmSingleLabel = createComponent(singleLabel);
+
+ expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
+
+ vmSingleLabel.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('i.fa');
+
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
new file mode 100644
index 00000000000..1c25d42682c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -0,0 +1,107 @@
+import Vue from 'vue';
+
+import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const createComponent = headerTitle => {
+ const Component = Vue.extend(dropdownCreateLabelComponent);
+
+ return mountComponent(Component, {
+ headerTitle,
+ });
+};
+
+describe('DropdownCreateLabelComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ gon.suggested_label_colors = mockSuggestedColors;
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('created', () => {
+ it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
+ expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
+ expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
+ });
+
+ it('renders `Go back` button on component header', () => {
+ const backButtonEl = vm.$el.querySelector(
+ '.dropdown-title button.dropdown-title-button.dropdown-menu-back',
+ );
+
+ expect(backButtonEl).not.toBe(null);
+ expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
+ });
+
+ it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title');
+
+ expect(headerEl.innerText.trim()).toContain('Create new label');
+ });
+
+ it('renders component header element with value of `headerTitle` prop', () => {
+ const headerTitle = 'Create project label';
+ const vmWithHeaderTitle = createComponent(headerTitle);
+ const headerEl = vmWithHeaderTitle.$el.querySelector('.dropdown-title');
+
+ expect(headerEl.innerText.trim()).toContain(headerTitle);
+ vmWithHeaderTitle.$destroy();
+ });
+
+ it('renders `Close` button on component header', () => {
+ const closeButtonEl = vm.$el.querySelector(
+ '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
+ );
+
+ expect(closeButtonEl).not.toBe(null);
+ expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
+ });
+
+ it('renders `Name new label` input element', () => {
+ expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
+ expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders suggested colors list elements', () => {
+ const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
+
+ expect(colorsListContainerEl).not.toBe(null);
+ expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
+
+ const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
+
+ expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
+ expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
+ });
+
+ it('renders color input element', () => {
+ expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
+ expect(
+ vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview'),
+ ).not.toBe(null);
+
+ expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders component action buttons', () => {
+ const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
+ const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
+
+ expect(createBtnEl).not.toBe(null);
+ expect(createBtnEl.innerText.trim()).toBe('Create');
+ expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
new file mode 100644
index 00000000000..989901a0012
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -0,0 +1,75 @@
+import Vue from 'vue';
+
+import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const createComponent = (
+ labelsWebUrl = mockConfig.labelsWebUrl,
+ createLabelTitle,
+ manageLabelsTitle,
+) => {
+ const Component = Vue.extend(dropdownFooterComponent);
+
+ return mountComponent(Component, {
+ labelsWebUrl,
+ createLabelTitle,
+ manageLabelsTitle,
+ });
+};
+
+describe('DropdownFooterComponent', () => {
+ const createLabelTitle = 'Create project label';
+ const manageLabelsTitle = 'Manage project labels';
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders link element with `Create new label` when `createLabelTitle` prop is not provided', () => {
+ const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
+
+ expect(createLabelEl).not.toBeNull();
+ expect(createLabelEl.innerText.trim()).toBe('Create new label');
+ });
+
+ it('renders link element with value of `createLabelTitle` prop', () => {
+ const vmWithCreateLabelTitle = createComponent(mockConfig.labelsWebUrl, createLabelTitle);
+ const createLabelEl = vmWithCreateLabelTitle.$el.querySelector(
+ '.dropdown-footer-list .dropdown-toggle-page',
+ );
+
+ expect(createLabelEl.innerText.trim()).toBe(createLabelTitle);
+ vmWithCreateLabelTitle.$destroy();
+ });
+
+ it('renders link element with `Manage labels` when `manageLabelsTitle` prop is not provided', () => {
+ const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
+
+ expect(manageLabelsEl).not.toBeNull();
+ expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
+ expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
+ });
+
+ it('renders link element with value of `manageLabelsTitle` prop', () => {
+ const vmWithManageLabelsTitle = createComponent(
+ mockConfig.labelsWebUrl,
+ createLabelTitle,
+ manageLabelsTitle,
+ );
+ const manageLabelsEl = vmWithManageLabelsTitle.$el.querySelector(
+ '.dropdown-footer-list .dropdown-external-link',
+ );
+
+ expect(manageLabelsEl.innerText.trim()).toBe(manageLabelsTitle);
+ vmWithManageLabelsTitle.$destroy();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
new file mode 100644
index 00000000000..c36a82e1a35
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+
+import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownHeaderComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownHeaderComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders header text element', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title span');
+
+ expect(headerEl.innerText.trim()).toBe('Assign labels');
+ });
+
+ it('renders `Close` button element', () => {
+ const closeBtnEl = vm.$el.querySelector(
+ '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
+ );
+
+ expect(closeBtnEl).not.toBeNull();
+ expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..2fffb2e495e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+
+import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownSearchInputComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+
+ expect(inputEl).not.toBeNull();
+ expect(inputEl.getAttribute('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ });
+
+ it('renders clear search icon element', () => {
+ expect(
+ vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
+ ).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
new file mode 100644
index 00000000000..1616e657c81
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+
+import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+const createComponent = (canEdit = true) => {
+ const Component = Vue.extend(dropdownTitleComponent);
+
+ return mountComponent(Component, {
+ canEdit,
+ });
+};
+
+describe('DropdownTitleComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders title text', () => {
+ expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
+ expect(vm.$el.innerText.trim()).toContain('Labels');
+ });
+
+ it('renders spinner icon element', () => {
+ expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull();
+ });
+
+ it('renders `Edit` button element', () => {
+ const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
+
+ expect(editBtnEl).not.toBeNull();
+ expect(editBtnEl.innerText.trim()).toBe('Edit');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
new file mode 100644
index 00000000000..517f2c01c46
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -0,0 +1,97 @@
+import Vue from 'vue';
+
+import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const createComponent = (labels = mockLabels) => {
+ const Component = Vue.extend(dropdownValueCollapsedComponent);
+
+ return mountComponent(Component, {
+ labels,
+ });
+};
+
+describe('DropdownValueCollapsedComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('labelsList', () => {
+ it('returns default text when `labels` prop is empty array', () => {
+ const vmEmptyLabels = createComponent([]);
+
+ expect(vmEmptyLabels.labelsList).toBe('Labels');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma when `labels` prop has more than one item', () => {
+ const labels = mockLabels.concat(mockLabels);
+ const vmMoreLabels = createComponent(labels);
+
+ const expectedText = labels.map(label => label.title).join(', ');
+
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
+ const mockMoreLabels = Object.assign([], mockLabels);
+ for (let i = 0; i < 6; i += 1) {
+ mockMoreLabels.unshift(mockLabels[0]);
+ }
+
+ const vmMoreLabels = createComponent(mockMoreLabels);
+
+ const expectedText = `${mockMoreLabels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ const text = mockLabels.map(label => label.title).join(', ');
+
+ expect(vm.labelsList).toBe(text);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onValueClick event on component', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.handleClick();
+
+ expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with tooltip`', () => {
+ expect(vm.$el.dataset.placement).toBe('left');
+ expect(vm.$el.dataset.container).toBe('body');
+ expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList);
+ });
+
+ it('renders tags icon element', () => {
+ expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ });
+
+ it('renders labels count', () => {
+ expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
new file mode 100644
index 00000000000..ec143fec5d9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import $ from 'jquery';
+
+import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+
+const createComponent = (
+ labels = mockLabels,
+ labelFilterBasePath = mockConfig.labelFilterBasePath,
+) => {
+ const Component = Vue.extend(dropdownValueComponent);
+
+ return mountComponent(Component, {
+ labels,
+ labelFilterBasePath,
+ enableScopedLabels: true,
+ });
+};
+
+describe('DropdownValueComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isEmpty', () => {
+ it('returns true if `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+
+ expect(vmEmptyLabels.isEmpty).toBe(true);
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns false if `labels` prop is empty', () => {
+ expect(vm.isEmpty).toBe(false);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('labelFilterUrl', () => {
+ it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
+ expect(
+ vm.labelFilterUrl({
+ title: 'Foo bar',
+ }),
+ ).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar');
+ });
+ });
+
+ describe('labelStyle', () => {
+ it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
+ const label = {
+ textColor: '#FFFFFF',
+ color: '#BADA55',
+ };
+ const styleObj = vm.labelStyle(label);
+
+ expect(styleObj.color).toBe(label.textColor);
+ expect(styleObj.backgroundColor).toBe(label.color);
+ });
+ });
+
+ describe('scopedLabelsDescription', () => {
+ it('returns html for tooltip', () => {
+ const html = vm.scopedLabelsDescription(mockLabels[1]);
+ const $el = $.parseHTML(html);
+
+ expect($el[0]).toHaveClass('scoped-label-tooltip-title');
+ expect($el[2].textContent).toEqual(mockLabels[1].description);
+ });
+ });
+
+ describe('showScopedLabels', () => {
+ it('returns true if the label is scoped label', () => {
+ expect(vm.showScopedLabels(mockLabels[1])).toBe(true);
+ });
+
+ it('returns false when label is a regular label', () => {
+ expect(vm.showScopedLabels(mockLabels[0])).toBe(false);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
+ expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(
+ true,
+ );
+ });
+
+ it('render slot content inside component when `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+
+ expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(
+ mockConfig.emptyValueText,
+ );
+ vmEmptyLabels.$destroy();
+ });
+
+ it('renders label element with filter URL', () => {
+ expect(vm.$el.querySelector('a').getAttribute('href')).toBe(
+ '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
+ );
+ });
+
+ it('renders label element and styles based on label details', () => {
+ const labelEl = vm.$el.querySelector('a span.badge.color-label');
+
+ expect(labelEl).not.toBeNull();
+ expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
+ expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
+ });
+
+ describe('label is of scoped-label type', () => {
+ it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
+ expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull();
+ });
+
+ it('renders anchor tag containing question icon', () => {
+ const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
+
+ expect(anchor).not.toBeNull();
+ expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
new file mode 100644
index 00000000000..5cf25ca6f81
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+
+describe('toggleSidebar', () => {
+ let vm;
+ beforeEach(() => {
+ const ToggleSidebar = Vue.extend(toggleSidebar);
+ vm = mountComponent(ToggleSidebar, {
+ collapsed: true,
+ });
+ });
+
+ it('should render << when collapsed', () => {
+ expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
+ });
+
+ it('should render >> when collapsed', () => {
+ vm.collapsed = false;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
+ });
+ });
+
+ it('should emit toggle event when button clicked', () => {
+ const toggle = jest.fn();
+ vm.$on('toggle', toggle);
+ vm.$el.click();
+
+ expect(toggle).toHaveBeenCalled();
+ });
+});
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index 00e31568a9e..f5eb628a982 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -177,6 +177,7 @@ describe 'Gitlab::Graphql::Authorization' do
describe 'type authorizations when applied to a relay connection' do
let(:query_string) { '{ object() { edges { node { name } } } }' }
+ let(:second_test_object) { double(name: 'Second thing') }
let(:type) do
type_factory do |type|
@@ -186,22 +187,41 @@ describe 'Gitlab::Graphql::Authorization' do
let(:query_type) do
query_factory do |query|
- query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object, second_test_object] }
end
end
subject { result.dig('object', 'edges') }
- it 'returns the protected field when user has permission' do
+ it 'returns only the elements visible to the user' do
permit(permission_single)
- expect(subject).not_to be_empty
+ expect(subject.size).to eq 1
expect(subject.first['node']).to eq('name' => test_object.name)
end
it 'returns nil when user is not authorized' do
expect(subject).to be_empty
end
+
+ describe 'limiting connections with multiple objects' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) do
+ [test_object, second_test_object]
+ end
+ end
+ end
+
+ let(:query_string) { '{ object(first: 1) { edges { node { name } } } }' }
+
+ it 'only checks permissions for the first object' do
+ expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true }
+ expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object)
+
+ expect(subject.size).to eq(1)
+ end
+ end
end
describe 'type authorizations when applied to a basic connection' do
@@ -222,28 +242,53 @@ describe 'Gitlab::Graphql::Authorization' do
include_examples 'authorization with a single permission'
end
- describe 'when connections do not follow the correct specification' do
- let(:query_string) { '{ object() { edges { node { name }} } }' }
+ describe 'Authorizations on active record relations' do
+ let!(:visible_project) { create(:project, :private) }
+ let!(:other_project) { create(:project, :private) }
+ let!(:visible_issues) { create_list(:issue, 2, project: visible_project) }
+ let!(:other_issues) { create_list(:issue, 2, project: other_project) }
+ let!(:user) { visible_project.owner }
- let(:type) do
- bad_node = type_factory do |type|
- type.graphql_name 'BadNode'
- type.field :bad_node, GraphQL::STRING_TYPE, null: true
+ let(:issue_type) do
+ type_factory do |type|
+ type.graphql_name 'FakeIssueType'
+ type.authorize :read_issue
+ type.field :id, GraphQL::ID_TYPE, null: false
end
-
+ end
+ let(:project_type) do |type|
type_factory do |type|
- type.field :edges, [bad_node], null: true
+ type.graphql_name 'FakeProjectType'
+ type.field :test_issues, issue_type.connection_type, null: false, resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]) }
end
end
-
let(:query_type) do
query_factory do |query|
- query.field :object, type, null: true
+ query.field :test_project, project_type, null: false, resolve: -> (_, _, _) { visible_project }
end
end
+ let(:query_string) do
+ <<~QRY
+ { testProject { testIssues(first: 3) { edges { node { id } } } } }
+ QRY
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ end
+
+ it 'renders the issues the user has access to' do
+ issue_edges = result['testProject']['testIssues']['edges']
+ issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }
+
+ expect(issue_edges.size).to eq(visible_issues.size)
+ expect(issue_ids).to eq(visible_issues.map { |i| i.id.to_s })
+ end
+
+ it 'does not check access on fields that will not be rendered' do
+ expect(Ability).not_to receive(:allowed?).with(user, :read_issue, other_issues.last)
- it 'throws an error' do
- expect { result }.to raise_error(Gitlab::Graphql::Errors::ConnectionDefinitionError)
+ result
end
end
@@ -276,6 +321,8 @@ describe 'Gitlab::Graphql::Authorization' do
def execute_query(query_type)
schema = Class.new(GraphQL::Schema) do
use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Connections
+
query(query_type)
end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 74e93b2c4df..05f10fb40f0 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -74,6 +74,6 @@ describe GitlabSchema do
end
def field_instrumenters
- described_class.instrumenters[:field]
+ described_class.instrumenters[:field] + described_class.instrumenters[:field_after_built_ins]
end
end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index e3a34762b62..9982288e206 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -28,4 +28,19 @@ describe Resolvers::BaseResolver do
expect(result).to eq(test: 1)
end
end
+
+ it 'increases complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1)
+
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7
+ end
+
+ it 'does not increase complexity when filtering by iids' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
+ end
end
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index ea7159eacf9..3140af27af5 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -46,6 +46,14 @@ describe ResolvesPipelines do
expect(resolve_pipelines({}, {})).to be_empty
end
+ it 'increases field complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: false, max_page_size: 1)
+
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { sha: 'foo' }, 1)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { sha: 'ref' }, 1)).to eq 4
+ end
+
def resolve_pipelines(args = {}, context = { current_user: current_user })
resolve(resolver, obj: project, args: args, ctx: context)
end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 5f9c180cbb7..bffcdbfe915 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -4,105 +4,127 @@ describe Resolvers::IssuesResolver do
include GraphqlHelpers
let(:current_user) { create(:user) }
- set(:project) { create(:project) }
- set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
- set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
- set(:label1) { create(:label, project: project) }
- set(:label2) { create(:label, project: project) }
-
- before do
- project.add_developer(current_user)
- create(:label_link, label: label1, target: issue1)
- create(:label_link, label: label1, target: issue2)
- create(:label_link, label: label2, target: issue2)
- end
- describe '#resolve' do
- it 'finds all issues' do
- expect(resolve_issues).to contain_exactly(issue1, issue2)
+ context "with a project" do
+ set(:project) { create(:project) }
+ set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
+ set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
+ set(:label1) { create(:label, project: project) }
+ set(:label2) { create(:label, project: project) }
+
+ before do
+ project.add_developer(current_user)
+ create(:label_link, label: label1, target: issue1)
+ create(:label_link, label: label1, target: issue2)
+ create(:label_link, label: label2, target: issue2)
end
- it 'filters by state' do
- expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
- expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
- end
+ describe '#resolve' do
+ it 'finds all issues' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- it 'filters by labels' do
- expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
- expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
- end
+ it 'filters by state' do
+ expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
+ expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
+ end
- describe 'filters by created_at' do
- it 'filters by created_before' do
- expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ it 'filters by labels' do
+ expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
+ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
- it 'filters by created_after' do
- expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by created_at' do
+ it 'filters by created_before' do
+ expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by created_after' do
+ expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by updated_at' do
- it 'filters by updated_before' do
- expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ describe 'filters by updated_at' do
+ it 'filters by updated_before' do
+ expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by updated_after' do
+ expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by updated_after' do
- expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by closed_at' do
+ let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+
+ it 'filters by closed_before' do
+ expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ end
+
+ it 'filters by closed_after' do
+ expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by closed_at' do
- let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+ it 'searches issues' do
+ expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
+ end
- it 'filters by closed_before' do
- expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ it 'sort issues' do
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
end
- it 'filters by closed_after' do
- expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'returns issues user can see' do
+ project.add_guest(current_user)
+
+ create(:issue, confidential: true)
+
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
end
- end
- it 'searches issues' do
- expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
- end
+ it 'finds a specific issue with iid' do
+ expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
- end
+ it 'finds a specific issue with iids' do
+ expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'returns issues user can see' do
- project.add_guest(current_user)
+ it 'finds multiple issues with iids' do
+ expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
+ .to contain_exactly(issue1, issue2)
+ end
- create(:issue, confidential: true)
+ it 'finds only the issues within the project we are looking at' do
+ another_project = create(:project)
+ iids = [issue1, issue2].map(&:iid)
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
+ iids.each do |iid|
+ create(:issue, project: another_project, iid: iid)
+ end
- it 'finds a specific issue with iid' do
- expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ end
end
+ end
- it 'finds a specific issue with iids' do
- expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ context "when passing a non existent, batch loaded project" do
+ let(:project) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
+ end
end
- it 'finds multiple issues with iids' do
- expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
- .to contain_exactly(issue1, issue2)
+ it "returns nil without breaking" do
+ expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
+ end
- it 'finds only the issues within the project we are looking at' do
- another_project = create(:project)
- iids = [issue1, issue2].map(&:iid)
-
- iids.each do |iid|
- create(:issue, project: another_project, iid: iid)
- end
+ it 'increases field complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
- expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
- end
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8
end
def resolve_issues(args = {}, context = { current_user: current_user })
diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb
index d4990c6492c..4fdbb3aa43e 100644
--- a/spec/graphql/resolvers/project_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_resolver_spec.rb
@@ -26,6 +26,14 @@ describe Resolvers::ProjectResolver do
end
end
+ it 'does not increase complexity depending on number of load limits' do
+ field1 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+ field2 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1)
+
+ expect(field1.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ expect(field2.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ end
+
def resolve_project(full_path)
resolve(described_class, args: { full_path: full_path })
end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
index b5697ee5245..4fe426e2447 100644
--- a/spec/graphql/types/base_field_spec.rb
+++ b/spec/graphql/types/base_field_spec.rb
@@ -4,6 +4,18 @@ require 'spec_helper'
describe Types::BaseField do
context 'when considering complexity' do
+ let(:resolver) do
+ Class.new(described_class) do
+ def self.resolver_complexity(args)
+ 2 if args[:foo]
+ end
+
+ def self.complexity_multiplier(args)
+ 0.01
+ end
+ end
+ end
+
it 'defaults to 1' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
@@ -15,5 +27,19 @@ describe Types::BaseField do
expect(field.to_graphql.complexity).to eq 12
end
+
+ it 'sets complexity depending on arguments for resolvers' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+
+ expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 3
+ end
+
+ it 'sets complexity depending on number load limits for resolvers' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+
+ expect(field.to_graphql.complexity.call({}, { first: 1 }, 2)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { first: 1, foo: true }, 2)).to eq 4
+ end
end
end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
new file mode 100644
index 00000000000..3dd5b602aa2
--- /dev/null
+++ b/spec/graphql/types/group_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Group'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
+
+ it { expect(described_class.graphql_name).to eq('Group') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_group) }
+end
diff --git a/spec/graphql/types/namespace_type.rb b/spec/graphql/types/namespace_type.rb
new file mode 100644
index 00000000000..7cd6a79ae5d
--- /dev/null
+++ b/spec/graphql/types/namespace_type.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Namespace'] do
+ it { expect(described_class.graphql_name).to eq('Namespace') }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 69e3ea8a4a9..b4626955816 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :group, :echo, :metadata) }
describe 'project field' do
subject { described_class.fields['project'] }
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index a049b5a6133..58eaf991d6e 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -67,27 +67,29 @@ describe LabelsHelper do
describe 'link_to_label' do
let(:project) { create(:project) }
let(:label) { create(:label, project: project) }
+ let(:subject) { nil }
+ let(:label_presenter) { label.present(issuable_subject: subject) }
context 'without subject' do
it "uses the label's project" do
- expect(link_to_label(label)).to match %r{<a href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a project as subject' do
let(:namespace) { build(:namespace, name: 'foo3') }
- let(:another_project) { build(:project, namespace: namespace, name: 'bar3') }
+ let(:subject) { build(:project, namespace: namespace, name: 'bar3') }
it 'links to project issues page' do
- expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a group as subject' do
- let(:group) { build(:group, name: 'bar') }
+ let(:subject) { build(:group, name: 'bar') }
it 'links to group issues page' do
- expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
@@ -95,7 +97,7 @@ describe LabelsHelper do
['issue', :issue, 'merge_request', :merge_request].each do |type|
context "set to #{type}" do
it 'links to correct page' do
- expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
end
@@ -104,14 +106,14 @@ describe LabelsHelper do
context 'with a tooltip argument' do
context 'set to false' do
it 'does not include the has-tooltip class' do
- expect(link_to_label(label, tooltip: false)).not_to match /has-tooltip/
+ expect(link_to_label(label_presenter, tooltip: false)).not_to match /has-tooltip/
end
end
end
context 'with block' do
it 'passes the block to link_to' do
- link = link_to_label(label) { 'Foo' }
+ link = link_to_label(label_presenter) { 'Foo' }
expect(link).to match('Foo')
end
end
@@ -119,8 +121,8 @@ describe LabelsHelper do
context 'without block' do
it 'uses render_colored_label as the link content' do
expect(self).to receive(:render_colored_label)
- .with(label, tooltip: true).and_return('Foo')
- expect(link_to_label(label)).to match('Foo')
+ .with(label_presenter, tooltip: true).and_return('Foo')
+ expect(link_to_label(label_presenter)).to match('Foo')
end
end
end
@@ -237,16 +239,24 @@ describe LabelsHelper do
end
end
- describe 'labels_sorted_by_title' do
+ describe 'presented_labels_sorted_by_title' do
+ let(:labels) do
+ [build(:label, title: 'a'),
+ build(:label, title: 'B'),
+ build(:label, title: 'c'),
+ build(:label, title: 'D')]
+ end
+
it 'sorts labels alphabetically' do
- label1 = double(:label, title: 'a')
- label2 = double(:label, title: 'B')
- label3 = double(:label, title: 'c')
- label4 = double(:label, title: 'D')
- labels = [label1, label2, label3, label4]
-
- expect(labels_sorted_by_title(labels))
- .to match_array([label2, label4, label1, label3])
+ sorted_ids = presented_labels_sorted_by_title(labels, nil).map(&:id)
+
+ expect(sorted_ids)
+ .to match_array([labels[1].id, labels[3].id, labels[0].id, labels[2].id])
+ end
+
+ it 'returns an array of label presenters' do
+ expect(presented_labels_sorted_by_title(labels, nil))
+ .to all(be_a(LabelPresenter))
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 37c63807c82..554cb861563 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -445,6 +445,10 @@ describe ProjectsHelper do
Project.all
end
+ before do
+ stub_feature_flags(project_list_filter_bar: false)
+ end
+
it 'returns true when there are projects' do
expect(helper.show_projects?(projects, {})).to eq(true)
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 03df9deafa1..50c74a7c2f9 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -18,4 +18,28 @@ describe StorageHelper do
expect(helper.storage_counter(100_000_000_000_000_000_000_000)).to eq("86,736.2 EB")
end
end
+
+ describe "#storage_counters_details" do
+ let(:namespace) { create :namespace }
+ let(:project) do
+ create(:project,
+ namespace: namespace,
+ statistics: build(:project_statistics,
+ repository_size: 10.kilobytes,
+ lfs_objects_size: 20.gigabytes,
+ build_artifacts_size: 30.megabytes))
+ end
+
+ let(:message) { '10 KB repositories, 30 MB build artifacts, 20 GB LFS' }
+
+ it 'works on ProjectStatistics' do
+ expect(helper.storage_counters_details(project.statistics)).to eq(message)
+ end
+
+ it 'works on Namespace.with_statistics' do
+ namespace_stats = Namespace.with_statistics.find(project.namespace.id)
+
+ expect(helper.storage_counters_details(namespace_stats)).to eq(message)
+ end
+ end
end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 6366be30079..726ce07a2d1 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -6,8 +6,8 @@ describe 'create_tokens' do
let(:secrets) { ActiveSupport::OrderedOptions.new }
- HEX_KEY = /\h{128}/
- RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
+ HEX_KEY = /\h{128}/.freeze
+ RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze
before do
allow(File).to receive(:write)
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index e537e0e8afc..494b3b934a8 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
describe('Api', () => {
const dummyApiVersion = 'v3000';
- const dummyUrlRoot = 'http://host.invalid';
+ const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
@@ -32,6 +32,18 @@ describe('Api', () => {
expect(builtUrl).toEqual(expectedOutput);
});
+
+ [null, '', '/'].forEach(root => {
+ it(`works when relative_url_root is ${root}`, () => {
+ window.gon.relative_url_root = root;
+ const input = '/api/:version/foo/bar';
+ const expectedOutput = `/api/${dummyApiVersion}/foo/bar`;
+
+ const builtUrl = Api.buildUrl(input);
+
+ expect(builtUrl).toEqual(expectedOutput);
+ });
+ });
});
describe('group', () => {
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
index 68b4f261617..33210794ba1 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -1,5 +1,7 @@
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
-import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const bmprPath = `${FIXTURES_PATH}/blob/balsamiq/test.bmpr`;
describe('Balsamiq integration spec', () => {
let container;
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index acf87580777..6fa3890483c 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
import renderPDF from '~/blob/pdf';
-import testPDF from '../../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let viewer;
diff --git a/spec/javascripts/boards/board_list_common_spec.js b/spec/javascripts/boards/board_list_common_spec.js
new file mode 100644
index 00000000000..cb337e4cc83
--- /dev/null
+++ b/spec/javascripts/boards/board_list_common_spec.js
@@ -0,0 +1,58 @@
+/* global List */
+/* global ListIssue */
+
+import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import Sortable from 'sortablejs';
+import BoardList from '~/boards/components/board_list.vue';
+
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import boardsStore from '~/boards/stores/boards_store';
+
+window.Sortable = Sortable;
+
+export default function createComponent({ done, listIssueProps = {}, componentProps = {} }) {
+ const el = document.createElement('div');
+
+ document.body.appendChild(el);
+ const mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ gl.boardService = mockBoardService();
+ boardsStore.create();
+
+ const BoardListComp = Vue.extend(BoardList);
+ const list = new List(listObj);
+ const issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [],
+ assignees: [],
+ ...listIssueProps,
+ });
+ list.issuesSize = 1;
+ list.issues.push(issue);
+
+ const component = new BoardListComp({
+ el,
+ propsData: {
+ disabled: false,
+ list,
+ issues: list.issues,
+ loading: false,
+ issueLinkBase: '/issues',
+ rootPath: '/',
+ ...componentProps,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+
+ return { component, mock };
+}
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 396fc823ef5..9c9b435d7fd 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -1,60 +1,13 @@
-/* global List */
-/* global ListIssue */
-
import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Sortable from 'sortablejs';
-import BoardList from '~/boards/components/board_list.vue';
import eventHub from '~/boards/eventhub';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
-
-window.Sortable = Sortable;
+import createComponent from './board_list_common_spec';
describe('Board list component', () => {
let mock;
let component;
beforeEach(done => {
- const el = document.createElement('div');
-
- document.body.appendChild(el);
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- gl.boardService = mockBoardService();
- boardsStore.create();
-
- const BoardListComp = Vue.extend(BoardList);
- const list = new List(listObj);
- const issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- });
- list.issuesSize = 1;
- list.issues.push(issue);
-
- component = new BoardListComp({
- el,
- propsData: {
- disabled: false,
- list,
- issues: list.issues,
- loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
+ ({ mock, component } = createComponent({ done }));
});
afterEach(() => {
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 6eda5047dd0..a5bf97bdcc2 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -285,4 +285,10 @@ describe('Issue card component', () => {
.catch(done.fail);
});
});
+
+ describe('weights', () => {
+ it('not shows weight component', () => {
+ expect(component.$el.querySelector('.board-card-weight')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 0d462a6f872..bb6fc6c693d 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -45,6 +45,7 @@ describe('List model', () => {
id: _.random(10000),
title: 'test',
color: 'red',
+ text_color: 'white',
},
});
list.save();
@@ -53,6 +54,8 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
+ expect(list.label.color).toBe('red');
+ expect(list.label.textColor).toBe('white');
done();
}, 0);
});
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 14fff9223f4..93a0f29af0a 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -16,6 +16,7 @@ export const listObj = {
title: 'Testing',
color: 'red',
description: 'testing;',
+ textColor: 'white',
},
};
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index bc9288e4150..124bdeea45d 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -29,6 +29,10 @@ describe('DiffContent', () => {
});
});
+ afterEach(() => {
+ vm.$destroy();
+ });
+
describe('text based files', () => {
it('should render diff inline view', done => {
vm.$store.state.diffs.diffViewType = 'inline';
@@ -49,6 +53,16 @@ describe('DiffContent', () => {
done();
});
});
+
+ it('renders rendering more lines loading icon', done => {
+ vm.diffFile.renderingLines = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
});
describe('empty files', () => {
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 0bc9da5ad0f..f7f0ab83c21 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,90 +1,103 @@
-import Vue from 'vue';
+import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+const localVue = createLocalVue();
+
describe('DiffDiscussions', () => {
- let vm;
+ let store;
+ let wrapper;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- function createComponent(props = {}) {
- const store = createStore();
-
- vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
- discussions: getDiscussionsMockData(),
- ...props,
- }).$mount();
- }
+ const createComponent = props => {
+ store = createStore();
+ wrapper = mount(localVue.extend(DiffDiscussions), {
+ store,
+ propsData: {
+ discussions: getDiscussionsMockData(),
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ expect(wrapper.find(NoteableDiscussion).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe(
+ discussionsMockData.notes.length,
+ );
});
});
describe('image commenting', () => {
+ const findDiffNotesToggle = () => wrapper.find('.js-diff-notes-toggle');
+
it('renders collapsible discussion button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
- 'diff-notes-collapse',
- );
+ expect(diffNotesToggle.exists()).toBe(true);
+ expect(diffNotesToggle.find(Icon).exists()).toBe(true);
+ expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ spyOn(wrapper.vm.$store, 'dispatch').and.stub();
+ const diffNotesToggle = findDiffNotesToggle();
+ diffNotesToggle.trigger('click');
- spyOn(vm.$store, 'dispatch').and.stub();
-
- vm.$el.querySelector('.js-diff-notes-toggle').click();
-
- expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
- discussionId: vm.discussions[0].id,
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ discussionId: discussionsMockData.id,
});
});
- it('renders expand button when discussion is collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
-
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
- expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
- 'btn-transparent badge badge-pill',
- );
+ it('renders expand button when discussion is collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- done();
- });
+ expect(diffNotesToggle.text().trim()).toBe('1');
+ expect(diffNotesToggle.classes()).toEqual(
+ jasmine.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
+ );
});
- it('hides discussion when collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
+ it('hides discussion when collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
-
- done();
- });
+ expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
- createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
-
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
+ createComponent({ renderAvatarBadge: true });
+ const noteableDiscussion = wrapper.find(NoteableDiscussion);
+
+ expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
+ expect(
+ noteableDiscussion
+ .find('.badge-pill')
+ .text()
+ .trim(),
+ ).toBe('1');
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index e1170c9762e..596a1ba5ad2 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -611,6 +611,9 @@ describe('diff_file_header', () => {
vm = mountComponentWithStore(Component, { props, store });
expect(viewFileButton().getAttribute('href')).toBe('view-path');
+ expect(viewFileButton().getAttribute('data-original-title')).toEqual(
+ `View file @ ${props.diffFile.content_sha.substr(0, 8)}`,
+ );
});
it('should not render external url view link if diff file has no external url', () => {
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index 32af9ea8ddd..27428197c1c 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -240,4 +240,5 @@ export default {
},
],
discussions: [],
+ renderingLines: false,
};
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index bca99caa920..c82dcadd2f1 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -36,6 +36,7 @@ import actions, {
fetchFullDiff,
toggleFullDiff,
setFileCollapsed,
+ setExpandedDiffLines,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -879,9 +880,9 @@ describe('DiffsStoreActions', () => {
it('commits REQUEST_FULL_DIFF', done => {
testAction(
receiveFullDiffSucess,
- { filePath: 'test', data: 'test' },
+ { filePath: 'test' },
{},
- [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test', data: 'test' } }],
+ [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }],
[],
done,
);
@@ -903,11 +904,8 @@ describe('DiffsStoreActions', () => {
describe('fetchFullDiff', () => {
let mock;
- let scrollToElementSpy;
beforeEach(() => {
- scrollToElementSpy = spyOnDependency(actions, 'scrollToElement').and.stub();
-
mock = new MockAdapter(axios);
});
@@ -921,28 +919,23 @@ describe('DiffsStoreActions', () => {
});
it('dispatches receiveFullDiffSucess', done => {
+ const file = {
+ context_lines_path: `${gl.TEST_HOST}/context`,
+ file_path: 'test',
+ file_hash: 'test',
+ };
testAction(
fetchFullDiff,
- { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' },
+ file,
null,
[],
- [{ type: 'receiveFullDiffSucess', payload: { filePath: 'test', data: ['test'] } }],
+ [
+ { type: 'receiveFullDiffSucess', payload: { filePath: 'test' } },
+ { type: 'setExpandedDiffLines', payload: { file, data: ['test'] } },
+ ],
done,
);
});
-
- it('scrolls to element', done => {
- fetchFullDiff(
- { dispatch() {} },
- { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' },
- )
- .then(() => {
- expect(scrollToElementSpy).toHaveBeenCalledWith('#test');
-
- done();
- })
- .catch(done.fail);
- });
});
describe('error', () => {
@@ -999,4 +992,63 @@ describe('DiffsStoreActions', () => {
);
});
});
+
+ describe('setExpandedDiffLines', () => {
+ beforeEach(() => {
+ spyOnDependency(actions, 'idleCallback').and.callFake(cb => {
+ cb({ timeRemaining: () => 50 });
+ });
+ });
+
+ it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', done => {
+ spyOnDependency(actions, 'convertExpandLines').and.callFake(() => ['test']);
+
+ testAction(
+ setExpandedDiffLines,
+ { file: { file_path: 'path' }, data: [] },
+ { diffViewType: 'inline' },
+ [
+ {
+ type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: ['test'] },
+ },
+ {
+ type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: ['test'] },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', done => {
+ const lines = new Array(501).fill().map((_, i) => `line-${i}`);
+ spyOnDependency(actions, 'convertExpandLines').and.callFake(() => lines);
+
+ testAction(
+ setExpandedDiffLines,
+ { file: { file_path: 'path' }, data: [] },
+ { diffViewType: 'inline' },
+ [
+ {
+ type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines },
+ },
+ {
+ type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: lines.slice(0, 200) },
+ },
+ { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' },
+ ...new Array(301).fill().map((_, i) => ({
+ type: 'ADD_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', line: `line-${i + 200}` },
+ })),
+ { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 5556a994a14..fa193e1d3b9 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -756,4 +756,98 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].viewer.collapsed).toBe(true);
});
});
+
+ describe('SET_HIDDEN_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`sets the ${hidden} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ lines: ['test'],
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual([]);
+ expect(file[`${hidden}_diff_lines`]).toEqual(['test']);
+ });
+ });
+ });
+
+ describe('SET_CURRENT_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`sets the ${current} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ lines: ['test'],
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ });
+ });
+ });
+
+ describe('ADD_CURRENT_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`pushes to ${current} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test',
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test2',
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test', 'test2']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ });
+ });
+ });
+
+ describe('TOGGLE_DIFF_FILE_RENDERING_MORE', () => {
+ it('toggles renderingLines on file', () => {
+ const file = { file_path: 'test', renderingLines: false };
+ const state = {
+ diffFiles: [file],
+ };
+
+ mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test');
+
+ expect(file.renderingLines).toBe(true);
+
+ mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test');
+
+ expect(file.renderingLines).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore
index 2507c8e7263..bed020f5b0a 100644
--- a/spec/javascripts/fixtures/.gitignore
+++ b/spec/javascripts/fixtures/.gitignore
@@ -1,3 +1,5 @@
*.html.raw
*.html
*.json
+*.pdf
+*.bmpr
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/static/images/green_box.png
index cd1ff9f9ade..cd1ff9f9ade 100644
--- a/spec/javascripts/fixtures/images/green_box.png
+++ b/spec/javascripts/fixtures/static/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/static/images/one_white_pixel.png
index 073fcf40a18..073fcf40a18 100644
--- a/spec/javascripts/fixtures/one_white_pixel.png
+++ b/spec/javascripts/fixtures/static/images/one_white_pixel.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/static/images/red_box.png
index 73b2927da0f..73b2927da0f 100644
--- a/spec/javascripts/fixtures/images/red_box.png
+++ b/spec/javascripts/fixtures/static/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/static/projects.json
index 68a150f602a..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/static/projects.json
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 57e31d933ca..8c7820ddb52 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -6,7 +6,7 @@ import '~/lib/utils/common_utils';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html');
- loadJSONFixtures('projects.json');
+ loadJSONFixtures('static/projects.json');
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
@@ -67,7 +67,7 @@ describe('glDropdown', function describeDropdown() {
loadFixtures('static/gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
+ this.projectsData = getJSONFixture('static/projects.json');
});
afterEach(() => {
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
index 878e17ac805..d19af6af2d7 100644
--- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -78,6 +78,8 @@ describe('new dropdown upload', () => {
type: 'blob',
content: 'plain text',
base64: false,
+ binary: false,
+ rawPath: '',
});
});
@@ -89,6 +91,8 @@ describe('new dropdown upload', () => {
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
base64: true,
+ binary: true,
+ rawPath: binaryTarget.result,
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 1e5b55af4ba..e6fb08bcc49 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -10,11 +10,19 @@ import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
+const RELATIVE_URL_ROOT = '/gitlab';
+
describe('IDE store file actions', () => {
let mock;
+ let originalGon;
beforeEach(() => {
mock = new MockAdapter(axios);
+ originalGon = window.gon;
+ window.gon = {
+ ...window.gon,
+ relative_url_root: RELATIVE_URL_ROOT,
+ };
spyOn(router, 'push');
});
@@ -22,6 +30,7 @@ describe('IDE store file actions', () => {
afterEach(() => {
mock.restore();
resetStore(store);
+ window.gon = originalGon;
});
describe('closeFile', () => {
@@ -173,13 +182,13 @@ describe('IDE store file actions', () => {
spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = `${gl.TEST_HOST}/getFileDataURL`;
+ localFile.url = `project/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
});
describe('success', () => {
beforeEach(() => {
- mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce(
+ mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
200,
{
blame_path: 'blame_path',
@@ -200,7 +209,9 @@ describe('IDE store file actions', () => {
store
.dispatch('getFileData', { path: localFile.path })
.then(() => {
- expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`);
+ expect(service.getFileData).toHaveBeenCalledWith(
+ `${RELATIVE_URL_ROOT}/project/getFileDataURL`,
+ );
done();
})
@@ -266,7 +277,7 @@ describe('IDE store file actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError();
+ mock.onGet(`project/getFileDataURL`).networkError();
});
it('dispatches error action', done => {
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index abc97f3072c..cdeb9b4b896 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -4,6 +4,7 @@ import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
import consts from '~/ide/stores/modules/commit/constants';
+import { commitActionTypes } from '~/ide/constants';
import { resetStore, file } from 'spec/ide/helpers';
describe('IDE commit module actions', () => {
@@ -294,7 +295,7 @@ describe('IDE commit module actions', () => {
commit_message: 'testing 123',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
@@ -321,7 +322,7 @@ describe('IDE commit module actions', () => {
commit_message: 'testing 123',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
index 734233100ab..548962c7a92 100644
--- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
@@ -69,18 +69,16 @@ describe('IDE file templates actions', () => {
describe('fetchTemplateTypes', () => {
describe('success', () => {
- let nextPage;
+ const pages = [[{ name: 'MIT' }], [{ name: 'Apache' }], [{ name: 'CC' }]];
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(() => [
- 200,
- [
- {
- name: 'MIT',
- },
- ],
- { 'X-NEXT-PAGE': nextPage },
- ]);
+ mock.onGet(/api\/(.*)\/templates\/licenses/).reply(({ params }) => {
+ const pageNum = params.page;
+ const page = pages[pageNum - 1];
+ const hasNextPage = pageNum < pages.length;
+
+ return [200, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
+ });
});
it('rejects if selectedTemplateType is empty', done => {
@@ -112,43 +110,15 @@ describe('IDE file templates actions', () => {
},
{
type: 'receiveTemplateTypesSuccess',
- payload: [
- {
- name: 'MIT',
- },
- ],
- },
- ],
- done,
- );
- });
-
- it('dispatches actions for next page', done => {
- nextPage = '2';
- state.selectedTemplateType = {
- key: 'licenses',
- };
-
- testAction(
- actions.fetchTemplateTypes,
- null,
- state,
- [],
- [
- {
- type: 'requestTemplateTypes',
+ payload: pages[0],
},
{
type: 'receiveTemplateTypesSuccess',
- payload: [
- {
- name: 'MIT',
- },
- ],
+ payload: pages[0].concat(pages[1]),
},
{
- type: 'fetchTemplateTypes',
- payload: 2,
+ type: 'receiveTemplateTypesSuccess',
+ payload: pages[0].concat(pages[1]).concat(pages[2]),
},
],
done,
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index c4f122efdda..debe1c4acee 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -1,4 +1,5 @@
import * as utils from '~/ide/stores/utils';
+import { commitActionTypes } from '~/ide/constants';
import { file } from '../helpers';
describe('Multi-file store utils', () => {
@@ -107,7 +108,7 @@ describe('Multi-file store utils', () => {
commit_message: 'commit message',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: 'staged',
content: 'updated file content',
encoding: 'text',
@@ -115,7 +116,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'create',
+ action: commitActionTypes.create,
file_path: 'added',
content: 'new file content',
encoding: 'base64',
@@ -123,7 +124,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'delete',
+ action: commitActionTypes.delete,
file_path: 'deletedFile',
content: undefined,
encoding: 'text',
@@ -170,7 +171,7 @@ describe('Multi-file store utils', () => {
commit_message: 'prebuilt test commit message',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: 'staged',
content: 'updated file content',
encoding: 'text',
@@ -178,7 +179,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'create',
+ action: commitActionTypes.create,
file_path: 'added',
content: 'new file content',
encoding: 'base64',
@@ -193,19 +194,19 @@ describe('Multi-file store utils', () => {
describe('commitActionForFile', () => {
it('returns deleted for deleted file', () => {
- expect(utils.commitActionForFile({ deleted: true })).toBe('delete');
+ expect(utils.commitActionForFile({ deleted: true })).toBe(commitActionTypes.delete);
});
it('returns create for tempFile', () => {
- expect(utils.commitActionForFile({ tempFile: true })).toBe('create');
+ expect(utils.commitActionForFile({ tempFile: true })).toBe(commitActionTypes.create);
});
it('returns move for moved file', () => {
- expect(utils.commitActionForFile({ prevPath: 'test' })).toBe('move');
+ expect(utils.commitActionForFile({ prevPath: 'test' })).toBe(commitActionTypes.move);
});
it('returns update by default', () => {
- expect(utils.commitActionForFile({})).toBe('update');
+ expect(utils.commitActionForFile({})).toBe(commitActionTypes.update);
});
});
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
deleted file mode 100644
index ab8642bf0dd..00000000000
--- a/spec/javascripts/import_projects/components/import_projects_table_spec.js
+++ /dev/null
@@ -1,188 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ImportProjectsTable', () => {
- let vm;
- let mock;
- let store;
- const reposPath = '/repos-path';
- const jobsPath = '/jobs-path';
- const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportProjectsTable = Vue.extend(importProjectsTable);
-
- const component = new ImportProjectsTable({
- store,
- propsData: {
- providerTitle,
- },
- }).$mount();
-
- store.dispatch('stopJobsPolling');
-
- return component;
- }
-
- beforeEach(() => {
- store = createStore();
- store.dispatch('setInitialData', { reposPath });
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- it('renders a loading icon whilst repos are loading', done => {
- mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders a table with imported projects and provider repos', done => {
- const response = {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders an empty state if there are no imported projects or provider repos', done => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('imports provider repos if bulk import button is clicked', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
-
- mock.onGet(reposPath).replyOnce(200, response);
- mock.onPost(importPath).replyOnce(200, importedProject);
-
- store.dispatch('setInitialData', { importPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('polls to update the status of imported projects', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
- };
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
- },
- ];
-
- mock.onGet(reposPath).replyOnce(200, response);
-
- store.dispatch('setInitialData', { importPath, jobsPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- mock.onGet(jobsPath).replyOnce(200, updatedProjects);
- return store.dispatch('restartJobsPolling');
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
deleted file mode 100644
index 7dac7e9ccc1..00000000000
--- a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import Vue from 'vue';
-import createStore from '~/import_projects/store';
-import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
-import STATUS_MAP from '~/import_projects/constants';
-
-describe('ImportedProjectTableRow', () => {
- let vm;
- const project = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'finished',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
-
- const store = createStore();
- return new ImportedProjectTableRow({
- store,
- propsData: {
- project: {
- ...project,
- },
- },
- }).$mount();
- }
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders an imported project table row', () => {
- vm = createComponent();
-
- const providerLink = vm.$el.querySelector('.js-provider-link');
- const statusObject = STATUS_MAP[project.importStatus];
-
- expect(vm.$el.classList.contains('js-imported-project')).toBe(true);
- expect(providerLink.href).toMatch(project.providerLink);
- expect(providerLink.textContent).toMatch(project.importSource);
- expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath);
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath);
- });
-});
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
deleted file mode 100644
index 4d2bacd2ad0..00000000000
--- a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import createStore from '~/import_projects/store';
-import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
-import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ProviderRepoTableRow', () => {
- let store;
- let vm;
- const repo = {
- id: 10,
- sanitizedName: 'sanitizedName',
- fullName: 'fullName',
- providerLink: 'providerLink',
- };
-
- function createComponent() {
- const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
-
- return new ProviderRepoTableRow({
- store,
- propsData: {
- repo: {
- ...repo,
- },
- },
- }).$mount();
- }
-
- beforeEach(() => {
- store = createStore();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders a provider repo table row', () => {
- vm = createComponent();
-
- const providerLink = vm.$el.querySelector('.js-provider-link');
- const statusObject = STATUS_MAP[STATUSES.NONE];
-
- expect(vm.$el.classList.contains('js-provider-repo')).toBe(true);
- expect(providerLink.href).toMatch(repo.providerLink);
- expect(providerLink.textContent).toMatch(repo.fullName);
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- expect(vm.$el.querySelector('.js-import-button')).not.toBeNull();
- });
-
- it('renders a select2 namespace select', () => {
- vm = createComponent();
-
- const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
-
- expect(dropdownTrigger).not.toBeNull();
- expect(dropdownTrigger.classList.contains('select2-container')).toBe(true);
-
- dropdownTrigger.click();
-
- expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
- });
-
- it('imports repo when clicking import button', done => {
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
- const mock = new MockAdapter(axios);
-
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
- mock.onPost(importPath).replyOnce(200);
- spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
-
- vm = createComponent();
-
- vm.$el.querySelector('.js-import-button').click();
-
- setTimeoutPromise()
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- })
- .then(() => mock.restore())
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/javascripts/import_projects/store/actions_spec.js
deleted file mode 100644
index 77850ee3283..00000000000
--- a/spec/javascripts/import_projects/store/actions_spec.js
+++ /dev/null
@@ -1,284 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import {
- SET_INITIAL_DATA,
- REQUEST_REPOS,
- RECEIVE_REPOS_SUCCESS,
- RECEIVE_REPOS_ERROR,
- REQUEST_IMPORT,
- RECEIVE_IMPORT_SUCCESS,
- RECEIVE_IMPORT_ERROR,
- RECEIVE_JOBS_SUCCESS,
-} from '~/import_projects/store/mutation_types';
-import {
- setInitialData,
- requestRepos,
- receiveReposSuccess,
- receiveReposError,
- fetchRepos,
- requestImport,
- receiveImportSuccess,
- receiveImportError,
- fetchImport,
- receiveJobsSuccess,
- fetchJobs,
- clearJobsEtagPoll,
- stopJobsPolling,
-} from '~/import_projects/store/actions';
-import state from '~/import_projects/store/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-
-describe('import_projects store actions', () => {
- let localState;
- const repoId = 1;
- const repos = [{ id: 1 }, { id: 2 }];
- const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } };
-
- beforeEach(() => {
- localState = state();
- });
-
- describe('setInitialData', () => {
- it(`commits ${SET_INITIAL_DATA} mutation`, done => {
- const initialData = {
- reposPath: 'reposPath',
- provider: 'provider',
- jobsPath: 'jobsPath',
- importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath',
- defaultTargetNamespace: 'defaultTargetNamespace',
- ciCdOnly: 'ciCdOnly',
- canSelectNamespace: 'canSelectNamespace',
- };
-
- testAction(
- setInitialData,
- initialData,
- localState,
- [{ type: SET_INITIAL_DATA, payload: initialData }],
- [],
- done,
- );
- });
- });
-
- describe('requestRepos', () => {
- it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => {
- testAction(
- requestRepos,
- null,
- localState,
- [{ type: REQUEST_REPOS, payload: null }],
- [],
- done,
- );
- });
- });
-
- describe('receiveReposSuccess', () => {
- it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => {
- testAction(
- receiveReposSuccess,
- repos,
- localState,
- [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }],
- [],
- done,
- );
- });
- });
-
- describe('receiveReposError', () => {
- it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => {
- testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done);
- });
- });
-
- describe('fetchRepos', () => {
- let mock;
-
- beforeEach(() => {
- localState.reposPath = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => mock.restore());
-
- it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => {
- const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
-
- testAction(
- fetchRepos,
- null,
- localState,
- [],
- [
- { type: 'requestRepos' },
- {
- type: 'receiveReposSuccess',
- payload: convertObjectPropsToCamelCase(payload, { deep: true }),
- },
- {
- type: 'fetchJobs',
- },
- ],
- done,
- );
- });
-
- it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
-
- testAction(
- fetchRepos,
- null,
- localState,
- [],
- [{ type: 'requestRepos' }, { type: 'receiveReposError' }],
- done,
- );
- });
- });
-
- describe('requestImport', () => {
- it(`commits ${REQUEST_IMPORT} mutation`, done => {
- testAction(
- requestImport,
- repoId,
- localState,
- [{ type: REQUEST_IMPORT, payload: repoId }],
- [],
- done,
- );
- });
- });
-
- describe('receiveImportSuccess', () => {
- it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => {
- const payload = { importedProject: { name: 'imported/project' }, repoId: 2 };
-
- testAction(
- receiveImportSuccess,
- payload,
- localState,
- [{ type: RECEIVE_IMPORT_SUCCESS, payload }],
- [],
- done,
- );
- });
- });
-
- describe('receiveImportError', () => {
- it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => {
- testAction(
- receiveImportError,
- repoId,
- localState,
- [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }],
- [],
- done,
- );
- });
- });
-
- describe('fetchImport', () => {
- let mock;
-
- beforeEach(() => {
- localState.importPath = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => mock.restore());
-
- it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => {
- const importedProject = { name: 'imported/project' };
- const importRepoId = importPayload.repo.id;
- mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
-
- testAction(
- fetchImport,
- importPayload,
- localState,
- [],
- [
- { type: 'requestImport', payload: importRepoId },
- {
- type: 'receiveImportSuccess',
- payload: {
- importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }),
- repoId: importRepoId,
- },
- },
- ],
- done,
- );
- });
-
- it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => {
- mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
-
- testAction(
- fetchImport,
- importPayload,
- localState,
- [],
- [
- { type: 'requestImport', payload: importPayload.repo.id },
- { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } },
- ],
- done,
- );
- });
- });
-
- describe('receiveJobsSuccess', () => {
- it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => {
- testAction(
- receiveJobsSuccess,
- repos,
- localState,
- [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
- [],
- done,
- );
- });
- });
-
- describe('fetchJobs', () => {
- let mock;
-
- beforeEach(() => {
- localState.jobsPath = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- stopJobsPolling();
- clearJobsEtagPoll();
- });
-
- afterEach(() => mock.restore());
-
- it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
- const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
-
- testAction(
- fetchJobs,
- null,
- localState,
- [],
- [
- {
- type: 'receiveJobsSuccess',
- payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
- },
- ],
- done,
- );
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index dfc889773c1..2770743937e 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -470,4 +470,51 @@ describe('Issuable output', () => {
.catch(done.fail);
});
});
+
+ describe('issueChanged', () => {
+ beforeEach(() => {
+ vm.store.formState.title = '';
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = '';
+ vm.initialTitleText = '';
+ });
+
+ it('returns true when title is changed', () => {
+ vm.store.formState.title = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when title is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => {
+ vm.store.formState.title = '';
+ vm.initialTitleText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns true when description is changed', () => {
+ vm.store.formState.description = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when description is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js
index 2c3efc8d4d4..f5f87a6bfbf 100644
--- a/spec/javascripts/issue_show/components/fields/description_spec.js
+++ b/spec/javascripts/issue_show/components/fields/description_spec.js
@@ -63,4 +63,8 @@ describe('Description field component', () => {
expect(eventHub.$emit).toHaveBeenCalled();
});
+
+ it('has a ref named `textarea`', () => {
+ expect(vm.$refs.textarea).not.toBeNull();
+ });
});
diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js
index 4b96a1feb29..62dff983250 100644
--- a/spec/javascripts/issue_show/components/fields/title_spec.js
+++ b/spec/javascripts/issue_show/components/fields/title_spec.js
@@ -41,4 +41,8 @@ describe('Title field component', () => {
expect(eventHub.$emit).toHaveBeenCalled();
});
+
+ it('has a ref named `input`', () => {
+ expect(vm.$refs.input).not.toBeNull();
+ });
});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index 523954013cf..b0f4ab2b12d 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -1,10 +1,17 @@
import Vue from 'vue';
import formComponent from '~/issue_show/components/form.vue';
+import eventHub from '~/issue_show/event_hub';
describe('Inline edit form component', () => {
let vm;
+ let autosave;
+ let autosaveObj;
beforeEach(done => {
+ autosaveObj = { reset: jasmine.createSpy() };
+
+ autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj);
+
const Component = Vue.extend(formComponent);
vm = new Component({
@@ -53,4 +60,22 @@ describe('Inline edit form component', () => {
done();
});
});
+
+ it('initialized Autosave on mount', () => {
+ expect(autosave).toHaveBeenCalledTimes(2);
+ });
+
+ it('calls reset on autosave when eventHub emits appropriate events', () => {
+ eventHub.$emit('close.form');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(2);
+
+ eventHub.$emit('delete.issuable');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(4);
+
+ eventHub.$emit('update.issuable');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(6);
+ });
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 11ab6c38a55..966aee72abb 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -113,7 +113,6 @@ describe('Issue', function() {
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
- mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 0398f184c0a..1a7f338c5fa 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -678,7 +678,7 @@ export const stages = [
icon: 'status_warning',
text: 'failed',
label: 'failed (allowed to fail)',
- group: 'failed_with_warnings',
+ group: 'failed-with-warnings',
tooltip: 'failed - (unknown failure) (allowed to fail)',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
@@ -710,7 +710,7 @@ export const stages = [
icon: 'status_warning',
text: 'failed',
label: 'failed (allowed to fail)',
- group: 'failed_with_warnings',
+ group: 'failed-with-warnings',
tooltip: 'failed - (unknown failure) (allowed to fail)',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
@@ -738,7 +738,7 @@ export const stages = [
icon: 'status_warning',
text: 'passed',
label: 'passed with warnings',
- group: 'success_with_warnings',
+ group: 'success-with-warnings',
tooltip: 'passed',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/pipelines/27#test',
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 431798c6ec3..cadcc15385f 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -58,7 +58,7 @@ describe('MergeRequest', function() {
{
merge_request: {
description: '- [ ] Task List Item',
- lock_version: undefined,
+ lock_version: 0,
update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
},
},
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index ce2c6c43c0f..fc722867b0b 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
-import { timeWindows } from '~/monitoring/constants';
+import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
@@ -37,6 +37,9 @@ describe('Dashboard', () => {
window.gon = {
...window.gon,
ee: false,
+ features: {
+ grafanaDashboardLink: true,
+ },
};
mock = new MockAdapter(axios);
@@ -175,7 +178,7 @@ describe('Dashboard', () => {
setTimeout(() => {
const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item[active="true"]',
+ '.js-environments-dropdown .dropdown-item.is-active',
);
expect(dropdownItems.length).toEqual(1);
@@ -248,6 +251,41 @@ describe('Dashboard', () => {
done();
});
});
+
+ it('shows a specific time window selected from the url params', done => {
+ spyOnDependency(Dashboard, 'getParameterValues').and.returnValue(['thirtyMinutes']);
+
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
+ });
+
+ setTimeout(() => {
+ const selectedTimeWindow = component.$el.querySelector(
+ '.js-time-window-dropdown [active="true"]',
+ );
+
+ expect(selectedTimeWindow.textContent.trim()).toEqual('30 minutes');
+ done();
+ });
+ });
+
+ it('defaults to the eight hours time window for non valid url parameters', done => {
+ spyOnDependency(Dashboard, 'getParameterValues').and.returnValue([
+ '<script>alert("XSS")</script>',
+ ]);
+
+ const component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true, showTimeWindowDropdown: true },
+ });
+
+ Vue.nextTick(() => {
+ expect(component.selectedTimeWindowKey).toEqual(timeWindowsKeyNames.eightHours);
+
+ done();
+ });
+ });
});
describe('when the window resizes', () => {
@@ -288,4 +326,63 @@ describe('Dashboard', () => {
.catch(done.fail);
});
});
+
+ describe('external dashboard link', () => {
+ let component;
+
+ beforeEach(() => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ describe('with feature flag enabled', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardPath: '/mockPath',
+ },
+ });
+ });
+
+ it('shows the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link').innerText).toContain(
+ 'View full dashboard',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('without feature flage enabled', () => {
+ beforeEach(() => {
+ window.gon.features.grafanaDashboardLink = false;
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardPath: '',
+ },
+ });
+ });
+
+ it('does not show the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link')).toBe(null);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index d8a980c874d..5bf6937c92e 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -32,4 +32,28 @@ describe('MonitoringStore', () => {
it('removes the data if all the values from a query are not defined', () => {
expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
});
+
+ it('assigns queries a metric id', () => {
+ expect(store.groups[1].metrics[0].queries[0].metricId).toEqual('100');
+ });
+
+ it('assigns metric id of null if metric has no id', () => {
+ const noId = MonitoringMock.data.map(group => ({
+ ...group,
+ ...{
+ metrics: group.metrics.map(metric => {
+ const { id, ...metricWithoutId } = metric;
+
+ return metricWithoutId;
+ }),
+ },
+ }));
+ store.storeMetrics(noId);
+
+ store.groups.forEach(group => {
+ group.metrics.forEach(metric => {
+ expect(metric.queries.every(query => query.metricId === null)).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 3304c79cdb7..efa864e7d00 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -130,29 +130,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('componentData', () => {
- it('should return first note object for placeholder note', () => {
- const data = {
- isPlaceholderNote: true,
- notes: [{ body: 'hello world!' }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data.notes[0]);
- });
-
- it('should return given note for nonplaceholder notes', () => {
- const data = {
- notes: [{ id: 12 }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data);
- });
- });
-
describe('action text', () => {
const commitId = 'razupaltuff';
const truncatedCommitId = commitId.substr(0, 8);
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 94ce6d8e222..39901276b8c 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -3,11 +3,12 @@ import $ from 'jquery';
import _ from 'underscore';
import { TEST_HOST } from 'spec/test_constants';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
-import * as actions from '~/notes/stores/actions';
+import actionsModule, * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import service from '~/notes/services/notes_service';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import {
@@ -18,11 +19,21 @@ import {
individualNote,
} from '../mock_data';
+const TEST_ERROR_MESSAGE = 'Test error message';
+
describe('Actions Notes Store', () => {
+ let commit;
+ let dispatch;
+ let state;
let store;
+ let flashSpy;
beforeEach(() => {
store = createStore();
+ commit = jasmine.createSpy('commit');
+ dispatch = jasmine.createSpy('dispatch');
+ state = {};
+ flashSpy = spyOnDependency(actionsModule, 'Flash');
});
afterEach(() => {
@@ -604,21 +615,6 @@ describe('Actions Notes Store', () => {
});
describe('updateOrCreateNotes', () => {
- let commit;
- let dispatch;
- let state;
-
- beforeEach(() => {
- commit = jasmine.createSpy('commit');
- dispatch = jasmine.createSpy('dispatch');
- state = {};
- });
-
- afterEach(() => {
- commit.calls.reset();
- dispatch.calls.reset();
- });
-
it('Updates existing note', () => {
const note = { id: 1234 };
const getters = { notesById: { 1234: note } };
@@ -751,4 +747,106 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('resolveDiscussion', () => {
+ let getters;
+ let discussionId;
+
+ beforeEach(() => {
+ discussionId = discussionMock.id;
+ state.discussions = [discussionMock];
+ getters = {
+ isDiscussionResolved: () => false,
+ };
+ });
+
+ it('when unresolved, dispatches action', done => {
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [
+ {
+ type: 'toggleResolveNote',
+ payload: {
+ endpoint: discussionMock.resolve_path,
+ isResolved: false,
+ discussion: true,
+ },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('when resolved, does nothing', done => {
+ getters.isDiscussionResolved = id => id === discussionId;
+
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('submitSuggestion', () => {
+ const discussionId = 'discussion-id';
+ const noteId = 'note-id';
+ const suggestionId = 'suggestion-id';
+ let flashContainer;
+
+ beforeEach(() => {
+ spyOn(service, 'applySuggestion');
+ dispatch.and.returnValue(Promise.resolve());
+ service.applySuggestion.and.returnValue(Promise.resolve());
+ flashContainer = {};
+ });
+
+ const testSubmitSuggestion = (done, expectFn) => {
+ actions
+ .submitSuggestion(
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+ )
+ .then(expectFn)
+ .then(done)
+ .catch(done.fail);
+ };
+
+ it('when service success, commits and resolves discussion', done => {
+ testSubmitSuggestion(done, () => {
+ expect(commit.calls.allArgs()).toEqual([
+ [mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
+ ]);
+
+ expect(dispatch.calls.allArgs()).toEqual([['resolveDiscussion', { discussionId }]]);
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when service fails, flashes error message', done => {
+ const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
+
+ service.applySuggestion.and.returnValue(Promise.reject(response));
+
+ testSubmitSuggestion(done, () => {
+ expect(commit).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(flashSpy).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer);
+ });
+ });
+
+ it('when resolve discussion fails, fail gracefully', done => {
+ dispatch.and.returnValue(Promise.reject());
+
+ testSubmitSuggestion(done, () => {
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
index 6df4ded92f0..7191b65b4cd 100644
--- a/spec/javascripts/pdf/index_spec.js
+++ b/spec/javascripts/pdf/index_spec.js
@@ -3,7 +3,9 @@ import { GlobalWorkerOptions } from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min';
import PDFLab from '~/pdf/index.vue';
-import pdf from '../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
GlobalWorkerOptions.workerSrc = workerSrc;
const Component = Vue.extend(PDFLab);
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
index b2e9fa42a63..f899b5b3a0d 100644
--- a/spec/javascripts/pdf/page_spec.js
+++ b/spec/javascripts/pdf/page_spec.js
@@ -4,7 +4,9 @@ import workerSrc from 'vendor/pdf.worker.min';
import PageComponent from '~/pdf/page/index.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import testPDF from 'spec/fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('Page component', () => {
const Component = Vue.extend(PageComponent);
diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
index e91685e50c5..8a7aa057186 100644
--- a/spec/javascripts/performance_bar/components/detailed_metric_spec.js
+++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
@@ -27,8 +27,8 @@ describe('detailedMetric', () => {
describe('when the current request has details', () => {
const requestDetails = [
- { duration: '100', feature: 'find_commit', request: 'abcdef' },
- { duration: '23', feature: 'rebase_in_progress', request: '' },
+ { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
+ { duration: '23', feature: 'rebase_in_progress', request: '', backtrace: ['world', 'hello'] },
];
beforeEach(() => {
@@ -54,9 +54,11 @@ describe('detailedMetric', () => {
});
it('adds a modal with a table of the details', () => {
- vm.$el.querySelectorAll('.performance-bar-modal td strong').forEach((duration, index) => {
- expect(duration.innerText).toContain(requestDetails[index].duration);
- });
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(1)')
+ .forEach((duration, index) => {
+ expect(duration.innerText).toContain(requestDetails[index].duration);
+ });
vm.$el
.querySelectorAll('.performance-bar-modal td:nth-child(2)')
@@ -65,10 +67,16 @@ describe('detailedMetric', () => {
});
vm.$el
- .querySelectorAll('.performance-bar-modal td:nth-child(3)')
+ .querySelectorAll('.performance-bar-modal td:nth-child(2)')
.forEach((request, index) => {
- expect(request.innerText).toEqual(requestDetails[index].request);
+ expect(request.innerText).toContain(requestDetails[index].request);
});
+
+ expect(vm.$el.querySelector('.text-expander.js-toggle-button')).not.toBeNull();
+
+ vm.$el.querySelectorAll('.performance-bar-modal td:nth-child(2)').forEach(request => {
+ expect(request.innerText).toContain('world');
+ });
});
it('displays the metric name', () => {
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 95717d659b8..321497b35b5 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -66,5 +66,16 @@ describe('pipeline graph action component', () => {
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/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
index 3240e8e4c1b..5183f8dd2d6 100644
--- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -70,4 +70,53 @@ describe('stage column component', () => {
);
});
});
+
+ describe('with action', () => {
+ it('renders action button', () => {
+ component = mountComponent(StageColumnComponent, {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ action: {
+ icon: 'play',
+ title: 'Play all',
+ path: 'action',
+ },
+ });
+
+ expect(component.$el.querySelector('.js-stage-action')).not.toBeNull();
+ });
+ });
+
+ describe('without action', () => {
+ it('does not render action button', () => {
+ component = mountComponent(StageColumnComponent, {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ });
+
+ expect(component.$el.querySelector('.js-stage-action')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/pipeline_triggerer_spec.js b/spec/javascripts/pipelines/pipeline_triggerer_spec.js
new file mode 100644
index 00000000000..8cf290f2663
--- /dev/null
+++ b/spec/javascripts/pipelines/pipeline_triggerer_spec.js
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue';
+
+describe('Pipelines Triggerer', () => {
+ let wrapper;
+
+ const mockData = {
+ pipeline: {
+ user: {
+ name: 'foo',
+ avatar_url: '/avatar',
+ path: '/path',
+ },
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = mount(pipelineTriggerer, {
+ propsData: mockData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render a table cell', () => {
+ expect(wrapper.contains('.table-section')).toBe(true);
+ });
+
+ it('should render triggerer information when triggerer is provided', () => {
+ const link = wrapper.find('.js-pipeline-url-user');
+
+ expect(link.attributes('href')).toEqual(mockData.pipeline.user.path);
+ expect(link.find('.js-user-avatar-image-toolip').text()).toEqual(mockData.pipeline.user.name);
+ expect(link.find('img.avatar').attributes('src')).toEqual(
+ `${mockData.pipeline.user.avatar_url}?width=26`,
+ );
+ });
+
+ it('should render "API" when no triggerer is provided', () => {
+ wrapper.setProps({
+ pipeline: {
+ user: null,
+ },
+ });
+
+ expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index faad49a78b0..aa196af2f33 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -42,54 +42,6 @@ describe('Pipeline Url Component', () => {
expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
});
- it('should render user information when a user is provided', () => {
- const mockData = {
- pipeline: {
- id: 1,
- path: 'foo',
- flags: {},
- user: {
- web_url: '/',
- name: 'foo',
- avatar_url: '/',
- path: '/',
- },
- },
- autoDevopsHelpPath: 'foo',
- };
-
- const component = new PipelineUrlComponent({
- propsData: mockData,
- }).$mount();
-
- const image = component.$el.querySelector('.js-pipeline-url-user img');
- const tooltip = component.$el.querySelector(
- '.js-pipeline-url-user .js-user-avatar-image-toolip',
- );
-
- expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual(
- mockData.pipeline.user.web_url,
- );
-
- expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
- expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
- });
-
- it('should render "API" when no user is provided', () => {
- const component = new PipelineUrlComponent({
- propsData: {
- pipeline: {
- id: 1,
- path: 'foo',
- flags: {},
- },
- autoDevopsHelpPath: 'foo',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
- });
-
it('should render latest, yaml invalid, merge request, and stuck flags when provided', () => {
const component = new PipelineUrlComponent({
propsData: {
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 234fc705a81..d47504d2f54 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -80,13 +80,13 @@ describe('Pipelines Table Row', () => {
it('should render user information', () => {
expect(
component.$el
- .querySelector('.table-section:nth-child(2) a:nth-child(3)')
+ .querySelector('.table-section:nth-child(3) .js-pipeline-url-user')
.getAttribute('href'),
).toEqual(pipeline.user.path);
expect(
component.$el
- .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+ .querySelector('.table-section:nth-child(3) .js-user-avatar-image-toolip')
.textContent.trim(),
).toEqual(pipeline.user.name);
});
diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js
index a503a54029f..6b9fe923624 100644
--- a/spec/javascripts/raven/index_spec.js
+++ b/spec/javascripts/raven/index_spec.js
@@ -5,19 +5,19 @@ describe('RavenConfig options', () => {
const sentryDsn = 'sentryDsn';
const currentUserId = 'currentUserId';
const gitlabUrl = 'gitlabUrl';
- const isProduction = 'isProduction';
+ const environment = 'test';
const revision = 'revision';
let indexReturnValue;
beforeEach(() => {
window.gon = {
sentry_dsn: sentryDsn,
+ sentry_environment: environment,
current_user_id: currentUserId,
gitlab_url: gitlabUrl,
revision,
};
- process.env.NODE_ENV = isProduction;
process.env.HEAD_COMMIT_SHA = revision;
spyOn(RavenConfig, 'init');
@@ -25,12 +25,12 @@ describe('RavenConfig options', () => {
indexReturnValue = index();
});
- it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => {
+ it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
expect(RavenConfig.init).toHaveBeenCalledWith({
sentryDsn,
currentUserId,
- whitelistUrls: [gitlabUrl],
- isProduction,
+ whitelistUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
release: revision,
tags: {
revision,
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
index 5cc59cc28d3..af634a0c196 100644
--- a/spec/javascripts/raven/raven_config_spec.js
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -69,8 +69,8 @@ describe('RavenConfig', () => {
let ravenConfig;
const options = {
sentryDsn: '//sentryDsn',
- whitelistUrls: ['//gitlabUrl'],
- isProduction: true,
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
release: 'revision',
tags: {
revision: 'revision',
@@ -95,7 +95,7 @@ describe('RavenConfig', () => {
release: options.release,
tags: options.tags,
whitelistUrls: options.whitelistUrls,
- environment: 'production',
+ environment: 'test',
ignoreErrors: ravenConfig.IGNORE_ERRORS,
ignoreUrls: ravenConfig.IGNORE_URLS,
shouldSendCallback: jasmine.any(Function),
@@ -106,8 +106,8 @@ describe('RavenConfig', () => {
expect(raven.install).toHaveBeenCalled();
});
- it('should set .environment to development if isProduction is false', () => {
- ravenConfig.options.isProduction = false;
+ it('should set environment from options', () => {
+ ravenConfig.options.environment = 'development';
RavenConfig.configure.call(ravenConfig);
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index 47fee5d2b21..4ae2141d5f0 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -24,12 +24,12 @@ describe('Assignee component', () => {
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
expect(collapsed.childElementCount).toEqual(1);
- expect(collapsed.children[0].getAttribute('aria-label')).toEqual('No Assignee');
+ expect(collapsed.children[0].getAttribute('aria-label')).toEqual('None');
expect(collapsed.children[0].classList.contains('fa')).toEqual(true);
expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true);
});
- it('displays only "No assignee" when no users are assigned and the issue is read-only', () => {
+ it('displays only "None" when no users are assigned and the issue is read-only', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -39,11 +39,11 @@ describe('Assignee component', () => {
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
- expect(componentTextNoUsers).toBe('No assignee');
+ expect(componentTextNoUsers).toBe('None');
expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1);
});
- it('displays only "No assignee" when no users are assigned and the issue can be edited', () => {
+ it('displays only "None" when no users are assigned and the issue can be edited', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -53,7 +53,7 @@ describe('Assignee component', () => {
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
- expect(componentTextNoUsers.indexOf('No assignee')).toEqual(0);
+ expect(componentTextNoUsers.indexOf('None')).toEqual(0);
expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0);
});
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index a820dd2d09c..77c206585fe 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -1,7 +1,9 @@
-export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
+export const FIXTURES_PATH = `/base/${
+ process.env.IS_GITLAB_EE ? 'ee/' : ''
+}spec/javascripts/fixtures`;
export const TEST_HOST = 'http://test.host';
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js
new file mode 100644
index 00000000000..8ec17efffb9
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
+import { GlLink } from '@gitlab/ui';
+
+describe('MrWidgetAlertMessage', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetAlertMessage), {
+ propsData: {},
+ localVue,
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when type is not provided', () => {
+ it('should render a red message', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "danger"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'danger' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "warning"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'warning' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('warning_message');
+ expect(wrapper.classes()).not.toContain('danger_message');
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is not provided', () => {
+ it('should not render a help icon/link', done => {
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is provided', () => {
+ it('should render a help icon/link', done => {
+ wrapper.setProps({ helpPath: '/path/to/help/docs' });
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toBe('/path/to/help/docs');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index de213210cfc..8ac6e6a7b44 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -78,6 +78,19 @@ describe('MRWidgetPipeline', () => {
);
});
+ it('should render CI error when no pipeline is provided', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
+ );
+ });
+
describe('with a pipeline', () => {
beforeEach(() => {
vm = mountComponent(Component, {
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 7ab203a6011..dda16375103 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -233,6 +233,7 @@ export default {
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
+ merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
squash: true,
};
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 3e8f73646c8..a0628fdcebe 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -183,6 +183,101 @@ describe('mrWidgetOptions', () => {
});
});
});
+
+ describe('showMergePipelineForkWarning', () => {
+ describe('when the source project and target project are the same', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', true);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 1);
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(false);
+ });
+ });
+
+ describe('when merge pipelines are not enabled', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', false);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 2);
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(false);
+ });
+ });
+
+ describe('when merge pipelines are enabled _and_ the source project and target project are different', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', true);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 2);
+ vm.$nextTick(done);
+ });
+
+ it('should be true', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(true);
+ });
+ });
+ });
+
+ describe('showTargetBranchAdvancedError', () => {
+ describe(`when the pipeline's target_sha property doesn't exist`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', undefined);
+ Vue.set(vm.mr, 'targetBranchSha', 'abcd');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the pipeline's target_sha matches the target branch's sha`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'abcd');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the merge request is not open`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', false);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the pipeline's target_sha does not match the target branch's sha`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be true', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(true);
+ });
+ });
+ });
});
describe('methods', () => {
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index c226704694c..e2cd0f084fd 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -36,8 +36,8 @@ describe('MergeRequestStore', () => {
expect(store.isPipelinePassing).toBe(true);
});
- it('is true when the CI status is `success_with_warnings`', () => {
- store.setData({ ...mockData, ci_status: 'success_with_warnings' });
+ it('is true when the CI status is `success-with-warnings`', () => {
+ store.setData({ ...mockData, ci_status: 'success-with-warnings' });
expect(store.isPipelinePassing).toBe(true);
});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index 4b0b7ba66e5..42481f8c334 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -59,7 +59,7 @@ describe('CI Badge Link Component', () => {
success_warining: {
text: 'passed',
label: 'passed',
- group: 'success_with_warnings',
+ group: 'success-with-warnings',
icon: 'status_warning',
details_path: 'status/warning',
},
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 4da8c6196b1..bdf802052b9 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
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
+import '~/behaviors/markdown/render_gfm';
describe('ContentViewer', () => {
let vm;
@@ -29,6 +30,7 @@ describe('ContentViewer', () => {
path: 'test.md',
content: '* Test',
projectPath: 'testproject',
+ type: 'markdown',
});
const previewContainer = vm.$el.querySelector('.md-previewer');
@@ -44,6 +46,7 @@ describe('ContentViewer', () => {
createComponent({
path: GREEN_BOX_IMAGE_URL,
fileSize: 1024,
+ type: 'image',
});
setTimeout(() => {
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 7f2e246d656..97c870f27d9 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
@@ -138,22 +138,6 @@ describe('ImageDiffViewer', () => {
done();
});
});
-
- it('drag handler is working', done => {
- vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('1px');
- expect(vm.$el.querySelector('.top-handle')).not.toBeNull();
-
- dragSlider(vm.$el.querySelector('.swipe-bar'), 40);
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('-20px');
- done();
- });
- });
- });
});
describe('onionSkin', () => {
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
deleted file mode 100644
index 9eac75fac96..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import Vue from 'vue';
-
-import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockAssigneesList } from 'spec/boards/mock_data';
-
-const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
- const Component = Vue.extend(IssueAssignees);
-
- return mountComponent(Component, {
- assignees,
- cssClass,
- });
-};
-
-describe('IssueAssigneesComponent', () => {
- 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);
- });
- });
-
- 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('assigneeCounterLabel', () => {
- it('should return count of additional assignees total assignees count more than maxAssigneeAvatars', () => {
- expect(vm.assigneeCounterLabel).toBe('+3');
- });
- });
- });
-
- describe('methods', () => {
- describe('avatarUrlTitle', () => {
- it('returns string containing 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);
- });
-
- it('renders assignee avatars', () => {
- expect(vm.$el.querySelectorAll('.user-avatar-link').length).toBe(2);
- });
-
- 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');
- });
-
- it('renders additional assignees count', () => {
- const avatarCounterEl = vm.$el.querySelector('.avatar-counter');
-
- expect(avatarCounterEl.innerText.trim()).toBe('+3');
- expect(avatarCounterEl.getAttribute('data-original-title')).toBe('3 more assignees');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
deleted file mode 100644
index 8fca2637326..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Vue from 'vue';
-
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockMilestone } from 'spec/boards/mock_data';
-
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return mountComponent(Component, {
- milestone,
- });
-};
-
-describe('IssueMilestoneComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', done => {
- const vmStartUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStartUndefined.isMilestoneStarted).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmStartUndefined.$destroy();
- });
-
- it('should return `true` when milestone start date is past current date', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.isMilestoneStarted).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
- });
-
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.isMilestonePastDue).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('should return `true` when milestone due is past current date', done => {
- const vmPastDue = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmPastDue.isMilestonePastDue).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmPastDue.$destroy();
- });
- });
-
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
-
- it('returns string containing absolute milestone start date when due date is not present', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('returns empty string when both milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
-
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', done => {
- const vmFuture = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
- })
- .then(done)
- .catch(done.fail);
-
- vmFuture.$destroy();
- });
-
- it('returns string containing milestone start date when date has already started and due date is not present', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.milestoneDatesHuman).toContain('Started');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
- const vmStarts = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarts.milestoneDatesHuman).toContain('Starts');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarts.$destroy();
- });
-
- it('returns empty string when milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
deleted file mode 100644
index aa7d6ea2e34..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import Vue from 'vue';
-import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const IssueWarning = Vue.extend(issueWarning);
-
-function formatWarning(string) {
- // Replace newlines with a space then replace multiple spaces with one space
- return string
- .trim()
- .replace(/\n/g, ' ')
- .replace(/\s\s+/g, ' ');
-}
-
-describe('Issue Warning Component', () => {
- describe('isLocked', () => {
- it('should render locked issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
- isLocked: true,
- });
-
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
- expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This issue is locked. Only project members can comment.',
- );
- });
- });
-
- describe('isConfidential', () => {
- it('should render confidential issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
- isConfidential: true,
- });
-
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
- expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This is a confidential issue. Your comment will not be visible to the public.',
- );
- });
- });
-
- describe('isLocked and isConfidential', () => {
- it('should render locked and confidential issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
- isLocked: true,
- isConfidential: true,
- });
-
- expect(vm.$el.querySelector('.icon')).toBeFalsy();
- expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- "This issue is confidential and locked. People without permission will never get a notification and won't be able to comment.",
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js b/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js
deleted file mode 100644
index 42198e92eea..00000000000
--- a/spec/javascripts/vue_shared/components/issue/related_issuable_item_spec.js
+++ /dev/null
@@ -1,194 +0,0 @@
-import Vue from 'vue';
-import { mount, createLocalVue } from '@vue/test-utils';
-import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import { defaultMilestone, defaultAssignees } from './related_issuable_mock_data';
-
-describe('RelatedIssuableItem', () => {
- let wrapper;
- const props = {
- idKey: 1,
- displayReference: 'gitlab-org/gitlab-test#1',
- pathIdSeparator: '#',
- path: `${gl.TEST_HOST}/path`,
- title: 'title',
- confidential: true,
- dueDate: '1990-12-31',
- weight: 10,
- createdAt: '2018-12-01T00:00:00.00Z',
- milestone: defaultMilestone,
- assignees: defaultAssignees,
- eventNamespace: 'relatedIssue',
- };
- const slots = {
- dueDate: '<div class="js-due-date-slot"></div>',
- weight: '<div class="js-weight-slot"></div>',
- };
-
- beforeEach(() => {
- const localVue = createLocalVue();
-
- wrapper = mount(localVue.extend(RelatedIssuableItem), {
- localVue,
- slots,
- sync: false,
- propsData: props,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('contains issuable-info-container class when canReorder is false', () => {
- expect(wrapper.props('canReorder')).toBe(false);
- expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
- });
-
- it('does not render token state', () => {
- expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
- });
-
- it('does not render remove button', () => {
- expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
- });
-
- describe('token title', () => {
- it('links to computedPath', () => {
- expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
- });
-
- it('renders confidential icon', () => {
- expect(wrapper.find('.confidential-icon').exists()).toBe(true);
- });
-
- it('renders title', () => {
- expect(wrapper.find('.item-title a').text()).toEqual(props.title);
- });
- });
-
- describe('token state', () => {
- let tokenState;
-
- beforeEach(done => {
- wrapper.setProps({ state: 'opened' });
-
- Vue.nextTick(() => {
- tokenState = wrapper.find('.issue-token-state-icon-open');
-
- done();
- });
- });
-
- it('renders if hasState', () => {
- expect(tokenState.exists()).toBe(true);
- });
-
- it('renders state title', () => {
- const stateTitle = tokenState.attributes('data-original-title');
-
- expect(stateTitle).toContain('<span class="bold">Opened</span>');
- expect(stateTitle).toContain(
- '<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>',
- );
- });
-
- it('renders aria label', () => {
- expect(tokenState.attributes('aria-label')).toEqual('opened');
- });
-
- it('renders open icon when open state', () => {
- expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
- });
-
- it('renders close icon when close state', done => {
- wrapper.setProps({
- state: 'closed',
- closedAt: '2018-12-01T00:00:00.00Z',
- });
-
- Vue.nextTick(() => {
- expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
-
- done();
- });
- });
- });
-
- describe('token metadata', () => {
- let tokenMetadata;
-
- beforeEach(done => {
- Vue.nextTick(() => {
- tokenMetadata = wrapper.find('.item-meta');
-
- done();
- });
- });
-
- it('renders item path and ID', () => {
- const pathAndID = tokenMetadata.find('.item-path-id').text();
-
- expect(pathAndID).toContain('gitlab-org/gitlab-test');
- expect(pathAndID).toContain('#1');
- });
-
- it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
- const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
-
- expect(milestoneIcon.attributes('href')).toContain('clock');
- expect(milestoneTitle.text()).toContain('Milestone title');
- });
-
- it('renders due date component', () => {
- expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
- });
-
- it('renders weight component', () => {
- expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
- });
- });
-
- describe('token assignees', () => {
- it('renders assignees avatars', () => {
- expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
- expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
- });
- });
-
- describe('remove button', () => {
- let removeBtn;
-
- beforeEach(done => {
- wrapper.setProps({ canRemove: true });
- Vue.nextTick(() => {
- removeBtn = wrapper.find({ ref: 'removeButton' });
-
- done();
- });
- });
-
- it('renders if canRemove', () => {
- expect(removeBtn.exists()).toBe(true);
- });
-
- it('renders disabled button when removeDisabled', done => {
- wrapper.vm.removeDisabled = true;
-
- Vue.nextTick(() => {
- expect(removeBtn.attributes('disabled')).toEqual('disabled');
-
- done();
- });
- });
-
- it('triggers onRemoveRequest when clicked', () => {
- removeBtn.trigger('click');
-
- const { relatedIssueRemoveRequest } = wrapper.emitted();
-
- expect(relatedIssueRemoveRequest.length).toBe(1);
- expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
deleted file mode 100644
index 12ee804f668..00000000000
--- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-import SuggestionDiffHeaderComponent from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
-
-const MOCK_DATA = {
- canApply: true,
- isApplied: false,
- helpPagePath: 'path_to_docs',
-};
-
-describe('Suggestion Diff component', () => {
- let vm;
-
- function createComponent(propsData) {
- const Component = Vue.extend(SuggestionDiffHeaderComponent);
-
- return new Component({
- propsData,
- }).$mount();
- }
-
- beforeEach(done => {
- vm = createComponent(MOCK_DATA);
- Vue.nextTick(done);
- });
-
- describe('init', () => {
- it('renders a suggestion header', () => {
- const header = vm.$el.querySelector('.qa-suggestion-diff-header');
-
- expect(header).not.toBeNull();
- expect(header.innerHTML.includes('Suggested change')).toBe(true);
- });
-
- it('renders a help button', () => {
- const helpBtn = vm.$el.querySelector('.js-help-btn');
-
- expect(helpBtn).not.toBeNull();
- });
-
- it('renders an apply button', () => {
- const applyBtn = vm.$el.querySelector('.qa-apply-btn');
-
- expect(applyBtn).not.toBeNull();
- expect(applyBtn.innerHTML.includes('Apply suggestion')).toBe(true);
- });
-
- it('does not render an apply button if `canApply` is set to false', () => {
- const props = Object.assign(MOCK_DATA, { canApply: false });
-
- vm = createComponent(props);
-
- expect(vm.$el.querySelector('.qa-apply-btn')).toBeNull();
- });
- });
-
- describe('applySuggestion', () => {
- it('emits when the apply button is clicked', () => {
- const props = Object.assign(MOCK_DATA, { canApply: true });
-
- vm = createComponent(props);
- spyOn(vm, '$emit');
- vm.applySuggestion();
-
- expect(vm.$emit).toHaveBeenCalled();
- });
-
- it('does not emit when the canApply is set to false', () => {
- spyOn(vm, '$emit');
- vm.canApply = false;
- vm.applySuggestion();
-
- expect(vm.$emit).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
deleted file mode 100644
index 45f131194ca..00000000000
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import Vue from 'vue';
-import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
-import createStore from '~/notes/stores';
-import { userDataMock } from '../../../notes/mock_data';
-
-describe('issue placeholder system note component', () => {
- let store;
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(issuePlaceholderNote);
- store = createStore();
- store.dispatch('setUserData', userDataMock);
- vm = new Component({
- store,
- propsData: { note: { body: 'Foo' } },
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('user information', () => {
- it('should render user avatar with link', () => {
- expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(
- userDataMock.path,
- );
-
- expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(
- `${userDataMock.avatar_url}?width=40`,
- );
- });
- });
-
- describe('note content', () => {
- it('should render note header information', () => {
- expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(
- userDataMock.path,
- );
-
- expect(
- vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim(),
- ).toEqual(`@${userDataMock.username}`);
- });
-
- it('should render note body', () => {
- expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
deleted file mode 100644
index 6013e85811a..00000000000
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('placeholder system note component', () => {
- let PlaceholderSystemNote;
- let vm;
-
- beforeEach(() => {
- PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render system note placeholder with plain text', () => {
- vm = mountComponent(PlaceholderSystemNote, {
- note: { body: 'This is a placeholder' },
- });
-
- expect(vm.$el.tagName).toEqual('LI');
- expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual(
- 'This is a placeholder',
- );
- });
-});
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js
deleted file mode 100644
index adcb1c858aa..00000000000
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import Vue from 'vue';
-import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
-import createStore from '~/notes/stores';
-
-describe('system note component', () => {
- let vm;
- let props;
-
- beforeEach(() => {
- props = {
- note: {
- id: '1424',
- author: {
- id: 1,
- name: 'Root',
- username: 'root',
- state: 'active',
- avatar_url: 'path',
- path: '/root',
- },
- note_html: '<p dir="auto">closed</p>',
- system_note_icon_name: 'status_closed',
- created_at: '2017-08-02T10:51:58.559Z',
- },
- };
-
- const store = createStore();
- store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
-
- const Component = Vue.extend(issueSystemNote);
- vm = new Component({
- store,
- propsData: props,
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render a list item with correct id', () => {
- expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`);
- });
-
- it('should render target class is note is target note', () => {
- expect(vm.$el.classList).toContain('target');
- });
-
- it('should render svg icon', () => {
- expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined();
- });
-
- // Redcarpet Markdown renderer wraps text in `<p>` tags
- // we need to strip them because they break layout of commit lists in system notes:
- // https://gitlab.com/gitlab-org/gitlab-ce/uploads/b07a10670919254f0220d3ff5c1aa110/jqzI.png
- it('removes wrapping paragraph from note HTML', () => {
- expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
index b95183747bb..268ced38f40 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
@@ -9,8 +9,8 @@ describe('ProjectListItem component', () => {
let wrapper;
let vm;
let options;
- loadJSONFixtures('projects.json');
- const project = getJSONFixture('projects.json')[0];
+ loadJSONFixtures('static/projects.json');
+ const project = getJSONFixture('static/projects.json')[0];
beforeEach(() => {
options = {
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 ba9ec8f2f19..34c0cd435cd 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
@@ -8,8 +8,8 @@ import { trimText } from 'spec/helpers/vue_component_helper';
describe('ProjectSelector component', () => {
let wrapper;
let vm;
- loadJSONFixtures('projects.json');
- const allProjects = getJSONFixture('projects.json');
+ loadJSONFixtures('static/projects.json');
+ const allProjects = getJSONFixture('static/projects.json');
const searchResults = allProjects.slice(0, 5);
let selected = [];
selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
deleted file mode 100644
index 6bff1521695..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('collapsedCalendarIcon', () => {
- let vm;
- beforeEach(() => {
- const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
- vm = mountComponent(CollapsedCalendarIcon, {
- containerClass: 'test-class',
- text: 'text',
- showIcon: false,
- });
- });
-
- it('should add class to container', () => {
- expect(vm.$el.classList.contains('test-class')).toEqual(true);
- });
-
- it('should hide calendar icon if showIcon', () => {
- expect(vm.$el.querySelector('.fa-calendar')).toBeNull();
- });
-
- it('should render text', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
- });
-
- it('should emit click event when container is clicked', () => {
- const click = jasmine.createSpy();
- vm.$on('click', click);
-
- vm.$el.click();
-
- expect(click).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
deleted file mode 100644
index c507a97d37e..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('collapsedGroupedDatePicker', () => {
- let vm;
- beforeEach(() => {
- const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
- vm = mountComponent(CollapsedGroupedDatePicker, {
- showToggleSidebar: true,
- });
- });
-
- describe('toggleCollapse events', () => {
- beforeEach(done => {
- spyOn(vm, 'toggleSidebar');
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
-
- it('should emit when collapsed-calendar-icon is clicked', () => {
- vm.$el.querySelector('.sidebar-collapsed-icon').click();
-
- expect(vm.toggleSidebar).toHaveBeenCalled();
- });
- });
-
- describe('minDate and maxDate', () => {
- beforeEach(done => {
- vm.minDate = new Date('07/17/2016');
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
- it('should render both collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(2);
- expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
- expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
- });
- });
-
- describe('minDate', () => {
- beforeEach(done => {
- vm.minDate = new Date('07/17/2016');
- Vue.nextTick(done);
- });
-
- it('should render minDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
- });
- });
-
- describe('maxDate', () => {
- beforeEach(done => {
- vm.maxDate = new Date('07/17/2017');
- Vue.nextTick(done);
- });
-
- it('should render maxDate in collapsed-calendar-icon', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
- });
- });
-
- describe('no dates', () => {
- it('should render None', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons.length).toEqual(1);
- expect(icons[0].innerText.trim()).toEqual('None');
- });
-
- it('should have tooltip as `Start and due date`', () => {
- const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
-
- expect(icons[0].dataset.originalTitle).toBe('Start and due date');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
deleted file mode 100644
index 805ba7b9947..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ /dev/null
@@ -1,121 +0,0 @@
-import Vue from 'vue';
-import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('sidebarDatePicker', () => {
- let vm;
- beforeEach(() => {
- const SidebarDatePicker = Vue.extend(sidebarDatePicker);
- vm = mountComponent(SidebarDatePicker, {
- label: 'label',
- isLoading: true,
- });
- });
-
- it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
- vm.$on('toggleCollapse', toggleCollapse);
-
- vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
-
- expect(toggleCollapse).toHaveBeenCalled();
- });
-
- it('should render collapsed-calendar-icon', () => {
- expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined();
- });
-
- it('should render label', () => {
- expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label');
- });
-
- it('should render loading-icon when isLoading', () => {
- expect(vm.$el.querySelector('.fa-spin')).toBeDefined();
- });
-
- it('should render value when not editing', () => {
- expect(vm.$el.querySelector('.value-content')).toBeDefined();
- });
-
- it('should render None if there is no selectedDate', () => {
- expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
- });
-
- it('should render date-picker when editing', done => {
- vm.editing = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.pika-label')).toBeDefined();
- done();
- });
- });
-
- describe('editable', () => {
- beforeEach(done => {
- vm.editable = true;
- Vue.nextTick(done);
- });
-
- it('should render edit button', () => {
- expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
- });
-
- it('should enable editing when edit button is clicked', done => {
- vm.isLoading = false;
- Vue.nextTick(() => {
- vm.$el.querySelector('.title .btn-blank').click();
-
- expect(vm.editing).toEqual(true);
- done();
- });
- });
- });
-
- it('should render date if selectedDate', done => {
- vm.selectedDate = new Date('07/07/2017');
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
- done();
- });
- });
-
- describe('selectedDate and editable', () => {
- beforeEach(done => {
- vm.selectedDate = new Date('07/07/2017');
- vm.editable = true;
- Vue.nextTick(done);
- });
-
- it('should render remove button if selectedDate and editable', () => {
- expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove');
- });
-
- it('should emit saveDate when remove button is clicked', () => {
- const saveDate = jasmine.createSpy();
- vm.$on('saveDate', saveDate);
-
- vm.$el.querySelector('.value-content .btn-blank').click();
-
- expect(saveDate).toHaveBeenCalled();
- });
- });
-
- describe('showToggleSidebar', () => {
- beforeEach(done => {
- vm.showToggleSidebar = true;
- Vue.nextTick(done);
- });
-
- it('should render toggle-sidebar when showToggleSidebar', () => {
- expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined();
- });
-
- it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
- vm.$on('toggleCollapse', toggleCollapse);
-
- vm.$el.querySelector('.title .gutter-toggle').click();
-
- expect(toggleCollapse).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
deleted file mode 100644
index c44b04009ca..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import Vue from 'vue';
-
-import LabelsSelect from '~/labels_select';
-import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const createComponent = (config = mockConfig) => {
- const Component = Vue.extend(baseComponent);
-
- return mountComponent(Component, config);
-};
-
-describe('BaseComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('hiddenInputName', () => {
- it('returns correct string when showCreate prop is `true`', () => {
- expect(vm.hiddenInputName).toBe('issue[label_names][]');
- });
-
- it('returns correct string when showCreate prop is `false`', () => {
- const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false });
- const vmNonEditable = createComponent(mockConfigNonEditable);
-
- expect(vmNonEditable.hiddenInputName).toBe('label_id[]');
- vmNonEditable.$destroy();
- });
- });
-
- describe('createLabelTitle', () => {
- it('returns `Create project label` when `isProject` prop is true', () => {
- expect(vm.createLabelTitle).toBe('Create project label');
- });
-
- it('return `Create group label` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
-
- expect(vmGroup.createLabelTitle).toBe('Create group label');
- vmGroup.$destroy();
- });
- });
-
- describe('manageLabelsTitle', () => {
- it('returns `Manage project labels` when `isProject` prop is true', () => {
- expect(vm.manageLabelsTitle).toBe('Manage project labels');
- });
-
- it('return `Manage group labels` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
-
- expect(vmGroup.manageLabelsTitle).toBe('Manage group labels');
- vmGroup.$destroy();
- });
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onLabelClick event with label and list of labels as params', () => {
- spyOn(vm, '$emit');
- vm.handleClick(mockLabels[0]);
-
- expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
- });
- });
-
- describe('handleCollapsedValueClick', () => {
- it('emits toggleCollapse event on component', () => {
- spyOn(vm, '$emit');
- vm.handleCollapsedValueClick();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
- });
- });
-
- describe('handleDropdownHidden', () => {
- it('emits onDropdownClose event on component', () => {
- spyOn(vm, '$emit');
- vm.handleDropdownHidden();
-
- expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
- });
- });
- });
-
- describe('mounted', () => {
- it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
- expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `block labels`', () => {
- expect(vm.$el.classList.contains('block')).toBe(true);
- expect(vm.$el.classList.contains('labels')).toBe(true);
- });
-
- it('renders `.selectbox` element', () => {
- expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
- expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
- });
-
- it('renders `.dropdown` element', () => {
- expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
- });
-
- it('renders `.dropdown-menu` element', () => {
- const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
-
- expect(dropdownMenuEl).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
- expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
deleted file mode 100644
index 0689fc1cf1f..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import Vue from 'vue';
-
-import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const componentConfig = Object.assign({}, mockConfig, {
- fieldName: 'label_id[]',
- labels: mockLabels,
- showExtraOptions: false,
-});
-
-const createComponent = (config = componentConfig) => {
- const Component = Vue.extend(dropdownButtonComponent);
-
- return mountComponent(Component, config);
-};
-
-describe('DropdownButtonComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('dropdownToggleText', () => {
- it('returns text as `Label` when `labels` prop is empty array', () => {
- const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] });
- const vmEmptyLabels = createComponent(mockEmptyLabels);
-
- expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
- vmEmptyLabels.$destroy();
- });
-
- it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
- const mockMoreLabels = Object.assign({}, componentConfig, {
- labels: mockLabels.concat(mockLabels),
- });
- const vmMoreLabels = createComponent(mockMoreLabels);
-
- expect(vmMoreLabels.dropdownToggleText).toBe(
- `Foo Label +${mockMoreLabels.labels.length - 1} more`,
- );
- vmMoreLabels.$destroy();
- });
-
- it('returns first label name when `labels` prop has only one item present', () => {
- const singleLabel = Object.assign({}, componentConfig, {
- labels: [mockLabels[0]],
- });
- const vmSingleLabel = createComponent(singleLabel);
-
- expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
-
- vmSingleLabel.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element of type `button`', () => {
- expect(vm.$el.nodeName).toBe('BUTTON');
- });
-
- it('renders component container element with required data attributes', () => {
- expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
- expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
- expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
- expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
- expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
- expect(vm.$el.dataset.showAny).not.toBeDefined();
- });
-
- it('renders dropdown toggle text element', () => {
- const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
-
- expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
- });
-
- it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('i.fa');
-
- expect(dropdownIconEl).not.toBeNull();
- expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
deleted file mode 100644
index b8f32f96332..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import Vue from 'vue';
-
-import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockSuggestedColors } from './mock_data';
-
-const createComponent = headerTitle => {
- const Component = Vue.extend(dropdownCreateLabelComponent);
-
- return mountComponent(Component, {
- headerTitle,
- });
-};
-
-describe('DropdownCreateLabelComponent', () => {
- let vm;
-
- beforeEach(() => {
- gon.suggested_label_colors = mockSuggestedColors;
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('created', () => {
- it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
- expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
- expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
- });
-
- it('renders `Go back` button on component header', () => {
- const backButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-back',
- );
-
- expect(backButtonEl).not.toBe(null);
- expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
- });
-
- it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
- const headerEl = vm.$el.querySelector('.dropdown-title');
-
- expect(headerEl.innerText.trim()).toContain('Create new label');
- });
-
- it('renders component header element with value of `headerTitle` prop', () => {
- const headerTitle = 'Create project label';
- const vmWithHeaderTitle = createComponent(headerTitle);
- const headerEl = vmWithHeaderTitle.$el.querySelector('.dropdown-title');
-
- expect(headerEl.innerText.trim()).toContain(headerTitle);
- vmWithHeaderTitle.$destroy();
- });
-
- it('renders `Close` button on component header', () => {
- const closeButtonEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
- );
-
- expect(closeButtonEl).not.toBe(null);
- expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
- });
-
- it('renders `Name new label` input element', () => {
- expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
- expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
- });
-
- it('renders suggested colors list elements', () => {
- const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
-
- expect(colorsListContainerEl).not.toBe(null);
- expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
-
- const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
-
- expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
- expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
- });
-
- it('renders color input element', () => {
- expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
- expect(
- vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview'),
- ).not.toBe(null);
-
- expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
- });
-
- it('renders component action buttons', () => {
- const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
- const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
-
- expect(createBtnEl).not.toBe(null);
- expect(createBtnEl.innerText.trim()).toBe('Create');
- expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
deleted file mode 100644
index 3711e9dac8c..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Vue from 'vue';
-
-import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig } from './mock_data';
-
-const createComponent = (
- labelsWebUrl = mockConfig.labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
-) => {
- const Component = Vue.extend(dropdownFooterComponent);
-
- return mountComponent(Component, {
- labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
- });
-};
-
-describe('DropdownFooterComponent', () => {
- const createLabelTitle = 'Create project label';
- const manageLabelsTitle = 'Manage project labels';
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders link element with `Create new label` when `createLabelTitle` prop is not provided', () => {
- const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
-
- expect(createLabelEl).not.toBeNull();
- expect(createLabelEl.innerText.trim()).toBe('Create new label');
- });
-
- it('renders link element with value of `createLabelTitle` prop', () => {
- const vmWithCreateLabelTitle = createComponent(mockConfig.labelsWebUrl, createLabelTitle);
- const createLabelEl = vmWithCreateLabelTitle.$el.querySelector(
- '.dropdown-footer-list .dropdown-toggle-page',
- );
-
- expect(createLabelEl.innerText.trim()).toBe(createLabelTitle);
- vmWithCreateLabelTitle.$destroy();
- });
-
- it('renders link element with `Manage labels` when `manageLabelsTitle` prop is not provided', () => {
- const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
-
- expect(manageLabelsEl).not.toBeNull();
- expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
- expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
- });
-
- it('renders link element with value of `manageLabelsTitle` prop', () => {
- const vmWithManageLabelsTitle = createComponent(
- mockConfig.labelsWebUrl,
- createLabelTitle,
- manageLabelsTitle,
- );
- const manageLabelsEl = vmWithManageLabelsTitle.$el.querySelector(
- '.dropdown-footer-list .dropdown-external-link',
- );
-
- expect(manageLabelsEl.innerText.trim()).toBe(manageLabelsTitle);
- vmWithManageLabelsTitle.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
deleted file mode 100644
index 115e21e4f9f..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-
-import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = () => {
- const Component = Vue.extend(dropdownHeaderComponent);
-
- return mountComponent(Component);
-};
-
-describe('DropdownHeaderComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders header text element', () => {
- const headerEl = vm.$el.querySelector('.dropdown-title span');
-
- expect(headerEl.innerText.trim()).toBe('Assign labels');
- });
-
- it('renders `Close` button element', () => {
- const closeBtnEl = vm.$el.querySelector(
- '.dropdown-title button.dropdown-title-button.dropdown-menu-close',
- );
-
- expect(closeBtnEl).not.toBeNull();
- expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
deleted file mode 100644
index c30e619e76b..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-
-import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = () => {
- const Component = Vue.extend(dropdownSearchInputComponent);
-
- return mountComponent(Component);
-};
-
-describe('DropdownSearchInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element with type `search`', () => {
- const inputEl = vm.$el.querySelector('input.dropdown-input-field');
-
- expect(inputEl).not.toBeNull();
- expect(inputEl.getAttribute('type')).toBe('search');
- });
-
- it('renders search icon element', () => {
- expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
- });
-
- it('renders clear search icon element', () => {
- expect(
- vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
- ).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
deleted file mode 100644
index 6c84d2e167c..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-
-import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = (canEdit = true) => {
- const Component = Vue.extend(dropdownTitleComponent);
-
- return mountComponent(Component, {
- canEdit,
- });
-};
-
-describe('DropdownTitleComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders title text', () => {
- expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
- expect(vm.$el.innerText.trim()).toContain('Labels');
- });
-
- it('renders spinner icon element', () => {
- expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull();
- });
-
- it('renders `Edit` button element', () => {
- const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
-
- expect(editBtnEl).not.toBeNull();
- expect(editBtnEl.innerText.trim()).toBe('Edit');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
deleted file mode 100644
index cb49fa31d20..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import Vue from 'vue';
-
-import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockLabels } from './mock_data';
-
-const createComponent = (labels = mockLabels) => {
- const Component = Vue.extend(dropdownValueCollapsedComponent);
-
- return mountComponent(Component, {
- labels,
- });
-};
-
-describe('DropdownValueCollapsedComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('labelsList', () => {
- it('returns empty text when `labels` prop is empty array', () => {
- const vmEmptyLabels = createComponent([]);
-
- expect(vmEmptyLabels.labelsList).toBe('');
- vmEmptyLabels.$destroy();
- });
-
- it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const labels = mockLabels.concat(mockLabels);
- const vmMoreLabels = createComponent(labels);
-
- const expectedText = labels.map(label => label.title).join(', ');
-
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
- });
-
- it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
- const mockMoreLabels = Object.assign([], mockLabels);
- for (let i = 0; i < 6; i += 1) {
- mockMoreLabels.unshift(mockLabels[0]);
- }
-
- const vmMoreLabels = createComponent(mockMoreLabels);
-
- const expectedText = `${mockMoreLabels
- .slice(0, 5)
- .map(label => label.title)
- .join(', ')}, and ${mockMoreLabels.length - 5} more`;
-
- expect(vmMoreLabels.labelsList).toBe(expectedText);
- vmMoreLabels.$destroy();
- });
-
- it('returns first label name when `labels` prop has only one item present', () => {
- const text = mockLabels.map(label => label.title).join(', ');
-
- expect(vm.labelsList).toBe(text);
- });
- });
- });
-
- describe('methods', () => {
- describe('handleClick', () => {
- it('emits onValueClick event on component', () => {
- spyOn(vm, '$emit');
- vm.handleClick();
-
- expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element with tooltip`', () => {
- expect(vm.$el.dataset.placement).toBe('left');
- expect(vm.$el.dataset.container).toBe('body');
- expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList);
- });
-
- it('renders tags icon element', () => {
- expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
- });
-
- it('renders labels count', () => {
- expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
deleted file mode 100644
index 35a9c300953..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import Vue from 'vue';
-import $ from 'jquery';
-
-import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
-
-const createComponent = (
- labels = mockLabels,
- labelFilterBasePath = mockConfig.labelFilterBasePath,
-) => {
- const Component = Vue.extend(dropdownValueComponent);
-
- return mountComponent(Component, {
- labels,
- labelFilterBasePath,
- enableScopedLabels: true,
- });
-};
-
-describe('DropdownValueComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isEmpty', () => {
- it('returns true if `labels` prop is empty', () => {
- const vmEmptyLabels = createComponent([]);
-
- expect(vmEmptyLabels.isEmpty).toBe(true);
- vmEmptyLabels.$destroy();
- });
-
- it('returns false if `labels` prop is empty', () => {
- expect(vm.isEmpty).toBe(false);
- });
- });
- });
-
- describe('methods', () => {
- describe('labelFilterUrl', () => {
- it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
- expect(
- vm.labelFilterUrl({
- title: 'Foo bar',
- }),
- ).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar');
- });
- });
-
- describe('labelStyle', () => {
- it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
- const label = {
- textColor: '#FFFFFF',
- color: '#BADA55',
- };
- const styleObj = vm.labelStyle(label);
-
- expect(styleObj.color).toBe(label.textColor);
- expect(styleObj.backgroundColor).toBe(label.color);
- });
- });
-
- describe('scopedLabelsDescription', () => {
- it('returns html for tooltip', () => {
- const html = vm.scopedLabelsDescription(mockLabels[1]);
- const $el = $.parseHTML(html);
-
- expect($el[0]).toHaveClass('scoped-label-tooltip-title');
- expect($el[2].textContent).toEqual(mockLabels[1].description);
- });
- });
-
- describe('showScopedLabels', () => {
- it('returns true if the label is scoped label', () => {
- expect(vm.showScopedLabels(mockLabels[1])).toBe(true);
- });
-
- it('returns false when label is a regular label', () => {
- expect(vm.showScopedLabels(mockLabels[0])).toBe(false);
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
- expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(
- true,
- );
- });
-
- it('render slot content inside component when `labels` prop is empty', () => {
- const vmEmptyLabels = createComponent([]);
-
- expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(
- mockConfig.emptyValueText,
- );
- vmEmptyLabels.$destroy();
- });
-
- it('renders label element with filter URL', () => {
- expect(vm.$el.querySelector('a').getAttribute('href')).toBe(
- '/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
- );
- });
-
- it('renders label element and styles based on label details', () => {
- const labelEl = vm.$el.querySelector('a span.badge.color-label');
-
- expect(labelEl).not.toBeNull();
- expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
- expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
- });
-
- describe('label is of scoped-label type', () => {
- it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
- expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull();
- });
-
- it('renders anchor tag containing question icon', () => {
- const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
-
- expect(anchor).not.toBeNull();
- expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
- });
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
deleted file mode 100644
index c911a129173..00000000000
--- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-describe('toggleSidebar', () => {
- let vm;
- beforeEach(() => {
- const ToggleSidebar = Vue.extend(toggleSidebar);
- vm = mountComponent(ToggleSidebar, {
- collapsed: true,
- });
- });
-
- it('should render << when collapsed', () => {
- expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
- });
-
- it('should render >> when collapsed', () => {
- vm.collapsed = false;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
- });
- });
-
- it('should emit toggle event when button clicked', () => {
- const toggle = jasmine.createSpy();
- vm.$on('toggle', toggle);
- vm.$el.click();
-
- expect(toggle).toHaveBeenCalled();
- });
-});
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
index 852558a83bc..c7e0d806d80 100644
--- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -61,6 +61,12 @@ describe('User Popover Component', () => {
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', () => {
@@ -117,6 +123,18 @@ describe('User Popover Component', () => {
'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', () => {
diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb
index 66af7f81535..99fe8795d91 100644
--- a/spec/lib/api/helpers/related_resources_helpers_spec.rb
+++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb
@@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do
Class.new.include(described_class).new
end
+ describe '#expose_path' do
+ let(:path) { '/api/v4/awesome_endpoint' }
+
+ context 'empty relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'slash relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'with relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq("/gitlab/root" + path)
+ end
+ end
+ end
+
describe '#expose_url' do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 08165f147bb..00916f80784 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -137,18 +137,6 @@ describe API::Helpers do
it_behaves_like 'user namespace finder'
end
- describe '#user_namespace' do
- let(:namespace_finder) do
- subject.user_namespace
- end
-
- before do
- allow(subject).to receive(:params).and_return({ id: namespace.id })
- end
-
- it_behaves_like 'user namespace finder'
- end
-
describe '#send_git_blob' do
let(:repository) { double }
let(:blob) { double(name: 'foobar') }
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 7213cd58ea7..4a9880ac85a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -58,6 +58,11 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation'
end
+ it 'removes any leading or trailing spaces' do
+ doc = filter(header(1, " \r\n\tTitle with spaces\r\n\t "))
+ expect(doc.css('h1 a').first.attr('href')).to eq '#title-with-spaces'
+ end
+
it 'appends a unique number to duplicates' do
doc = filter(header(1, 'One') + header(2, 'One'))
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index e7ff9169f1b..d3f7f1ded16 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -177,7 +177,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
end
before do
- allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation)
+ allow_any_instance_of(ActiveRecord::Relation)
.to receive(:update_all).and_raise(exception)
end
diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
index 7c7e58d6bb7..582396275ed 100644
--- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
@@ -51,7 +51,7 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201
statuses[:pending]]
end
- it 'recovers from unique constraint violation only twice' do
+ it 'recovers from unique constraint violation only twice', :quarantine do
allow(described_class::Migratable::Stage)
.to receive(:find_by).and_return(nil)
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index 1e90a2ef27f..cc09804fd53 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -105,6 +105,7 @@ describe Gitlab::BitbucketServerImport::Importer do
expect(merge_request.metrics.merged_by).to eq(project.owner)
expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp)
expect(merge_request.merge_commit_sha).to eq('12345678')
+ expect(merge_request.state_id).to eq(3)
end
it 'imports comments' do
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index 8d5ab27a17c..71b64a3b9df 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -77,117 +77,85 @@ describe Gitlab::Checks::BranchCheck do
let(:oldrev) { '0000000000000000000000000000000000000000' }
let(:ref) { 'refs/heads/feature' }
- context 'protected branch creation feature is disabled' do
+ context 'user can push to branch' do
before do
- stub_feature_flags(protected_branch_creation: false)
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .with('feature')
+ .and_return(true)
end
- context 'user is not allowed to push to protected branch' do
- before do
- allow(user_access)
- .to receive(:can_push_to_branch?)
- .and_return(false)
- end
-
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to protected branches on this project.')
- end
+ it 'does not raise an error' do
+ expect { subject.validate! }.not_to raise_error
end
+ end
- context 'user is allowed to push to protected branch' do
- before do
- allow(user_access)
- .to receive(:can_push_to_branch?)
- .and_return(true)
- end
-
- it 'does not raise an error' do
- expect { subject.validate! }.not_to raise_error
- end
+ context 'user cannot push to branch' do
+ before do
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .with('feature')
+ .and_return(false)
end
- end
- context 'protected branch creation feature is enabled' do
- context 'user can push to branch' do
+ context 'user cannot merge to branch' do
before do
allow(user_access)
- .to receive(:can_push_to_branch?)
+ .to receive(:can_merge_to_branch?)
.with('feature')
- .and_return(true)
+ .and_return(false)
end
- it 'does not raise an error' do
- expect { subject.validate! }.not_to raise_error
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.')
end
end
- context 'user cannot push to branch' do
+ context 'user can merge to branch' do
before do
allow(user_access)
- .to receive(:can_push_to_branch?)
+ .to receive(:can_merge_to_branch?)
.with('feature')
- .and_return(false)
+ .and_return(true)
+
+ allow(project.repository)
+ .to receive(:branch_names_contains_sha)
+ .with(newrev)
+ .and_return(['branch'])
end
- context 'user cannot merge to branch' do
+ context "newrev isn't in any protected branches" do
before do
- allow(user_access)
- .to receive(:can_merge_to_branch?)
- .with('feature')
+ allow(ProtectedBranch)
+ .to receive(:any_protected?)
+ .with(project, ['branch'])
.and_return(false)
end
it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.')
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.')
end
end
- context 'user can merge to branch' do
+ context 'newrev is included in a protected branch' do
before do
- allow(user_access)
- .to receive(:can_merge_to_branch?)
- .with('feature')
+ allow(ProtectedBranch)
+ .to receive(:any_protected?)
+ .with(project, ['branch'])
.and_return(true)
-
- allow(project.repository)
- .to receive(:branch_names_contains_sha)
- .with(newrev)
- .and_return(['branch'])
end
- context "newrev isn't in any protected branches" do
- before do
- allow(ProtectedBranch)
- .to receive(:any_protected?)
- .with(project, ['branch'])
- .and_return(false)
- end
+ context 'via web interface' do
+ let(:protocol) { 'web' }
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.')
+ it 'allows branch creation' do
+ expect { subject.validate! }.not_to raise_error
end
end
- context 'newrev is included in a protected branch' do
- before do
- allow(ProtectedBranch)
- .to receive(:any_protected?)
- .with(project, ['branch'])
- .and_return(true)
- end
-
- context 'via web interface' do
- let(:protocol) { 'web' }
-
- it 'allows branch creation' do
- expect { subject.validate! }.not_to raise_error
- end
- end
-
- context 'via SSH' do
- it 'raises an error' do
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.')
- end
+ context 'via SSH' do
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.')
end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb
index 35f8069c8a4..dad14e100a7 100644
--- a/spec/lib/gitlab/checks/lfs_check_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_check_spec.rb
@@ -27,6 +27,18 @@ describe Gitlab::Checks::LfsCheck do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
+ context 'with lfs_check feature disabled' do
+ before do
+ stub_feature_flags(lfs_check: false)
+ end
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
+
+ subject.validate!
+ end
+ end
+
context 'deletion' do
let(:changes) { { oldrev: oldrev, ref: ref } }
diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
index 62dcd80fad7..5387863bd07 100644
--- a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
+++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
let!(:deployment) { create(:deployment, deployable: build) }
context 'and a cluster to deploy to' do
- let(:cluster) { create(:cluster, projects: [build.project]) }
+ let(:cluster) { create(:cluster, :group) }
before do
allow(build.deployment).to receive(:cluster).and_return(cluster)
@@ -28,11 +28,23 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
it { is_expected.to be_truthy }
+ context 'and the cluster is not managed' do
+ let(:cluster) { create(:cluster, :not_managed, projects: [build.project]) }
+
+ it { is_expected.to be_falsey }
+ end
+
context 'and a namespace is already created for this project' do
let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) }
it { is_expected.to be_falsey }
end
+
+ context 'and cluster is project type' do
+ let(:cluster) { create(:cluster, :project) }
+
+ it { is_expected.to be_falsey }
+ end
end
context 'and no cluster to deploy to' do
@@ -52,7 +64,7 @@ describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
subject { described_class.new(build).complete! }
context 'completion is required' do
- let(:cluster) { create(:cluster, projects: [build.project]) }
+ let(:cluster) { create(:cluster, :group) }
before do
allow(build.deployment).to receive(:cluster).and_return(cluster)
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index e94bb44f990..0f58a4f1d44 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -270,5 +270,27 @@ describe Gitlab::Ci::Config::External::Processor do
end
end
end
+
+ context 'when config includes an external configuration file via SSL web request' do
+ before do
+ stub_request(:get, 'https://sha256.badssl.com/fake.yml').to_return(body: 'image: ruby:2.6', status: 200)
+ stub_request(:get, 'https://self-signed.badssl.com/fake.yml')
+ .to_raise(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed (self signed certificate)'))
+ end
+
+ context 'with an acceptable certificate' do
+ let(:values) { { include: 'https://sha256.badssl.com/fake.yml' } }
+
+ it { is_expected.to include(image: 'ruby:2.6') }
+ end
+
+ context 'with a self-signed certificate' do
+ let(:values) { { include: 'https://self-signed.badssl.com/fake.yml' } }
+
+ it 'returns a reportable configuration error' do
+ expect { subject }.to raise_error(described_class::IncludeError, /certificate verify failed/)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
new file mode 100644
index 00000000000..9aa2f4efd67
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('!=', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns true when left and right are not equal' do
+ allow(left).to receive(:evaluate).and_return(1)
+ allow(right).to receive(:evaluate).and_return(2)
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate(VARIABLE: 3)).to eq true
+ end
+
+ it 'returns false when left and right are equal' do
+ allow(left).to receive(:evaluate).and_return(1)
+ allow(right).to receive(:evaluate).and_return(1)
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate(VARIABLE: 3)).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
new file mode 100644
index 00000000000..fa3b9651fb4
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -0,0 +1,80 @@
+require 'fast_spec_helper'
+require_dependency 're2'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('!~', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns true when left and right do not match' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('something'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'returns false when left and right match' do
+ allow(left).to receive(:evaluate).and_return('my-awesome-string')
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'supports matching against a nil value' do
+ allow(left).to receive(:evaluate).and_return(nil)
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq true
+ end
+
+ it 'supports multiline strings' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+
+ it 'supports regexp flags' do
+ allow(left).to receive(:evaluate).and_return <<~TEXT
+ My AWESOME content
+ TEXT
+
+ allow(right).to receive(:evaluate)
+ .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate).to eq false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 11e73294f18..a9fd809409b 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -101,6 +101,18 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
"$EMPTY_VARIABLE =~ /var.*/" | false
"$UNDEFINED_VARIABLE =~ /var.*/" | false
"$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ '$PRESENT_VARIABLE != "my variable"' | false
+ '"my variable" != $PRESENT_VARIABLE' | false
+ '$PRESENT_VARIABLE != null' | true
+ '$EMPTY_VARIABLE != null' | true
+ '"" != $EMPTY_VARIABLE' | false
+ '$UNDEFINED_VARIABLE != null' | false
+ 'null != $UNDEFINED_VARIABLE' | false
+ "$PRESENT_VARIABLE !~ /var.*e$/" | false
+ "$PRESENT_VARIABLE !~ /^var.*/" | true
+ "$EMPTY_VARIABLE !~ /var.*/" | true
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
+ "$PRESENT_VARIABLE !~ /VAR.*/i" | false
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index b6231510b91..025439f1b6e 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -163,11 +163,11 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
- .to eq [Gitlab::Ci::Status::Build::Canceled, Gitlab::Ci::Status::Build::Retryable]
+ .to eq [Gitlab::Ci::Status::Build::Canceled]
end
- it 'fabricates a retryable build status' do
- expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
+ it 'does not fabricate a retryable build status' do
+ expect(status).not_to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
@@ -177,7 +177,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.illustration).to include(:image, :size, :title)
expect(status.label).to eq 'canceled'
expect(status).to have_details
- expect(status).to have_action
+ expect(status).not_to have_action
end
end
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
index af03d5a1308..2a5915d75d0 100644
--- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do
describe '#group' do
it 'returns status failed with warnings status group' do
- expect(subject.group).to eq 'failed_with_warnings'
+ expect(subject.group).to eq 'failed-with-warnings'
end
end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index dee4f4efd1b..4b299170c87 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
context 'when stage has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ (HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status|
context "when core status is #{core_status}" do
before do
create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
@@ -64,4 +64,20 @@ describe Gitlab::Ci::Status::Stage::Factory do
expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
end
end
+
+ context 'when stage has manual builds' do
+ (HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status|
+ context "when status is #{core_status}" do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:ci_build, pipeline: pipeline, stage: 'build', status: :manual)
+ end
+
+ it 'fabricates a play manual status' do
+ expect(status).to be_a(Gitlab::Ci::Status::Stage::PlayManual)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
new file mode 100644
index 00000000000..b0113b00ef0
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::PlayManual do
+ let(:stage) { double('stage') }
+ let(:play_manual) { described_class.new(stage) }
+
+ describe '#action_icon' do
+ subject { play_manual.action_icon }
+
+ it { is_expected.to eq('play') }
+ end
+
+ describe '#action_button_title' do
+ subject { play_manual.action_button_title }
+
+ it { is_expected.to eq('Play all manual') }
+ end
+
+ describe '#action_title' do
+ subject { play_manual.action_title }
+
+ it { is_expected.to eq('Play all manual') }
+ end
+
+ describe '#action_path' do
+ let(:stage) { create(:ci_stage_entity, status: 'manual') }
+ let(:pipeline) { stage.pipeline }
+ let(:play_manual) { stage.detailed_status(create(:user)) }
+
+ subject { play_manual.action_path }
+
+ it { is_expected.to eq("/#{pipeline.project.full_path}/pipelines/#{pipeline.id}/stages/#{stage.name}/play_manual") }
+ end
+
+ describe '#action_method' do
+ subject { play_manual.action_method }
+
+ it { is_expected.to eq(:post) }
+ end
+
+ describe '.matches?' do
+ let(:user) { double('user') }
+
+ subject { described_class.matches?(stage, user) }
+
+ context 'when stage is skipped' do
+ let(:stage) { create(:ci_stage_entity, status: :skipped) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is manual' do
+ let(:stage) { create(:ci_stage_entity, status: :manual) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is scheduled' do
+ let(:stage) { create(:ci_stage_entity, status: :scheduled) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is success' do
+ let(:stage) { create(:ci_stage_entity, status: :success) }
+
+ context 'and does not have manual builds' do
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 6d05545d1d8..9493b1d89f2 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#group' do
- it { expect(subject.group).to eq 'success_with_warnings' }
+ it { expect(subject.group).to eq 'success-with-warnings' }
end
describe '.matches?' do
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 3ff2fe18c15..613814df23f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -137,19 +137,5 @@ describe Gitlab::Ci::Variables::Collection::Item do
.to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false)
end
end
-
- context 'when variable masking is disabled' do
- before do
- stub_feature_flags(variable_masking: false)
- end
-
- it 'does not expose the masked field to the runner' do
- runner_variable = described_class
- .new(key: 'VAR', value: 'value', masked: true)
- .to_runner_variable
-
- expect(runner_variable).to eq(key: 'VAR', value: 'value', public: true)
- end
- end
end
end
diff --git a/spec/lib/gitlab/correlation_id_spec.rb b/spec/lib/gitlab/correlation_id_spec.rb
deleted file mode 100644
index 584d1f48386..00000000000
--- a/spec/lib/gitlab/correlation_id_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::CorrelationId do
- describe '.use_id' do
- it 'yields when executed' do
- expect { |blk| described_class.use_id('id', &blk) }.to yield_control
- end
-
- it 'stacks correlation ids' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do |current_id|
- expect(current_id).to eq('id2')
- end
- end
- end
-
- it 'for missing correlation id it generates random one' do
- described_class.use_id('id1') do
- described_class.use_id(nil) do |current_id|
- expect(current_id).not_to be_empty
- expect(current_id).not_to eq('id1')
- end
- end
- end
- end
-
- describe '.current_id' do
- subject { described_class.current_id }
-
- it 'returns last correlation id' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do
- is_expected.to eq('id2')
- end
- end
- end
- end
-
- describe '.current_or_new_id' do
- subject { described_class.current_or_new_id }
-
- context 'when correlation id is set' do
- it 'returns last correlation id' do
- described_class.use_id('id1') do
- is_expected.to eq('id1')
- end
- end
- end
-
- context 'when correlation id is missing' do
- it 'returns a new correlation id' do
- expect(described_class).to receive(:new_id)
- .and_call_original
-
- is_expected.not_to be_empty
- end
- end
- end
-
- describe '.ids' do
- subject { described_class.send(:ids) }
-
- it 'returns empty list if not correlation is used' do
- is_expected.to be_empty
- end
-
- it 'returns list if correlation ids are used' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do
- is_expected.to eq(%w(id1 id2))
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 66cd8171c12..32b90041c64 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -191,9 +191,8 @@ describe Gitlab::Danger::Helper do
expect(helper.changes_by_category).to eq(
backend: %w[foo.rb],
database: %w[db/foo],
- docs: %w[foo.md],
frontend: %w[foo.js],
- none: %w[ee/changelogs/foo.yml],
+ none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo],
unknown: %w[foo]
)
@@ -202,13 +201,13 @@ describe Gitlab::Danger::Helper do
describe '#category_for_file' do
where(:path, :expected_category) do
- 'doc/foo' | :docs
- 'CONTRIBUTING.md' | :docs
- 'LICENSE' | :docs
- 'MAINTENANCE.md' | :docs
- 'PHILOSOPHY.md' | :docs
- 'PROCESS.md' | :docs
- 'README.md' | :docs
+ 'doc/foo' | :none
+ 'CONTRIBUTING.md' | :none
+ 'LICENSE' | :none
+ 'MAINTENANCE.md' | :none
+ 'PHILOSOPHY.md' | :none
+ 'PROCESS.md' | :none
+ 'README.md' | :none
'ee/doc/foo' | :unknown
'ee/README' | :unknown
@@ -272,8 +271,8 @@ describe Gitlab::Danger::Helper do
'foo/bar.rb' | :backend
'foo/bar.js' | :frontend
- 'foo/bar.txt' | :docs
- 'foo/bar.md' | :docs
+ 'foo/bar.txt' | :none
+ 'foo/bar.md' | :none
end
with_them do
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
new file mode 100644
index 00000000000..0a6e2302b09
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Deployment do
+ describe '.build' do
+ it 'returns the object kind for a deployment' do
+ deployment = build(:deployment)
+
+ data = described_class.build(deployment)
+
+ expect(data[:object_kind]).to eq('deployment')
+ end
+
+ it 'returns data for the given build' do
+ environment = create(:environment, name: "somewhere")
+ project = create(:project, :repository, name: 'myproj')
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
+ deployable = deployment.deployable
+ expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
+ expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user)
+ expected_commit_url = Gitlab::UrlBuilder.build(commit)
+
+ data = described_class.build(deployment)
+
+ expect(data[:status]).to eq('failed')
+ expect(data[:deployable_id]).to eq(deployable.id)
+ expect(data[:deployable_url]).to eq(expected_deployable_url)
+ expect(data[:environment]).to eq("somewhere")
+ expect(data[:project]).to eq(project.hook_attrs)
+ expect(data[:short_sha]).to eq(deployment.short_sha)
+ expect(data[:user]).to eq(deployment.user.hook_attrs)
+ expect(data[:user_url]).to eq(expected_user_url)
+ expect(data[:commit_url]).to eq(expected_commit_url)
+ expect(data[:commit_title]).to eq(commit.title)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 0c4decc6518..46ad674a1eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do
describe '.build' do
let(:data) do
- described_class.build(project, user, Gitlab::Git::BLANK_SHA,
- '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
- 'refs/tags/v1.1.0')
+ described_class.build(
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
+ ref: 'refs/tags/v1.1.0')
end
it { expect(data).to be_a(Hash) }
@@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do
include_examples 'deprecated repository hook data'
it 'does not raise an error when given nil commits' do
- expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) }
+ expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) }
.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
index fe26ebb8796..15ee8c40b55 100644
--- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
@@ -3,31 +3,32 @@
require 'spec_helper'
describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
+ def fake_file(offset)
+ {
+ text: 'foo',
+ type: 'new',
+ index: 2 + offset,
+ old_pos: 10 + offset,
+ new_pos: 11 + offset,
+ line_code: 'xpto',
+ rich_text: '<blips>blops</blips>'
+ }
+ end
+
+ let(:mapping) do
+ {
+ 3 => [
+ fake_file(0),
+ fake_file(1)
+ ],
+ 4 => [
+ fake_file(2)
+ ]
+ }
+ end
+
describe '#write_multiple' do
it 'sets multiple keys serializing content as JSON' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 10,
- new_pos: 11,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- },
- {
- text: 'foo',
- type: 'new',
- index: 3,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blops>blips</blops>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
mapping.each do |key, value|
@@ -41,53 +42,16 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
describe '#read_multiple' do
it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- },
- {
- text: 'foo',
- type: 'new',
- index: 3,
- old_pos: 10,
- new_pos: 11,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
found = described_class.read_multiple(mapping.keys)
- expect(found.size).to eq(1)
+ expect(found.size).to eq(2)
expect(found.first.size).to eq(2)
expect(found.first).to all(be_a(Gitlab::Diff::Line))
end
it 'returns nil when cached key is not found' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
found = described_class.read_multiple([2, 3])
@@ -95,8 +59,30 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
expect(found.size).to eq(2)
expect(found.first).to eq(nil)
- expect(found.second.size).to eq(1)
+ expect(found.second.size).to eq(2)
expect(found.second).to all(be_a(Gitlab::Diff::Line))
end
end
+
+ describe '#clear_multiple' do
+ it 'removes all named keys' do
+ described_class.write_multiple(mapping)
+
+ described_class.clear_multiple(mapping.keys)
+
+ expect(described_class.read_multiple(mapping.keys)).to all(be_nil)
+ end
+
+ it 'only removed named keys' do
+ to_clear, to_leave = mapping.keys
+
+ described_class.write_multiple(mapping)
+ described_class.clear_multiple([to_clear])
+
+ cleared, left = described_class.read_multiple([to_clear, to_leave])
+
+ expect(cleared).to be_nil
+ expect(left).to all(be_a(Gitlab::Diff::Line))
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 10bc82e24d1..1c24244c3a6 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -341,7 +341,7 @@ describe Gitlab::Git::Blob, :seed_helper do
it { expect(blob.mode).to eq("100755") }
end
- context 'file with Chinese text' do
+ context 'file with Japanese text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
it { expect(blob.name).to eq("テスト.txt") }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 507bf222810..25052a79916 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -380,7 +380,32 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- describe '#batch_by_oid' do
+ shared_examples '.batch_by_oid' do
+ context 'with multiple OIDs' do
+ let(:oids) { [SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID] }
+
+ it 'returns multiple commits' do
+ commits = described_class.batch_by_oid(repository, oids)
+
+ expect(commits.count).to eq(2)
+ expect(commits).to all( be_a(Gitlab::Git::Commit) )
+ expect(commits.first.sha).to eq(SeedRepo::Commit::ID)
+ expect(commits.second.sha).to eq(SeedRepo::FirstCommit::ID)
+ end
+ end
+
+ context 'when oids is empty' do
+ it 'returns empty commits' do
+ commits = described_class.batch_by_oid(repository, [])
+
+ expect(commits.count).to eq(0)
+ end
+ end
+ end
+
+ describe '.batch_by_oid with Gitaly enabled' do
+ it_should_behave_like '.batch_by_oid'
+
context 'when oids is empty' do
it 'makes no Gitaly request' do
expect(Gitlab::GitalyClient).not_to receive(:call)
@@ -390,6 +415,16 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
+ describe '.batch_by_oid with Rugged enabled', :enable_rugged do
+ it_should_behave_like '.batch_by_oid'
+
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+
+ described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
+ end
+ end
+
shared_examples 'extracting commit signature' do
context 'when the commit is signed' do
let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index 0d5069568e1..ebeb7b7b633 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Git::ObjectPool do
+ include RepoHelpers
+
let(:pool_repository) { create(:pool_repository) }
let(:source_repository) { pool_repository.source_project.repository }
@@ -76,4 +78,43 @@ describe Gitlab::Git::ObjectPool do
end
end
end
+
+ describe '#fetch' do
+ let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) }
+ let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) }
+ let(:commit_count) { source_repository.commit_count }
+
+ context "when the object's pool repository exists" do
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+ end
+
+ context "when the object's pool repository does not exist" do
+ before do
+ subject.delete
+ end
+
+ it "re-creates the object pool's repository" do
+ subject.fetch
+
+ expect(subject.repository.exists?).to be true
+ end
+
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+
+ it 'fetches objects from the source repository' do
+ new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid
+
+ expect(subject.repository.exists?).to be false
+
+ subject.fetch
+
+ expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count)
+ expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/repository_cleaner_spec.rb b/spec/lib/gitlab/git/repository_cleaner_spec.rb
index 6602f22843f..7bba0107e58 100644
--- a/spec/lib/gitlab/git/repository_cleaner_spec.rb
+++ b/spec/lib/gitlab/git/repository_cleaner_spec.rb
@@ -6,55 +6,62 @@ describe Gitlab::Git::RepositoryCleaner do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:head_sha) { repository.head_commit.id }
- let(:object_map_data) { "#{head_sha} #{'0' * 40}" }
+ let(:object_map_data) { "#{head_sha} #{Gitlab::Git::BLANK_SHA}" }
- subject(:cleaner) { described_class.new(repository.raw) }
+ let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] }
+ let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] }
- describe '#apply_bfg_object_map' do
- let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] }
- let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] }
+ subject(:cleaner) { described_class.new(repository.raw) }
+ shared_examples_for '#apply_bfg_object_map_stream' do
before do
(clean_refs + keep_refs).each { |ref| repository.create_ref(head_sha, ref) }
end
- context 'from StringIO' do
- let(:object_map) { StringIO.new(object_map_data) }
+ it 'removes internal references' do
+ entries = []
- it 'removes internal references' do
- cleaner.apply_bfg_object_map(object_map)
+ cleaner.apply_bfg_object_map_stream(object_map) do |rsp|
+ entries.concat(rsp.entries)
+ end
- aggregate_failures do
- clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy }
- keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy }
- end
+ aggregate_failures do
+ clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(false) }
+ keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(true) }
+
+ expect(entries).to contain_exactly(
+ Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new(
+ type: :COMMIT,
+ old_oid: head_sha,
+ new_oid: Gitlab::Git::BLANK_SHA
+ )
+ )
end
end
+ end
- context 'from Gitlab::HttpIO' do
- let(:url) { 'http://example.com/bfg_object_map.txt' }
- let(:tempfile) { Tempfile.new }
- let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) }
+ describe '#apply_bfg_object_map_stream (from StringIO)' do
+ let(:object_map) { StringIO.new(object_map_data) }
- around do |example|
- tempfile.write(object_map_data)
- tempfile.close
+ include_examples '#apply_bfg_object_map_stream'
+ end
- example.run
- ensure
- tempfile.unlink
- end
+ describe '#apply_bfg_object_map_stream (from Gitlab::HttpIO)' do
+ let(:url) { 'http://example.com/bfg_object_map.txt' }
+ let(:tempfile) { Tempfile.new }
+ let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) }
- it 'removes internal references' do
- stub_remote_url_200(url, tempfile.path)
+ around do |example|
+ tempfile.write(object_map_data)
+ tempfile.close
- cleaner.apply_bfg_object_map(object_map)
+ stub_remote_url_200(url, tempfile.path)
- aggregate_failures do
- clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy }
- keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy }
- end
- end
+ example.run
+ ensure
+ tempfile.unlink
end
+
+ include_examples '#apply_bfg_object_map_stream'
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 088f8acf554..7644d83992f 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Repository, :seed_helper do
include Gitlab::EncodingHelper
+ include RepoHelpers
using RSpec::Parameterized::TableSyntax
shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method|
@@ -95,6 +96,12 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#create_repository' do
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do
+ subject { repository.create_repository }
+ end
+ end
+
describe '#branch_names' do
subject { repository.branch_names }
@@ -152,13 +159,14 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:append_sha) { true }
let(:ref) { 'master' }
let(:format) { nil }
+ let(:path) { nil }
let(:expected_extension) { 'tar.gz' }
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) }
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
it 'sets CommitId to the commit SHA' do
expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
@@ -176,6 +184,14 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(metadata['ArchivePath']).to eq(expected_path)
end
+ context 'path is set' do
+ let(:path) { 'foo/bar' }
+
+ it 'appends the path to the prefix' do
+ expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar")
+ end
+ end
+
context 'append_sha varies archive path and filename' do
where(:append_sha, :ref, :expected_prefix) do
sha = SeedRepo::LastCommit::ID
@@ -1957,6 +1973,70 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#compare_source_branch' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') }
+
+ context 'within same repository' do
+ it 'does not create a temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', repository, 'feature', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+
+ it 'returns empty commits when source ref does not exist' do
+ compare = repository.compare_source_branch('master', repository, 'non-existent-branch', straight: false)
+
+ expect(compare.commits).to be_empty
+ end
+ end
+
+ context 'with different repositories' do
+ context 'when ref is known by source repo, but not by target' do
+ before do
+ mutable_repository.write_ref('another-branch', 'feature')
+ end
+
+ it 'creates temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+ end
+
+ context 'when ref is known by source and target repos' do
+ before do
+ mutable_repository.write_ref('another-branch', 'feature')
+ repository.write_ref('another-branch', 'feature')
+ end
+
+ it 'does not create a temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+ end
+
+ context 'when ref is unknown by source repo' do
+ it 'returns nil when source ref does not exist' do
+ expect(repository).to receive(:fetch_source_branch!).and_call_original
+ expect(repository).to receive(:delete_refs).and_call_original
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'non-existent-branch', straight: false)
+ expect(compare).to be_nil
+ end
+ end
+ end
+ end
+
describe '#checksum' do
it 'calculates the checksum for non-empty repo' do
expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059'
@@ -2130,86 +2210,48 @@ describe Gitlab::Git::Repository, :seed_helper do
repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
- # Build the options hash that's passed to Rugged::Commit#create
- def commit_options(repo, index, target, ref, message)
- options = {}
- options[:tree] = index.write_tree(repo)
- options[:author] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:committer] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:message] ||= message
- options[:parents] = repo.empty? ? [] : [target].compact
- options[:update_ref] = ref
-
- options
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of CHANGELOG with a single new line of text.
- def new_commit_edit_old_file(repo)
- oid = repo.write("I replaced the changelog with this text", :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
-
- options = commit_options(
- repo,
- index,
- repo.head.target,
- "HEAD",
- "Edit CHANGELOG in its original location"
- )
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of the specified file_path with new text.
- def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
- oid = repo.write(text, :blob)
- index = repo.index
- index.read_tree(branch.target.tree)
- index.add(path: file_path, oid: oid, mode: 0100644)
- options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of encoding/CHANGELOG with new text.
- def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
- branch = repo.branches[branch_name]
- new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
- # CHANGELOG file to the encoding/ directory.
- def new_commit_move_file(repo)
- blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
- file_content = repo.lookup(blob_oid).content
- oid = repo.write(file_content, :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
- index.remove("CHANGELOG")
-
- options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
def refs(dir)
IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
line.split("\t").last
end
end
+
+ describe '#disconnect_alternates' do
+ let(:project) { create(:project, :repository) }
+ let(:pool_repository) { create(:pool_repository) }
+ let(:repository) { project.repository }
+ let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
+ let(:object_pool) { pool_repository.object_pool }
+ let(:object_pool_path) { File.join(TestEnv.repos_path, object_pool.repository.relative_path) }
+ let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) }
+
+ before do
+ object_pool.create
+ end
+
+ it 'does not raise an error when disconnecting a non-linked repository' do
+ expect { repository.disconnect_alternates }.not_to raise_error
+ end
+
+ it 'removes the alternates file' do
+ object_pool.link(repository)
+
+ alternates_file = File.join(repository_path, "objects", "info", "alternates")
+ expect(File.exist?(alternates_file)).to be_truthy
+
+ repository.disconnect_alternates
+
+ expect(File.exist?(alternates_file)).to be_falsey
+ end
+
+ it 'can still access objects in the object pool' do
+ object_pool.link(repository)
+ new_commit = new_commit_edit_old_file(object_pool_rugged)
+ expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid)
+
+ repository.disconnect_alternates
+
+ expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid)
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index ded5d7576df..1e577392949 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -21,13 +21,13 @@ describe Gitlab::Git::Wiki do
end
it 'returns all the pages' do
- expect(subject.pages.count).to eq(2)
- expect(subject.pages.first.title).to eq 'page1'
- expect(subject.pages.last.title).to eq 'page2'
+ expect(subject.list_pages.count).to eq(2)
+ expect(subject.list_pages.first.title).to eq 'page1'
+ expect(subject.list_pages.last.title).to eq 'page2'
end
it 'returns only one page' do
- pages = subject.pages(limit: 1)
+ pages = subject.list_pages(limit: 1)
expect(pages.count).to eq(1)
expect(pages.first.title).to eq 'page1'
@@ -62,8 +62,8 @@ describe Gitlab::Git::Wiki do
subject.delete_page('*', commit_details('whatever'))
- expect(subject.pages.count).to eq 1
- expect(subject.pages.first.title).to eq 'page1'
+ expect(subject.list_pages.count).to eq 1
+ expect(subject.list_pages.first.title).to eq 'page1'
end
end
@@ -87,7 +87,7 @@ describe Gitlab::Git::Wiki do
with_them do
subject { wiki.preview_slug(title, format) }
- let(:gitaly_slug) { wiki.pages.first }
+ let(:gitaly_slug) { wiki.list_pages.first }
it { is_expected.to eq(expected_slug) }
@@ -96,7 +96,7 @@ describe Gitlab::Git::Wiki do
create_page(title, 'content', format: format)
- gitaly_slug = wiki.pages.first.url_path
+ gitaly_slug = wiki.list_pages.first.url_path
is_expected.to eq(gitaly_slug)
end
diff --git a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
index 369deff732a..c42332dc27b 100644
--- a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
@@ -6,14 +6,14 @@ describe Gitlab::GitalyClient::CleanupService do
let(:relative_path) { project.disk_path + '.git' }
let(:client) { described_class.new(project.repository) }
- describe '#apply_bfg_object_map' do
- it 'sends an apply_bfg_object_map message' do
+ describe '#apply_bfg_object_map_stream' do
+ it 'sends an apply_bfg_object_map_stream message' do
expect_any_instance_of(Gitaly::CleanupService::Stub)
- .to receive(:apply_bfg_object_map)
+ .to receive(:apply_bfg_object_map_stream)
.with(kind_of(Enumerator), kind_of(Hash))
- .and_return(double)
+ .and_return([])
- client.apply_bfg_object_map(StringIO.new)
+ client.apply_bfg_object_map_stream(StringIO.new)
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
index 149b7ec5bb0..0e0c3d329b5 100644
--- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do
end
end
end
+
+ describe '#fetch' do
+ before do
+ subject.delete
+ end
+
+ it 'restores the pool repository objects' do
+ subject.fetch(project.repository)
+
+ expect(object_pool.repository.exists?).to be(true)
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ subject.delete
+
+ expect { subject.fetch(project.repository) }.not_to raise_error
+ end
+ 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 0dab39575b9..0bb6e582159 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -165,4 +165,15 @@ describe Gitlab::GitalyClient::RefService do
client.delete_refs(except_with_prefixes: prefixes)
end
end
+
+ describe '#pack_refs' do
+ it 'sends a pack_refs message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:pack_refs)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:pack_refs_response))
+
+ client.pack_refs
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 46ca2340389..09de7ca6afd 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -231,4 +231,34 @@ describe Gitlab::GitalyClient::RepositoryService do
client.raw_changes_between('deadbeef', 'deadpork')
end
end
+
+ describe '#disconnect_alternates' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
+ let(:pool_repository) { create(:pool_repository) }
+ let(:object_pool) { pool_repository.object_pool }
+ let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) }
+
+ before do
+ object_pool_service.create(repository)
+ object_pool_service.link_repository(repository)
+ end
+
+ it 'deletes the alternates file' do
+ repository.disconnect_alternates
+
+ alternates_file = File.join(repository_path, "objects", "info", "alternates")
+
+ expect(File.exist?(alternates_file)).to be_falsey
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ repository.disconnect_alternates
+
+ expect { repository.disconnect_alternates }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
index d82c9c28da0..4fa8e97aca0 100644
--- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::GitalyClient::WikiService do
end
end
- describe '#get_all_pages' do
+ describe '#load_all_pages' do
let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
let(:response) do
[
@@ -63,7 +63,7 @@ describe Gitlab::GitalyClient::WikiService do
let(:wiki_page_2) { subject[1].first }
let(:wiki_page_2_version) { subject[1].last }
- subject { client.get_all_pages }
+ subject { client.load_all_pages }
it 'sends a wiki_get_all_pages message' do
expect_any_instance_of(Gitaly::WikiService::Stub)
@@ -99,7 +99,7 @@ describe Gitlab::GitalyClient::WikiService do
end
context 'with limits' do
- subject { client.get_all_pages(limit: 1) }
+ subject { client.load_all_pages(limit: 1) }
it 'sends a request with the limit' do
expect_any_instance_of(Gitaly::WikiService::Stub)
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index f1acb1d9bc4..da1eb0c2618 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -142,6 +142,48 @@ describe Gitlab::GitalyClient do
end
end
+ describe '.request_kwargs' do
+ context 'when catfile-cache feature is enabled' do
+ before do
+ stub_feature_flags('gitaly_catfile-cache': true)
+ end
+
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
+ end
+ end
+ end
+
+ context 'when catfile-cache feature is disabled' do
+ before do
+ stub_feature_flags({ 'gitaly_catfile-cache': false })
+ end
+
+ it 'does not set the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).not_to include('gitaly-session-id')
+ end
+ end
+ end
+
describe 'enforce_gitaly_request_limits?' do
def call_gitaly(count = 1)
(1..count).each 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 7901ae005d9..dab5767ece1 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -98,6 +98,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
description: 'This is my issue',
milestone_id: milestone.id,
state: :opened,
+ state_id: 1,
created_at: created_at,
updated_at: updated_at
},
@@ -127,6 +128,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
description: "*Created by: alice*\n\nThis is my issue",
milestone_id: milestone.id,
state: :opened,
+ state_id: 1,
created_at: created_at,
updated_at: updated_at
},
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 2e4a7c36fb8..6d614c6527a 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
@@ -93,6 +93,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
assignee_id: user.id,
@@ -138,6 +139,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: project.creator_id,
assignee_id: user.id,
@@ -184,6 +186,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'master-42',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
assignee_id: user.id,
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
index 6114aca0616..aec9c4baf0a 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -5,92 +5,118 @@ require 'spec_helper'
# Also see spec/graphql/features/authorization_spec.rb for
# integration tests of AuthorizeFieldService
describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
- describe '#build_checker' do
- let(:current_user) { double(:current_user) }
- let(:abilities) { [double(:first_ability), double(:last_ability)] }
-
- context 'when authorizing against the object' do
- let(:checker) do
- service = described_class.new(double(resolve_proc: proc {}))
- allow(service).to receive(:authorizations).and_return(abilities)
- service.__send__(:build_checker, current_user, nil)
- end
+ def type(type_authorizations = [])
+ Class.new(Types::BaseObject) do
+ graphql_name "TestType"
- it 'returns a checker which checks for a single object' do
- object = double(:object)
+ authorize type_authorizations
+ end
+ end
- abilities.each do |ability|
- spy_ability_check_for(ability, object, passed: true)
- end
+ def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value")
+ Class.new(Types::BaseObject) do
+ graphql_name "TestTypeWithField"
+ field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value}
+ end
+ end
- expect(checker.call(object)).to eq(object)
- end
+ let(:current_user) { double(:current_user) }
+ subject(:service) { described_class.new(field) }
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
+ describe "#authorized_resolve" do
+ let(:presented_object) { double("presented object") }
+ let(:presented_type) { double("parent type", object: presented_object) }
+ subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, object, passed: true)
+ context "scalar types" do
+ shared_examples "checking permissions on the presented object" do
+ it "checks the abilities on the object being presented and returns the value" do
+ expected_permissions.each do |permission|
+ spy_ability_check_for(permission, presented_object, passed: true)
end
+
+ expect(resolved).to eq("Resolved value")
end
- expect(checker.call(objects)).to eq(objects)
+ it "returns nil if the value wasn't authorized" do
+ allow(Ability).to receive(:allowed?).and_return false
+
+ expect(resolved).to be_nil
+ end
end
- context 'when some objects would not pass the check' do
- it 'returns nil when it is single object' do
- disallowed = double(:object)
+ context "when the field is a built-in scalar type" do
+ let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
- spy_ability_check_for(abilities.first, disallowed, passed: false)
+ it_behaves_like "checking permissions on the presented object"
+ end
- expect(checker.call(disallowed)).to be_nil
- end
+ context "when the field is a list of scalar types" do
+ let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
+
+ it_behaves_like "checking permissions on the presented object"
+ end
- it 'returns only objects which passed when there are more than one' do
- allowed = double(:allowed)
- disallowed = double(:disallowed)
+ context "when the field is sub-classed scalar type" do
+ let(:field) { type_with_field(Types::TimeType, :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
- spy_ability_check_for(abilities.first, disallowed, passed: false)
+ it_behaves_like "checking permissions on the presented object"
+ end
- abilities.each do |ability|
- spy_ability_check_for(ability, allowed, passed: true)
- end
+ context "when the field is a list of sub-classed scalar types" do
+ let(:field) { type_with_field([Types::TimeType], :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
- expect(checker.call([disallowed, allowed])).to contain_exactly(allowed)
- end
+ it_behaves_like "checking permissions on the presented object"
end
end
- context 'when authorizing against another object' do
- let(:authorizing_obj) { double(:object) }
+ context "when the field is a specific type" do
+ let(:custom_type) { type(:read_type) }
+ let(:object_in_field) { double("presented in field") }
+ let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql }
- let(:checker) do
- service = described_class.new(double(resolve_proc: proc {}))
- allow(service).to receive(:authorizations).and_return(abilities)
- service.__send__(:build_checker, current_user, authorizing_obj)
- end
+ it "checks both field & type permissions" do
+ spy_ability_check_for(:read_field, object_in_field, passed: true)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
- it 'returns a checker which checks for a single object' do
- object = double(:object)
+ expect(resolved).to eq(object_in_field)
+ end
- abilities.each do |ability|
- spy_ability_check_for(ability, authorizing_obj, passed: true)
- end
+ it "returns nil if viewing was not allowed" do
+ spy_ability_check_for(:read_field, object_in_field, passed: false)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
- expect(checker.call(object)).to eq(object)
+ expect(resolved).to be_nil
end
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
+ context "when the field is a list" do
+ let(:object_1) { double("presented in field 1") }
+ let(:object_2) { double("presented in field 2") }
+ let(:presented_types) { [double(object: object_1), double(object: object_2)] }
+ let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, authorizing_obj, passed: true)
- end
+ it "checks all permissions" do
+ allow(Ability).to receive(:allowed?) { true }
+
+ spy_ability_check_for(:read_field, object_1, passed: true)
+ spy_ability_check_for(:read_type, object_1, passed: true)
+ spy_ability_check_for(:read_field, object_2, passed: true)
+ spy_ability_check_for(:read_type, object_2, passed: true)
+
+ expect(resolved).to eq(presented_types)
end
- expect(checker.call(objects)).to eq(objects)
+ it "filters out objects that the user cannot see" do
+ allow(Ability).to receive(:allowed?) { true }
+
+ spy_ability_check_for(:read_type, object_1, passed: false)
+
+ expect(resolved.map(&:object)).to contain_exactly(object_2)
+ 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
index 9bcc1e78a78..fefa2881b18 100644
--- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
@@ -85,6 +85,11 @@ describe Gitlab::Graphql::Connections::KeysetConnection 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 } }
diff --git a/spec/lib/gitlab/graphql/generic_tracing_spec.rb b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
new file mode 100644
index 00000000000..ae92dcc40af
--- /dev/null
+++ b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::GenericTracing do
+ let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') }
+
+ it 'updates graphql histogram with expected labels' do
+ query = 'query { users { id } }'
+ tracer = described_class.new
+
+ allow(tracer)
+ .to receive(:graphql_duration_seconds)
+ .and_return(graphql_duration_seconds_histogram)
+
+ expect_metric('graphql.lex', 'lex')
+ expect_metric('graphql.parse', 'parse')
+ expect_metric('graphql.validate', 'validate')
+ expect_metric('graphql.analyze', 'analyze_multiplex')
+ expect_metric('graphql.execute', 'execute_query_lazy')
+ expect_metric('graphql.execute', 'execute_multiplex')
+
+ GitlabSchema.execute(query, context: { tracers: [tracer] })
+ end
+
+ context "when labkit tracing is enabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(true)
+ end
+
+ it 'yields with labkit tracing' do
+ expected_tags = {
+ 'component' => 'web',
+ 'span.kind' => 'server',
+ 'platform_key' => 'pkey',
+ 'key' => 'key'
+ }
+
+ expect(Labkit::Tracing)
+ .to receive(:with_tracing)
+ .with(operation_name: "pkey.key", tags: expected_tags)
+ .and_yield
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ end
+
+ context "when labkit tracing is disabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(false)
+ end
+
+ it 'yields without measurement' do
+ expect(Labkit::Tracing).not_to receive(:with_tracing)
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ end
+
+ private
+
+ def expect_metric(platform_key, key)
+ expect(graphql_duration_seconds_histogram)
+ .to receive(:observe)
+ .with({ platform_key: platform_key, key: key }, be > 0.0)
+ end
+end
diff --git a/spec/lib/gitlab/graphql/tracing_spec.rb b/spec/lib/gitlab/graphql/tracing_spec.rb
deleted file mode 100644
index 7300a9a572e..00000000000
--- a/spec/lib/gitlab/graphql/tracing_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Graphql::Tracing do
- let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') }
-
- it 'updates graphql histogram with expected labels' do
- query = 'query { users { id } }'
- tracer = described_class.new
-
- allow(tracer)
- .to receive(:graphql_duration_seconds)
- .and_return(graphql_duration_seconds_histogram)
-
- expect_metric('graphql.lex', 'lex')
- expect_metric('graphql.parse', 'parse')
- expect_metric('graphql.validate', 'validate')
- expect_metric('graphql.analyze', 'analyze_multiplex')
- expect_metric('graphql.execute', 'execute_query_lazy')
- expect_metric('graphql.execute', 'execute_multiplex')
-
- GitlabSchema.execute(query, context: { tracers: [tracer] })
- end
-
- private
-
- def expect_metric(platform_key, key)
- expect(graphql_duration_seconds_histogram)
- .to receive(:observe)
- .with({ platform_key: platform_key, key: key }, be > 0.0)
- end
-end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index ec17ad8541f..7c4ac62790e 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -32,5 +32,17 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
strategy.execute(user, project)
end
+
+ context 'when upload fails' do
+ it 'stores the export error' do
+ stub_request(:post, example_url).to_return(status: [404, 'Page not found'])
+
+ strategy.execute(user, project)
+
+ errors = project.import_export_shared.errors
+ expect(errors).not_to be_empty
+ expect(errors.first).to eq "Error uploading the project. Code 404: Page not found"
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 54369ff75f4..2242543daad 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -99,7 +99,7 @@ merge_requests:
- timelogs
- head_pipeline
- latest_merge_request_diff
-- merge_request_pipelines
+- pipelines_for_merge_request
- merge_request_assignees
- suggestions
- assignees
@@ -322,6 +322,7 @@ project:
- pool_repository
- kubernetes_namespaces
- error_tracking_setting
+- metrics_setting
award_emoji:
- awardable
- user
@@ -360,3 +361,5 @@ error_tracking_setting:
- project
suggestions:
- note
+metrics_setting:
+- project
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 87ab81d8169..ddfbb020a55 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -29,7 +29,7 @@ describe 'Import/Export attribute configuration' do
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
- relation_attributes = relation_class.new.attributes.keys
+ relation_attributes = relation_class.new.attributes.keys - relation_class.encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index ebb62124cb1..9093d21647a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -423,6 +423,7 @@ Service:
- wiki_page_events
- confidential_issues_events
- confidential_note_events
+- deployment_events
ProjectHook:
- id
- url
@@ -606,7 +607,6 @@ ResourceLabelEvent:
- user_id
- created_at
ErrorTracking::ProjectErrorTrackingSetting:
-- id
- api_url
- project_id
- project_name
@@ -626,3 +626,8 @@ MergeRequestAssignee:
- id
- user_id
- merge_request_id
+ProjectMetricsSetting:
+- project_id
+- external_dashboard_url
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index cff7dd58c8c..d3d9fe9948a 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::JsonLogger do
describe '#format_message' do
before do
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
end
it 'formats strings' do
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 8433d40b2ea..24ce397ec3d 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -33,6 +33,28 @@ describe Gitlab::Kubernetes::Helm::Api do
end
end
+ describe '#uninstall' do
+ before do
+ allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:delete_pod).and_return(nil)
+ allow(namespace).to receive(:ensure_exists!).once
+ end
+
+ it 'ensures the namespace exists before creating the POD' do
+ expect(namespace).to receive(:ensure_exists!).once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+
+ it 'removes an existing pod before installing' do
+ expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+ end
+
describe '#install' do
before do
allow(client).to receive(:create_pod).and_return(nil)
diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
new file mode 100644
index 00000000000..cae92305b19
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm::DeleteCommand do
+ let(:app_name) { 'app-name' }
+ let(:rbac) { true }
+ let(:files) { {} }
+ let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
+
+ subject { delete_command }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ helm delete --purge app-name
+ EOS
+ end
+ end
+
+ context 'when there is a ca.pem file' do
+ let(:files) { { 'ca.pem': 'some file content' } }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ #{helm_delete_command}
+ EOS
+ end
+
+ let(:helm_delete_command) do
+ <<~EOS.squish
+ helm delete --purge app-name
+ --tls
+ --tls-ca-cert /data/helm/app-name/config/ca.pem
+ --tls-cert /data/helm/app-name/config/cert.pem
+ --tls-key /data/helm/app-name/config/key.pem
+ EOS
+ end
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { delete_command.pod_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a pod that uses the tiller serviceAccountName' do
+ expect(subject.spec.serviceAccountName).to eq('tiller')
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates a pod that uses the default serviceAccountName' do
+ expect(subject.spec.serviceAcccountName).to be_nil
+ end
+ end
+ end
+
+ describe '#pod_name' do
+ subject { delete_command.pod_name }
+
+ it { is_expected.to eq('uninstall-app-name') }
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
new file mode 100644
index 00000000000..e88eb140b35
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+ let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
+
+ describe '.find' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_call) { described_class.find(project, nil, environment, dashboard_path) }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the system dashboard is specified' do
+ let(:dashboard_path) { system_dashboard_path }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when no dashboard is specified' do
+ let(:service_call) { described_class.find(project, nil, environment) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+
+ describe '.find_all_paths' do
+ let(:all_dashboard_paths) { described_class.find_all_paths(project) }
+ let(:system_dashboard) { { path: system_dashboard_path, default: true } }
+
+ it 'includes only the system dashboard by default' do
+ expect(all_dashboard_paths).to eq([system_dashboard])
+ end
+
+ context 'when the project contains dashboards' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it 'includes system and project dashboards' do
+ project_dashboard = { path: dashboard_path, default: false }
+
+ expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
new file mode 100644
index 00000000000..be3c1095bd7
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Processor do
+ let(:project) { build(:project) }
+ let(:environment) { build(:environment, project: project) }
+ let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+
+ describe 'process' do
+ let(:process_params) { [project, environment, dashboard_yml] }
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) }
+
+ context 'when dashboard config corresponds to common metrics' do
+ let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
+
+ it 'inserts metric ids into the config' do
+ target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
+
+ expect(target_metric).to include(:metric_id)
+ expect(target_metric[:metric_id]).to eq(common_metric.id)
+ end
+ end
+
+ context 'when the project has associated metrics' do
+ let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) }
+ let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) }
+ let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) }
+
+ it 'includes project-specific metrics' do
+ expect(all_metrics).to include get_metric_details(project_system_metric)
+ expect(all_metrics).to include get_metric_details(project_response_metric)
+ expect(all_metrics).to include get_metric_details(project_business_metric)
+ end
+
+ it 'orders groups by priority and panels by weight' do
+ expected_metrics_order = [
+ 'metric_b', # group priority 10, panel weight 1
+ 'metric_a2', # group priority 1, panel weight 2
+ 'metric_a1', # group priority 1, panel weight 1
+ project_business_metric.id, # group priority 0, panel weight nil (0)
+ project_response_metric.id, # group priority -5, panel weight nil (0)
+ project_system_metric.id, # group priority -10, panel weight nil (0)
+ ]
+ actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] }
+
+ expect(actual_metrics_order).to eq expected_metrics_order
+ end
+
+ context 'when the dashboard should not include project metrics' do
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) }
+
+ it 'includes only dashboard metrics' do
+ metrics = all_metrics.map { |m| m[:id] }
+
+ expect(metrics.length).to be(3)
+ expect(metrics).to eq %w(metric_b metric_a2 metric_a1)
+ end
+ end
+ end
+
+ shared_examples_for 'errors with message' do |expected_message|
+ it 'raises a DashboardLayoutError' do
+ error_class = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ expect { dashboard }.to raise_error(error_class, expected_message)
+ end
+ end
+
+ context 'when the dashboard is missing panel_groups' do
+ let(:dashboard_yml) { {} }
+
+ it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
+ end
+
+ context 'when the dashboard contains a panel_group which is missing panels' do
+ let(:dashboard_yml) { { panel_groups: [{}] } }
+
+ it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
+ end
+
+ context 'when the dashboard contains a panel which is missing metrics' do
+ let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } }
+
+ it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics'
+ end
+ end
+
+ private
+
+ def all_metrics
+ dashboard[:panel_groups].map do |group|
+ group[:panels].map { |panel| panel[:metrics] }
+ end.flatten
+ end
+
+ def get_metric_details(metric)
+ {
+ query_range: metric.query,
+ unit: metric.unit,
+ label: metric.legend,
+ metric_id: metric.id
+ }
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
new file mode 100644
index 00000000000..162beb0268a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:user) { build(:user) }
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+ end
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'and the dashboard is then deleted' do
+ it 'does not return the previously cached dashboard' do
+ described_class.new(*service_params).get_dashboard
+
+ delete_project_dashboard(project, user, dashboard_path)
+
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ end
+ end
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
new file mode 100644
index 00000000000..e71ce2481a3
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH }
+ let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'when called with a non-system dashboard' do
+ let(:dashboard_path) { 'garbage/dashboard/path' }
+
+ # We want to alwaus return the system dashboard.
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb
new file mode 100644
index 00000000000..c0af2ede32a
--- /dev/null
+++ b/spec/lib/gitlab/namespaced_session_store_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::NamespacedSessionStore do
+ let(:key) { :some_key }
+ subject { described_class.new(key) }
+
+ it 'stores data under the specified key' do
+ Gitlab::Session.with_session({}) do
+ subject[:new_data] = 123
+
+ expect(Thread.current[:session_storage][key]).to eq(new_data: 123)
+ end
+ end
+
+ it 'retrieves data from the given key' do
+ Thread.current[:session_storage] = { key => { existing_data: 123 } }
+
+ expect(subject[:existing_data]).to eq 123
+ end
+end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 857862a2abd..84b2e2dc823 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -120,10 +120,10 @@ describe Gitlab::PathRegex do
# - Followed by one or more path-parts not starting with `:` or `*`
# - Followed by a path-part that includes a wildcard parameter `*`
# At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
- STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
- NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
- ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
- WILDCARD_SEGMENT = /\*/
+ STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}.freeze
+ NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}.freeze
+ ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}.freeze
+ WILDCARD_SEGMENT = /\*/.freeze
let(:namespaced_wildcard_routes) do
routes_without_format.select do |p|
p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
@@ -144,7 +144,7 @@ describe Gitlab::PathRegex do
end.uniq
end
- STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
+ STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}.freeze
let(:group_routes) do
routes_without_format.select do |path|
path =~ STARTING_WITH_GROUP
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 9f2214f7ce7..5af52db7a1f 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -27,13 +27,13 @@ describe Gitlab::Profiler do
it 'sends a POST request when data is passed' do
post_data = '{"a":1}'
- expect(app).to receive(:post).with(anything, post_data, anything)
+ expect(app).to receive(:post).with(anything, params: post_data, headers: anything)
described_class.profile('/', post_data: post_data)
end
it 'uses the private_token for auth if given' do
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', private_token: private_token)
@@ -51,7 +51,7 @@ describe Gitlab::Profiler do
user = double(:user)
expect(described_class).to receive(:with_user).with(nil).and_call_original
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', user: user, private_token: private_token)
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index 78c74266c61..048f4af6020 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
+ let(:project) { environment.project }
let(:environment) { create(:environment) }
let(:slug) { environment.slug }
@@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do
end
context 'with deployment platform' do
- let(:kube_namespace) { environment.deployment_platform.actual_namespace }
+ context 'with project cluster' do
+ let(:kube_namespace) { environment.deployment_platform.actual_namespace }
- before do
- create(:cluster, :provided_by_user, projects: [environment.project])
+ before do
+ create(:cluster, :project, :provided_by_user, projects: [project])
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
end
- it { is_expected.to include(kube_namespace: kube_namespace) }
+ context 'with group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) }
+ let(:group) { create(:group) }
+ let(:project2) { create(:project) }
+ let(:kube_namespace) { k8s_ns.namespace }
+
+ let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) }
+ let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) }
+
+ before do
+ group.projects << project
+ group.projects << project2
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index 1128eaf8560..ae522a588ee 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Sentry do
context 'when exceptions should not be raised' do
before do
allow(described_class).to receive(:should_raise_for_dev?).and_return(false)
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'logs the exception with all attributes passed' do
@@ -65,7 +65,7 @@ describe Gitlab::Sentry do
before do
allow(described_class).to receive(:enabled?).and_return(true)
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'calls Raven.capture_exception' do
diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb
new file mode 100644
index 00000000000..8db73f0ec7b
--- /dev/null
+++ b/spec/lib/gitlab/session_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Session do
+ it 'uses the current thread as a data store' do
+ Thread.current[:session_storage] = { a: :b }
+
+ expect(described_class.current).to eq(a: :b)
+ ensure
+ Thread.current[:session_storage] = nil
+ end
+
+ describe '#with_session' do
+ it 'sets session hash' do
+ described_class.with_session(one: 1) do
+ expect(described_class.current).to eq(one: 1)
+ end
+ end
+
+ it 'restores current store after' do
+ described_class.with_session(two: 2) { }
+
+ expect(described_class.current).to eq nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
index a138ad7c910..0ff694d409b 100644
--- a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationInjector do
it 'injects into payload the correlation id' do
expect_any_instance_of(described_class).to receive(:call).and_call_original
- Gitlab::CorrelationId.use_id('new-correlation-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
TestWorker.perform_async(1234)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
index 94ae4ffa184..8410467ef1f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do
expect_any_instance_of(described_class).to receive(:call).and_call_original
expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
- expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id')
+ expect(Labkit::Correlation::CorrelationId.current_id).to eq('new-correlation-id')
end
Sidekiq::Client.push(
diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb
deleted file mode 100644
index 945490f0988..00000000000
--- a/spec/lib/gitlab/tracing/factory_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Factory do
- describe '.create_tracer' do
- let(:service_name) { 'rspec' }
-
- context "when tracing is not configured" do
- it 'ignores null connection strings' do
- expect(described_class.create_tracer(service_name, nil)).to be_nil
- end
-
- it 'ignores empty connection strings' do
- expect(described_class.create_tracer(service_name, '')).to be_nil
- end
-
- it 'ignores unknown implementations' do
- expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil
- end
-
- it 'ignores invalid connection strings' do
- expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil
- end
- end
-
- context "when tracing is configured with jaeger" do
- let(:mock_tracer) { double('tracer') }
-
- it 'processes default connections' do
- expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer)
-
- expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer)
- end
-
- it 'processes connections with parameters' do
- expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer)
-
- expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb b/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb
deleted file mode 100644
index 7f5aecb7baa..00000000000
--- a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::GRPCInterceptor do
- subject { described_class.instance }
-
- shared_examples_for "a grpc interceptor method" do
- let(:custom_error) { Class.new(StandardError) }
-
- it 'yields' do
- expect { |b| method.call(kwargs, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { method.call(kwargs) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-
- describe '#request_response' do
- let(:method) { subject.method(:request_response) }
- let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#client_streamer' do
- let(:method) { subject.method(:client_streamer) }
- let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#server_streamer' do
- let(:method) { subject.method(:server_streamer) }
- let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#bidi_streamer' do
- let(:method) { subject.method(:bidi_streamer) }
- let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-end
diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb
deleted file mode 100644
index 3d6a007cfd9..00000000000
--- a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::JaegerFactory do
- describe '.create_tracer' do
- let(:service_name) { 'rspec' }
-
- shared_examples_for 'a jaeger tracer' do
- it 'responds to active_span methods' do
- expect(tracer).to respond_to(:active_span)
- end
-
- it 'yields control' do
- expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control
- end
- end
-
- context 'processes default connections' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, {}) }
- end
- end
-
- context 'handles debug options' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) }
- end
- end
-
- context 'handles const sampler' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) }
- end
- end
-
- context 'handles probabilistic sampler' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) }
- end
- end
-
- context 'handles http_endpoint configurations' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) }
- end
- end
-
- context 'handles udp_endpoint configurations' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) }
- end
- end
-
- context 'ignores invalid parameters' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) }
- end
- end
-
- context 'accepts the debug parameter when strict_parser is set' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) }
- end
- end
-
- it 'rejects invalid parameters when strict_parser is set' do
- expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError)
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rack_middleware_spec.rb b/spec/lib/gitlab/tracing/rack_middleware_spec.rb
deleted file mode 100644
index 13d4d8a89f7..00000000000
--- a/spec/lib/gitlab/tracing/rack_middleware_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Tracing::RackMiddleware do
- using RSpec::Parameterized::TableSyntax
-
- describe '#call' do
- context 'for normal middleware flow' do
- let(:fake_app) { -> (env) { fake_app_response } }
- subject { described_class.new(fake_app) }
- let(:request) { }
-
- context 'for 200 responses' do
- let(:fake_app_response) { [200, { 'Content-Type': 'text/plain' }, ['OK']] }
-
- it 'delegates correctly' do
- expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
- end
- end
-
- context 'for 500 responses' do
- let(:fake_app_response) { [500, { 'Content-Type': 'text/plain' }, ['Error']] }
-
- it 'delegates correctly' do
- expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
- end
- end
- end
-
- context 'when an application is raising an exception' do
- let(:custom_error) { Class.new(StandardError) }
- let(:fake_app) { ->(env) { raise custom_error } }
-
- subject { described_class.new(fake_app) }
-
- it 'delegates propagates exceptions correctly' do
- expect { subject.call(Rack::MockRequest.env_for("/")) }.to raise_error(custom_error)
- end
- end
- end
-
- describe '.build_sanitized_url_from_env' do
- def env_for_url(url)
- env = Rack::MockRequest.env_for(input_url)
- env['action_dispatch.parameter_filter'] = [/token/]
-
- env
- end
-
- where(:input_url, :output_url) do
- '/gitlab-org/gitlab-ce' | 'http://example.org/gitlab-org/gitlab-ce'
- '/gitlab-org/gitlab-ce?safe=1' | 'http://example.org/gitlab-org/gitlab-ce?safe=1'
- '/gitlab-org/gitlab-ce?private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?private_token=%5BFILTERED%5D'
- '/gitlab-org/gitlab-ce?mixed=1&private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?mixed=1&private_token=%5BFILTERED%5D'
- end
-
- with_them do
- it { expect(described_class.build_sanitized_url_from_env(env_for_url(input_url))).to eq(output_url) }
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
deleted file mode 100644
index 0bbaf5968ed..00000000000
--- a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-describe Gitlab::Tracing::Rails::ActionViewSubscriber do
- using RSpec::Parameterized::TableSyntax
-
- shared_examples 'an actionview notification' do
- it 'notifies the tracer when the hash contains null values' do
- expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.public_send(notify_method, start, finish, payload)
- end
-
- it 'notifies the tracer when the payload is missing values' do
- expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.public_send(notify_method, start, finish, payload.compact)
- end
-
- it 'does not throw exceptions when with the default tracer' do
- expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error
- end
- end
-
- describe '.instrument' do
- it 'is unsubscribeable' do
- unsubscribe = described_class.instrument
-
- expect(unsubscribe).not_to be_nil
- expect { unsubscribe.call }.not_to raise_error
- end
- end
-
- describe '#notify_render_template' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_template' }
- let(:notify_method) { :notify_render_template }
-
- where(:identifier, :layout, :exception) do
- nil | nil | nil
- "" | nil | nil
- "show.haml" | nil | nil
- nil | "" | nil
- nil | "layout.haml" | nil
- nil | nil | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier,
- layout: layout
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier,
- 'template.layout' => layout
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-
- describe '#notify_render_collection' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_collection' }
- let(:notify_method) { :notify_render_collection }
-
- where(
- :identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do
- nil | nil | 0 | nil | 0 | nil
- "" | nil | 0 | nil | 0 | nil
- "show.haml" | nil | 0 | nil | 0 | nil
- nil | 0 | 0 | nil | 0 | nil
- nil | 1 | 1 | nil | 0 | nil
- nil | nil | 0 | 0 | 0 | nil
- nil | nil | 0 | 1 | 1 | nil
- nil | nil | 0 | nil | 0 | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier,
- count: count,
- cache_hits: cache_hits
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier,
- 'template.count' => expected_count,
- 'template.cache.hits' => expected_cache_hits
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-
- describe '#notify_render_partial' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_partial' }
- let(:notify_method) { :notify_render_partial }
-
- where(:identifier, :exception) do
- nil | nil
- "" | nil
- "show.haml" | nil
- nil | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
deleted file mode 100644
index 7bd0875fa68..00000000000
--- a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do
- using RSpec::Parameterized::TableSyntax
-
- describe '.instrument' do
- it 'is unsubscribeable' do
- unsubscribe = described_class.instrument
-
- expect(unsubscribe).not_to be_nil
- expect { unsubscribe.call }.not_to raise_error
- end
- end
-
- describe '#notify' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
-
- where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do
- nil | "active_record:sqlquery" | nil | nil | nil | false | nil
- "" | "active_record:sqlquery" | nil | nil | nil | false | nil
- "User Load" | "active_record:User Load" | nil | nil | nil | false | nil
- "Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil
- nil | "active_record:sqlquery" | nil | 123 | nil | false | nil
- nil | "active_record:sqlquery" | nil | nil | false | false | nil
- nil | "active_record:sqlquery" | nil | nil | true | true | nil
- nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users"
- end
-
- with_them do
- def payload
- {
- name: name,
- exception: exception,
- connection_id: connection_id,
- cached: cached,
- sql: sql
- }
- end
-
- def expected_tags
- {
- "component" => "ActiveRecord",
- "span.kind" => "client",
- "db.type" => "sql",
- "db.connection_id" => connection_id,
- "db.cached" => cached_response,
- "db.statement" => sql
- }
- end
-
- it 'notifies the tracer when the hash contains null values' do
- expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.notify(start, finish, payload)
- end
-
- it 'notifies the tracer when the payload is missing values' do
- expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.notify(start, finish, payload.compact)
- end
-
- it 'does not throw exceptions when with the default tracer' do
- expect { subject.notify(start, finish, payload) }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb
deleted file mode 100644
index 3755860b5ba..00000000000
--- a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Sidekiq::ClientMiddleware do
- describe '#call' do
- let(:worker_class) { 'test_worker_class' }
- let(:job) do
- {
- 'class' => "jobclass",
- 'queue' => "jobqueue",
- 'retry' => 0,
- 'args' => %w{1 2 3}
- }
- end
- let(:queue) { 'test_queue' }
- let(:redis_pool) { double("redis_pool") }
- let(:custom_error) { Class.new(StandardError) }
- let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
-
- subject { described_class.new }
-
- it 'yields' do
- expect(subject).to receive(:in_tracing_span).with(
- operation_name: "sidekiq:jobclass",
- tags: {
- "component" => "sidekiq",
- "span.kind" => "client",
- "sidekiq.queue" => "jobqueue",
- "sidekiq.jid" => nil,
- "sidekiq.retry" => "0",
- "sidekiq.args" => "1, 2, 3"
- }
- ).and_yield(span)
-
- expect { |b| subject.call(worker_class, job, queue, redis_pool, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { subject.call(worker_class, job, queue, redis_pool) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb
deleted file mode 100644
index c3087de785a..00000000000
--- a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Sidekiq::ServerMiddleware do
- describe '#call' do
- let(:worker_class) { 'test_worker_class' }
- let(:job) do
- {
- 'class' => "jobclass",
- 'queue' => "jobqueue",
- 'retry' => 0,
- 'args' => %w{1 2 3}
- }
- end
- let(:queue) { 'test_queue' }
- let(:custom_error) { Class.new(StandardError) }
- let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
- subject { described_class.new }
-
- it 'yields' do
- expect(subject).to receive(:in_tracing_span).with(
- hash_including(
- operation_name: "sidekiq:jobclass",
- tags: {
- "component" => "sidekiq",
- "span.kind" => "server",
- "sidekiq.queue" => "jobqueue",
- "sidekiq.jid" => nil,
- "sidekiq.retry" => "0",
- "sidekiq.args" => "1, 2, 3"
- }
- )
- ).and_yield(span)
-
- expect { |b| subject.call(worker_class, job, queue, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { subject.call(worker_class, job, queue) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 549cc5ac057..4e910e67ac2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -57,6 +57,8 @@ describe Gitlab::UsageData do
database
avg_cycle_analytics
web_ide_commits
+ influxdb_metrics_enabled
+ prometheus_metrics_enabled
))
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index d02d9be5c5c..f8332757fcd 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -16,40 +16,80 @@ describe Gitlab::Workhorse do
let(:ref) { 'master' }
let(:format) { 'zip' }
let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
- let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) }
- let(:gitaly_params) do
- base_params.merge(
- 'GitalyServer' => {
- 'address' => Gitlab::GitalyClient.address(project.repository_storage),
- 'token' => Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys
- )
- end
+ let(:path) { 'some/path' if Feature.enabled?(:git_archive_path, default_enabled: true) }
+ let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) }
let(:cache_disabled) { false }
subject do
- described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil)
+ described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil, path: path)
end
before do
allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled)
end
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(git_archive_path: false)
+ end
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to include(gitaly_params)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expected_params = metadata.merge(
+ 'GitalyRepository' => repository.gitaly_repository.to_h,
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ }
+ )
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq(expected_params.deep_stringify_keys)
+ end
+
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
+
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
+ end
+ end
end
- context 'when archive caching is disabled' do
- let(:cache_disabled) { true }
+ context 'feature flag enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => Base64.encode64(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: Gitaly::GetArchiveRequest::Format::ZIP,
+ path: path
+ ).to_proto
+ )
+ }.deep_stringify_keys)
+ end
- it 'tells workhorse not to use the cache' do
- _, _, params = decode_workhorse_header(subject)
- expect(params).to include({ 'DisableCache' => true })
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
+
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
+ end
end
end
diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb
index f35d9464d48..4e77dcc97e6 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 -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized -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,7 +23,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 -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
diff --git a/spec/lib/quality/seeders/issues_spec.rb b/spec/lib/quality/seeders/issues_spec.rb
new file mode 100644
index 00000000000..e17414a541a
--- /dev/null
+++ b/spec/lib/quality/seeders/issues_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Quality::Seeders::Issues do
+ let(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
+
+ describe '#seed' do
+ it 'seeds issues' do
+ issues_created = subject.seed(backfill_weeks: 1, average_issues_per_week: 1)
+
+ expect(issues_created).to be_between(0, 2)
+ expect(project.issues.count).to eq(issues_created)
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index fee1d701e3a..8f348b1b053 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -701,6 +701,8 @@ describe Notify do
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
+ is_expected.to have_body_text 'leave the project'
+ is_expected.to have_body_text project_url(project, leave: 1)
end
end
@@ -1144,6 +1146,8 @@ describe Notify do
is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
+ is_expected.to have_body_text 'leave the group'
+ is_expected.to have_body_text group_url(group, leave: 1)
end
end
diff --git a/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
new file mode 100644
index 00000000000..105c05bb7ca
--- /dev/null
+++ b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190506135400_schedule_sync_issuables_state_id_where_nil')
+
+describe ScheduleSyncIssuablesStateIdWhereNil, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:migration) { described_class.new }
+ let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
+ let(:project) { projects.create!(namespace_id: group.id) }
+
+ shared_examples 'scheduling migrations' do
+ before do
+ Sidekiq::Worker.clear_all
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'correctly schedules issuable sync background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration).to be_scheduled_delayed_migration(120.seconds, resource_1.id, resource_3.id)
+ expect(migration).to be_scheduled_delayed_migration(240.seconds, resource_5.id, resource_5.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe '#up' do
+ context 'issues' do
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::ISSUES_MIGRATION }
+ let!(:resource_1) { issues.create!(description: 'first', state: 'opened', state_id: nil) }
+ let!(:resource_2) { issues.create!(description: 'second', state: 'closed', state_id: 2) }
+ let!(:resource_3) { issues.create!(description: 'third', state: 'closed', state_id: nil) }
+ let!(:resource_4) { issues.create!(description: 'fourth', state: 'closed', state_id: 2) }
+ let!(:resource_5) { issues.create!(description: 'fifth', state: 'closed', state_id: nil) }
+ end
+ end
+
+ context 'merge requests' do
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::MERGE_REQUESTS_MIGRATION }
+ let!(:resource_1) { merge_requests.create!(state: 'opened', state_id: nil, target_project_id: project.id, target_branch: 'feature1', source_branch: 'master') }
+ let!(:resource_2) { merge_requests.create!(state: 'closed', state_id: 2, target_project_id: project.id, target_branch: 'feature2', source_branch: 'master') }
+ let!(:resource_3) { merge_requests.create!(state: 'merged', state_id: nil, target_project_id: project.id, target_branch: 'feature3', source_branch: 'master') }
+ let!(:resource_4) { merge_requests.create!(state: 'locked', state_id: 3, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') }
+ let!(:resource_5) { merge_requests.create!(state: 'locked', state_id: nil, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') }
+ end
+ end
+ end
+end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index fd25132ed3a..cc90a998d3f 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -11,6 +11,25 @@ describe ApplicationRecord do
end
end
+ describe '.safe_ensure_unique' do
+ let(:model) { build(:suggestion) }
+ let(:klass) { model.class }
+
+ before do
+ allow(model).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'returns false when ActiveRecord::RecordNotUnique is raised' do
+ expect(model).to receive(:save).once
+ expect(klass.safe_ensure_unique { model.save }).to be_falsey
+ end
+
+ it 'retries based on retry count specified' do
+ expect(model).to receive(:save).exactly(3).times
+ expect(klass.safe_ensure_unique(retries: 2) { model.save }).to be_falsey
+ end
+ end
+
describe '.safe_find_or_create_by' do
it 'creates the user avoiding race conditions' do
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index c7d7dbac736..f8dc1541dd3 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -31,6 +31,20 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
+ it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("myemail@example.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value("myemail@test.example.com").for(:lets_encrypt_notification_email) }
+
+ context "when user accepted let's encrypt terms of service" do
+ before do
+ setting.update(lets_encrypt_terms_of_service_accepted: true)
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index d0e1688cce3..8364293b908 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -43,6 +43,21 @@ describe Blob do
changelog.id
contributing.id
end
+
+ it 'does not include blobs from previous requests in later requests' do
+ changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
+
+ # Access property so the values are loaded
+ changelog.id
+ contributing.id
+
+ readme = described_class.lazy(project, commit_id, 'README.md')
+
+ expect(project.repository).to receive(:blobs_at).with([[commit_id, 'README.md']]).once.and_call_original
+
+ readme.id
+ end
end
describe '#data' do
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index aacfbe3f180..eb32198265b 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -10,6 +10,8 @@ describe Ci::Bridge do
create(:ci_bridge, pipeline: pipeline)
end
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
+
describe '#tags' do
it 'only has a bridge tag' do
expect(bridge.tags).to eq [:bridge]
@@ -33,7 +35,7 @@ describe Ci::Bridge do
CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PIPELINE_IID
CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
- CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION
+ CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED
]
expect(bridge.scoped_variables_hash.keys).to include(*variables)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 1352a2de2d7..9b489baf163 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -28,9 +28,14 @@ describe Ci::Build do
it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
it { is_expected.to be_a(ArtifactMigratable) }
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { FactoryBot.build(:ci_build, pipeline: pipeline, artifacts_size: 23) }
+ end
+
describe 'associations' do
it 'has a bidirectional relationship with projects' do
expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:builds)
@@ -166,8 +171,8 @@ describe Ci::Build do
end
end
- describe '.with_test_reports' do
- subject { described_class.with_test_reports }
+ describe '.with_reports' do
+ subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
context 'when build has a test report' do
let!(:build) { create(:ci_build, :success, :test_reports) }
@@ -852,6 +857,10 @@ describe Ci::Build do
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
+ before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
+ end
+
it 'has deployments record with created status' do
expect(deployment).to be_created
expect(environment.name).to eq('review/master')
@@ -1425,7 +1434,7 @@ describe Ci::Build do
build.cancel!
end
- it { is_expected.to be_retryable }
+ it { is_expected.not_to be_retryable }
end
end
@@ -1955,7 +1964,7 @@ describe Ci::Build do
context 'when build has been canceled' do
subject { build_stubbed(:ci_build, :manual, status: :canceled) }
- it { is_expected.to be_playable }
+ it { is_expected.not_to be_playable }
end
context 'when build is successful' do
@@ -2119,54 +2128,6 @@ describe Ci::Build do
end
end
- context 'when updating the build' do
- let(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics' do
- build.artifacts_size = 42
-
- expect(build).to receive(:update_project_statistics_after_save).and_call_original
-
- expect { build.save! }
- .to change { build.project.statistics.reload.build_artifacts_size }
- .by(19)
- end
-
- context 'when the artifact size stays the same' do
- it 'does not update project statistics' do
- build.name = 'changed'
-
- expect(build).not_to receive(:update_project_statistics_after_save)
-
- build.save!
- end
- end
- end
-
- context 'when destroying the build' do
- let!(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- expect { build.destroy! }
- .to change { build.project.statistics.reload.build_artifacts_size }
- .by(-23)
- end
-
- context 'when the build is destroyed due to the project being destroyed' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- build.project.update(pending_delete: true)
- build.project.destroy!
- end
- end
- end
-
describe '#variables' do
let(:container_registry_enabled) { false }
@@ -2227,7 +2188,8 @@ describe Ci::Build do
{ 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 },
- { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false }
+ { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s, public: true, masked: false }
]
end
@@ -2312,6 +2274,19 @@ describe Ci::Build do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build belongs to a pipeline for merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_branch: 'improve/awesome') }
+ let(:pipeline) { merge_request.all_pipelines.first }
+ let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
+
+ it 'returns values based on source ref' do
+ is_expected.to include(
+ { key: 'CI_COMMIT_REF_NAME', value: 'improve/awesome', public: true, masked: false },
+ { key: 'CI_COMMIT_REF_SLUG', value: 'improve-awesome', public: true, masked: false }
+ )
+ end
+ end
+
context 'when build has an environment' do
let(:environment_variables) do
[
@@ -2703,6 +2678,8 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'returns static predefined variables' do
expect(build.variables.size).to be >= 28
expect(build.variables)
@@ -2752,6 +2729,8 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
@@ -2838,7 +2817,7 @@ describe Ci::Build do
context 'when ref is merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
context 'when ref is protected' do
@@ -2896,7 +2875,7 @@ describe Ci::Build do
context 'when ref is merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
context 'when ref is protected' do
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index b3999765e5f..406a69f3bbc 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe Ci::GroupVariable do
subject { build(:ci_group_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
+
it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Maskable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index d7abd54eec1..5964f66c398 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe Ci::JobArtifact do
let(:artifact) { create(:ci_job_artifact, :archive) }
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
+
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:job) }
@@ -102,12 +106,6 @@ describe Ci::JobArtifact do
it 'sets the size from the file size' do
expect(artifact.size).to eq(106365)
end
-
- it 'updates the project statistics' do
- expect { artifact }
- .to change { project.statistics.reload.build_artifacts_size }
- .by(106365)
- end
end
context 'updating the artifact file' do
@@ -115,12 +113,6 @@ describe Ci::JobArtifact do
artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
expect(artifact.size).to eq(1062)
end
-
- it 'updates the project statistics' do
- expect { artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png')) }
- .to change { artifact.project.statistics.reload.build_artifacts_size }
- .by(1062 - 106365)
- end
end
describe 'validates file format' do
@@ -259,34 +251,6 @@ describe Ci::JobArtifact do
end
end
- context 'when destroying the artifact' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- it 'updates the project statistics' do
- artifact = build.job_artifacts.first
-
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- expect { artifact.destroy }
- .to change { project.statistics.reload.build_artifacts_size }
- .by(-106365)
- end
-
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- project.update(pending_delete: true)
- project.destroy!
- end
- end
- end
-
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
index be0307518eb..bb812cc0533 100644
--- a/spec/models/ci/legacy_stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -272,4 +272,6 @@ describe Ci::LegacyStage do
def create_job(type, status: 'success', stage: stage_name, **opts)
create(type, pipeline: pipeline, stage: stage, status: status, **opts)
end
+
+ it_behaves_like 'manual playable stage', :ci_stage
end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 81913f4a3b6..1bfc14d2839 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -35,6 +35,15 @@ describe Ci::PipelineSchedule do
expect(pipeline_schedule).not_to be_valid
end
end
+
+ context 'when cron contains trailing whitespaces' do
+ it 'strips the attribute' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ')
+
+ expect(pipeline_schedule).to be_valid
+ expect(pipeline_schedule.cron).to eq('0 0 * * *')
+ end
+ end
end
describe '#set_next_run_at' do
diff --git a/spec/models/ci/pipeline_schedule_variable_spec.rb b/spec/models/ci/pipeline_schedule_variable_spec.rb
index 3c9379ecb0d..c96a24d5042 100644
--- a/spec/models/ci/pipeline_schedule_variable_spec.rb
+++ b/spec/models/ci/pipeline_schedule_variable_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
describe Ci::PipelineScheduleVariable do
subject { build(:ci_pipeline_schedule_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index f3e78630c1b..a0319b3eb0a 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -382,6 +382,54 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#source_ref' do
+ subject { pipeline.source_ref }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'returns source ref' do
+ is_expected.to eq('feature')
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'returns source ref' do
+ is_expected.to eq(merge_request.source_branch)
+ end
+ end
+ end
+
+ describe '#source_ref_slug' do
+ subject { pipeline.source_ref_slug }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'slugifies with the source ref' do
+ expect(Gitlab::Utils).to receive(:slugify).with('feature')
+
+ subject
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'slugifies with the source ref of the merge request' do
+ expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch)
+
+ subject
+ end
+ end
+ end
+
describe '.triggered_for_branch' do
subject { described_class.triggered_for_branch(ref) }
@@ -418,7 +466,7 @@ describe Ci::Pipeline, :mailer do
target_branch: 'master')
end
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
it 'does not return the pipeline' do
is_expected.to be_empty
@@ -426,6 +474,26 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.with_reports' do
+ subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
+
+ context 'when pipeline has a test report' do
+ let!(:pipeline_with_report) { create(:ci_pipeline, :with_test_reports) }
+
+ it 'selects the pipeline' do
+ is_expected.to eq([pipeline_with_report])
+ end
+ end
+
+ context 'when pipeline does not have metrics reports' do
+ let!(:pipeline_without_report) { create(:ci_empty_pipeline) }
+
+ it 'does not select the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.merge_request_event' do
subject { described_class.merge_request_event }
@@ -670,7 +738,8 @@ describe Ci::Pipeline, :mailer do
CI_PIPELINE_SOURCE
CI_COMMIT_MESSAGE
CI_COMMIT_TITLE
- CI_COMMIT_DESCRIPTION]
+ CI_COMMIT_DESCRIPTION
+ CI_COMMIT_REF_PROTECTED]
end
context 'when source is merge request' do
@@ -2705,18 +2774,19 @@ describe Ci::Pipeline, :mailer do
end
describe '#latest_builds_with_artifacts' do
- let!(:pipeline) { create(:ci_pipeline, :success) }
-
- let!(:build) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline)
- end
+ let!(:fresh_build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let!(:stale_build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) }
it 'returns an Array' do
expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
end
- it 'returns the latest builds' do
- expect(pipeline.latest_builds_with_artifacts).to eq([build])
+ it 'returns the latest builds with non-expired artifacts' do
+ expect(pipeline.latest_builds_with_artifacts).to contain_exactly(fresh_build)
+ end
+
+ it 'does not return builds with expired artifacts' do
+ expect(pipeline.latest_builds_with_artifacts).not_to include(stale_build)
end
it 'memoizes the returned relation' do
@@ -2728,8 +2798,8 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#has_test_reports?' do
- subject { pipeline.has_test_reports? }
+ describe '#has_reports?' do
+ subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }
context 'when pipeline has builds with test reports' do
before do
@@ -2872,4 +2942,36 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#find_stage_by_name' do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:stage_name) { 'test' }
+
+ let(:stage) do
+ create(:ci_stage_entity,
+ pipeline: pipeline,
+ project: pipeline.project,
+ name: 'test')
+ end
+
+ before do
+ create_list(:ci_build, 2, pipeline: pipeline, stage: stage.name)
+ end
+
+ subject { pipeline.find_stage_by_name!(stage_name) }
+
+ context 'when stage exists' do
+ it { is_expected.to eq(stage) }
+ end
+
+ context 'when stage does not exist' do
+ let(:stage_name) { 'build' }
+
+ it 'raises an ActiveRecord exception' do
+ expect do
+ subject
+ end.to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb
index 2ecb688299a..e8c7ce088e2 100644
--- a/spec/models/ci/pipeline_variable_spec.rb
+++ b/spec/models/ci/pipeline_variable_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
describe Ci::PipelineVariable do
subject { build(:ci_pipeline_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
+
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_id) }
describe '#hook_attrs' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 2cb581696a0..f735a89f69f 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -73,9 +73,8 @@ describe Ci::Runner do
end
it 'fails to save a group assigned to a project runner even if the runner is already saved' do
- group_runner
-
- expect { create(:group, runners: [project_runner]) }
+ group.runners << project_runner
+ expect { group.save! }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 661958390e2..85cd32fb03a 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -285,4 +285,6 @@ describe Ci::Stage, :models do
end
end
end
+
+ it_behaves_like 'manual playable stage', :ci_stage_entity
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index d2df6b3344e..a231c7eaed8 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -5,8 +5,9 @@ require 'spec_helper'
describe Ci::Variable do
subject { build(:ci_variable) }
+ it_behaves_like "CI variable"
+
describe 'validations' do
- it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Maskable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index 5cd80edb3a1..8d853a04e33 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -10,6 +10,12 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application version specs', :clusters_applications_cert_managers
include_examples 'cluster application initial status specs'
+ describe '#can_uninstall?' do
+ subject { cert_manager.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
let(:cert_email) { 'admin@example.com' }
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index f177d493a2e..6ea6c110d62 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -18,6 +18,14 @@ describe Clusters::Applications::Helm do
it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
end
+ describe '#can_uninstall?' do
+ let(:helm) { create(:clusters_applications_helm) }
+
+ subject { helm.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#issue_client_cert' do
let(:application) { create(:clusters_applications_helm) }
subject { application.issue_client_cert }
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 113d29b5551..292ddabd2d8 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -18,6 +18,12 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe '#can_uninstall?' do
+ subject { ingress.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#make_installed!' do
before do
application.make_installed!
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 1a7363b64f9..fc9ebed863e 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -10,6 +10,15 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to belong_to(:oauth_application) }
+ describe '#can_uninstall?' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#set_initial_status' do
before do
jupyter.set_initial_status
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 5e68f2634da..d5974f47190 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -39,6 +39,12 @@ describe Clusters::Applications::Knative do
end
end
+ describe '#can_uninstall?' do
+ subject { knative.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#schedule_status_update with external_ip' do
let(:application) { create(:clusters_applications_knative, :installed) }
@@ -109,7 +115,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command }
it 'is initialized with latest version' do
- expect(subject.version).to eq('0.3.0')
+ expect(subject.version).to eq('0.5.0')
end
it_behaves_like 'a command'
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index e8ba9737c23..26267c64112 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -11,6 +11,21 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
+ describe 'after_destroy' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus_service) { project.create_prometheus_service(active: true) }
+
+ it 'deactivates prometheus_service after destroy' do
+ expect do
+ application.destroy!
+
+ prometheus_service.reload
+ end.to change(prometheus_service, :active).from(true).to(false)
+ end
+ end
+
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
@@ -23,12 +38,20 @@ describe Clusters::Applications::Prometheus do
end
it 'ensures Prometheus service is activated' do
- expect(prometheus_service).to receive(:update).with(active: true)
+ expect(prometheus_service).to receive(:update!).with(active: true)
subject.make_installed
end
end
+ describe '#can_uninstall?' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.can_uninstall? }
+
+ it { is_expected.to be_truthy }
+ end
+
describe '#prometheus_client' do
context 'cluster is nil' do
it 'returns nil' do
@@ -134,6 +157,34 @@ describe Clusters::Applications::Prometheus do
end
end
+ describe '#uninstall_command' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.uninstall_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
+
+ it 'has the application name' do
+ expect(subject.name).to eq('prometheus')
+ end
+
+ it 'has files' do
+ expect(subject.files).to eq(prometheus.files)
+ end
+
+ it 'is rbac' do
+ expect(subject).to be_rbac
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ prometheus.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ end
+ end
+
describe '#upgrade_command' do
let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 399a13f82cb..4f0cd0efe9c 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -13,6 +13,14 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
+ describe '#can_uninstall?' do
+ let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
+
+ subject { gitlab_runner.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
@@ -24,7 +32,7 @@ describe Clusters::Applications::Runner do
it 'is initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.3.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -42,7 +50,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.3.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
end
end
end
@@ -61,8 +69,8 @@ describe Clusters::Applications::Runner do
expect(values).to include('privileged: true')
expect(values).to include('image: ubuntu:16.04')
expect(values).to include('resources')
- expect(values).to match(/runnerToken: '?#{ci_runner.token}/)
- expect(values).to match(/gitlabUrl: '?#{Gitlab::Routing.url_helpers.root_url}/)
+ expect(values).to match(/runnerToken: '?#{Regexp.escape(ci_runner.token)}/)
+ expect(values).to match(/gitlabUrl: '?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/)
end
context 'without a runner' do
@@ -75,7 +83,7 @@ describe Clusters::Applications::Runner do
end
it 'uses the new runner token' do
- expect(values).to match(/runnerToken: '?#{runner.token}/)
+ expect(values).to match(/runnerToken: '?#{Regexp.escape(runner.token)}/)
end
end
@@ -106,6 +114,18 @@ describe Clusters::Applications::Runner do
expect(runner.groups).to eq [group]
end
end
+
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, :with_installed_helm, :instance) }
+
+ include_examples 'runner creation'
+
+ it 'creates an instance runner' do
+ subject
+
+ expect(runner).to be_instance_type
+ end
+ end
end
context 'with duplicated values on vendor/runner/values.yaml' do
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 894ef3fb956..58203da5b22 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -95,6 +95,24 @@ describe Clusters::Cluster do
it { is_expected.to contain_exactly(cluster) }
end
+ describe '.managed' do
+ subject do
+ described_class.managed
+ end
+
+ context 'cluster is not managed' do
+ let!(:cluster) { create(:cluster, :not_managed) }
+
+ it { is_expected.not_to include(cluster) }
+ end
+
+ context 'cluster is managed' do
+ let!(:cluster) { create(:cluster) }
+
+ it { is_expected.to include(cluster) }
+ end
+ end
+
describe '.missing_kubernetes_namespace' do
let!(:cluster) { create(:cluster, :provided_by_gcp, :project) }
let(:project) { cluster.project }
@@ -307,6 +325,15 @@ describe Clusters::Cluster do
end
end
+ context 'when group and instance have configured kubernetes clusters' do
+ let(:project) { create(:project, group: group) }
+ let!(:instance_cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it 'returns clusters in order, descending the hierachy' do
+ is_expected.to eq([group_cluster, instance_cluster])
+ end
+ end
+
context 'when sub-group has configured kubernetes cluster', :nested_groups do
let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) }
let(:sub_group) { sub_group_cluster.group }
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 0281dd2c303..e35d14f2282 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -331,6 +331,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
)
end
+
+ context 'the cluster is not managed' do
+ let!(:cluster) { create(:cluster, :group, :not_managed, platform_kubernetes: kubernetes) }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
+ )
+ end
+ end
end
context 'kubernetes namespace exists for the project' do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index ca2f9e36c98..017cca0541e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -449,7 +449,7 @@ describe CommitStatus do
end
it "lock" do
- is_expected.to be true
+ is_expected.to be_truthy
end
it "raise exception when trying to update" do
@@ -463,7 +463,7 @@ describe CommitStatus do
end
it "do not lock" do
- is_expected.to be false
+ is_expected.to be_falsey
end
it "save correctly" do
diff --git a/spec/models/concerns/has_ref_spec.rb b/spec/models/concerns/has_ref_spec.rb
index 6805731fed3..66b25c77430 100644
--- a/spec/models/concerns/has_ref_spec.rb
+++ b/spec/models/concerns/has_ref_spec.rb
@@ -19,7 +19,7 @@ describe HasRef do
context 'when it was triggered by merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
let(:build) { create(:ci_build, pipeline: pipeline) }
it 'returns false' do
@@ -68,7 +68,7 @@ describe HasRef do
context 'when it is triggered by a merge request' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
let(:build) { create(:ci_build, tag: false, pipeline: pipeline) }
it 'returns nil' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index d9170d5fa07..f51322e1404 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -102,6 +102,13 @@ describe Deployment do
deployment.succeed!
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.succeed!
+ end
end
context 'when deployment failed' do
@@ -115,6 +122,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.drop!
+ end
end
context 'when deployment was canceled' do
@@ -128,6 +142,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.cancel!
+ end
end
end
@@ -379,6 +400,12 @@ describe Deployment do
it { is_expected.to be_nil }
end
+ context 'project uses the kubernetes service for deployments' do
+ let!(:service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to be_nil }
+ end
+
context 'project has a deployment platform' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) }
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index ff2382838ae..806b4f61bd8 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -93,7 +93,7 @@ describe InternalId do
before do
described_class.reset_column_information
# Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
let(:init) { double('block') }
@@ -104,6 +104,66 @@ describe InternalId do
expect(init).to receive(:call).with(issue).and_return(val)
expect(subject).to eq(val + 1)
end
+
+ it 'always attempts to generate internal IDs in production mode' do
+ allow(Rails.env).to receive(:test?).and_return(false)
+ val = rand(1..100)
+ generator = double(generate: val)
+ expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator)
+
+ expect(subject).to eq(val)
+ end
+ end
+ end
+
+ describe '.reset' do
+ subject { described_class.reset(issue, scope, usage, value) }
+
+ context 'in the absence of a record' do
+ let(:value) { 2 }
+
+ it 'does not revert back the value' do
+ expect { subject }.not_to change { described_class.count }
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when valid iid is used to reset' do
+ let!(:value) { generate_next }
+
+ context 'and iid is a latest one' do
+ it 'does rewind and next generated value is the same' do
+ expect(subject).to be_truthy
+ expect(generate_next).to eq(value)
+ end
+ end
+
+ context 'and iid is not a latest one' do
+ it 'does not rewind' do
+ generate_next
+
+ expect(subject).to be_falsey
+ expect(generate_next).to be > value
+ end
+ end
+
+ def generate_next
+ described_class.generate_next(issue, scope, usage, init)
+ end
+ end
+
+ context 'with an insufficient schema version' do
+ let(:value) { 2 }
+
+ before do
+ described_class.reset_column_information
+ # Project factory will also call the current_version
+ expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ end
+
+ it 'does not reset any of the iids' do
+ expect(subject).to be_falsey
+ end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 0cd69cb4817..cc777cbf749 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -55,6 +55,29 @@ describe Issue do
end
end
+ describe 'locking' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:lock_version) do
+ [
+ [0],
+ ["0"]
+ ]
+ end
+
+ with_them do
+ it 'works when an issue has a NULL lock_version' do
+ issue = create(:issue)
+
+ described_class.where(id: issue.id).update_all('lock_version = NULL')
+
+ issue.update!(lock_version: lock_version, title: 'locking test')
+
+ expect(issue.reload.title).to eq('locking test')
+ end
+ end
+ end
+
describe '#order_by_position_and_priority' do
let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c68c3ce2abe..782a84f922b 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -70,6 +70,16 @@ describe Member do
expect(child_member).not_to be_valid
end
+ # Membership in a subgroup confers certain access rights, such as being
+ # able to merge or push code to protected branches.
+ it "is valid with an equal level" do
+ child_member.access_level = GroupMember::DEVELOPER
+
+ child_member.validate
+
+ expect(child_member).to be_valid
+ end
+
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index f61857ea5ff..c72b6e9033d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -31,6 +31,29 @@ describe MergeRequest do
end
end
+ describe 'locking' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:lock_version) do
+ [
+ [0],
+ ["0"]
+ ]
+ end
+
+ with_them do
+ it 'works when a merge request has a NULL lock_version' do
+ merge_request = create(:merge_request)
+
+ described_class.where(id: merge_request.id).update_all('lock_version = NULL')
+
+ merge_request.update!(lock_version: lock_version, title: 'locking test')
+
+ expect(merge_request.reload.title).to eq('locking test')
+ end
+ end
+ end
+
describe '#squash_in_progress?' do
let(:repo_path) do
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
@@ -2123,7 +2146,7 @@ describe MergeRequest do
end
context 'when merges are not restricted to green builds' do
- subject { build(:merge_request, target_project: build(:project, only_allow_merge_if_pipeline_succeeds: false)) }
+ subject { build(:merge_request, target_project: create(:project, only_allow_merge_if_pipeline_succeeds: false)) }
context 'and a failed pipeline is associated' do
before do
@@ -2262,6 +2285,50 @@ describe MergeRequest do
end
end
+ describe "#environments" do
+ subject { merge_request.environments }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request, project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+
+ it 'returns environments' do
+ is_expected.to eq(pipeline.environments)
+ expect(subject.count).to be(1)
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'returns empty relation' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe "#reload_diff" do
it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
user = create(:user)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 387d1221c76..bfde367c47f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -63,6 +63,11 @@ describe Namespace do
end
end
+ describe 'delegate' do
+ it { is_expected.to delegate_method(:name).to(:owner).with_prefix.with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:avatar_url).to(:owner).with_arguments(allow_nil: true) }
+ end
+
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
@@ -141,20 +146,20 @@ describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- storage_size: 606,
repository_size: 101,
lfs_objects_size: 202,
- build_artifacts_size: 303))
+ build_artifacts_size: 303,
+ packages_size: 404))
end
let(:project2) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- storage_size: 60,
repository_size: 10,
lfs_objects_size: 20,
- build_artifacts_size: 30))
+ build_artifacts_size: 30,
+ packages_size: 40))
end
it "sums all project storage counters in the namespace" do
@@ -162,10 +167,11 @@ describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 666
+ expect(statistics.storage_size).to eq 1110
expect(statistics.repository_size).to eq 111
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
+ expect(statistics.packages_size).to eq 444
end
it "correctly handles namespaces without projects" do
@@ -175,6 +181,7 @@ describe Namespace do
expect(statistics.repository_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
+ expect(statistics.packages_size).to eq 0
end
end
@@ -738,22 +745,25 @@ describe Namespace do
end
end
- describe '#full_path_was' do
+ describe '#full_path_before_last_save' do
context 'when the group has no parent' do
- it 'returns the path was' do
- group = create(:group, parent: nil)
- expect(group.full_path_was).to eq(group.path_was)
+ it 'returns the path before last save' do
+ group = create(:group)
+
+ group.update(parent: nil)
+
+ expect(group.full_path_before_last_save).to eq(group.path_before_last_save)
end
end
context 'when a parent is assigned to a group with no previous parent' do
- it 'returns the path was' do
+ it 'returns the path before last save' do
group = create(:group, parent: nil)
-
parent = create(:group)
- group.parent = parent
- expect(group.full_path_was).to eq("#{group.path_was}")
+ group.update(parent: parent)
+
+ expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}")
end
end
@@ -761,9 +771,10 @@ describe Namespace do
it 'returns the parent full path' do
parent = create(:group)
group = create(:group, parent: parent)
- group.parent = nil
- expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
+ group.update(parent: nil)
+
+ expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
end
@@ -772,8 +783,10 @@ describe Namespace do
parent = create(:group)
group = create(:group, parent: parent)
new_parent = create(:group)
- group.parent = new_parent
- expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
+
+ group.update(parent: new_parent)
+
+ expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
end
end
@@ -801,4 +814,21 @@ describe Namespace do
end
end
end
+
+ describe '#user?' do
+ subject { namespace.user? }
+
+ context 'when type is a user' do
+ let(:user) { create(:user) }
+ let(:namespace) { user.namespace }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when type is a group' do
+ let(:namespace) { create(:group) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
end
diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb
index 99eeac8d778..b15bedd257e 100644
--- a/spec/models/note_diff_file_spec.rb
+++ b/spec/models/note_diff_file_spec.rb
@@ -10,4 +10,31 @@ describe NoteDiffFile do
describe 'validations' do
it { is_expected.to validate_presence_of(:diff_note) }
end
+
+ describe '.referencing_sha' do
+ let!(:diff_note) { create(:diff_note_on_commit) }
+
+ let(:note_diff_file) { diff_note.note_diff_file }
+ let(:project) { diff_note.project }
+
+ it 'finds note diff files by project and sha' do
+ found = described_class.referencing_sha(diff_note.commit_id, project_id: project.id)
+
+ expect(found).to contain_exactly(note_diff_file)
+ end
+
+ it 'excludes note diff files with the wrong project' do
+ other_project = create(:project)
+
+ found = described_class.referencing_sha(diff_note.commit_id, project_id: other_project.id)
+
+ expect(found).to be_empty
+ end
+
+ it 'excludes note diff files with the wrong sha' do
+ found = described_class.referencing_sha(Gitlab::Git::BLANK_SHA, project_id: project.id)
+
+ expect(found).to be_empty
+ end
+ end
end
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 3710f2be287..1b1ede6b14c 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -9,11 +9,43 @@ describe NotificationRecipient do
subject(:recipient) { described_class.new(user, :watch, target: target, project: project) }
- it 'denies access to a target when cross project access is denied' do
- allow(Ability).to receive(:allowed?).and_call_original
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false)
+ describe '#has_access?' do
+ before do
+ allow(user).to receive(:can?).and_call_original
+ end
+
+ context 'user cannot read cross project' do
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_cross_project).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'user cannot read build' do
+ let(:target) { build(:ci_pipeline) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_build, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
- expect(recipient.has_access?).to be_falsy
+ context 'user cannot read commit' do
+ let(:target) { build(:commit) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_commit, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'target has no policy' do
+ let(:target) { double.as_null_object }
+
+ it 'returns true' do
+ expect(recipient.has_access?).to eq true
+ end
+ end
end
context '#notification_setting' do
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 142ddebbbf8..ec4d4517f82 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -344,4 +344,32 @@ describe PagesDomain do
end
end
end
+
+ describe '.for_removal' do
+ subject { described_class.for_removal }
+
+ context 'when domain is not schedule for removal' do
+ let!(:domain) { create :pages_domain }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when domain is scheduled for removal yesterday' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.ago }
+
+ it 'returns domain' do
+ is_expected.to eq([domain])
+ end
+ end
+
+ context 'when domain is scheduled for removal tomorrow' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.from_now }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index e5a3a3ad66e..ae00f9df89e 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe PoolRepository do
describe 'associations' do
it { is_expected.to belong_to(:shard) }
- it { is_expected.to have_one(:source_project) }
+ it { is_expected.to belong_to(:source_project) }
it { is_expected.to have_many(:member_projects) }
end
diff --git a/spec/models/project_metrics_setting_spec.rb b/spec/models/project_metrics_setting_spec.rb
new file mode 100644
index 00000000000..7df01625ba1
--- /dev/null
+++ b/spec/models/project_metrics_setting_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectMetricsSetting do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ context 'when external_dashboard_url is over 255 chars' do
+ before do
+ subject.external_dashboard_url = 'https://' + 'a' * 250
+ end
+
+ it 'fails validation' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:external_dashboard_url])
+ .to include('is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'with unsafe url' do
+ before do
+ subject.external_dashboard_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'non ascii chars in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://gitlab.com/api/0/projects/project1/something€'
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'internal url in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://192.168.1.1'
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'external_dashboard_url is blank' do
+ before do
+ subject.external_dashboard_url = ''
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/project_services/chat_message/deployment_message_spec.rb
new file mode 100644
index 00000000000..42c1689db3d
--- /dev/null
+++ b/spec/models/project_services/chat_message/deployment_message_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ChatMessage::DeploymentMessage do
+ describe '#pretext' do
+ it 'returns a message with the data returned by the deployment data builder' do
+ environment = create(:environment, name: "myenvironment")
+ project = create(:project, :repository)
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq("Deploy to myenvironment succeeded")
+ end
+
+ it 'returns a message for a successful deployment' do
+ data = {
+ status: 'success',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production succeeded')
+ end
+
+ it 'returns a message for a failed deployment' do
+ data = {
+ status: 'failed',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production failed')
+ end
+
+ it 'returns a message for a canceled deployment' do
+ data = {
+ status: 'canceled',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production canceled')
+ end
+
+ it 'returns a message for a deployment to another environment' do
+ data = {
+ status: 'success',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging succeeded')
+ end
+
+ it 'returns a message for a deployment with any other status' do
+ data = {
+ status: 'unknown',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging unknown')
+ end
+ end
+
+ describe '#attachments' do
+ def deployment_data(params)
+ {
+ object_kind: "deployment",
+ status: "success",
+ deployable_id: 3,
+ deployable_url: "deployable_url",
+ environment: "sandbox",
+ project: {
+ name: "greatproject",
+ web_url: "project_web_url",
+ path_with_namespace: "project_path_with_namespace"
+ },
+ user: {
+ name: "Jane Person",
+ username: "jane"
+ },
+ user_url: "user_url",
+ short_sha: "12345678",
+ commit_url: "commit_url",
+ commit_title: "commit title text"
+ }.merge(params)
+ end
+
+ it 'returns attachments with the data returned by the deployment data builder' do
+ user = create(:user, name: "John Smith", username: "smith")
+ namespace = create(:namespace, name: "myspace")
+ project = create(:project, :repository, namespace: namespace, name: "myproject")
+ commit = project.commit('HEAD')
+ environment = create(:environment, name: "myenvironment", project: project)
+ ci_build = create(:ci_build, project: project)
+ deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
+ job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
+ commit_url = Gitlab::UrlBuilder.build(deployment.commit)
+ user_url = Gitlab::Routing.url_helpers.user_url(user)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}",
+ color: "good"
+ }])
+ end
+
+ it 'returns attachments for a failed deployment' do
+ data = deployment_data(status: 'failed')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "danger"
+ }])
+ end
+
+ it 'returns attachments for a canceled deployment' do
+ data = deployment_data(status: 'canceled')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "warning"
+ }])
+ end
+
+ it 'uses a neutral color for a deployment with any other status' do
+ data = deployment_data(status: 'some-new-status-we-make-in-the-future')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "#334455"
+ }])
+ end
+ end
+end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index fd9e33c1781..a04b984c1f6 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -98,12 +98,11 @@ describe HipchatService do
context 'tag_push events' do
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build(
- project,
- user,
- Gitlab::Git::BLANK_SHA,
- '1' * 40,
- 'refs/tags/test',
- [])
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '1' * 40,
+ ref: 'refs/tags/test')
end
it "calls Hipchat API for tag push events" do
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 521d5265753..c025d7c882e 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -30,6 +30,12 @@ describe MicrosoftTeamsService do
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
set(:project) { create(:project, :repository, :wiki_repo) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9f6a0b53281..2a17bd6002e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3164,61 +3164,105 @@ describe Project do
end
describe '.with_feature_available_for_user' do
- let!(:user) { create(:user) }
- let!(:feature) { MergeRequest }
- let!(:project) { create(:project, :public, :merge_requests_enabled) }
+ let(:user) { create(:user) }
+ let(:feature) { MergeRequest }
subject { described_class.with_feature_available_for_user(feature, user) }
- context 'when user has access to project' do
- subject { described_class.with_feature_available_for_user(feature, user) }
+ shared_examples 'feature disabled' do
+ let(:project) { create(:project, :public, :merge_requests_disabled) }
+
+ it 'does not return projects with the project feature disabled' do
+ is_expected.not_to include(project)
+ end
+ end
+
+ shared_examples 'feature public' do
+ let(:project) { create(:project, :public, :merge_requests_public) }
+
+ it 'returns projects with the project feature public' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature enabled' do
+ let(:project) { create(:project, :public, :merge_requests_enabled) }
+
+ it 'returns projects with the project feature enabled' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature access level is nil' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns projects with the project feature access level nil' do
+ project.project_feature.update(merge_requests_access_level: nil)
+
+ is_expected.to include(project)
+ end
+ end
+ context 'with user' do
before do
project.add_guest(user)
end
- context 'when public project' do
- context 'when feature is public' do
- it 'returns project' do
- is_expected.to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ context 'when user does not has access to the feature' do
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
end
end
- context 'when feature is private' do
- let!(:project) { create(:project, :public, :merge_requests_private) }
-
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ context 'when user has access to the feature' do
+ it 'returns projects with the project feature private' do
+ project.add_reporter(user)
is_expected.to include(project)
end
-
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
- end
end
end
+ end
- context 'when private project' do
- let!(:project) { create(:project) }
+ context 'user is an admin' do
+ let(:user) { create(:user, :admin) }
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
- is_expected.to include(project)
- end
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
+ it 'returns projects with the project feature private' do
+ is_expected.to include(project)
end
end
end
- context 'when user does not have access to project' do
- let!(:project) { create(:project) }
+ context 'without user' do
+ let(:user) { nil }
- it 'does not return project when user cant access project' do
- is_expected.not_to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
+ end
end
end
end
@@ -4702,6 +4746,8 @@ describe Project do
it 'returns that pool repository' do
expect(subject).not_to be_empty
expect(subject[:pool_repository]).to be_persisted
+
+ expect(project.reload.pool_repository).to eq(subject[:pool_repository])
end
end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index c670b6aac56..738398a06f9 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -124,16 +124,30 @@ describe ProjectStatistics do
end
describe '.increment_statistic' do
- it 'increases the statistic by that amount' do
- expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) }
- .to change { statistics.reload.build_artifacts_size }
- .by(13)
+ shared_examples 'a statistic that increases storage_size' do
+ it 'increases the statistic by that amount' do
+ expect { described_class.increment_statistic(project.id, stat, 13) }
+ .to change { statistics.reload.send(stat) || 0 }
+ .by(13)
+ end
+
+ it 'increases also storage size by that amount' do
+ expect { described_class.increment_statistic(project.id, stat, 20) }
+ .to change { statistics.reload.storage_size }
+ .by(20)
+ end
end
- it 'increases also storage size by that amount' do
- expect { described_class.increment_statistic(project.id, :build_artifacts_size, 20) }
- .to change { statistics.reload.storage_size }
- .by(20)
+ context 'when adjusting :build_artifacts_size' do
+ let(:stat) { :build_artifacts_size }
+
+ it_behaves_like 'a statistic that increases storage_size'
+ end
+
+ context 'when adjusting :packages_size' do
+ let(:stat) { :packages_size }
+
+ it_behaves_like 'a statistic that increases storage_size'
end
context 'when the amount is 0' do
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 2525a6aebe0..d12dd97bb9e 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -109,8 +109,7 @@ describe ProjectWiki do
subject { super().empty? }
it { is_expected.to be_falsey }
- # Re-enable this when https://gitlab.com/gitlab-org/gitaly/issues/1204 is fixed
- xit 'only instantiates a Wiki page once' do
+ it 'only instantiates a Wiki page once' do
expect(WikiPage).to receive(:new).once.and_call_original
subject
@@ -119,22 +118,65 @@ describe ProjectWiki do
end
end
- describe "#pages" do
+ describe "#list_pages" do
+ let(:wiki_pages) { subject.list_pages }
+
before do
- create_page("index", "This is an awesome new Gollum Wiki")
- @pages = subject.pages
+ create_page("index", "This is an index")
+ create_page("index2", "This is an index2")
+ create_page("an index3", "This is an index3")
end
after do
- destroy_page(@pages.first.page)
+ wiki_pages.each do |wiki_page|
+ destroy_page(wiki_page.page)
+ end
end
it "returns an array of WikiPage instances" do
- expect(@pages.first).to be_a WikiPage
+ expect(wiki_pages.first).to be_a WikiPage
+ end
+
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
+ end
+
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
+ end
+
+ context "with limit option" do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
end
- it "returns the correct number of pages" do
- expect(@pages.count).to eq(1)
+ context "with sorting options" do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
+
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+
+ it 'returns pages sorted by created_at' do
+ pages = ['index', 'index2', 'an index3']
+
+ expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
+ expect(subject.list_pages(sort: 'created_at', direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+ end
+
+ context "with load_content option" do
+ let(:pages) { subject.list_pages(load_content: true) }
+
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq("This is an index3")
+ expect(pages.second.content).to eq("This is an index")
+ expect(pages.third.content).to eq("This is an index2")
+ end
end
end
@@ -144,7 +186,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it "returns the latest version of the page if it exists" do
@@ -195,7 +237,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it 'finds the page defined as _sidebar' do
@@ -242,12 +284,12 @@ describe ProjectWiki do
describe "#create_page" do
after do
- destroy_page(subject.pages.first.page)
+ destroy_page(subject.list_pages.first.page)
end
it "creates a new wiki page" do
expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.pages.count).to eq(1)
+ expect(subject.list_pages.count).to eq(1)
end
it "returns false when a duplicate page exists" do
@@ -262,7 +304,7 @@ describe ProjectWiki do
it "sets the correct commit message" do
subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.pages.first.page.version.message).to eq("commit message")
+ expect(subject.list_pages.first.page.version.message).to eq("commit message")
end
it 'sets the correct commit email' do
@@ -293,7 +335,7 @@ describe ProjectWiki do
format: :markdown,
message: "updated page"
)
- @page = subject.pages.first.page
+ @page = subject.list_pages(load_content: true).first.page
end
after do
@@ -337,7 +379,7 @@ describe ProjectWiki do
it "deletes the page" do
subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
+ expect(subject.list_pages.count).to eq(0)
end
it 'sets the correct commit email' do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 0b19a4f8efc..7c106ce6b85 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -49,6 +49,11 @@ RSpec.describe Release do
it 'counts the link as an asset' do
is_expected.to eq(1 + Releases::Source::FORMATS.count)
end
+
+ it "excludes sources count when asked" do
+ assets_count = release.assets_count(except: [:sources])
+ expect(assets_count).to eq(1)
+ end
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index f743dfed31f..e14b19db915 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -373,6 +373,22 @@ describe RemoteMirror, :mailer do
end
end
+ describe '#disabled?' do
+ subject { remote_mirror.disabled? }
+
+ context 'when disabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: false) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when enabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: true) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
def create_mirror(params)
project = FactoryBot.create(:project, :repository)
project.remote_mirrors.create!(params)
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 3f5d285bc2c..9ff0f355fd4 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -217,6 +217,25 @@ describe Repository do
expect(result.size).to eq(0)
end
+
+ context 'with a commit with invalid UTF-8 path' do
+ def create_commit_with_invalid_utf8_path
+ rugged = rugged_repo(repository)
+ blob_id = Rugged::Blob.from_buffer(rugged, "some contents")
+ tree_builder = Rugged::Tree::Builder.new(rugged)
+ tree_builder.insert({ oid: blob_id, name: "hello\x80world", filemode: 0100644 })
+ tree_id = tree_builder.write
+ user = { email: "jcai@gitlab.com", time: Time.now, name: "John Cai" }
+
+ Rugged::Commit.create(rugged, message: 'some commit message', parents: [rugged.head.target.oid], tree: tree_id, committer: user, author: user)
+ end
+
+ it 'does not raise an error' do
+ commit = create_commit_with_invalid_utf8_path
+
+ expect { repository.list_last_commits_for_tree(commit, '.', offset: 0) }.not_to raise_error
+ end
+ end
end
describe '#last_commit_for_path' do
@@ -1432,6 +1451,91 @@ describe Repository do
end
end
+ describe '#rebase' do
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
+
+ shared_examples_for 'a method that can rebase successfully' do
+ it 'returns the rebase commit sha' do
+ rebase_commit_sha = repository.rebase(user, merge_request)
+ head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
+
+ expect(rebase_commit_sha).to eq(head_sha)
+ end
+
+ it 'sets the `rebase_commit_sha` for the given merge request' do
+ rebase_commit_sha = repository.rebase(user, merge_request)
+
+ expect(rebase_commit_sha).not_to be_nil
+ expect(merge_request.rebase_commit_sha).to eq(rebase_commit_sha)
+ end
+ end
+
+ context 'when two_step_rebase feature is enabled' do
+ before do
+ stub_feature_flags(two_step_rebase: true)
+ end
+
+ it_behaves_like 'a method that can rebase successfully'
+
+ it 'executes the new Gitaly RPC' do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rebase)
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:user_rebase)
+
+ repository.rebase(user, merge_request)
+ end
+
+ describe 'rolling back the `rebase_commit_sha`' do
+ let(:new_sha) { Digest::SHA1.hexdigest('foo') }
+
+ it 'does not rollback when there are no errors' do
+ second_response = double(pre_receive_error: nil, git_error: nil)
+ mock_gitaly(second_response)
+
+ repository.rebase(user, merge_request)
+
+ expect(merge_request.reload.rebase_commit_sha).to eq(new_sha)
+ end
+
+ it 'does rollback when an error is encountered in the second step' do
+ second_response = double(pre_receive_error: 'my_error', git_error: nil)
+ mock_gitaly(second_response)
+
+ expect do
+ repository.rebase(user, merge_request)
+ end.to raise_error(Gitlab::Git::PreReceiveError)
+
+ expect(merge_request.reload.rebase_commit_sha).to be_nil
+ end
+
+ def mock_gitaly(second_response)
+ responses = [
+ double(rebase_sha: new_sha).as_null_object,
+ second_response
+ ]
+
+ expect_any_instance_of(
+ Gitaly::OperationService::Stub
+ ).to receive(:user_rebase_confirmable).and_return(responses.each)
+ end
+ end
+ end
+
+ context 'when two_step_rebase feature is disabled' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'a method that can rebase successfully'
+
+ it 'executes the deprecated Gitaly RPC' do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:user_rebase)
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:rebase)
+
+ repository.rebase(user, merge_request)
+ end
+ end
+ end
+
describe '#revert' do
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
@@ -1618,6 +1722,7 @@ describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
+ :metrics_dashboard_paths,
:xcode_project?
])
@@ -2468,4 +2573,69 @@ describe Repository do
repository.merge_base('master', 'fix')
end
end
+
+ describe '#create_if_not_exists' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ it 'creates the repository if it did not exist' do
+ expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
+ end
+
+ it 'calls out to the repository client to create a repo' do
+ expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+
+ context 'it does nothing if the repository already existed' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does nothing if the repository already existed' do
+ expect(repository.raw.gitaly_repository_client).not_to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+ end
+
+ context 'when the repository exists but the cache is not up to date' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does not raise errors' do
+ allow(repository).to receive(:exists?).and_return(false)
+ expect(repository.raw).to receive(:create_repository).and_call_original
+
+ expect { repository.create_if_not_exists }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#blobs_metadata" do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ def expect_metadata_blob(thing)
+ expect(thing).to be_a(Blob)
+ expect(thing.data).to be_empty
+ end
+
+ it "returns blob metadata in batch for HEAD" do
+ result = repository.blobs_metadata(["bar/branch-test.txt", "README.md", "does/not/exist"])
+
+ expect_metadata_blob(result.first)
+ expect_metadata_blob(result.second)
+ expect(result.size).to eq(2)
+ end
+
+ it "returns blob metadata for a specified ref" do
+ result = repository.blobs_metadata(["files/ruby/feature.rb"], "feature")
+
+ expect_metadata_blob(result.first)
+ end
+
+ it "performs a single gitaly call", :request_store do
+ expect { repository.blobs_metadata(["bar/branch-test.txt", "readme.txt", "does/not/exist"]) }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index d5c85c11195..520a06e138e 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -44,47 +44,49 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages)
end
- context 'sort by title' do
- let(:grouped_entries) { described_class.group_by_directory(wiki.pages) }
- let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }
-
- it 'returns an array with pages and directories' do
- grouped_entries.each_with_index do |page_or_dir, i|
- expected_page_or_dir = expected_grouped_entries[i]
- expected_slugs = get_slugs(expected_page_or_dir)
- slugs = get_slugs(page_or_dir)
-
- expect(slugs).to match_array(expected_slugs)
+ context "#list_pages" do
+ context 'sort by title' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) }
+ let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }
+
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
+
+ expect(slugs).to match_array(expected_slugs)
+ end
end
end
- end
- context 'sort by created_at' do
- let(:grouped_entries) { described_class.group_by_directory(wiki.pages(sort: 'created_at')) }
- let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] }
+ context 'sort by created_at' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) }
+ let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] }
- it 'returns an array with pages and directories' do
- grouped_entries.each_with_index do |page_or_dir, i|
- expected_page_or_dir = expected_grouped_entries[i]
- expected_slugs = get_slugs(expected_page_or_dir)
- slugs = get_slugs(page_or_dir)
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
- expect(slugs).to match_array(expected_slugs)
+ expect(slugs).to match_array(expected_slugs)
+ end
end
end
- end
- it 'returns an array with retained order with directories at the top' do
- expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
+ it 'returns an array with retained order with directories at the top' do
+ expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
- grouped_entries = described_class.group_by_directory(wiki.pages)
+ grouped_entries = described_class.group_by_directory(wiki.list_pages)
- actual_order =
- grouped_entries.map do |page_or_dir|
- get_slugs(page_or_dir)
- end
- .flatten
- expect(actual_order).to eq(expected_order)
+ actual_order =
+ grouped_entries.map do |page_or_dir|
+ get_slugs(page_or_dir)
+ end
+ .flatten
+ expect(actual_order).to eq(expected_order)
+ end
end
end
end
@@ -386,7 +388,7 @@ describe WikiPage do
it "deletes the page" do
@page.delete
- expect(wiki.pages).to be_empty
+ expect(wiki.list_pages).to be_empty
end
it "returns true" do
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index b2f0ca1bc30..cc3dde154dc 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -66,5 +66,21 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_disallowed :admin_cluster }
end
end
+
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, :instance) }
+
+ context 'when user' do
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when admin' do
+ let(:user) { create(:admin) }
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
end
end
diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb
new file mode 100644
index 00000000000..9d755c6d29d
--- /dev/null
+++ b/spec/policies/clusters/instance_policy_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::InstancePolicy do
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, Clusters::Instance.new) }
+
+ describe 'rules' do
+ context 'when user' do
+ it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when admin' do
+ let(:user) { create(:admin) }
+
+ context 'with instance_level_clusters enabled' do
+ it { expect(policy).to be_allowed :read_cluster }
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'with instance_level_clusters disabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+ end
+ end
+end
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index a38e0dbd797..097000ceb6a 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -14,13 +14,6 @@ describe PersonalSnippetPolicy do
]
end
- let(:comment_permissions) do
- [
- :comment_personal_snippet,
- :create_note
- ]
- end
-
def permissions(user)
described_class.new(user, snippet)
end
@@ -33,7 +26,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -44,7 +37,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -55,7 +48,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -70,7 +63,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -81,7 +74,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -92,7 +85,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -103,7 +96,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -118,7 +111,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -129,7 +122,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -140,7 +133,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -151,7 +144,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -162,7 +155,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 42f8bf3137b..8075fcade5f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -17,7 +17,7 @@ describe ProjectPolicy do
read_project_for_iids read_issue_iid read_label
read_milestone read_project_snippet read_project_member read_note
create_project create_issue create_note upload_file create_merge_request_in
- award_emoji
+ award_emoji read_release
]
end
@@ -26,7 +26,7 @@ describe ProjectPolicy do
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
- read_merge_request download_wiki_code read_sentry_issue read_release
+ read_merge_request download_wiki_code read_sentry_issue
]
end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index a9d786bc872..42701a5f8d1 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -210,6 +210,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq('Group cluster') }
end
+
+ context 'instance_type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to eq('Instance cluster') }
+ end
end
describe '#show_path' do
@@ -227,6 +233,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(group_cluster_path(group, cluster)) }
end
+
+ context 'instance_type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to eq(admin_cluster_path(cluster)) }
+ end
end
describe '#read_only_kubernetes_platform_fields?' do
diff --git a/spec/presenters/label_presenter_spec.rb b/spec/presenters/label_presenter_spec.rb
new file mode 100644
index 00000000000..fae8188670f
--- /dev/null
+++ b/spec/presenters/label_presenter_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe LabelPresenter do
+ include Gitlab::Routing.url_helpers
+
+ set(:group) { create(:group) }
+ set(:project) { create(:project, group: group) }
+ let(:label) { build_stubbed(:label, project: project).present(issuable_subject: project) }
+ let(:group_label) { build_stubbed(:group_label, group: group).present(issuable_subject: project) }
+
+ describe '#edit_path' do
+ context 'with group label' do
+ subject { group_label.edit_path }
+
+ it { is_expected.to eq(edit_group_label_path(group, group_label)) }
+ end
+
+ context 'with project label' do
+ subject { label.edit_path }
+
+ it { is_expected.to eq(edit_project_label_path(project, label)) }
+ end
+ end
+
+ describe '#destroy_path' do
+ context 'with group label' do
+ subject { group_label.destroy_path }
+
+ it { is_expected.to eq(group_label_path(group, group_label)) }
+ end
+
+ context 'with project label' do
+ subject { label.destroy_path }
+
+ it { is_expected.to eq(project_label_path(project, label)) }
+ end
+ end
+
+ describe '#filter_path' do
+ context 'with group as context subject' do
+ let(:label_in_group) { build_stubbed(:label, project: project).present(issuable_subject: group) }
+ subject { label_in_group.filter_path }
+
+ it { is_expected.to eq(issues_group_path(group, label_name: [label_in_group.title])) }
+ end
+
+ context 'with project as context subject' do
+ subject { label.filter_path }
+
+ it { is_expected.to eq(namespace_project_issues_path(group, project, label_name: [label.title])) }
+ end
+ end
+
+ describe '#can_subscribe_to_label_in_different_levels?' do
+ it 'returns true for group labels in project context' do
+ expect(group_label.can_subscribe_to_label_in_different_levels?).to be_truthy
+ end
+
+ it 'returns false for project labels in project context' do
+ expect(label.can_subscribe_to_label_in_different_levels?).to be_falsey
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 4a0f91c4c7a..451dc88880c 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -40,8 +40,8 @@ describe MergeRequestPresenter do
allow(pipeline).to receive(:has_warnings?) { true }
end
- it 'returns "success_with_warnings"' do
- is_expected.to eq('success_with_warnings')
+ it 'returns "success-with-warnings"' do
+ is_expected.to eq('success-with-warnings')
end
end
@@ -439,6 +439,52 @@ describe MergeRequestPresenter do
end
end
+ describe '#source_branch_link' do
+ subject { presenter.source_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when source branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:source_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.source_branch_commits_path}\">#{presenter.source_branch}</a>")
+ end
+ end
+
+ context 'when source branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.source_branch}</span>")
+ end
+ end
+ end
+
+ describe '#target_branch_link' do
+ subject { presenter.target_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when target branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:target_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.target_branch_commits_path}\">#{presenter.target_branch}</a>")
+ end
+ end
+
+ context 'when target branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:target_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.target_branch}</span>")
+ end
+ end
+ end
+
describe '#source_branch_with_namespace_link' do
subject do
described_class.new(resource, current_user: user).source_branch_with_namespace_link
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 456de5f1b9a..5bf80f6e318 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -411,4 +411,23 @@ describe ProjectPresenter do
end
end
end
+
+ describe '#statistics_buttons' do
+ let(:project) { build(:project) }
+ let(:presenter) { described_class.new(project, current_user: user) }
+
+ it 'orders the items correctly' do
+ allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
+ 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')
+ allow(presenter).to receive(:contribution_guide_path).and_return('fake_path')
+
+ buttons = presenter.statistics_buttons(show_auto_devops_callout: false)
+ expect(buttons.map(&:label)).to start_with(
+ a_string_including('README'),
+ a_string_including('CONTRIBUTING')
+ )
+ end
+ end
end
diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb
deleted file mode 100644
index d6b6d83d648..00000000000
--- a/spec/rack_servers/configs/puma.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only
-# Note: as per the convention in `config/puma.example.development.rb`,
-# this file will replace `/home/git` with the actual working directory
-
-directory '/home/git'
-threads 1, 10
-queue_requests false
-pidfile '/home/git/gitlab/tmp/pids/puma.pid'
-bind 'unix:///home/git/gitlab/tmp/tests/puma.socket'
-workers 1
-preload_app!
-worker_timeout 60
-
-require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
-require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
-
-before_fork do
- Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options
- Gitlab::Cluster::LifecycleEvents.do_before_fork
-end
-
-Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
-on_worker_boot do
- Gitlab::Cluster::LifecycleEvents.do_worker_start
- File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid)
-end
-
-on_restart do
- Gitlab::Cluster::LifecycleEvents.do_master_restart
-end
diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb
index 891df4f1a66..8290473821c 100644
--- a/spec/rack_servers/puma_spec.rb
+++ b/spec/rack_servers/puma_spec.rb
@@ -1,20 +1,20 @@
# frozen_string_literal: true
-require 'fileutils'
+require 'spec_helper'
+require 'fileutils'
require 'excon'
-require 'spec_helper'
-
describe 'Puma' do
before(:all) do
- project_root = File.expand_path('../..', __dir__)
-
- config_lines = File.read('spec/rack_servers/configs/puma.rb')
- .gsub('/home/git/gitlab', project_root)
- .gsub('/home/git', project_root)
-
- config_path = File.join(project_root, "tmp/tests/puma.rb")
+ project_root = Rails.root.to_s
+ config_lines = File.read(Rails.root.join('config/puma.example.development.rb'))
+ .gsub('config.ru', File.join(__dir__, 'configs/config.ru'))
+ .gsub('workers 2', 'workers 1')
+ .gsub('/home/git/gitlab.socket', File.join(project_root, 'tmp/tests/puma.socket'))
+ .gsub('on_worker_boot do', "on_worker_boot do\nFile.write('#{File.join(project_root, 'tmp/tests/puma-worker-ready')}', Process.pid)")
+ .gsub(%r{/home/git(/gitlab)?}, project_root)
+ config_path = File.join(project_root, 'tmp/tests/puma.rb')
@socket_path = File.join(project_root, 'tmp/tests/puma.socket')
File.write(config_path, config_lines)
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 1271324a2ba..1dd0cb4817c 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -73,7 +73,7 @@ describe API::Badges do
let(:badge) { source.badges.first }
context "as a #{type}" do
- it 'returns 200' do
+ it 'returns 200', :quarantine do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
@@ -193,7 +193,7 @@ describe API::Badges do
end
context 'when authenticated as a maintainer/owner' do
- it 'updates the member' do
+ it 'updates the member', :quarantine do
put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer),
params: { link_url: example_url, image_url: example_url2 }
@@ -239,7 +239,7 @@ describe API::Badges do
end
end
- context 'when authenticated as a maintainer/owner' do
+ context 'when authenticated as a maintainer/owner', :quarantine do
it 'deletes the badge' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer)
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 35c448d187d..16036297ec7 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -13,7 +13,7 @@ describe API::Discussions do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
- it_behaves_like 'discussions API', 'projects', 'issues', 'iid' do
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_invididual_notes: true do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }
@@ -37,7 +37,7 @@ describe API::Discussions do
let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) }
let(:parent) { project }
- it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid', can_reply_to_invididual_notes: true
it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid'
it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid'
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 065b16c6221..018691e8099 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -164,139 +164,4 @@ describe API::Events do
expect(json_response['message']).to eq('404 User Not Found')
end
end
-
- describe 'GET /projects/:id/events' do
- context 'when unauthenticated ' do
- it 'returns 404 for private project' do
- get api("/projects/#{private_project.id}/events")
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 200 status for a public project' do
- public_project = create(:project, :public)
-
- get api("/projects/#{public_project.id}/events")
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context 'with inaccessible events' do
- let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
- let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
- let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
- let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
- let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
-
- it 'returns only accessible events' do
- get api("/projects/#{public_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(1)
- end
-
- it 'returns all events when the user has access' do
- get api("/projects/#{public_project.id}/events", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(2)
- end
- end
-
- context 'pagination' do
- let(:public_project) { create(:project, :public) }
-
- before do
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 1'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-10'))
- create(:event,
- project: public_project,
- target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-11'))
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 2'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-12'))
- end
-
- it 'correctly returns the second page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 1')
- expect(titles).not_to include('Confidential event')
- end
-
- it 'correctly returns the first page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 2')
- expect(titles).not_to include('Confidential event')
- end
- end
-
- context 'when not permitted to read' do
- it 'returns 404' do
- get api("/projects/#{private_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when authenticated' do
- it 'returns project events' do
- get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", 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.size).to eq(1)
- end
-
- it 'returns 404 if project does not exist' do
- get api("/projects/1234/events", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
-
- before do
- create_event(merge_request1)
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.count
-
- create_event(merge_request2)
-
- expect do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.not_to exceed_all_query_limit(control_count)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response.size).to eq(2)
- expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
- end
-
- def create_event(target)
- create(:event, project: private_project, author: user, target: target)
- end
- end
- end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index f95f460fd14..b63b4fb34df 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -5,7 +5,7 @@ describe 'GitlabSchema configurations' do
it 'shows an error if complexity is too high' do
project = create(:project, :repository)
- query = graphql_query_for('project', { 'fullPath' => project.full_path }, "id\nname\ndescription")
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
new file mode 100644
index 00000000000..8ff95cc9af2
--- /dev/null
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Based on spec/requests/api/groups_spec.rb
+# Should follow closely in order to ensure all situations are covered
+describe 'getting group information' do
+ include GraphqlHelpers
+ include UploadHelpers
+
+ let(:user1) { create(:user, can_create_group: false) }
+ let(:user2) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:public_group) { create(:group, :public) }
+ let(:private_group) { create(:group, :private) }
+
+ # similar to the API "GET /groups/:id"
+ describe "Query group(fullPath)" do
+ def group_query(group)
+ graphql_query_for('group', 'fullPath' => group.full_path)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(group_query(public_group))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns nil for a private group' do
+ post_graphql(group_query(private_group))
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'returns a public group' do
+ post_graphql(group_query(public_group))
+
+ expect(graphql_data['group']).not_to be_nil
+ end
+ end
+
+ context "when authenticated as user" do
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let!(:group2) { create(:group, :private) }
+
+ before do
+ group1.add_owner(user1)
+ group2.add_owner(user2)
+ end
+
+ it "returns one of user1's groups" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ post_graphql(group_query(group1), current_user: user1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(graphql_data['group']['id']).to eq(group1.id.to_s)
+ expect(graphql_data['group']['name']).to eq(group1.name)
+ expect(graphql_data['group']['path']).to eq(group1.path)
+ expect(graphql_data['group']['description']).to eq(group1.description)
+ expect(graphql_data['group']['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
+ expect(graphql_data['group']['avatarUrl']).to eq(group1.avatar_url(only_path: false))
+ expect(graphql_data['group']['webUrl']).to eq(group1.web_url)
+ expect(graphql_data['group']['requestAccessEnabled']).to eq(group1.request_access_enabled)
+ expect(graphql_data['group']['fullName']).to eq(group1.full_name)
+ expect(graphql_data['group']['fullPath']).to eq(group1.full_path)
+ expect(graphql_data['group']['parentId']).to eq(group1.parent_id)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+
+ post_graphql(query, current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it "does not return a group not attached to user1" do
+ private_group.add_owner(user2)
+
+ post_graphql(group_query(private_group), current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'avoids N+1 queries' do
+ post_graphql(group_query(group1), current_user: admin)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(group_query(group1), current_user: admin)
+ end.count
+
+ create(:project, namespace: group1)
+
+ expect do
+ post_graphql(group_query(group1), current_user: admin)
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns any existing group" do
+ post_graphql(group_query(private_group), current_user: admin)
+
+ expect(graphql_data['group']['name']).to eq(private_group.name)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+ post_graphql(query, current_user: admin)
+
+ expect(graphql_data['group']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index c2934430821..4f9f916f22e 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -7,8 +7,8 @@ describe 'getting an issue list for a project' do
let(:current_user) { create(:user) }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let!(:issues) do
- create(:issue, project: project, discussion_locked: true)
- create(:issue, project: project)
+ [create(:issue, project: project, discussion_locked: true),
+ create(:issue, project: project)]
end
let(:fields) do
<<~QUERY
@@ -47,6 +47,30 @@ describe 'getting an issue list for a project' do
expect(issues_data[1]['node']['discussionLocked']).to eq true
end
+ context 'when limiting the number of results' do
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ "issues(first: 1) { #{fields} }"
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ 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)
+
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
context 'when the user does not have access to the issue' do
it 'returns nil' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 66b9aae4b58..d50bae3dc47 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -51,6 +51,7 @@ describe API::GroupVariables do
expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
+ expect(json_response['variable_type']).to eq(variable.variable_type)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -94,17 +95,19 @@ describe API::GroupVariables do
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'creates variable with optional attributes' do
expect do
- post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
end.to change {group.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
end
it 'does not allow to duplicate variable key' do
@@ -145,7 +148,7 @@ describe API::GroupVariables do
initial_variable = group.variables.reload.first
value_before = initial_variable.value
- put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { value: 'VALUE_1_UP', protected: true }
+ put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true }
updated_variable = group.variables.reload.first
@@ -153,6 +156,7 @@ describe API::GroupVariables do
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
+ expect(json_response['variable_type']).to eq('file')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a0c64d295c0..25a312cb734 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -251,7 +251,7 @@ describe API::Helpers do
correlation_id: 'new-correlation-id'
}, extra: {})
- Gitlab::CorrelationId.use_id('new-correlation-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception)
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 1ce8f520962..bae0302f3ff 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -959,7 +959,9 @@ describe API::Internal do
it 'creates a new merge request' do
expect do
- post api('/internal/post_receive'), params: valid_params
+ Sidekiq::Testing.fake! do
+ post api('/internal/post_receive'), params: valid_params
+ end
end.to change { MergeRequest.count }.by(1)
end
@@ -998,15 +1000,13 @@ describe API::Internal do
context 'when the feature is disabled' do
it 'does not invoke MergeRequests::PushOptionsHandlerService' do
- Feature.disable(:mr_push_options)
+ stub_feature_flags(mr_push_options: false)
- expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+ expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
expect do
post api('/internal/post_receive'), params: valid_params
end.not_to change { MergeRequest.count }
-
- Feature.enable(:mr_push_options)
end
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 86484ce62f8..0fa34688371 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1480,12 +1480,20 @@ describe API::Issues do
let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
context 'by an admin' do
- it 'sets the creation time on the new issue' do
+ before do
post api("/projects/#{project.id}/issues", admin), params: params
+ end
+ it 'sets the creation time on the new issue' do
expect(response).to have_gitlab_http_status(201)
expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
end
+
+ it 'sets the system notes timestamp based on creation time' do
+ issue = Issue.find(json_response['id'])
+
+ expect(issue.resource_label_events.last.created_at).to be_like_time(creation_time)
+ end
end
context 'by a project owner' do
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index ed2ef4c730b..c14507de186 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -863,7 +863,7 @@ describe API::Jobs do
end
describe 'POST /projects/:id/jobs/:job_id/retry' do
- let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :failed, pipeline: pipeline) }
before do
post api("/projects/#{project.id}/jobs/#{job.id}/retry", api_user)
@@ -873,7 +873,7 @@ describe API::Jobs do
context 'user with :update_build permission' do
it 'retries non-running job' do
expect(response).to have_gitlab_http_status(201)
- expect(project.builds.first.status).to eq('canceled')
+ expect(project.builds.first.status).to eq('failed')
expect(json_response['status']).to eq('pending')
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 79edbb301f2..48869cab4da 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -236,7 +236,7 @@ describe API::Members do
params: { user_id: stranger.id, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
+ expect(json_response['message']['access_level']).to eq(["should be greater than or equal to Developer inherited membership from group #{parent.name}"])
end
it 'creates the member if group level is lower', :nested_groups do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5c94a87529b..007f3517e64 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1495,6 +1495,33 @@ describe API::MergeRequests do
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
+ context 'when the MR requires pipeline success' do
+ it 'returns 405 if the pipeline is missing' do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:merge_when_pipeline_succeeds).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
+
+ expect(response).to have_gitlab_http_status(405)
+ expect(json_response['message']).to eq('Not allowed: pipeline does not exist')
+ end
+ end
+
+ context 'when the request requires pipeline success' do
+ it 'returns 405 if the pipeline is missing' do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:merge_when_pipeline_succeeds).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user),
+ params: { merge_when_pipeline_succeeds: true }
+
+ expect(response).to have_gitlab_http_status(405)
+ expect(json_response['message']).to eq('Not allowed: pipeline does not exist')
+ end
+ end
+
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 870ef34437f..072bd02f2ac 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -91,6 +91,7 @@ describe API::PipelineSchedules do
let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
before do
+ pipeline_schedule.variables << build(:ci_pipeline_schedule_variable)
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
end
@@ -331,13 +332,14 @@ describe API::PipelineSchedules do
it 'creates pipeline_schedule_variable' do
expect do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
- params: params
+ params: params.merge(variable_type: 'file')
end.to change { pipeline_schedule.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['key']).to eq(params[:key])
expect(json_response['value']).to eq(params[:value])
+ expect(json_response['variable_type']).to eq('file')
end
end
@@ -389,11 +391,12 @@ describe API::PipelineSchedules do
context 'authenticated user with valid permissions' do
it 'updates pipeline_schedule_variable' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer),
- params: { value: 'updated_value' }
+ params: { value: 'updated_value', variable_type: 'file' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['value']).to eq('updated_value')
+ expect(json_response['variable_type']).to eq('file')
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 26158231444..35b3dd219f7 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -294,6 +294,7 @@ describe API::Pipelines do
expect(variable.key).to eq(expected_variable['key'])
expect(variable.value).to eq(expected_variable['value'])
+ expect(variable.variable_type).to eq(expected_variable['variable_type'])
end
end
@@ -314,7 +315,7 @@ describe API::Pipelines do
end
context 'variables given' do
- let(:variables) { [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
+ let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
it 'creates and returns a new pipeline using the given variables' do
expect do
@@ -330,7 +331,7 @@ describe API::Pipelines do
end
describe 'using variables conditions' do
- let(:variables) { [{ 'key' => 'STAGING', 'value' => 'true' }] }
+ let(:variables) { [{ 'variable_type' => 'env_var', 'key' => 'STAGING', 'value' => 'true' }] }
before do
config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } })
@@ -467,7 +468,7 @@ describe API::Pipelines do
subject
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" })
+ expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
end
end
end
@@ -488,7 +489,7 @@ describe API::Pipelines do
subject
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to contain_exactly({ "key" => "foo", "value" => "bar" })
+ expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
end
end
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 94e6ca2c07c..5357be3cdee 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -189,6 +189,7 @@ describe API::ProjectClusters do
{
name: 'test-cluster',
domain: 'domain.example.com',
+ managed: false,
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
@@ -220,6 +221,7 @@ describe API::ProjectClusters do
expect(cluster_result.project).to eq(project)
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
+ expect(cluster_result.managed).to be_falsy
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.namespace).to eq(namespace)
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
new file mode 100644
index 00000000000..43df9993eb9
--- /dev/null
+++ b/spec/requests/api/project_events_spec.rb
@@ -0,0 +1,156 @@
+require 'spec_helper'
+
+describe API::ProjectEvents do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+
+ describe 'GET /projects/:id/events' do
+ context 'when unauthenticated ' do
+ it 'returns 404 for private project' do
+ get api("/projects/#{private_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 200 status for a public project' do
+ public_project = create(:project, :public)
+
+ get api("/projects/#{public_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'with inaccessible events' do
+ let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
+ let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
+ let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
+ let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
+ let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
+
+ it 'returns only accessible events' do
+ get api("/projects/#{public_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns all events when the user has access' do
+ get api("/projects/#{public_project.id}/events", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(2)
+ end
+ end
+
+ context 'pagination' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 1'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-10'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-11'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 2'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-12'))
+ end
+
+ it 'correctly returns the second page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 1')
+ expect(titles).not_to include('Confidential event')
+ end
+
+ it 'correctly returns the first page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 2')
+ expect(titles).not_to include('Confidential event')
+ end
+ end
+
+ context 'when not permitted to read' do
+ it 'returns 404' do
+ get api("/projects/#{private_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns project events' do
+ get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", 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.size).to eq(1)
+ end
+
+ it 'returns 404 if project does not exist' do
+ get api("/projects/1234/events", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context 'when the requesting token does not have "api" scope' do
+ let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
+
+ it 'returns a "403" response' do
+ get api("/projects/#{private_project.id}/events", personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when exists some events' do
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+
+ before do
+ create_event(merge_request1)
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.count
+
+ create_event(merge_request2)
+
+ expect do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.not_to exceed_all_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
+ end
+
+ def create_event(target)
+ create(:event, project: private_project, author: user, target: target)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 352ea448c00..16d306f39cd 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -504,8 +504,9 @@ describe API::Projects do
project4.add_reporter(user2)
end
- it 'returns an array of groups the user has at least developer access' do
+ it 'returns an array of projects the user has at least developer access' do
get api('/projects', user2), params: { min_access_level: 30 }
+
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -1047,7 +1048,6 @@ describe API::Projects do
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['owner']).to be_a Hash
- expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
expect(json_response['issues_enabled']).to be_present
@@ -1135,7 +1135,9 @@ describe API::Projects do
'path' => user.namespace.path,
'kind' => user.namespace.kind,
'full_path' => user.namespace.full_path,
- 'parent_id' => nil
+ 'parent_id' => nil,
+ 'avatar_url' => user.avatar_url,
+ 'web_url' => Gitlab::Routing.url_helpers.user_url(user)
})
end
@@ -1337,6 +1339,37 @@ describe API::Projects do
end
end
end
+
+ context 'when project belongs to a group namespace' do
+ let(:group) { create(:group, :with_avatar) }
+ let(:project) { create(:project, namespace: group) }
+ let!(:project_member) { create(:project_member, :developer, user: user, project: project) }
+
+ it 'returns group web_url and avatar_url' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ group_data = json_response['namespace']
+ expect(group_data['web_url']).to eq(group.web_url)
+ expect(group_data['avatar_url']).to eq(group.avatar_url)
+ end
+ end
+
+ context 'when project belongs to a user namespace' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'returns user web_url and avatar_url' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ user_data = json_response['namespace']
+ expect(user_data['web_url']).to eq("http://localhost/#{user.username}")
+ expect(user_data['avatar_url']).to eq(user.avatar_url)
+ end
+ end
end
context 'with external authorization' do
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 71ec091c42c..8603fa2a73d 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -52,7 +52,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases", maintainer)
- expect(response).to match_response_schema('releases')
+ expect(response).to match_response_schema('public_api/v4/releases')
end
end
@@ -69,10 +69,25 @@ describe API::Releases do
end
context 'when user is a guest' do
- it 'responds 403 Forbidden' do
+ let!(:release) do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ created_at: 2.days.ago)
+ end
+
+ it 'responds 200 OK' do
get api("/projects/#{project.id}/releases", guest)
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "does not expose tag, commit and source code" do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to match_response_schema('public_api/v4/release/releases_for_guest')
+ expect(json_response[0]['assets']['count']).to eq(release.links.count)
end
context 'when project is public' do
@@ -83,6 +98,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it "exposes tag, commit and source code" do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to match_response_schema('public_api/v4/releases')
+ expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count)
+ end
end
end
@@ -135,7 +157,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
it 'contains source information as assets' do
@@ -225,6 +247,17 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it "exposes tag and commit" do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ created_at: 2.days.ago)
+ get api("/projects/#{project.id}/releases/v0.1", guest)
+
+ expect(response).to match_response_schema('public_api/v4/release')
+ end
end
end
end
@@ -306,7 +339,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
it 'does not create a new tag' do
@@ -378,7 +411,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
end
@@ -532,7 +565,7 @@ describe API::Releases do
it 'matches response schema' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
context 'when user tries to update sha' do
@@ -624,7 +657,7 @@ describe API::Releases do
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
context 'when there are no corresponding releases' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 3585a827838..b331da1acba 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -68,7 +68,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
post api('/runners'), params: { token: group.runners_token }
expect(response).to have_http_status 201
- expect(group.runners.size).to eq(1)
+ expect(group.runners.reload.size).to eq(1)
runner = Ci::Runner.first
expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(group.runners_token)
@@ -168,6 +168,32 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when access_level is provided for Runner' do
+ context 'when access_level is set to ref_protected' do
+ it 'creates runner' do
+ post api('/runners'), params: {
+ token: registration_token,
+ access_level: 'ref_protected'
+ }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.ref_protected?).to be true
+ end
+ end
+
+ context 'when access_level is set to not_protected' do
+ it 'creates runner' do
+ post api('/runners'), params: {
+ token: registration_token,
+ access_level: 'not_protected'
+ }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.ref_protected?).to be false
+ end
+ end
+ end
+
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), params: {
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fffe878ddbd..d898319e709 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -378,7 +378,7 @@ describe API::Tags do
post api(route, user), params: { description: description }
expect(response).to have_gitlab_http_status(201)
- expect(response).to match_response_schema('public_api/v4/release')
+ expect(response).to match_response_schema('public_api/v4/release/tag_release')
expect(json_response['tag_name']).to eq(tag_name)
expect(json_response['description']).to eq(description)
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 5df6baf0ddf..cc07869a744 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -43,6 +43,7 @@ describe API::Variables do
expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -80,17 +81,19 @@ describe API::Variables do
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'creates variable with optional attributes' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
end.to change {project.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
end
it 'does not allow to duplicate variable key' do
@@ -125,7 +128,7 @@ describe API::Variables do
initial_variable = project.variables.reload.first
value_before = initial_variable.value
- put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { value: 'VALUE_1_UP', protected: true }
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true }
updated_variable = project.variables.reload.first
@@ -133,6 +136,7 @@ describe API::Variables do
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
+ expect(updated_variable.variable_type).to eq('file')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 49021f5d1b7..a12646ea222 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -251,8 +251,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), nil] }
- let(:other_user_get_args) { [rss_url(other_user), nil] }
+ let(:get_args) { [rss_url(user), params: nil] }
+ let(:other_user_get_args) { [rss_url(other_user), params: nil] }
it_behaves_like 'rate-limited token-authenticated requests'
end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 53271550e8b..00ca394a50b 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -133,5 +133,19 @@ describe "Groups", "routing" do
let(:resource) { create(:group, parent: parent, path: 'activity') }
end
end
+
+ describe 'subgroup "boards"' do
+ it 'shows group show page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq/boards', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/boards')).to route_to('groups#show', id: 'gitlabhq/boards')
+ end
+
+ it 'shows boards index page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
+ end
+ end
end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
new file mode 100644
index 00000000000..6a041ffdd6c
--- /dev/null
+++ b/spec/routing/uploads_routing_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Uploads', 'routing' do
+ it 'allows creating uploads for personal snippets' do
+ expect(post('/uploads/personal_snippet?id=1')).to route_to(
+ controller: 'uploads',
+ action: 'create',
+ model: 'personal_snippet',
+ id: '1'
+ )
+ end
+
+ it 'does not allow creating uploads for other models' do
+ UploadsController::MODEL_CLASSES.keys.compact.each do |model|
+ next if model == 'personal_snippet'
+
+ expect(post("/uploads/#{model}?id=1")).not_to be_routable
+ end
+ end
+end
diff --git a/spec/rubocop/cop/include_action_view_context_spec.rb b/spec/rubocop/cop/include_action_view_context_spec.rb
new file mode 100644
index 00000000000..c888555b54f
--- /dev/null
+++ b/spec/rubocop/cop/include_action_view_context_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/include_action_view_context'
+
+describe RuboCop::Cop::IncludeActionViewContext do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `ActionView::Context` is included' do
+ let(:source) { 'include ActionView::Context' }
+ let(:correct_source) { 'include ::Gitlab::ActionViewOutput::Context' }
+
+ it 'registers an offense' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['ActionView::Context'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+
+ context 'when `ActionView::Context` is not included' do
+ it 'registers no offense' do
+ inspect_source('include Context')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 7e151c3744e..f38a18fcf59 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -21,6 +21,10 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil
end
+ it 'has can_uninstall' do
+ expect(subject[:can_uninstall]).to be_falsey
+ end
+
context 'non-helm application' do
let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index dba7fd91747..47f767ae4ab 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -137,7 +137,7 @@ describe PipelineEntity do
context 'when pipeline is detached merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
it 'makes detached flag true' do
expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
@@ -185,7 +185,7 @@ describe PipelineEntity do
context 'when pipeline is merge request pipeline' do
let(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
let(:project) { merge_request.target_project }
- let(:pipeline) { merge_request.merge_request_pipelines.first }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
it 'makes detached flag false' do
expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index d9023036534..54e6abc2d3a 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -156,8 +156,8 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
-
expected_queries = Gitlab.ee? ? 38 : 31
+
expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 2034c7891ef..6b1185d1283 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -48,6 +48,10 @@ describe StageEntity do
expect(subject[:title]).to eq 'test: passed'
end
+ it 'does not contain play_details info' do
+ expect(subject[:status][:action]).not_to be_present
+ end
+
context 'when the jobs should be grouped' do
let(:entity) { described_class.new(stage, request: request, grouped: true) }
@@ -66,5 +70,29 @@ describe StageEntity do
end
end
end
+
+ context 'with a skipped stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'skipped') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
+
+ context 'with a scheduled stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'scheduled') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
+
+ context 'with a manual stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'manual') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
end
end
diff --git a/spec/serializers/stage_serializer_spec.rb b/spec/serializers/stage_serializer_spec.rb
new file mode 100644
index 00000000000..aae17cfbcb9
--- /dev/null
+++ b/spec/serializers/stage_serializer_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StageSerializer do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:resource) { create(:ci_stage_entity) }
+
+ let(:serializer) do
+ described_class.new(current_user: user, project: project)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'with a single entity' do
+ it 'serializes the stage object' do
+ expect(subject[:name]).to eq(resource.name)
+ end
+ end
+
+ context 'with an array of entities' do
+ let(:resource) { create_list(:ci_stage_entity, 2) }
+
+ it 'serializes the array of pipelines' do
+ expect(subject).not_to be_empty
+ 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 6f8a76e9d56..9a3ac75e418 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -25,7 +25,8 @@ describe Ci::CreatePipelineService do
merge_request: nil,
push_options: nil,
source_sha: nil,
- target_sha: nil)
+ target_sha: nil,
+ save_on_errors: true)
params = { ref: ref,
before: '00000000',
after: after,
@@ -36,7 +37,7 @@ describe Ci::CreatePipelineService do
target_sha: target_sha }
described_class.new(project, user, params).execute(
- source, trigger_request: trigger_request, merge_request: merge_request)
+ source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request)
end
# rubocop:enable Metrics/ParameterLists
@@ -57,6 +58,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to eq(project.ci_pipelines.last)
expect(pipeline).to have_attributes(user: user)
expect(pipeline).to have_attributes(status: 'pending')
+ expect(pipeline.iid).not_to be_nil
expect(pipeline.repository_source?).to be true
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
end
@@ -446,6 +448,43 @@ describe Ci::CreatePipelineService do
expect(Ci::Build.all).to be_empty
expect(Ci::Pipeline.count).to eq(0)
end
+
+ describe '#iid' do
+ let(:internal_id) do
+ InternalId.find_by(project_id: project.id, usage: :ci_pipelines)
+ end
+
+ before do
+ expect_any_instance_of(Ci::Pipeline).to receive(:ensure_project_iid!)
+ .and_call_original
+ end
+
+ context 'when ci_pipeline_rewind_iid is enabled' do
+ before do
+ stub_feature_flags(ci_pipeline_rewind_iid: true)
+ end
+
+ it 'rewinds iid' do
+ result = execute_service
+
+ expect(result).not_to be_persisted
+ expect(internal_id.last_value).to eq(0)
+ end
+ end
+
+ context 'when ci_pipeline_rewind_iid is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_rewind_iid: false)
+ end
+
+ it 'does not rewind iid' do
+ result = execute_service
+
+ expect(result).not_to be_persisted
+ expect(internal_id.last_value).to eq(1)
+ end
+ end
+ end
end
context 'with manual actions' do
@@ -734,7 +773,7 @@ describe Ci::CreatePipelineService do
end
end
- describe 'Merge request pipelines' do
+ describe 'Pipelines for merge requests' do
let(:pipeline) do
execute_service(source: source,
merge_request: merge_request,
@@ -778,12 +817,14 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: Gitlab::Git.ref_name(ref_name),
+ source_branch: 'feature',
target_project: project,
target_branch: 'master')
end
- it 'creates a merge request pipeline' do
+ let(:ref_name) { merge_request.ref_path }
+
+ it 'creates a detached merge request pipeline' do
expect(pipeline).to be_persisted
expect(pipeline).to be_merge_request_event
expect(pipeline.merge_request).to eq(merge_request)
@@ -798,6 +839,13 @@ describe Ci::CreatePipelineService do
expect(pipeline.target_sha).to be_nil
end
+ it 'schedules update for the head pipeline of the merge request' do
+ expect(UpdateHeadPipelineForMergeRequestWorker)
+ .to receive(:perform_async).with(merge_request.id)
+
+ pipeline
+ end
+
context 'when target sha is specified' do
let(:target_sha) { merge_request.target_branch_sha }
@@ -819,15 +867,16 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: Gitlab::Git.ref_name(ref_name),
+ source_branch: 'feature',
target_project: target_project,
target_branch: 'master')
end
+ let(:ref_name) { 'refs/heads/feature' }
let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- it 'creates a merge request pipeline in the forked project' do
+ it 'creates a legacy detached merge request pipeline in the forked project' do
expect(pipeline).to be_persisted
expect(project.ci_pipelines).to eq([pipeline])
expect(target_project.ci_pipelines).to be_empty
@@ -845,7 +894,7 @@ describe Ci::CreatePipelineService do
}
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."])
end
@@ -855,7 +904,7 @@ describe Ci::CreatePipelineService do
context 'when merge request is not specified' do
let(:merge_request) { nil }
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:merge_request]).to eq(["can't be blank"])
end
@@ -889,7 +938,7 @@ describe Ci::CreatePipelineService do
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -900,7 +949,7 @@ describe Ci::CreatePipelineService do
context 'when merge request is not specified' do
let(:merge_request) { nil }
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -929,7 +978,7 @@ describe Ci::CreatePipelineService do
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -960,7 +1009,7 @@ describe Ci::CreatePipelineService do
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -989,7 +1038,7 @@ describe Ci::CreatePipelineService do
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb
new file mode 100644
index 00000000000..5d812745c7f
--- /dev/null
+++ b/spec/services/ci/play_manual_stage_service_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PlayManualStageService, '#execute' do
+ let(:current_user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, user: current_user) }
+ let(:project) { pipeline.project }
+ let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
+ let(:stage_status) { 'manual' }
+
+ let(:stage) do
+ create(:ci_stage_entity,
+ pipeline: pipeline,
+ project: project,
+ name: 'test')
+ end
+
+ before do
+ project.add_maintainer(current_user)
+ create_builds_for_stage(status: stage_status)
+ end
+
+ context 'when pipeline has manual builds' do
+ before do
+ service.execute(stage)
+ end
+
+ it 'starts manual builds from pipeline' do
+ expect(pipeline.builds.manual.count).to eq(0)
+ end
+
+ it 'updates manual builds' do
+ pipeline.builds.each do |build|
+ expect(build.user).to eq(current_user)
+ end
+ end
+ end
+
+ context 'when pipeline has no manual builds' do
+ let(:stage_status) { 'failed' }
+
+ before do
+ service.execute(stage)
+ end
+
+ it 'does not update the builds' do
+ expect(pipeline.builds.failed.count).to eq(3)
+ end
+ end
+
+ context 'when user does not have permission on a specific build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:play)
+ .and_raise(Gitlab::Access::AccessDeniedError)
+
+ service.execute(stage)
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error)
+ .exactly(stage.builds.manual.count)
+
+ service.execute(stage)
+ end
+ end
+
+ def create_builds_for_stage(options)
+ options.merge!({
+ when: 'manual',
+ pipeline: pipeline,
+ stage: stage.name,
+ stage_id: stage.id,
+ user: pipeline.user
+ })
+
+ create_list(:ci_build, 3, options)
+ end
+end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index fc630b7098a..df2376384ca 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -30,7 +30,7 @@ describe Ci::RetryBuildService do
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_performance
- job_artifacts_codequality scheduled_at].freeze
+ job_artifacts_codequality job_artifacts_metrics scheduled_at].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 31b8d540356..890fa5bc009 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -105,6 +105,82 @@ describe Ci::StopEnvironmentsService do
end
end
+ describe '#execute_for_merge_request' do
+ subject { service.execute_for_merge_request(merge_request) }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+ let(:user) { create(:user) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+ let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) }
+
+ before do
+ review_job.deployment.success!
+ end
+
+ it 'has active environment at first' do
+ expect(pipeline.environments.first).to be_available
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'stops the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_stopped
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'does not raise exception' do
+ expect { subject }.not_to raise_exception
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+ end
+
def expect_environment_stopped_on(branch)
expect_any_instance_of(Environment)
.to receive(:stop!)
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
index 8ad90aaf720..a54bd85a11a 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -18,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context "when phase is #{a_phase}" do
- context 'when not timeouted' do
+ context 'when not timed_out' do
it 'reschedule a new check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod)
@@ -113,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted, :updating) }
+ let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
@@ -174,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted) }
+ let(:application) { create(:clusters_applications_helm, :timed_out) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
new file mode 100644
index 00000000000..9ab83d913f5
--- /dev/null
+++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::CheckUninstallProgressService do
+ RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
+
+ let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
+ let(:service) { described_class.new(application) }
+ let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
+ let(:errors) { nil }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:installation_errors).and_return(errors)
+ allow(service).to receive(:remove_installation_pod)
+ end
+
+ shared_examples 'a not yet terminated installation' do |a_phase|
+ let(:phase) { a_phase }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ context "when phase is #{a_phase}" do
+ context 'when not timed_out' do
+ it 'reschedule a new check' do
+ expect(worker_class).to receive(:perform_in).once
+ expect(service).not_to receive(:remove_installation_pod)
+
+ expect do
+ service.execute
+
+ application.reload
+ end.not_to change(application, :status)
+
+ expect(application.status_reason).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'when application is installing' do
+ RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+
+ context 'when installation POD succeeded' do
+ let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'destroys the application' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_destroyed
+ end
+
+ context 'an error occurs while destroying' do
+ before do
+ expect(application).to receive(:destroy!).once.and_raise("destroy failed")
+ end
+
+ it 'still removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'makes the application uninstall_errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
+ end
+ end
+ end
+
+ context 'when installation POD failed' do
+ let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
+ let(:errors) { 'test installation failed' }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ context 'when timed out' do
+ let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ context 'when installation raises a Kubeclient::HttpError' do
+ let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:logger) { service.send(:logger) }
+ let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
+
+ before do
+ application.update!(cluster: cluster)
+
+ expect(service).to receive(:installation_phase).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'Unauthorized' }
+ let(:error_code) { 401 }
+ end
+
+ it 'shows the response code from the error' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Kubernetes error: 401')
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 20555873503..bb86a742f0e 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -151,8 +151,8 @@ describe Clusters::Applications::CreateService do
'helm' | :application_helm | true | false
'ingress' | :application_ingress | true | true
'runner' | :application_runner | true | true
+ 'prometheus' | :application_prometheus | true | true
'jupyter' | :application_jupyter | false | true
- 'prometheus' | :application_prometheus | false | true
end
with_them do
diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb
new file mode 100644
index 00000000000..8d9dc6a0f11
--- /dev/null
+++ b/spec/services/clusters/applications/destroy_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::DestroyService, '#execute' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:user) { create(:user) }
+ let(:params) { { application: 'prometheus' } }
+ let(:service) { described_class.new(cluster, user, params) }
+ let(:test_request) { double }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ subject { service.execute(test_request) }
+
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ context 'application is not installed' do
+ it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
+ .and not_change { Clusters::Applications::Prometheus.count }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+
+ context 'application is installed' do
+ context 'application is schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'makes application scheduled!' do
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+
+ it 'schedules UninstallWorker' do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id)
+
+ subject
+ end
+ end
+
+ context 'application is not schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :updating, cluster: cluster)
+ end
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { StateMachines::InvalidTransition }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb
new file mode 100644
index 00000000000..16497d752b2
--- /dev/null
+++ b/spec/services/clusters/applications/uninstall_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::UninstallService, '#execute' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand))
+ allow(worker_class).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application to be uninstalling' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_uninstalling
+ end
+
+ it 'schedule async installation status check' do
+ expect(worker_class).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when k8s cluster communication fails' do
+ let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to match('Kubernetes error: 500')
+ end
+ end
+
+ context 'a non kubernetes error happens' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:error) { StandardError.new('something bad happened') }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Failed to uninstall.')
+ end
+ end
+end
diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb
index da0cb42b3a1..f3e852726f4 100644
--- a/spec/services/clusters/build_service_spec.rb
+++ b/spec/services/clusters/build_service_spec.rb
@@ -21,5 +21,13 @@ describe Clusters::BuildService do
is_expected.to be_group_type
end
end
+
+ describe 'when cluster subject is an instance' do
+ let(:cluster_subject) { Clusters::Instance.new }
+
+ it 'sets the cluster_type to instance_type' do
+ is_expected.to be_instance_type
+ end
+ end
end
end
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
index 58ab3c3cf73..94c35228955 100644
--- a/spec/services/clusters/refresh_service_spec.rb
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -93,15 +93,39 @@ describe Clusters::RefreshService do
let(:group) { cluster.group }
let(:project) { create(:project, group: group) }
- include_examples 'creates a kubernetes namespace'
+ context 'when ci_preparing_state feature flag is enabled' do
+ include_examples 'does not create a kubernetes namespace'
- context 'when project already has kubernetes namespace' do
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
+ end
+
+ context 'when ci_preparing_state feature flag is disabled' do
before do
- create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ stub_feature_flags(ci_preparing_state: false)
end
- include_examples 'does not create a kubernetes namespace'
+ include_examples 'creates a kubernetes namespace'
+
+ context 'when project already has kubernetes namespace' do
+ before do
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
+ end
+
+ include_examples 'does not create a kubernetes namespace'
+ end
end
end
+
+ context 'cluster is not managed' do
+ let!(:cluster) { create(:cluster, :project, :not_managed, projects: [project]) }
+
+ include_examples 'does not create a kubernetes namespace'
+ end
end
end
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 0e4ef69ec19..fadd43635a6 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -19,5 +19,18 @@ describe CompareService do
it { expect(subject.diffs.size).to eq(3) }
end
+
+ context 'compare with target branch that does not exist' do
+ subject { service.execute(project, 'non-existent-ref') }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'compare with source branch that does not exist' do
+ let(:service) { described_class.new(project, 'non-existent-branch') }
+ subject { service.execute(project, 'non-existent-ref') }
+
+ it { expect(subject).to be_nil }
+ end
end
end
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
index 43a70d33d2d..b8064c2cbc1 100644
--- a/spec/services/delete_branch_service_spec.rb
+++ b/spec/services/delete_branch_service_spec.rb
@@ -8,20 +8,24 @@ describe DeleteBranchService do
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
+ shared_examples 'a deleted branch' do |branch_name|
+ it 'removes the branch' do
+ expect(branch_exists?(branch_name)).to be true
+
+ result = service.execute(branch_name)
+
+ expect(result.status).to eq :success
+ expect(branch_exists?(branch_name)).to be false
+ end
+ end
+
describe '#execute' do
context 'when user has access to push to repository' do
before do
project.add_developer(user)
end
- it 'removes the branch' do
- expect(branch_exists?('feature')).to be true
-
- result = service.execute('feature')
-
- expect(result[:status]).to eq :success
- expect(branch_exists?('feature')).to be false
- end
+ it_behaves_like 'a deleted branch', 'feature'
end
context 'when user does not have access to push to repository' do
@@ -30,8 +34,8 @@ describe DeleteBranchService do
result = service.execute('feature')
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'You dont have push access to repo'
+ expect(result.status).to eq :error
+ expect(result.message).to eq 'You dont have push access to repo'
expect(branch_exists?('feature')).to be true
end
end
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index 2c6e86069f3..888eea6e91e 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -64,6 +64,25 @@ describe Lfs::FileTransformer do
expect(result.encoding).to eq('text')
end
+ context 'when an actual file is passed' do
+ let(:file) { Tempfile.new(file_path) }
+
+ before do
+ file.write(file_content)
+ file.rewind
+ end
+
+ after do
+ file.unlink
+ end
+
+ it "creates an LfsObject with the file's content" do
+ subject.new_file(file_path, file)
+
+ expect(LfsObject.last.file.read).to eq file_content
+ end
+ end
+
context "when doesn't use LFS" do
let(:file_path) { 'other.filetype' }
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index aa7dfda4950..ffa612cf315 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -74,6 +74,14 @@ describe MergeRequests::CloseService do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
+
context 'current user is not authorized to close merge request' do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index c795176a1e4..ed48f4b1e44 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -195,7 +195,7 @@ describe MergeRequests::CreateService do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(1)
+ expect(merge_request.pipelines_for_merge_request.count).to eq(1)
expect(merge_request.actual_head_pipeline).to be_detached_merge_request_pipeline
end
@@ -247,7 +247,7 @@ describe MergeRequests::CreateService do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
+ expect(merge_request.pipelines_for_merge_request.count).to eq(0)
end
end
@@ -281,7 +281,7 @@ describe MergeRequests::CreateService do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
+ expect(merge_request.pipelines_for_merge_request.count).to eq(0)
end
end
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 24d09c1fd00..0ac23050caf 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -104,7 +104,7 @@ describe MergeRequests::MergeToRefService do
it_behaves_like 'MergeService for target ref'
end
- context 'when merge commit with squash' do
+ context 'when merge commit with squash', :quarantine do
before do
merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature')
end
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 7b87913ab8b..ffc86f68469 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -62,5 +62,13 @@ describe MergeRequests::PostMergeService do
expect(merge_request.reload).to be_merged
end
+
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
end
end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 0d9523a4c2c..a443e4588d9 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -73,7 +73,7 @@ describe MergeRequests::RebaseService do
end
context 'valid params' do
- describe 'successful rebase' do
+ shared_examples_for 'a service that can execute a successful rebase' do
before do
service.execute(merge_request)
end
@@ -99,6 +99,22 @@ describe MergeRequests::RebaseService do
end
end
+ context 'when the two_step_rebase feature is enabled' do
+ before do
+ stub_feature_flags(two_step_rebase: true)
+ end
+
+ it_behaves_like 'a service that can execute a successful rebase'
+ end
+
+ context 'when the two_step_rebase feature is disabled' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'a service that can execute a successful rebase'
+ end
+
context 'fork' do
describe 'successful fork rebase' do
let(:forked_project) do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d20b2d81763..7258428589f 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -166,8 +166,8 @@ describe MergeRequests::RefreshService do
it 'create detached merge request pipeline with commits' do
expect { subject }
- .to change { @merge_request.merge_request_pipelines.count }.by(1)
- .and change { @another_merge_request.merge_request_pipelines.count }.by(0)
+ .to change { @merge_request.pipelines_for_merge_request.count }.by(1)
+ .and change { @another_merge_request.pipelines_for_merge_request.count }.by(0)
expect(@merge_request.has_commits?).to be_truthy
expect(@another_merge_request.has_commits?).to be_falsy
@@ -175,13 +175,13 @@ describe MergeRequests::RefreshService do
it 'does not create detached merge request pipeline for forked project' do
expect { subject }
- .not_to change { @fork_merge_request.merge_request_pipelines.count }
+ .not_to change { @fork_merge_request.pipelines_for_merge_request.count }
end
it 'create detached merge request pipeline for non-fork merge request' do
subject
- expect(@merge_request.merge_request_pipelines.first)
+ expect(@merge_request.pipelines_for_merge_request.first)
.to be_detached_merge_request_pipeline
end
@@ -190,7 +190,7 @@ describe MergeRequests::RefreshService do
it 'does not create detached merge request pipeline' do
expect { subject }
- .not_to change { @merge_request.merge_request_pipelines.count }
+ .not_to change { @merge_request.pipelines_for_merge_request.count }
end
end
@@ -199,9 +199,9 @@ describe MergeRequests::RefreshService do
it 'creates legacy detached merge request pipeline for fork merge request' do
expect { subject }
- .to change { @fork_merge_request.merge_request_pipelines.count }.by(1)
+ .to change { @fork_merge_request.pipelines_for_merge_request.count }.by(1)
- expect(@fork_merge_request.merge_request_pipelines.first)
+ expect(@fork_merge_request.pipelines_for_merge_request.first)
.to be_legacy_detached_merge_request_pipeline
end
end
@@ -214,7 +214,7 @@ describe MergeRequests::RefreshService do
it 'create legacy detached merge request pipeline for non-fork merge request' do
subject
- expect(@merge_request.merge_request_pipelines.first)
+ expect(@merge_request.pipelines_for_merge_request.first)
.to be_legacy_detached_merge_request_pipeline
end
end
@@ -245,11 +245,11 @@ describe MergeRequests::RefreshService do
it 'does not re-create a duplicate detached merge request pipeline' do
expect do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
- end.to change { @merge_request.merge_request_pipelines.count }.by(1)
+ end.to change { @merge_request.pipelines_for_merge_request.count }.by(1)
expect do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
- end.not_to change { @merge_request.merge_request_pipelines.count }
+ end.not_to change { @merge_request.pipelines_for_merge_request.count }
end
end
end
@@ -266,7 +266,7 @@ describe MergeRequests::RefreshService do
it 'does not create a detached merge request pipeline' do
expect { subject }
- .not_to change { @merge_request.merge_request_pipelines.count }
+ .not_to change { @merge_request.pipelines_for_merge_request.count }
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8e34406133a..4b40c86574f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -643,6 +643,64 @@ describe NotificationService, :mailer do
end
end
+ describe 'Participating project notification settings have priority over group and global settings if available' do
+ let!(:group) { create(:group) }
+ let!(:maintainer) { group.add_owner(create(:user, username: 'maintainer')).user }
+ let!(:user1) { group.add_developer(create(:user, username: 'user_with_project_and_custom_setting')).user }
+
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: '' }
+
+ before do
+ reset_delivered_emails!
+
+ create_notification_setting(user1, project, :participating)
+ end
+
+ context 'custom on group' do
+ [nil, true].each do |new_issue_value|
+ value_caption = new_issue_value || 'nil'
+ it "does not send an email to user1 when a new issue is created and new_issue is set to #{value_caption}" do
+ update_custom_notification(:new_issue, user1, resource: group, value: new_issue_value)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+ end
+
+ context 'watch on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(group).update!(level: :watch)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+
+ context 'custom on global, global on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(nil).update!(level: :custom)
+
+ user1.notification_settings_for(group).update!(level: :global)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+
+ context 'watch on global, global on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(nil).update!(level: :watch)
+
+ user1.notification_settings_for(group).update!(level: :global)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+ end
+
describe 'Issues' do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
@@ -660,7 +718,7 @@ describe NotificationService, :mailer do
end
describe '#new_issue' do
- it do
+ it 'notifies the expected users' do
notification.new_issue(issue, @u_disabled)
should_email(assignee)
@@ -1639,7 +1697,7 @@ describe NotificationService, :mailer do
end
describe '#project_was_moved' do
- it do
+ it 'notifies the expected users' do
notification.project_was_moved(project, "gitlab/gitlab")
should_email(@u_watcher)
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
index 29eabc86327..5c246854eb7 100644
--- a/spec/services/projects/cleanup_service_spec.rb
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -6,13 +6,13 @@ describe Projects::CleanupService do
let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) }
let(:object_map) { project.bfg_object_map }
+ let(:cleaner) { service.__send__(:repository_cleaner) }
+
subject(:service) { described_class.new(project) }
describe '#execute' do
- it 'runs the apply_bfg_object_map gitaly RPC' do
- expect_next_instance_of(Gitlab::Git::RepositoryCleaner) do |cleaner|
- expect(cleaner).to receive(:apply_bfg_object_map).with(kind_of(IO))
- end
+ it 'runs the apply_bfg_object_map_stream gitaly RPC' do
+ expect(cleaner).to receive(:apply_bfg_object_map_stream).with(kind_of(IO))
service.execute
end
@@ -37,10 +37,91 @@ describe Projects::CleanupService do
expect(object_map.exists?).to be_falsy
end
+ context 'with a tainted merge request diff' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:diff) { merge_request.merge_request_diff }
+ let(:entry) { build_entry(diff.commits.first.id) }
+
+ before do
+ allow(cleaner)
+ .to receive(:apply_bfg_object_map_stream)
+ .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
+ end
+
+ it 'removes the tainted commit from the database' do
+ service.execute
+
+ expect(MergeRequestDiff.exists?(diff.id)).to be_falsy
+ end
+
+ it 'ignores non-commit responses from Gitaly' do
+ entry.type = :UNKNOWN
+
+ service.execute
+
+ expect(MergeRequestDiff.exists?(diff.id)).to be_truthy
+ end
+ end
+
+ context 'with a tainted diff note' do
+ let(:diff_note) { create(:diff_note_on_commit, project: project) }
+ let(:note_diff_file) { diff_note.note_diff_file }
+ let(:entry) { build_entry(diff_note.commit_id) }
+
+ let(:highlight_cache) { Gitlab::DiscussionsDiff::HighlightCache }
+ let(:cache_id) { note_diff_file.id }
+
+ before do
+ allow(cleaner)
+ .to receive(:apply_bfg_object_map_stream)
+ .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
+ end
+
+ it 'removes the tainted commit from the database' do
+ service.execute
+
+ expect(NoteDiffFile.exists?(note_diff_file.id)).to be_falsy
+ end
+
+ it 'removes the highlight cache from redis' do
+ write_cache(highlight_cache, cache_id, [{}])
+
+ expect(read_cache(highlight_cache, cache_id)).not_to be_nil
+
+ service.execute
+
+ expect(read_cache(highlight_cache, cache_id)).to be_nil
+ end
+
+ it 'ignores non-commit responses from Gitaly' do
+ entry.type = :UNKNOWN
+
+ service.execute
+
+ expect(NoteDiffFile.exists?(note_diff_file.id)).to be_truthy
+ end
+ end
+
it 'raises an error if no object map can be found' do
object_map.remove!
expect { service.execute }.to raise_error(described_class::NoUploadError)
end
end
+
+ def build_entry(old_oid)
+ Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new(
+ type: :COMMIT,
+ old_oid: old_oid,
+ new_oid: Gitlab::Git::BLANK_SHA
+ )
+ end
+
+ def read_cache(cache, key)
+ cache.read_multiple([key]).first
+ end
+
+ def write_cache(cache, key, value)
+ cache.write_multiple(key => value)
+ end
end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 368cf123c3e..f651db70cbd 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -81,6 +81,9 @@ describe Projects::HousekeepingService do
# At push 10, 20, ... (except those above)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
+ # At push 6, 12, 18, ... (except those above)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :pack_refs, :the_lease_key, :the_uuid)
+ .exactly(27).times
201.times do
subject.increment!
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 7f233a52f50..d9f9ede8ecd 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -5,15 +5,11 @@ require 'spec_helper'
describe Projects::ImportService do
let!(:project) { create(:project) }
let(:user) { project.creator }
- let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
subject { described_class.new(project, user) }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
- allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
end
describe '#async?' do
@@ -77,7 +73,6 @@ describe Projects::ImportService do
context 'when repository creation succeeds' do
it 'does not download lfs files' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -114,7 +109,6 @@ describe Projects::ImportService do
context 'when repository import scheduled' do
it 'does not download lfs objects' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -130,7 +124,7 @@ describe Projects::ImportService do
it 'succeeds if repository import is successful' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({})
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :success)
result = subject.execute
@@ -146,6 +140,19 @@ describe Projects::ImportService do
expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]"
end
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
+ expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
+
context 'when repository import scheduled' do
before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
@@ -155,10 +162,7 @@ describe Projects::ImportService do
it 'downloads lfs objects if lfs_enabled is enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(true)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
@@ -166,7 +170,6 @@ describe Projects::ImportService do
it 'does not download lfs objects if lfs_enabled is not enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(false)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -208,7 +211,6 @@ describe Projects::ImportService do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -216,13 +218,22 @@ describe Projects::ImportService do
it 'does not have a custom repository importer downloads lfs objects' do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
+
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index f1c0f5b9576..d8427d0bf78 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadLinkListService do
@@ -85,7 +84,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
end
describe '#get_download_links' do
- it 'raise errorif request fails' do
+ it 'raise error if request fails' do
allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request'))
expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
index cde3f2d6155..f4470b50753 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
index 5c9ca99df7c..7ca20a6d751 100644
--- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -1,148 +1,63 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsImportService do
+ let(:project) { create(:project) }
+ let(:user) { project.creator }
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
- let(:group) { create(:group, lfs_enabled: true)}
- let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
- let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
- let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
- let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
- let(:all_oids) { existing_lfs_objects.merge(oids) }
- let(:remote_uri) { URI.parse(lfs_endpoint) }
-
- subject { described_class.new(project) }
-
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
- end
-
- describe '#execute' do
- context 'when no lfs pointer is linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
- end
-
- it 'retrieves all lfs pointers in the project repository' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
-
- subject.execute
- end
-
- it 'links existent lfs objects to the project' do
- expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
-
- subject.execute
- end
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+ subject { described_class.new(project, user) }
- subject.execute
- end
+ context 'when lfs is enabled for the project' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
end
- context 'when some lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- end
+ it 'downloads lfs objects' do
+ service = double
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
+ expect(service).to receive(:execute).twice
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+ result = subject.execute
- subject.execute
- end
+ expect(result[:status]).to eq :success
end
- context 'when all lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
- end
+ context 'when no downloadable lfs object links' do
+ it 'does not call LfsDownloadService' do
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return({})
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
- it 'retrieves no download links' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+ result = subject.execute
- expect(subject.execute).to be_empty
+ expect(result[:status]).to eq :success
end
end
- context 'when lfsconfig file exists' do
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
- end
-
- context 'when url points to the same import url host' do
- let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
- let(:service) { double }
-
- before do
- allow(service).to receive(:execute)
- end
- it 'downloads lfs object using the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
-
- subject.execute
- end
-
- context 'when import url has credentials' do
- let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
-
- it 'adds the credentials to the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
- .and_return(service)
-
- subject.execute
- end
-
- context 'when url has its own credentials' do
- let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+ context 'when an exception is raised' do
+ it 'returns error' do
+ error_message = "error message"
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_raise(StandardError, error_message)
- it 'does not add the import url credentials' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: remote_uri)
- .and_return(service)
+ result = subject.execute
- subject.execute
- end
- end
- end
- end
-
- context 'when url points to a third party service' do
- let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
-
- it 'disables lfs from the project' do
- expect(project.lfs_enabled?).to be_truthy
-
- subject.execute
-
- expect(project.lfs_enabled?).to be_falsey
- end
-
- it 'does not download anything' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
-
- subject.execute
- end
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
end
end
end
- describe '#default_endpoint_uri' do
- let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+ context 'when lfs is not enabled for the project' do
+ it 'does not download lfs objects' do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ expect(Projects::LfsPointers::LfsObjectDownloadListService).not_to receive(:new)
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
+
+ result = subject.execute
- it 'adds suffix .git if the url does not have it' do
- expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ expect(result[:status]).to eq :success
end
end
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 5caa9de732e..849601c4a63 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
require 'spec_helper'
describe Projects::LfsPointers::LfsLinkService do
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
new file mode 100644
index 00000000000..9dac29765a2
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsObjectDownloadListService do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
+ let(:group) { create(:group, lfs_enabled: true)}
+ let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
+ let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+ let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
+ let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:all_oids) { existing_lfs_objects.merge(oids) }
+ let(:remote_uri) { URI.parse(lfs_endpoint) }
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
+ end
+
+ describe '#execute' do
+ context 'when no lfs pointer is linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ end
+
+ it 'retrieves all lfs pointers in the project repository' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'links existent lfs objects to the project' do
+ expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when some lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when all lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
+ end
+
+ it 'retrieves no download links' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+
+ expect(subject.execute).to be_empty
+ end
+ end
+
+ context 'when lfsconfig file exists' do
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
+ end
+
+ context 'when url points to the same import url host' do
+ let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
+ let(:service) { double }
+
+ before do
+ allow(service).to receive(:execute)
+ end
+
+ it 'downloads lfs object using the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
+
+ subject.execute
+ end
+
+ context 'when import url has credentials' do
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
+
+ it 'adds the credentials to the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
+ .and_return(service)
+
+ subject.execute
+ end
+
+ context 'when url has its own credentials' do
+ let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+
+ it 'does not add the import url credentials' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: remote_uri)
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when url points to a third party service' do
+ let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
+
+ it 'disables lfs from the project' do
+ expect(project.lfs_enabled?).to be_truthy
+
+ subject.execute
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'does not download anything' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ describe '#default_endpoint_uri' do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+
+ it 'adds suffix .git if the url does not have it' do
+ expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ end
+ end
+end
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 86b1ec83f50..7e765659b9d 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -11,6 +11,56 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
+ context 'metrics dashboard setting' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'http://gitlab.com'
+ }
+ }
+ end
+
+ context 'without existing metrics dashboard setting' do
+ it 'creates a setting' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+ end
+
+ context 'with existing metrics dashboard setting' do
+ before do
+ create(:project_metrics_setting, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+
+ context 'with blank external_dashboard_url in params' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: ''
+ }
+ }
+ end
+
+ it 'destroys the metrics_setting entry in DB' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting).to be_nil
+ end
+ end
+ end
+ end
+
context 'error tracking' do
context 'with existing error tracking setting' do
let(:params) do
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index c37147e7d3b..5ad30b58511 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -440,6 +440,8 @@ describe Projects::UpdateService do
context 'when auto devops is set to instance setting' do
before do
project.create_auto_devops!(enabled: nil)
+ project.reload
+
allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true)
end
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
new file mode 100644
index 00000000000..30bd4d6820b
--- /dev/null
+++ b/spec/services/service_response_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+ActiveSupport::Dependencies.autoload_paths << 'app/services'
+
+describe ServiceResponse do
+ describe '.success' do
+ it 'creates a successful response without a message' do
+ expect(described_class.success).to be_success
+ end
+
+ it 'creates a successful response with a message' do
+ response = described_class.success(message: 'Good orange')
+
+ expect(response).to be_success
+ expect(response.message).to eq('Good orange')
+ end
+ end
+
+ describe '.error' do
+ it 'creates a failed response without HTTP status' do
+ response = described_class.error(message: 'Bad apple')
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ end
+
+ it 'creates a failed response with HTTP status' do
+ response = described_class.error(message: 'Bad apple', http_status: 400)
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.http_status).to eq(400)
+ end
+ end
+
+ describe '#success?' do
+ it 'returns true for a successful response' do
+ expect(described_class.success.success?).to eq(true)
+ end
+
+ it 'returns false for a failed response' do
+ expect(described_class.error(message: 'Bad apple').success?).to eq(false)
+ end
+ end
+
+ describe '#error?' do
+ it 'returns false for a successful response' do
+ expect(described_class.success.error?).to eq(false)
+ end
+
+ it 'returns true for a failed response' do
+ expect(described_class.error(message: 'Bad apple').error?).to eq(true)
+ end
+ end
+end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 5dc7394b84f..f5c6e972953 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -86,20 +86,20 @@ describe SystemHooksService do
context 'group_rename' do
it 'contains old and new path' do
- allow(group).to receive(:path_was).and_return('old-path')
+ allow(group).to receive(:path_before_last_save).and_return('old-path')
data = event_data(group, :rename)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path)
expect(data[:path]).to eq(group.path)
expect(data[:full_path]).to eq(group.path)
- expect(data[:old_path]).to eq(group.path_was)
- expect(data[:old_full_path]).to eq(group.path_was)
+ expect(data[:old_path]).to eq(group.path_before_last_save)
+ expect(data[:old_full_path]).to eq(group.path_before_last_save)
end
it 'contains old and new full_path for subgroup' do
subgroup = create(:group, parent: group)
- allow(subgroup).to receive(:path_was).and_return('old-path')
+ allow(subgroup).to receive(:path_before_last_save).and_return('old-path')
data = event_data(subgroup, :rename)
@@ -110,13 +110,13 @@ describe SystemHooksService do
context 'user_rename' do
it 'contains old and new username' do
- allow(user).to receive(:username_was).and_return('old-username')
+ allow(user).to receive(:username_before_last_save).and_return('old-username')
data = event_data(user, :rename)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
expect(data[:username]).to eq(user.username)
- expect(data[:old_username]).to eq(user.username_was)
+ expect(data[:old_username]).to eq(user.username_before_last_save)
end
end
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 1447b9d4126..2a553e18807 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -75,6 +75,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'confidential issues' do
context 'when a user is not an author of confidential issue' do
it 'removes only confidential issues todos' do
@@ -246,6 +253,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'when user is not member' do
it 'removes only confidential issues todos' do
expect { subject }.to change { Todo.count }.from(5).to(4)
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
index c664bac39fc..7dc52f6816a 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/update_deployment_service_spec.rb
@@ -22,6 +22,7 @@ describe UpdateDeploymentService do
subject(:service) { described_class.new(deployment) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 1c79af34538..4a5f4509a7b 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -210,6 +210,8 @@ describe Users::DestroyService do
describe "calls the before/after callbacks" do
it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
service.execute(user)
@@ -219,6 +221,8 @@ describe Users::DestroyService do
group_member = create(:group_member)
group_member.group.group_members.create(user: user, access_level: 40)
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
service.execute(user)
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index b808fa6d91a..40206775aed 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -80,7 +80,7 @@ describe Users::MigrateToGhostUserService do
context "when record migration fails with a rollback exception" do
before do
- expect_any_instance_of(MergeRequest::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback)
end
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index e5c7b5bb9a7..f2b3b44d223 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -57,12 +57,12 @@ describe VerifyPagesDomainService do
expect(domain).not_to be_verified
end
- it 'disables domain and shedules it for removal' do
- Timecop.freeze do
- service.execute
- expect(domain).not_to be_enabled
- expect(domain.remove_at).to be_within(1.second).of(1.week.from_now)
- end
+ it 'disables domain and shedules it for removal in 1 week' do
+ service.execute
+
+ expect(domain).not_to be_enabled
+
+ expect(domain.remove_at).to be_like_time(7.days.from_now)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 60db3e1bc46..9266bee34d6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -53,6 +53,7 @@ RSpec.configure do |config|
config.display_try_failure_messages = true
config.infer_spec_type_from_file_location!
+ config.full_backtrace = !!ENV['CI']
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
location = metadata[:location]
@@ -83,6 +84,7 @@ RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers, type: :feature
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitHelpers, type: :feature
config.include EmailHelpers, :mailer, type: :mailer
config.include Warden::Test::Helpers, type: :request
config.include Gitlab::Routing, type: :routing
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 18a7a392c12..875a9a76e12 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -17,6 +17,8 @@ JS_CONSOLE_FILTER = Regexp.union([
"Download the Vue Devtools extension"
])
+CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
+
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
# This enables access to logs with `page.driver.manage.get_log(:browser)`
@@ -29,7 +31,7 @@ Capybara.register_driver :chrome do |app|
)
options = Selenium::WebDriver::Chrome::Options.new
- options.add_argument("window-size=1240,1400")
+ options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
# Chrome won't work properly in a Docker container in sandbox mode
options.add_argument("no-sandbox")
@@ -78,8 +80,11 @@ RSpec.configure do |config|
protocol: 'http')
# reset window size between tests
- unless session.current_window.size == [1240, 1400]
- session.current_window.resize_to(1240, 1400) rescue nil
+ unless session.current_window.size == CAPYBARA_WINDOW_SIZE
+ begin
+ session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE)
+ rescue # ?
+ end
end
end
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 72912ffb89d..a0c77eecb61 100644
--- a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -25,9 +25,13 @@ shared_context 'Ldap::OmniauthCallbacksController' do
described_class.define_providers!
Rails.application.reload_routes!
- mock_auth_hash(provider.to_s, uid, user.email)
+ @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, uid, user.email)
stub_omniauth_provider(provider, context: request)
allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?)
end
+
+ after do
+ Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
+ end
end
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index ad6e1064499..ed049daba80 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -16,7 +16,9 @@ module EmailHelpers
end
def should_email(user, times: 1, recipients: email_recipients)
- expect(sent_to_user(user, recipients: recipients)).to eq(times)
+ amount = sent_to_user(user, recipients: recipients)
+ failed_message = lambda { "User #{user.username} (#{user.id}): email test failed (expected #{times}, got #{amount})" }
+ expect(amount).to eq(times), failed_message
end
def should_not_email(user, recipients: email_recipients)
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 89517fde6e2..38f30a14409 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -23,8 +23,18 @@ module Spec
def preview_note(text)
page.within('.js-main-target-form') do
- fill_in('note[note]', with: text)
+ filled_text = fill_in('note[note]', with: text)
+
+ begin
+ # Dismiss quick action prompt if it appears
+ filled_text.parent.send_keys(:escape)
+ rescue Selenium::WebDriver::Error::ElementNotInteractableError
+ # It's fine if we can't escape when there's no prompt.
+ end
+
click_on('Preview')
+
+ yield if block_given?
end
end
end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 03057a102c5..34ef185ea27 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -78,20 +78,17 @@ module FilteredSearchHelpers
# .tokens-container to make sure the correct names and values are rendered
def expect_tokens(tokens)
page.within '.filtered-search-box .tokens-container' do
- page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index|
- token_name = tokens[index][:name]
- token_value = tokens[index][:value]
- token_emoji = tokens[index][:emoji_name]
+ token_elements = page.all(:css, 'li.filtered-search-token')
- expect(el.find('.name')).to have_content(token_name)
+ tokens.each_with_index do |token, index|
+ el = token_elements[index]
- if token_value
- expect(el.find('.value')).to have_content(token_value)
- end
+ expect(el.find('.name')).to have_content(token[:name])
+ expect(el.find('.value')).to have_content(token[:value]) if token[:value].present?
# gl-emoji content is blank when the emoji unicode is not supported
- if token_emoji
- selector = %(gl-emoji[data-name="#{token_emoji}"])
+ if token[:emoji_name].present?
+ selector = %(gl-emoji[data-name="#{token[:emoji_name]}"])
expect(el.find('.value')).to have_css(selector)
end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index f59f42ee902..b49d743fb9a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -61,7 +61,14 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h
- { input_variable_name_for_mutation(name) => graphql_input }.to_json
+ result = { input_variable_name_for_mutation(name) => graphql_input }
+
+ # Avoid trying to serialize multipart data into JSON
+ if graphql_input.values.none? { |value| io_value?(value) }
+ result.to_json
+ else
+ result
+ end
end
def input_variable_name_for_mutation(mutation_name)
@@ -83,7 +90,8 @@ module GraphqlHelpers
end
def wrap_fields(fields)
- return unless fields.strip.present?
+ fields = Array.wrap(fields).join("\n")
+ return unless fields.present?
<<~FIELDS
{
@@ -161,6 +169,10 @@ module GraphqlHelpers
field.arguments.values.any? { |argument| argument.type.non_null? }
end
+ def io_value?(value)
+ Array.wrap(value).any? { |v| v.respond_to?(:to_io) }
+ end
+
def field_type(field)
field_type = field.type
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 9cae8f934db..494398dc4de 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -15,7 +15,7 @@ module JavaScriptFixturesHelpers
end
def fixture_root_path
- 'spec/javascripts/fixtures'
+ (Gitlab.ee? ? 'ee/' : '') + 'spec/javascripts/fixtures'
end
# Public: Removes all fixture files from given directory
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 4a0cf62a661..0bb2d2510c2 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -118,7 +118,10 @@ module LoginHelpers
response_object: response_object
}
})
+ original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
+
+ original_env_config_omniauth_auth
end
def saml_xml(raw_saml_response)
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
new file mode 100644
index 00000000000..1f36b0e217c
--- /dev/null
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module MetricsDashboardHelpers
+ def project_with_dashboard(dashboard_path, dashboard_yml = nil)
+ dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')
+
+ create(:project, :custom_repo, files: { dashboard_path => dashboard_yml })
+ end
+
+ def delete_project_dashboard(project, user, dashboard_path)
+ project.repository.delete_file(
+ user,
+ dashboard_path,
+ branch_name: 'master',
+ message: 'Delete dashboard'
+ )
+
+ project.repository.refresh_method_caches([:metrics_dashboard])
+ end
+
+ shared_examples_for 'misconfigured dashboard service response' do |status_code|
+ it 'returns an appropriate message and status code' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(status_code)
+ end
+ end
+
+ shared_examples_for 'valid dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it 'returns a json representation of the dashboard' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+ end
+end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
index 9dc1f1de436..4230d315d9b 100644
--- a/spec/support/helpers/mobile_helpers.rb
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -8,7 +8,7 @@ module MobileHelpers
end
def restore_window_size
- resize_window(1366, 768)
+ resize_window(*CAPYBARA_WINDOW_SIZE)
end
def resize_window(width, height)
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
index 8d84510fb73..44c2051598c 100644
--- a/spec/support/helpers/notification_helpers.rb
+++ b/spec/support/helpers/notification_helpers.rb
@@ -17,11 +17,15 @@ module NotificationHelpers
def create_user_with_notification(level, username, resource = project)
user = create(:user, username: username)
+ create_notification_setting(user, resource, level)
+
+ user
+ end
+
+ def create_notification_setting(user, resource, level)
setting = user.notification_settings_for(resource)
setting.level = level
setting.save
-
- user
end
# Create custom notifications
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 7ce63375d34..c4ae62b25e4 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -17,7 +17,7 @@ module ActiveRecord
def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
- if values[:name]&.include?("CACHE") && skip_cached
+ if values[:cached] && skip_cached
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 4af90f4af79..44d95a029af 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -11,6 +11,8 @@ module RepoHelpers
# blob.path # => 'files/js/commit.js.coffee'
# blob.data # => 'class Commit...'
#
+ # Build the options hash that's passed to Rugged::Commit#create
+
def sample_blob
OpenStruct.new(
oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3',
@@ -129,4 +131,80 @@ eos
file_content: content
).execute
end
+
+ def commit_options(repo, index, target, ref, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [target].compact
+ options[:update_ref] = ref
+
+ options
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of CHANGELOG with a single new line of text.
+ def new_commit_edit_old_file(repo)
+ oid = repo.write("I replaced the changelog with this text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repo,
+ index,
+ repo.head.target,
+ "HEAD",
+ "Edit CHANGELOG in its original location"
+ )
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of the specified file_path with new text.
+ def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
+ oid = repo.write(text, :blob)
+ index = repo.index
+ index.read_tree(branch.target.tree)
+ index.add(path: file_path, oid: oid, mode: 0100644)
+ options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
+ branch = repo.branches[branch_name]
+ new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
+ # CHANGELOG file to the encoding/ directory.
+ def new_commit_move_file(repo)
+ blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
+ file_content = repo.lookup(blob_oid).content
+ oid = repo.write(file_content, :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+ index.remove("CHANGELOG")
+
+ options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
end
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index f4f0415985c..87672c8896d 100644
--- a/spec/support/helpers/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
@@ -35,6 +35,10 @@ module Select2Helper
execute_script("$('#{selector}').select2('open');")
end
+ def close_select2(selector)
+ execute_script("$('#{selector}').select2('close');")
+ end
+
def scroll_select2_to_bottom(selector)
evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
end
diff --git a/spec/support/helpers/test_request_helpers.rb b/spec/support/helpers/test_request_helpers.rb
index 5a84d67bdfc..39e5dafb059 100644
--- a/spec/support/helpers/test_request_helpers.rb
+++ b/spec/support/helpers/test_request_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module TestRequestHelpers
- def test_request(remote_ip: '127.0.0.1')
- ActionController::TestRequest.new({ remote_ip: remote_ip }, ActionController::TestSession.new)
+ def test_request(remote_ip: '127.0.0.1', controller: nil)
+ ActionController::TestRequest.new({ remote_ip: remote_ip }, ActionController::TestSession.new, controller)
end
end
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index c7f878b7371..45b9faa0fea 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -50,20 +50,6 @@ module WaitForRequests
finished_all_vue_resource_requests?
end
- # Waits until the passed block returns true
- def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
- wait_until = Time.now + max_wait_time.seconds
- loop do
- break if yield
-
- if Time.now > wait_until
- raise "Condition not met: #{condition_name}"
- else
- sleep(polling_interval)
- end
- end
- end
-
def finished_all_vue_resource_requests?
Capybara.page.evaluate_script('window.activeVueResources || 0').zero?
end
diff --git a/spec/support/helpers/wait_helpers.rb b/spec/support/helpers/wait_helpers.rb
new file mode 100644
index 00000000000..7e8e25798e8
--- /dev/null
+++ b/spec/support/helpers/wait_helpers.rb
@@ -0,0 +1,20 @@
+module WaitHelpers
+ extend self
+
+ # Waits until the passed block returns true
+ def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01, reload: false)
+ wait_until = Time.now + max_wait_time.seconds
+ loop do
+ result = yield
+ break if result
+
+ page.refresh if reload
+
+ if Time.now > wait_until
+ raise "Condition not met: #{condition_name}"
+ else
+ sleep(polling_interval)
+ end
+ end
+ end
+end
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
new file mode 100644
index 00000000000..ede16d1c1e2
--- /dev/null
+++ b/spec/support/protected_branch_helpers.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ProtectedBranchHelpers
+ def set_allowed_to(operation, option = 'Maintainers', form: '.js-new-protected-branch')
+ within form do
+ select_elem = find(".js-allowed-to-#{operation}")
+ select_elem.click
+
+ wait_for_requests
+
+ within('.dropdown-content') do
+ Array(option).each { |opt| click_on(opt) }
+ end
+
+ # Enhanced select is used in EE, therefore an extra click is needed.
+ select_elem.click if select_elem['aria-expanded'] == 'true'
+ end
+ end
+
+ def set_protected_branch_name(branch_name)
+ find('.js-protected-branch-select').click
+ find('.dropdown-input-field').set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ def set_defaults
+ set_allowed_to('merge')
+ set_allowed_to('push')
+ end
+end
diff --git a/spec/support/protected_tag_helpers.rb b/spec/support/protected_tag_helpers.rb
new file mode 100644
index 00000000000..fe9be856286
--- /dev/null
+++ b/spec/support/protected_tag_helpers.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative 'protected_branch_helpers'
+
+module ProtectedTagHelpers
+ include ::ProtectedBranchHelpers
+
+ def set_allowed_to(operation, option = 'Maintainers', form: '.new-protected-tag')
+ super
+ end
+
+ def set_protected_tag_name(tag_name)
+ find('.js-protected-tag-select').click
+ find('.dropdown-input-field').set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
+ end
+end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 62ae95df8c0..1284415da1f 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -45,7 +45,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
context "race conditions" do
context "when #{record_class_name} migration fails and is rolled back" do
before do
- expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback)
end
@@ -66,7 +66,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
context "when #{record_class_name} migration fails with a non-rollback exception" do
before do
- expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ArgumentError)
end
diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb
index ee5cfcd850d..54d9f5b15f2 100644
--- a/spec/support/shared_context/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_context/policies/project_policy_shared_context.rb
@@ -24,8 +24,7 @@ RSpec.shared_context 'ProjectPolicy context' do
download_code fork_project create_project_snippet update_issue
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
- read_merge_request download_wiki_code read_sentry_issue read_release
- read_prometheus
+ read_merge_request download_wiki_code read_sentry_issue read_prometheus
]
end
diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
index e7ec24c5b7e..421303c97be 100644
--- a/spec/support/shared_examples/application_setting_examples.rb
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -249,4 +249,43 @@ RSpec.shared_examples 'application settings examples' do
expect(setting.password_authentication_enabled_for_web?).to be_falsey
end
+
+ describe 'sentry settings' do
+ context 'when the sentry settings are not set in gitlab.yml' do
+ it 'fallbacks to the settings in the database' do
+ setting.sentry_enabled = true
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = true
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(nil)
+
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+ end
+ end
+
+ context 'when the sentry settings are set in gitlab.yml' do
+ it 'does not fallback to the settings in the database' do
+ setting.sentry_enabled = false
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = false
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
+
+ expect(setting).not_to receive(:read_attribute)
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43'
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb
new file mode 100644
index 00000000000..925974ed11e
--- /dev/null
+++ b/spec/support/shared_examples/ci/stage_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+shared_examples 'manual playable stage' do |stage_type|
+ let(:stage) { build(stage_type, status: status) }
+
+ describe '#manual_playable?' do
+ subject { stage.manual_playable? }
+
+ context 'when is manual' do
+ let(:status) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when is scheduled' do
+ let(:status) { 'scheduled' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when is skipped' do
+ let(:status) { 'skipped' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index c603421d748..db935bcb388 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -329,14 +329,6 @@ shared_examples_for 'trace with disabled live trace feature' do
it_behaves_like 'read successfully with IO'
end
- context 'when current_path (with project_ci_id) exists' do
- before do
- expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
context 'when db trace exists' do
before do
build.send(:write_attribute, :trace, "data")
@@ -396,37 +388,6 @@ shared_examples_for 'trace with disabled live trace feature' do
end
end
- context 'deprecated path' do
- let(:path) { trace.send(:deprecated_path) }
-
- context 'with valid ci_id' do
- before do
- build.project.update(ci_id: 1000)
-
- FileUtils.mkdir_p(File.dirname(path))
-
- File.open(path, "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'without valid ci_id' do
- it "does not return deprecated path" do
- expect(path).to be_nil
- end
- end
- end
-
context 'stored in database' do
before do
build.send(:write_attribute, :trace, "data")
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index b615a8f54cf..e80722857ec 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -120,4 +120,16 @@ shared_examples 'PATCH #update updates variables' do
expect(response).to match_response_schema('variables')
end
end
+
+ context 'for variables of type file' do
+ let(:variables_attributes) do
+ [
+ new_variable_attributes.merge(variable_type: 'file')
+ ]
+ end
+
+ it 'creates new variable of type file' do
+ expect { subject }.to change { owner.variables.file.count }.by(1)
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
index c659be8f13a..a248f60d23e 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -52,20 +52,20 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid)
subject
- expect(instance.public_send(internal_id_attribute)).to eq(iid)
+ expect(read_internal_id).to eq(iid)
end
it 'does not overwrite an existing internal id' do
- instance.public_send("#{internal_id_attribute}=", 4711)
+ write_internal_id(4711)
- expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
+ expect { subject }.not_to change { read_internal_id }
end
context 'when the instance has an internal ID set' do
let(:internal_id) { 9001 }
it 'calls InternalId.update_last_value and sets the `last_value` to that of the instance' do
- instance.send("#{internal_id_attribute}=", internal_id)
+ write_internal_id(internal_id)
expect(InternalId)
.to receive(:track_greatest)
@@ -75,5 +75,39 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
end
end
end
+
+ describe "#reset_scope_internal_id_attribute" do
+ it 'rewinds the allocated IID' do
+ expect { ensure_scope_attribute! }.not_to raise_error
+ expect(read_internal_id).not_to be_nil
+
+ expect(reset_scope_attribute).to be_nil
+ expect(read_internal_id).to be_nil
+ end
+
+ it 'allocates the same IID' do
+ internal_id = ensure_scope_attribute!
+ reset_scope_attribute
+ expect(read_internal_id).to be_nil
+
+ expect(ensure_scope_attribute!).to eq(internal_id)
+ end
+ end
+
+ def ensure_scope_attribute!
+ instance.public_send(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ def reset_scope_attribute
+ instance.public_send(:"reset_#{scope}_#{internal_id_attribute}")
+ end
+
+ def read_internal_id
+ instance.public_send(internal_id_attribute)
+ end
+
+ def write_internal_id(value)
+ instance.public_send(:"#{internal_id_attribute}=", value)
+ end
end
end
diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_spec.rb
index cf1d52a9616..0a302e7d030 100644
--- a/spec/support/shared_examples/models/chat_service_spec.rb
+++ b/spec/support/shared_examples/models/chat_service_spec.rb
@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name|
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -64,7 +70,7 @@ shared_examples_for "chat service" do |service_name|
context "with not default branch" do
let(:sample_data) do
- Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch")
+ Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "not-the-default-branch")
end
context "when notify_only_default_branch enabled" do
diff --git a/spec/support/shared_examples/models/ci_variable_shared_examples.rb b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
new file mode 100644
index 00000000000..f93de8b6ff1
--- /dev/null
+++ b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_examples_for 'CI variable' do
+ it { is_expected.to include_module(HasVariable) }
+
+ describe "variable type" do
+ it 'defines variable types' do
+ expect(described_class.variable_types).to eq({ "env_var" => 1, "file" => 2 })
+ end
+
+ it "defaults variable type to env_var" do
+ expect(subject.variable_type).to eq("env_var")
+ end
+
+ it "supports variable type file" do
+ variable = described_class.new(variable_type: :file)
+ expect(variable).to be_file
+ end
+ end
+
+ it 'strips whitespaces when assigning key' do
+ subject.key = " SECRET "
+ expect(subject.key).to eq("SECRET")
+ end
+
+ it 'can convert to runner variable' do
+ expect(subject.to_runner_variable.keys).to include(:key, :value, :public, :file)
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
index 1f76b981292..d6490a808ce 100644
--- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
@@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name|
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
+ describe '#can_uninstall?' do
+ it 'calls allowed_to_uninstall?' do
+ expect(subject).to receive(:allowed_to_uninstall?).and_return(true)
+
+ expect(subject.can_uninstall?).to be_truthy
+ end
+ end
+
describe '#name' do
it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name)
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 033b65bdc84..bd3661471f8 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
@@ -1,6 +1,32 @@
shared_examples 'cluster application helm specs' do |application_name|
let(:application) { create(application_name) }
+ describe '#uninstall_command' do
+ subject { application.uninstall_command }
+
+ 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
+
+ it 'is rbac' do
+ expect(subject).to be_rbac
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ application.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ end
+ end
+
describe '#files' do
subject { application.files }
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index b8c19cab0c4..4525c03837f 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to eq(reason)
end
end
+
+ context 'application is uninstalling' do
+ subject { create(application_name, :uninstalling) }
+
+ it 'is uninstall_errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_uninstall_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
end
describe '#make_scheduled' do
@@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_scheduled
end
+ describe 'when installed' do
+ subject { create(application_name, :installed) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+ end
+
describe 'when was errored' do
subject { create(application_name, :errored) }
@@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil
end
end
+
+ describe 'when was uninstall_errored' do
+ subject { create(application_name, :uninstall_errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+
+ describe '#make_uninstalling' do
+ subject { create(application_name, :scheduled) }
+
+ it 'is uninstalling' do
+ subject.make_uninstalling!
+
+ expect(subject).to be_uninstalling
+ end
end
end
@@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name|
using RSpec::Parameterized::TableSyntax
where(:trait, :available) do
- :not_installable | false
- :installable | false
- :scheduled | false
- :installing | false
- :installed | true
- :updating | false
- :updated | true
- :errored | false
- :update_errored | false
- :timeouted | false
+ :not_installable | false
+ :installable | false
+ :scheduled | false
+ :installing | false
+ :installed | true
+ :updating | false
+ :updated | true
+ :errored | false
+ :update_errored | false
+ :uninstalling | false
+ :uninstall_errored | false
+ :timed_out | false
end
with_them do
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 77376496854..e5375bc8280 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -41,7 +41,7 @@ shared_examples_for 'inherited access level as a member of entity' do
member.update(access_level: Gitlab::Access::REPORTER)
- expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
+ expect(member.errors.full_messages).to eq(["Access level should be greater than or equal to Developer inherited membership from group #{parent_entity.name}"])
end
it 'allows changing the level from a non existing member' do
diff --git a/spec/support/shared_examples/models/update_project_statistics_spec.rb b/spec/support/shared_examples/models/update_project_statistics_spec.rb
new file mode 100644
index 00000000000..7a04e940ee5
--- /dev/null
+++ b/spec/support/shared_examples/models/update_project_statistics_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples_for 'UpdateProjectStatistics' do
+ let(:project) { subject.project }
+ let(:stat) { described_class.statistic_name }
+ let(:attribute) { described_class.statistic_attribute }
+
+ def reload_stat
+ project.statistics.reload.send(stat).to_i
+ end
+
+ def read_attribute
+ subject.read_attribute(attribute).to_i
+ end
+
+ it { is_expected.to be_new_record }
+
+ context 'when creating' do
+ it 'updates the project statistics' do
+ delta = read_attribute
+
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
+ end
+
+ context 'when updating' do
+ before do
+ subject.save!
+ end
+
+ it 'updates project statistics' do
+ delta = 42
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ subject.write_attribute(attribute, read_attribute + delta)
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
+ end
+
+ context 'when destroying' do
+ before do
+ subject.save!
+ end
+
+ it 'updates the project statistics' do
+ delta = -read_attribute
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { subject.destroy }
+ .to change { reload_stat }
+ .by(delta)
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update(pending_delete: true)
+ project.destroy!
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index e0d0b790a0e..a79a61bc708 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
shared_examples 'close quick action' do |issuable_type|
+ include Spec::Support::Helpers::Features::NotesHelpers
+
before do
project.add_maintainer(maintainer)
gitlab_sign_in(maintainer)
@@ -76,10 +78,7 @@ shared_examples 'close quick action' do |issuable_type|
it 'explains close quick action' do
visit public_send("project_#{issuable_type}_path", project, issuable)
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "this is done, close\n/close"
- click_on 'Preview'
-
+ preview_note("this is done, close\n/close") do
expect(page).not_to have_content '/close'
expect(page).to have_content 'this is done, close'
expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}."
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
index 953e67b0423..a0b0d888769 100644
--- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -1,4 +1,93 @@
# frozen_string_literal: true
shared_examples 'move quick action' do
+ context 'move the issue to another project' do
+ let(:target_project) { create(:project, :public) }
+
+ context 'when the project is valid' do
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ it 'moves the issue' do
+ add_note("/move #{target_project.full_path}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'Issues 1'
+ end
+ end
+
+ context 'when the project is valid but the user not authorized' do
+ let(:project_unauthorized) { create(:project, :public) }
+
+ it 'does not move the issue' do
+ add_note("/move #{project_unauthorized.full_path}")
+
+ wait_for_requests
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the project is invalid' do
+ it 'does not move the issue' do
+ add_note("/move not/valid")
+
+ wait_for_requests
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the user issues multiple commands' do
+ let(:milestone) { create(:milestone, title: '1.0', project: project) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:wontfix) { create(:label, project: project, title: 'wontfix') }
+
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ shared_examples 'applies the commands to issues in both projects, target and source' do
+ it "applies quick actions" do
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+
+ visit project_issue_path(project, issue)
+ expect(page).to have_content 'Closed'
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+ end
+ end
+
+ context 'applies multiple commands with move command in the end' do
+ before do
+ add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+
+ context 'applies multiple commands with move command in the begining' do
+ before do
+ add_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
index dd1676a08e2..657c2a60d24 100644
--- a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
@@ -11,7 +11,7 @@ shared_examples 'remove_due_date quick action' do
visit project_issue_path(project, issue)
page.within '.due_date' do
- expect(page).to have_content 'No due date'
+ expect(page).to have_content 'None'
end
end
end
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
index eff8e401bad..96f79081d26 100644
--- a/spec/support/shared_examples/requests/api/discussions.rb
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -1,4 +1,4 @@
-shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
+shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_invididual_notes: false|
describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
it "returns an array of discussions" do
get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
@@ -136,13 +136,25 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(400)
end
- it "returns a 400 bad request error if discussion is individual note" do
- note.update_attribute(:type, nil)
+ context 'when the discussion is an individual note' do
+ before do
+ note.update!(type: nil)
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' }
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' }
+ end
- expect(response).to have_gitlab_http_status(400)
+ if can_reply_to_invididual_notes
+ it 'creates a new discussion' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['type']).to eq('DiscussionNote')
+ end
+ else
+ it 'returns 400 bad request' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 940c24c8d67..36c486dbdd6 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "calls Slack/Mattermost API for deployment events" do
+ deployment_event_data = { object_kind: 'deployment' }
+
+ chat_service.execute(deployment_event_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
it 'uses the username as an option for slack when configured' do
allow(chat_service).to receive(:username).and_return(username)
@@ -267,7 +275,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -284,7 +292,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'still notifies about pushed tags' do
ref = "#{Gitlab::Git::TAG_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -299,7 +307,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'notifies about all push events' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 825c1cabc14..2dea48e3a88 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -3,9 +3,18 @@ require 'spec_helper'
describe ImportExportUploader do
let(:model) { build_stubbed(:import_export_upload) }
let(:upload) { create(:upload, model: model) }
+ let(:import_export_upload) { ImportExportUpload.new }
subject { described_class.new(model, :import_file) }
+ context 'local store' do
+ describe '#move_to_store' do
+ it 'returns true' do
+ expect(subject.move_to_store).to be true
+ end
+ end
+ end
+
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
@@ -16,5 +25,28 @@ describe ImportExportUploader do
it_behaves_like 'builds correct paths',
store_dir: %r[import_export_upload/import_file/],
upload_path: %r[import_export_upload/import_file/]
+
+ describe '#move_to_store' do
+ it 'returns false' do
+ expect(subject.move_to_store).to be false
+ end
+ end
+
+ describe 'with an export file directly uploaded' do
+ let(:tempfile) { Tempfile.new(['test', '.gz']) }
+
+ before do
+ stub_uploads_object_storage(described_class, direct_upload: true)
+ import_export_upload.export_file = tempfile
+ end
+
+ it 'cleans up cached file' do
+ cache_dir = File.join(import_export_upload.export_file.cache_path(nil), '*')
+
+ import_export_upload.save!
+
+ expect(Dir[cache_dir]).to be_empty
+ end
+ end
end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 9ce9a353913..a62830c35f1 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -771,6 +771,14 @@ describe ObjectStorage do
expect { avatars }.not_to exceed_query_limit(1)
end
+ it 'does not attempt to replace methods' do
+ models.each do |model|
+ expect(model.avatar.upload).to receive(:method_missing).and_call_original
+
+ model.avatar.upload.path
+ end
+ end
+
it 'fetches a unique upload for each model' do
expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
diff --git a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
new file mode 100644
index 00000000000..a7d3dc09fd4
--- /dev/null
+++ b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'notify/pipeline_failed_email.text.erb' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :failed,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ it 'renders the email correctly' do
+ job = create(:ci_build, :failed, pipeline: pipeline, project: pipeline.project)
+
+ render
+
+ expect(rendered).to have_content('Your pipeline has failed')
+ expect(rendered).to have_content(pipeline.project.name)
+ expect(rendered).to have_content(pipeline.git_commit_message.truncate(50))
+ expect(rendered).to have_content(pipeline.commit.author_name)
+ expect(rendered).to have_content("##{pipeline.id}")
+ expect(rendered).to have_content(pipeline.user.name)
+ expect(rendered).to have_content("/-/jobs/#{job.id}/raw")
+ end
+end
diff --git a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
deleted file mode 100644
index 9424795749d..00000000000
--- a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-describe 'projects/issues/_merge_requests_status.html.haml' do
- around do |ex|
- Timecop.freeze(Date.new(2018, 7, 22)) do
- ex.run
- end
- end
-
- it 'shows date of status change in tooltip' do
- merge_request = create(:merge_request, created_at: 1.month.ago)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Opened.*about 1 month ago")
- end
-
- it 'shows only status in tooltip if date is not set' do
- merge_request = create(:merge_request, state: :closed)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Closed")
- end
-end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index 1d9c6d36ad7..1ca9eaf8fdb 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -19,6 +19,7 @@ describe 'projects/issues/show' do
context 'when the issue is closed' do
before do
allow(issue).to receive(:closed?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
end
context 'when the issue was moved' do
@@ -28,16 +29,30 @@ describe 'projects/issues/show' do
issue.moved_to = new_issue
end
- it 'shows "Closed (moved)" if an issue has been moved' do
- render
+ context 'when user can see the moved issue' do
+ before do
+ project.add_developer(user)
+ end
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ it 'shows "Closed (moved)" if an issue has been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ end
+
+ it 'links "moved" to the new issue the original issue was moved to' do
+ render
+
+ expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
- it 'links "moved" to the new issue the original issue was moved to' do
- render
+ context 'when user cannot see moved issue' do
+ it 'does not show moved issue link' do
+ render
- expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ expect(rendered).not_to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index cb97d17988c..34355e27544 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -1,20 +1,53 @@
require 'spec_helper'
describe 'projects/tags/index' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository) }
+ let(:tags) { TagsFinder.new(project.repository, {}).execute }
+ let(:git_tag) { project.repository.tags.last }
+ let(:release) { create(:release, project: project, sha: git_tag.target_commit.sha) }
+ let(:pipeline) { create(:ci_pipeline, :success, project: project, ref: git_tag.name, sha: release.sha) }
before do
assign(:project, project)
assign(:repository, project.repository)
- assign(:tags, [])
+ assign(:releases, project.releases)
+ assign(:tags, Kaminari.paginate_array(tags).page(0))
+ assign(:tags_pipelines, { git_tag.name => pipeline })
allow(view).to receive(:current_ref).and_return('master')
- allow(view).to receive(:can?).and_return(false)
+ allow(view).to receive(:current_user).and_return(project.namespace.owner)
end
it 'defaults sort dropdown toggle to last updated' do
render
-
expect(rendered).to have_button('Last updated')
end
+
+ context 'when the most recent build for a tag has artifacts' do
+ let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ it 'renders the Artifacts section in the download list' do
+ render
+ expect(rendered).to have_selector('li', text: 'Artifacts')
+ end
+
+ it 'renders artifact download links' do
+ render
+ expect(rendered).to have_link(href: latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: 'test'))
+ end
+ end
+
+ context 'when the most recent build for a tag has expired artifacts' do
+ let!(:build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) }
+
+ it 'does not render the Artifacts section in the download list' do
+ render
+ expect(rendered).not_to have_selector('li', text: 'Artifacts')
+ end
+
+ it 'does not render artifact download links' do
+ render
+ expect(rendered).not_to have_link(href: latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: 'test'))
+ end
+ end
end
diff --git a/spec/views/shared/_label_row.html.haml.rb b/spec/views/shared/_label_row.html.haml.rb
new file mode 100644
index 00000000000..a58d5efc1e3
--- /dev/null
+++ b/spec/views/shared/_label_row.html.haml.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'shared/_label_row.html.haml' do
+ label_types = {
+ 'project label': :label,
+ 'group label': :group_label
+ }
+
+ label_types.each do |label_type, label_factory|
+ let!(:label) { create(label_factory) }
+
+ context "for a #{label_type}" do
+ it 'has a non-linked label title' do
+ render 'shared/label_row', label: label
+
+ expect(rendered).not_to have_css('a', text: label.title)
+ end
+
+ it "has Issues link for #{label_type}" do
+ render 'shared/label_row', label: label
+
+ expect(rendered).to have_css('a', text: 'Issues')
+ end
+
+ it "has Merge request link for #{label_type}" do
+ render 'shared/label_row', label: label
+
+ expect(rendered).to have_css('a', text: 'Merge requests')
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index 065aeaf2b65..ffe8796ded9 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -15,6 +15,7 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, :deploy_to_production) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
Deployment.delete_all
build.reload
end
diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb
index 83f76809435..daf014ac574 100644
--- a/spec/workers/cluster_configure_worker_spec.rb
+++ b/spec/workers/cluster_configure_worker_spec.rb
@@ -10,25 +10,35 @@ describe ClusterConfigureWorker, '#perform' do
stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled)
end
- context 'when group cluster' do
- let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { cluster.group }
+ shared_examples 'configured cluster' do
+ it 'creates a namespace' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_cluster).with(cluster).once
+
+ worker.perform(cluster.id)
+ end
+ end
- context 'when group has no projects' do
- it 'does not create a namespace' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+ shared_examples 'unconfigured cluster' do
+ it 'does not create a namespace' do
+ expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
- worker.perform(cluster.id)
- end
+ worker.perform(cluster.id)
end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
context 'when group has a project' do
let!(:project) { create(:project, group: group) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+ it_behaves_like 'configured cluster'
+
+ context 'ci_preparing_state feature is enabled' do
+ let(:ci_preparing_state_enabled) { true }
- worker.perform(cluster.id)
+ it_behaves_like 'unconfigured cluster'
end
end
@@ -36,29 +46,33 @@ describe ClusterConfigureWorker, '#perform' do
let!(:subgroup) { create(:group, parent: group) }
let!(:project) { create(:project, group: subgroup) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
+ it_behaves_like 'configured cluster'
- worker.perform(cluster.id)
+ context 'ci_preparing_state feature is enabled' do
+ let(:ci_preparing_state_enabled) { true }
+
+ it_behaves_like 'unconfigured cluster'
end
end
end
context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
-
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- described_class.new.perform(cluster.id)
- end
+ it_behaves_like 'configured cluster'
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ it_behaves_like 'configured cluster'
+ end
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ context 'when cluster is not managed' do
+ let(:cluster) { create(:cluster, :not_managed) }
+
+ it 'does not configure the cluster' do
+ expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
described_class.new.perform(cluster.id)
end
@@ -71,15 +85,4 @@ describe ClusterConfigureWorker, '#perform' do
described_class.new.perform(123)
end
end
-
- context 'ci_preparing_state feature is enabled' do
- let(:cluster) { create(:cluster) }
- let(:ci_preparing_state_enabled) { true }
-
- it 'does not configure the cluster' do
- expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
-
- described_class.new.perform(cluster.id)
- end
- end
end
diff --git a/spec/workers/cluster_project_configure_worker_spec.rb b/spec/workers/cluster_project_configure_worker_spec.rb
index afdea55adf4..2ac9d0f61b4 100644
--- a/spec/workers/cluster_project_configure_worker_spec.rb
+++ b/spec/workers/cluster_project_configure_worker_spec.rb
@@ -4,18 +4,11 @@ require 'spec_helper'
describe ClusterProjectConfigureWorker, '#perform' do
let(:worker) { described_class.new }
+ let(:cluster) { create(:cluster, :project) }
- context 'ci_preparing_state feature is enabled' do
- let(:cluster) { create(:cluster) }
+ it 'configures the cluster' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_project)
- before do
- stub_feature_flags(ci_preparing_state: true)
- end
-
- it 'does not configure the cluster' do
- expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_project)
-
- described_class.new.perform(cluster.id)
- end
+ described_class.new.perform(cluster.projects.first.id)
end
end
diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
new file mode 100644
index 00000000000..aaf5c9defc4
--- /dev/null
+++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do
+ let(:app) { create(:clusters_applications_helm) }
+ let(:app_name) { app.name }
+ let(:app_id) { app.id }
+
+ subject { described_class.new.perform(app_name, app_id) }
+
+ context 'app exists' do
+ let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) }
+
+ it 'calls the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service)
+ expect(service).to receive(:execute).once
+
+ subject
+ end
+ end
+
+ context 'app does not exist' do
+ let(:app_id) { 0 }
+
+ it 'does not call the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/workers/deployments/finished_worker_spec.rb b/spec/workers/deployments/finished_worker_spec.rb
new file mode 100644
index 00000000000..df62821e2cd
--- /dev/null
+++ b/spec/workers/deployments/finished_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::FinishedWorker do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before do
+ allow(ProjectServiceWorker).to receive(:perform_async)
+ end
+
+ it 'executes project services for deployment_hooks' do
+ deployment = create(:deployment)
+ project = deployment.project
+ service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).to have_received(:perform_async).with(service.id, an_instance_of(Hash))
+ end
+
+ it 'does not execute an inactive service' do
+ deployment = create(:deployment)
+ project = deployment.project
+ create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+
+ it 'does nothing if a deployment with the given id does not exist' do
+ worker.perform(0)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 2459638c3e6..cc1c23bb9e7 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -115,6 +115,19 @@ describe GitGarbageCollectWorker do
end
end
+ context "pack_refs" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it "calls Gitaly" do
+ expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:pack_refs)
+ .and_return(nil)
+
+ subject.perform(project.id, :pack_refs, lease_key, lease_uuid)
+ end
+ end
+
context "repack_incremental" do
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb
new file mode 100644
index 00000000000..0e1171e8491
--- /dev/null
+++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomainRemovalCronWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'when there is domain which should be removed' do
+ let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) }
+
+ before do
+ stub_feature_flags(remove_disabled_domains: true)
+ end
+
+ it 'removes domain' do
+ expect { worker.perform }.to change { PagesDomain.count }.by(-1)
+ expect(PagesDomain.exists?).to eq(false)
+ end
+
+ context 'when domain removal is disabled' do
+ before do
+ stub_feature_flags(remove_disabled_domains: false)
+ end
+
+ it 'does not remove pages domain' do
+ expect { worker.perform }.not_to change { PagesDomain.count }
+ expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
+ end
+ end
+ end
+
+ context 'where there is a domain which scheduled for removal in the future' do
+ let!(:domain_for_removal) { create(:pages_domain, :scheduled_for_removal) }
+
+ it 'does not remove pages domain' do
+ expect { worker.perform }.not_to change { PagesDomain.count }
+ expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb
index 9b479da1cb6..186824a444f 100644
--- a/spec/workers/pages_domain_verification_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb
@@ -6,11 +6,11 @@ describe PagesDomainVerificationCronWorker do
subject(:worker) { described_class.new }
describe '#perform' do
- it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
- verified = create(:pages_domain)
- reverify = create(:pages_domain, :reverify)
- disabled = create(:pages_domain, :disabled)
+ let!(:verified) { create(:pages_domain) }
+ let!(:reverify) { create(:pages_domain, :reverify) }
+ let!(:disabled) { create(:pages_domain, :disabled) }
+ it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
[reverify, disabled].each do |domain|
expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id)
end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index f23910d23be..8c604b13297 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe PipelineScheduleWorker do
+ include ExclusiveLeaseHelpers
+
subject { described_class.new.perform }
set(:project) { create(:project, :repository) }
@@ -39,6 +41,16 @@ describe PipelineScheduleWorker do
it_behaves_like 'successful scheduling'
+ context 'when exclusive lease has already been taken by the other instance' do
+ before do
+ stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT)
+ end
+
+ it 'raises an error and does not start creating pipelines' do
+ expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+ end
+
context 'when the latest commit contains [ci skip]' do
before do
allow_any_instance_of(Ci::Pipeline)
diff --git a/yarn.lock b/yarn.lock
index 41758181610..42b6fc642c4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -658,17 +658,18 @@
eslint-plugin-promise "^4.1.1"
eslint-plugin-vue "^5.0.0"
-"@gitlab/svgs@^1.59.0":
- version "1.59.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.59.0.tgz#affcf9596d736836d37469bb4aea2226ac03e087"
- integrity sha512-dokGyyLRRsoBKO70KP1g+ZsDGyTK/RIHWDmvWI6Bx5AxQ3UqAzVXn2OIb3owjJAexyRG1uBmJrriiVVyHznQ4g==
+"@gitlab/svgs@^1.60.0":
+ version "1.60.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.60.0.tgz#c24fe0d40193f4b722e858c2f766b1c80f961cb7"
+ integrity sha512-Io13vB7LMmPHwUXOoHI+dBYRKDoL0E1dw0b+kJOjesFo+uogaggXJJ4WNEGbWTueYyLckv1YhePACG2xgoLWow==
-"@gitlab/ui@^3.2.0":
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.2.0.tgz#3a44ac806a22b87fe45e6edfa410cb9355164f04"
- integrity sha512-If2ngMIw0jWAdQ1q3PfB8sDhCXz1r3DsRm1X5Vy767kZ2TeFd7SGBp5KP5ceMGGpQ4TTYU/V8IqYnQbTXfPKRw==
+"@gitlab/ui@^3.10.0":
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.0.tgz#0f16772b7fe8052dabd37aba2ae436255b9e0f0a"
+ integrity sha512-po6fh2T8esa2Nach73AYLdoTg8N0YrRa5GkJk5GoxVrHYoAUD8T1Rn3pXXXKSsQdQcYjIZJ6uvY8sL+qg+Yjww==
dependencies:
"@babel/standalone" "^7.0.0"
+ "@gitlab/vue-toasted" "^1.2.1"
bootstrap-vue "^2.0.0-rc.11"
copy-to-clipboard "^3.0.8"
echarts "^4.2.0-rc.2"
@@ -679,6 +680,11 @@
vue "^2.5.21"
vue-loader "^15.4.2"
+"@gitlab/vue-toasted@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263"
+ integrity sha512-ve2PLxKqrwNpsd+4bV5zGJT5+H5N/VJBZoFS2Vp1mH5cUDBYIHTzDmbS6AbBGUDh0F3TxmFMiqfXfpO/1VjBNQ==
+
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -2735,17 +2741,17 @@ cyclist@~0.2.2:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
-d3-array@1, d3-array@1.2.1, d3-array@^1.2.0, d3-array@^1.2.1:
+d3-array@1, d3-array@1.2.1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
integrity sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==
-d3-axis@1.0.8, d3-axis@^1.0.8:
+d3-axis@1, d3-axis@1.0.8, d3-axis@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
integrity sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo=
-d3-brush@1.0.4, d3-brush@^1.0.4:
+d3-brush@1, d3-brush@1.0.4, d3-brush@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
integrity sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q=
@@ -2756,7 +2762,7 @@ d3-brush@1.0.4, d3-brush@^1.0.4:
d3-selection "1"
d3-transition "1"
-d3-chord@1.0.4:
+d3-chord@1, d3-chord@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
integrity sha1-fexPC6iG9xP+ERxF92NBT290yiw=
@@ -2774,6 +2780,13 @@ d3-color@1, d3-color@1.0.3:
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
integrity sha1-vHZD/KjlOoNH4vva/6I2eWtYUJs=
+d3-contour@1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
+ integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==
+ dependencies:
+ d3-array "^1.1.1"
+
d3-dispatch@1, d3-dispatch@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
@@ -2801,7 +2814,14 @@ d3-ease@1, d3-ease@1.0.3, d3-ease@^1.0.3:
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
integrity sha1-aL+8NJM4o4DETYrMT7wzBKotjA4=
-d3-force@1.1.0:
+d3-fetch@1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.1.2.tgz#957c8fbc6d4480599ba191b1b2518bf86b3e1be2"
+ integrity sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==
+ dependencies:
+ d3-dsv "1"
+
+d3-force@1, d3-force@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
integrity sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==
@@ -2816,14 +2836,14 @@ d3-format@1, d3-format@1.2.2:
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
integrity sha512-zH9CfF/3C8zUI47nsiKfD0+AGDEuM8LwBIP7pBVpyR4l/sKkZqITmMtxRp04rwBrlshIZ17XeFAaovN3++wzkw==
-d3-geo@1.9.1:
+d3-geo@1, d3-geo@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
integrity sha512-l9wL/cEQkyZQYXw3xbmLsH3eQ5ij+icNfo4r0GrLa5rOCZR/e/3am45IQ0FvQ5uMsv+77zBRunLc9ufTWSQYFA==
dependencies:
d3-array "1"
-d3-hierarchy@1.1.5:
+d3-hierarchy@1, d3-hierarchy@1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
integrity sha1-ochFxC+Eoga88cAcAQmOpN2qeiY=
@@ -2840,7 +2860,7 @@ d3-path@1, d3-path@1.0.5:
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
integrity sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q=
-d3-polygon@1.0.3:
+d3-polygon@1, d3-polygon@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
integrity sha1-FoiOkCZGCTPysXllKtN4Ik04LGI=
@@ -2855,7 +2875,7 @@ d3-queue@3.0.7:
resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
integrity sha1-yTouVLQXwJWRKdfXP2z31Ckudhg=
-d3-random@1.1.0:
+d3-random@1, d3-random@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
integrity sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM=
@@ -2870,6 +2890,14 @@ d3-request@1.0.6:
d3-dsv "1"
xmlhttprequest "1"
+d3-scale-chromatic@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.3.3.tgz#dad4366f0edcb288f490128979c3c793583ed3c0"
+ integrity sha512-BWTipif1CimXcYfT02LKjAyItX5gKiwxuPRgr4xM58JwlLocWbjPLI7aMEjkcoOQXMkYsmNsvv3d2yl/OKuHHw==
+ dependencies:
+ d3-color "1"
+ d3-interpolate "1"
+
d3-scale@1.0.7, d3-scale@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
@@ -2883,12 +2911,24 @@ d3-scale@1.0.7, d3-scale@^1.0.7:
d3-time "1"
d3-time-format "2"
+d3-scale@2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
+ integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0, d3-selection@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
integrity sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA==
-d3-shape@1.2.0, d3-shape@^1.2.0:
+d3-shape@1, d3-shape@1.2.0, d3-shape@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
integrity sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=
@@ -2924,12 +2964,12 @@ d3-transition@1, d3-transition@1.1.1, d3-transition@^1.1.1:
d3-selection "^1.1.0"
d3-timer "1"
-d3-voronoi@1.1.2:
+d3-voronoi@1, d3-voronoi@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
integrity sha1-Fodmfo8TotFYyAwUgMWinLDYlzw=
-d3-zoom@1.7.1:
+d3-zoom@1, d3-zoom@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
integrity sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ==
@@ -2976,6 +3016,43 @@ 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==
+ dependencies:
+ d3-array "1"
+ d3-axis "1"
+ d3-brush "1"
+ d3-chord "1"
+ d3-collection "1"
+ d3-color "1"
+ d3-contour "1"
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-dsv "1"
+ d3-ease "1"
+ d3-fetch "1"
+ d3-force "1"
+ d3-format "1"
+ d3-geo "1"
+ d3-hierarchy "1"
+ d3-interpolate "1"
+ d3-path "1"
+ d3-polygon "1"
+ d3-quadtree "1"
+ d3-random "1"
+ d3-scale "2"
+ d3-scale-chromatic "1"
+ d3-selection "1"
+ d3-shape "1"
+ d3-time "1"
+ d3-time-format "2"
+ d3-timer "1"
+ d3-transition "1"
+ 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"
@@ -4937,10 +5014,10 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3"
minimalistic-assert "^1.0.0"
-he@^1.1.0, he@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
- integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
+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.13.0:
version "9.13.1"
@@ -7007,19 +7084,19 @@ merge@^1.2.0:
resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
-mermaid@^8.0.0-rc.8:
- version "8.0.0-rc.8"
- resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72"
- integrity sha512-GbF9jHWfqE7YGx9vQySmBxy2Ahlclxmpk4tJ9ntNyafENl96s96ggUK/NQS5ydYoFab6MavTm4YMTIPKqWVvPQ==
+mermaid@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-8.0.0.tgz#8f6c75017e788a8c3997e20c5e5046c2b88d1a8f"
+ integrity sha512-vUQRykev0A6RtxIVqQT3a9TDxcSbdZbQF5JDyKgidnYuJy8BE8jp6LM+HKDSQuroKm6buu4NlpMO+qhxIP/cTg==
dependencies:
- d3 "^4.13.0"
+ d3 "^5.7.0"
dagre-d3-renderer "^0.5.8"
dagre-layout "^0.8.8"
graphlibrary "^2.2.0"
- he "^1.1.1"
- lodash "^4.17.5"
- moment "^2.21.0"
- scope-css "^1.0.5"
+ he "^1.2.0"
+ lodash "^4.17.11"
+ moment "^2.23.0"
+ scope-css "^1.2.1"
methods@~1.1.2:
version "1.1.2"
@@ -7171,10 +7248,10 @@ 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"
-moment@2.x, moment@^2.10.2, moment@^2.21.0:
- version "2.23.0"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225"
- integrity sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA==
+moment@2.x, moment@^2.10.2, moment@^2.23.0:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
monaco-editor-webpack-plugin@^1.7.0:
version "1.7.0"
@@ -9266,7 +9343,7 @@ schema-utils@^1.0.0:
ajv-errors "^1.0.0"
ajv-keywords "^3.1.0"
-scope-css@^1.0.5:
+scope-css@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/scope-css/-/scope-css-1.2.1.tgz#c35768bc900cad030a3e0d663a818c0f6a57f40e"
integrity sha512-UjLRmyEYaDNiOS673xlVkZFlVCtckJR/dKgr434VMm7Lb+AOOqXKdAcY7PpGlJYErjXXJzKN7HWo4uRPiZZG0Q==